{"openapi":"3.1.0","info":{"title":"AgriMbaza QA Evaluation API","description":"\nAPI exposed by our team to the **AgriMbaza Platform**. It serves four tracks:\nSimple QA and Reasoning (`POST /v1/llm`), speech recognition (`POST /v1/asr`) and speech\nsynthesis (`POST /v1/tts`), plus readiness (`GET /v1/health`) and model information\n(`GET /v1/info`).\n\n### Authentication\nEvery `/v1` endpoint requires `<token>`. In Swagger UI, click **Authorize**, paste\nthe token (without the `Bearer` prefix) and confirm; requests made with **Try it out** then\ninclude the header. The token is never part of the example requests. This documentation and\n`/openapi.json` are public; the API endpoints are not.\n\n","version":"1.0.0"},"paths":{"/v1/health":{"get":{"tags":["Service"],"summary":"Readiness check","description":"Returns 200 when the service is ready to accept evaluation requests, 503 otherwise.\n\nThe check confirms that the configuration loaded, that a provider exists for every enabled\ntrack, and that each provider passes a cheap readiness probe. For OpenAI the probe looks up\neach configured model with the configured API key, which is not billed; it catches an invalid\nkey or a misspelled model name. A successful probe is cached for `HEALTH_CHECK_TTL_SECONDS`\n(default 60 s). It cannot detect an exhausted quota; that surfaces as 429 on the track endpoints.","operationId":"health_v1_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}},"401":{"description":"Missing, malformed or incorrect bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"unauthorized","message":"Missing or invalid bearer token."}}}}},"503":{"description":"The service is not ready to accept evaluation requests.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"not_ready","message":"No providers are initialised."}}}}}},"security":[{"HTTPBearer":[]}]}},"/v1/info":{"get":{"tags":["Service"],"summary":"Configured provider, model and tracks","description":"Describes the models this service is configured to use.\n\n`provider` and `model_id` refer to the LLM that serves `simple_qa` and `reasoning`, as the\nevaluation specification expects a single model description. `components` lists the\nprovider and model for each enabled component, since ASR and TTS use different models.\nAll values come from the running configuration.","operationId":"info_v1_info_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InfoResponse"}}}},"401":{"description":"Missing, malformed or incorrect bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"unauthorized","message":"Missing or invalid bearer token."}}}}}},"security":[{"HTTPBearer":[]}]}},"/v1/llm":{"post":{"tags":["LLM"],"summary":"Answer a Simple QA or Reasoning question","description":"Answers one question. Each request is independent; no conversation history is kept.\n\nThe question is sent to the configured LLM with instructions to answer in the question's\nlanguage (unless the question asks otherwise) and to return structured output.\n\n- **simple_qa**: returns `answer`; `thinking` is `null`. Evaluation time limit: 60 s.\n- **reasoning**: returns the explanation of the steps in `thinking` and the conclusion in\n  `answer`. `thinking` is an explanation the model writes for readers, not hidden model\n  reasoning. Evaluation time limit: 180 s.\n\nThe provider call is cancelled at the time limit minus `TIMEOUT_MARGIN_SECONDS` (default 5 s)\nand a 504 is returned. The JSON response is never larger than 5 MiB; larger output returns 502.","operationId":"llm_v1_llm_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LLMRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LLMResponse"}}}},"401":{"description":"Missing, malformed or incorrect bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"unauthorized","message":"Missing or invalid bearer token."}}}}},"404":{"description":"The requested track is disabled on this service.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"track_disabled","message":"The reasoning track is not enabled on this service."}}}}},"422":{"description":"The request failed validation. Not worth retrying.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"invalid_request","message":"language: Input should be 'rw' or 'en'"}}}}},"429":{"description":"The provider is rate limiting this service. Retry after `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying (1-60).","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"upstream_rate_limited","message":"OpenAI rate limit reached."}}}}},"500":{"description":"The provider rejected the request because of a configuration problem on this service (for example an invalid API key or model id).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"upstream_rejected_request","message":"OpenAI rejected the request (HTTP 401)."}}}}},"502":{"description":"The provider failed or returned output that does not satisfy this API's contract.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"upstream_error","message":"OpenAI returned HTTP 500."}}}}},"503":{"description":"The provider could not be reached, or the track is not initialised.","headers":{"Retry-After":{"description":"Seconds to wait before retrying (1-60).","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"upstream_unavailable","message":"Could not reach OpenAI."}}}}},"504":{"description":"The provider did not answer before the track deadline.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"upstream_timeout","message":"The reasoning provider did not answer within 175 s."}}}}}},"security":[{"HTTPBearer":[]}]}},"/v1/asr":{"post":{"tags":["ASR"],"summary":"Transcribe a Kinyarwanda WAV clip","description":"Transcribes one uploaded WAV clip and returns the transcript exactly as the model produced it.\n\nThe service checks only that the file starts with a RIFF/WAVE header; the audio encoding is\nleft to the provider. The transcript is not translated, normalised or otherwise changed, and\nmay be an empty string.\n\nEvaluation time limit: 60 s. The provider call is cancelled at 60 s minus\n`TIMEOUT_MARGIN_SECONDS` (default 5 s) and a 504 is returned.","operationId":"asr_v1_asr_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_asr_v1_asr_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ASRResponse"}}}},"401":{"description":"Missing, malformed or incorrect bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"unauthorized","message":"Missing or invalid bearer token."}}}}},"404":{"description":"The ASR track is disabled on this service.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"track_disabled","message":"The asr track is not enabled on this service."}}}}},"413":{"description":"The upload exceeds `ASR_MAX_UPLOAD_BYTES`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"upload_too_large","message":"The audio upload exceeds 26214400 bytes."}}}}},"422":{"description":"Missing fields, unsupported language, or the file has no RIFF/WAVE header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"invalid_audio","message":"The uploaded file is not a WAV file (missing RIFF/WAVE header)."}}}}},"429":{"description":"The provider is rate limiting this service. Retry after `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying (1-60).","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"upstream_rate_limited","message":"OpenAI rate limit reached."}}}}},"500":{"description":"The provider rejected the request because of a configuration problem on this service (for example an invalid API key or model id).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"upstream_rejected_request","message":"OpenAI rejected the request (HTTP 401)."}}}}},"502":{"description":"The provider failed or returned output that does not satisfy this API's contract.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"upstream_error","message":"OpenAI returned HTTP 500."}}}}},"503":{"description":"The provider could not be reached, or the track is not initialised.","headers":{"Retry-After":{"description":"Seconds to wait before retrying (1-60).","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"upstream_unavailable","message":"Could not reach OpenAI."}}}}},"504":{"description":"The provider did not answer before the track deadline.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"upstream_timeout","message":"The reasoning provider did not answer within 175 s."}}}}}},"security":[{"HTTPBearer":[]}]}},"/v1/tts":{"post":{"tags":["TTS"],"summary":"Synthesise Kinyarwanda speech as WAV","description":"Synthesises `text` and returns the audio as WAV bytes in the response body.\n\nThe response is `Content-Type: audio/wav`, not JSON. Before returning 200 the service checks\nthat the audio starts with `RIFF`, has `WAVE` at bytes 8-11, has RIFF and data sizes that\nmatch the file, is at most 30 MiB, and lasts more than 0 s and at most 30 s. Audio that fails\nany check returns 502; text is never truncated to make it fit.\n\nThe full clip is generated and validated before the first byte is sent, so time to first\nbyte is close to the total response time. Evaluation time limit: 60 s.","operationId":"tts_v1_tts_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TTSRequest"}}},"required":true},"responses":{"200":{"description":"A complete PCM WAV file (RIFF/WAVE, real header sizes, 0 < duration ≤ 30 s, ≤ 30 MiB).","content":{"audio/wav":{"schema":{"type":"string","format":"binary"}}}},"401":{"description":"Missing, malformed or incorrect bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"unauthorized","message":"Missing or invalid bearer token."}}}}},"404":{"description":"The TTS track is disabled on this service.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"track_disabled","message":"The tts track is not enabled on this service."}}}}},"422":{"description":"Missing fields, unsupported language, or `text` longer than `TTS_MAX_TEXT_CHARS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"text_too_long","message":"`text` exceeds the 4096-character limit."}}}}},"429":{"description":"The provider is rate limiting this service. Retry after `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying (1-60).","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"upstream_rate_limited","message":"OpenAI rate limit reached."}}}}},"500":{"description":"The provider rejected the request because of a configuration problem on this service (for example an invalid API key or model id).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"upstream_rejected_request","message":"OpenAI rejected the request (HTTP 401)."}}}}},"502":{"description":"The provider failed or returned output that does not satisfy this API's contract.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"upstream_error","message":"OpenAI returned HTTP 500."}}}}},"503":{"description":"The provider could not be reached, or the track is not initialised.","headers":{"Retry-After":{"description":"Seconds to wait before retrying (1-60).","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"upstream_unavailable","message":"Could not reach OpenAI."}}}}},"504":{"description":"The provider did not answer before the track deadline.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"upstream_timeout","message":"The reasoning provider did not answer within 175 s."}}}}}},"security":[{"HTTPBearer":[]}]}}},"components":{"schemas":{"ASRResponse":{"properties":{"transcript":{"type":"string","title":"Transcript","description":"Transcript exactly as returned by the ASR model, with no normalisation. May be an empty string."}},"type":"object","required":["transcript"],"title":"ASRResponse","examples":[{"transcript":"Ibigori bikenera amazi ahagije mu gihe cyo kumera."}]},"Body_asr_v1_asr_post":{"properties":{"audio":{"type":"string","contentMediaType":"application/octet-stream","title":"Audio","description":"The clip as a WAV file, conventionally named `<id>.wav` with content type `audio/wav`. Maximum size: `ASR_MAX_UPLOAD_BYTES` (default 25 MiB, the OpenAI transcription upload limit)."},"id":{"type":"string","maxLength":256,"minLength":1,"title":"Id","description":"Clip identifier assigned by the evaluator. Used for the upstream filename and log correlation."},"language":{"type":"string","const":"rw","title":"Language","description":"Language spoken in the clip. Only `rw` (Kinyarwanda) is accepted."}},"type":"object","required":["audio","id","language"],"title":"Body_asr_v1_asr_post"},"ComponentInfo":{"properties":{"provider":{"type":"string","title":"Provider","description":"Provider name as configured, e.g. `openai`."},"model_id":{"type":"string","title":"Model Id","description":"Model identifier sent to the provider."},"voice":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Voice","description":"TTS voice. Only set for the TTS component."}},"type":"object","required":["provider","model_id"],"title":"ComponentInfo"},"ErrorDetail":{"properties":{"code":{"type":"string","title":"Code","description":"Stable machine-readable error code, e.g. `unauthorized`, `upstream_timeout`."},"message":{"type":"string","title":"Message","description":"Human-readable explanation. Never contains secrets or request content."}},"type":"object","required":["code","message"],"title":"ErrorDetail"},"ErrorResponse":{"properties":{"error":{"$ref":"#/components/schemas/ErrorDetail"}},"type":"object","required":["error"],"title":"ErrorResponse"},"HealthResponse":{"properties":{"status":{"type":"string","const":"ok","title":"Status","description":"Always `ok` when the response status is 200."},"tracks":{"items":{"type":"string","enum":["simple_qa","reasoning","asr","tts"]},"type":"array","title":"Tracks","description":"Tracks that are configured and initialised."}},"type":"object","required":["status","tracks"],"title":"HealthResponse","examples":[{"status":"ok","tracks":["simple_qa","reasoning","asr","tts"]}]},"InfoResponse":{"properties":{"provider":{"type":"string","title":"Provider","description":"Provider of the LLM used for `simple_qa` and `reasoning`."},"model_id":{"type":"string","title":"Model Id","description":"Model identifier of the LLM used for `simple_qa` and `reasoning`."},"version":{"type":"string","title":"Version","description":"Version of this API service."},"tracks":{"items":{"type":"string","enum":["simple_qa","reasoning","asr","tts"]},"type":"array","title":"Tracks","description":"Tracks this service is configured to serve."},"components":{"additionalProperties":{"$ref":"#/components/schemas/ComponentInfo"},"propertyNames":{"enum":["llm","asr","tts"]},"type":"object","title":"Components","description":"Provider and model per component. ASR and TTS may use different models from the LLM."}},"type":"object","required":["provider","model_id","version","tracks","components"],"title":"InfoResponse","examples":[{"components":{"asr":{"model_id":"gpt-transcribe","provider":"openai"},"llm":{"model_id":"gpt-6.1-terra","provider":"openai"},"tts":{"model_id":"gpt-4o-mini-tts","provider":"openai","voice":"marin"}},"model_id":"gpt-6.1-terra","provider":"openai","tracks":["simple_qa","reasoning","asr","tts"],"version":"1.0.0"}]},"LLMRequest":{"properties":{"id":{"type":"string","maxLength":256,"minLength":1,"title":"Id","description":"Question identifier assigned by the evaluator. Used for log correlation only."},"language":{"type":"string","enum":["rw","en"],"title":"Language","description":"Language of the question: `rw` (Kinyarwanda) or `en` (English)."},"question_type":{"type":"string","enum":["simple_qa","reasoning"],"title":"Question Type","description":"`simple_qa` returns a short answer; `reasoning` also returns the explanation in `thinking`."},"question":{"type":"string","maxLength":100000,"minLength":1,"title":"Question","description":"The question text, sent to the model as-is. Maximum 100,000 characters."}},"type":"object","required":["id","language","question_type","question"],"title":"LLMRequest","examples":[{"id":"llm_dc4112d36b57","language":"en","question":"A farmer plants maize at 75 cm between rows and 25 cm between plants, one seed per hole. How many plants are there per hectare?","question_type":"reasoning"},{"id":"llm_5a01c9e2f7b4","language":"rw","question":"Ni ryari ari byiza gutera ibishyimbo mu Rwanda?","question_type":"simple_qa"}]},"LLMResponse":{"properties":{"answer":{"type":"string","title":"Answer","description":"The model's answer, or for `reasoning` its final conclusion. Always present; may be an empty string."},"thinking":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Thinking","description":"For `reasoning`: an explanation of the steps behind the answer, written by the model for human readers. For `simple_qa`: always `null`."}},"type":"object","required":["answer","thinking"],"title":"LLMResponse","examples":[{"answer":"About 53,333 plants per hectare.","thinking":"One hectare is 10,000 m². Each plant occupies 0.75 m × 0.25 m = 0.1875 m². 10,000 / 0.1875 ≈ 53,333."}]},"TTSRequest":{"properties":{"id":{"type":"string","maxLength":256,"minLength":1,"title":"Id","description":"Clip identifier assigned by the evaluator. Used for log correlation only."},"language":{"type":"string","const":"rw","title":"Language","description":"Language of `text`. Only `rw` (Kinyarwanda) is accepted."},"text":{"type":"string","minLength":1,"title":"Text","description":"The sentence to speak. Must not exceed `TTS_MAX_TEXT_CHARS` characters (default 4,096, the OpenAI speech input limit)."}},"type":"object","required":["id","language","text"],"title":"TTSRequest","examples":[{"id":"tts_ag_base_001","language":"rw","text":"Ahantu hakorerwa ubucuruzi ni ahantu hahurira abaguzi n'abacuruzi."}]}},"securitySchemes":{"HTTPBearer":{"type":"http","description":"Bearer token.","scheme":"bearer"}}},"tags":[{"name":"Service","description":"Readiness and configuration."},{"name":"LLM","description":"Simple QA and Reasoning tracks."},{"name":"ASR","description":"Automatic speech recognition track."},{"name":"TTS","description":"Text-to-speech track."}]}