{"openapi":"3.1.0","info":{"title":"Tadly API","description":"One key, every open model. Endpoints follow the OpenAI request shape, so an existing OpenAI client works by changing its base URL.","version":"0.1.0"},"servers":[{"url":"https://api.tadly.dev","description":"Production"}],"paths":{"/v1/audio/speech":{"post":{"summary":"Generate speech","description":"Turn text into spoken audio. The success body is the encoded audio itself, not JSON; read the response Content-Type for the container you actually got.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SpeechRequest"}}},"required":true},"responses":{"200":{"description":"The audio bytes, in the format response_format asked for — audio/mpeg, audio/opus, audio/aac, audio/flac, audio/wav or audio/pcm.","content":{"audio/*":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"A field failed validation, or the model's task_type does not match this endpoint. Unparseable JSON is a 400 too, in plain text; an error relayed from a model is passed through in that model's own shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}},"text/plain":{"schema":{"type":"string"}}}},"401":{"description":"The API key is missing, malformed, expired, revoked or blocked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"402":{"description":"The wallet has no spendable balance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"No model with that id is active for API use, or it has no route that can serve it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"413":{"description":"`input` is longer than 4096 characters. code is input_too_long, and the count is characters, not bytes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"415":{"description":"A JSON endpoint was called without Content-Type: application/json.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}},"text/plain":{"schema":{"type":"string"}}}},"422":{"description":"The body is valid JSON but the wrong shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}},"text/plain":{"schema":{"type":"string"}}}},"429":{"description":"A per-key rate limit, or every backend for the model is busy. Read error.code and honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"The request could not be completed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"502":{"description":"Every backend on the route failed, or the model returned nothing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"503":{"description":"Temporarily unavailable: the model is cold-starting, the gateway is shedding load, or authentication is degraded. Read error.code and honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"504":{"description":"No backend answered in time. code is backend_timeout. Honour Retry-After, then retry or send a shorter input.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/audio/transcriptions":{"post":{"summary":"Transcribe audio","description":"Transcribe recorded speech. multipart/form-data with a `file` part up to 25 MiB, plus optional model, language, prompt, response_format, temperature and timestamp_granularities[]. response_format selects the body: json, verbose_json, text, srt or vtt.","requestBody":{"content":{"multipart/form-data":{"schema":{"type":"object","required":["file","model"],"properties":{"file":{"type":"string","format":"binary","description":"The audio, up to 25 MiB."},"language":{"type":"string"},"model":{"type":"string"},"prompt":{"type":"string"},"response_format":{"type":"string"},"stream":{"type":"string"},"temperature":{"type":"string"},"timestamp_granularities[]":{"type":"string"}}}}},"required":true},"responses":{"200":{"description":"The transcript, in the shape response_format asked for.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/TranscriptionResponse"},{"type":"object","description":"The verbose transcript. Passed through from the model when it sends one, otherwise synthesised — so only `text` is guaranteed, and a passed-through object may carry fields beyond these.","required":["text"],"properties":{"duration":{"type":"number"},"language":{"type":"string"},"segments":{"type":"array","items":{"type":"object"},"description":"Timed cues. Empty when the model returns no timings."},"task":{"type":"string"},"text":{"type":"string"}}}],"description":"`json` returns the first shape, `verbose_json` the second."}},"text/plain":{"schema":{"type":"string"}},"text/vtt":{"schema":{"type":"string"}}}},"400":{"description":"A field failed validation, or the model's task_type does not match this endpoint. Unparseable JSON is a 400 too, in plain text; an error relayed from a model is passed through in that model's own shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}},"text/plain":{"schema":{"type":"string"}}}},"401":{"description":"The API key is missing, malformed, expired, revoked or blocked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"402":{"description":"The wallet has no spendable balance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"No model with that id is active for API use, or it has no route that can serve it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"413":{"description":"The audio is over 25 MiB, or over the limit of every runtime on the route.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"415":{"description":"The upload was not recognised as audio.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"422":{"description":"The body is valid JSON but the wrong shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}},"text/plain":{"schema":{"type":"string"}}}},"429":{"description":"A per-key rate limit, or every backend for the model is busy. Read error.code and honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"The request could not be completed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"502":{"description":"Every backend on the route failed, or the model returned nothing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"503":{"description":"Temporarily unavailable: the model is cold-starting, the gateway is shedding load, or authentication is degraded. Read error.code and honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"504":{"description":"No backend answered in time. code is backend_timeout. Honour Retry-After, then retry or send a shorter input.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/audio/translations":{"post":{"summary":"Translate audio to English","description":"Identical to /v1/audio/transcriptions in request, models, response formats and errors. Only the output differs: it is always English.","requestBody":{"content":{"multipart/form-data":{"schema":{"type":"object","required":["file","model"],"properties":{"file":{"type":"string","format":"binary","description":"The audio, up to 25 MiB."},"language":{"type":"string"},"model":{"type":"string"},"prompt":{"type":"string"},"response_format":{"type":"string"},"stream":{"type":"string"},"temperature":{"type":"string"},"timestamp_granularities[]":{"type":"string"}}}}},"required":true},"responses":{"200":{"description":"The English transcript, in the shape response_format asked for.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/TranscriptionResponse"},{"type":"object","description":"The verbose transcript. Passed through from the model when it sends one, otherwise synthesised — so only `text` is guaranteed, and a passed-through object may carry fields beyond these.","required":["text"],"properties":{"duration":{"type":"number"},"language":{"type":"string"},"segments":{"type":"array","items":{"type":"object"},"description":"Timed cues. Empty when the model returns no timings."},"task":{"type":"string"},"text":{"type":"string"}}}],"description":"`json` returns the first shape, `verbose_json` the second."}},"text/plain":{"schema":{"type":"string"}},"text/vtt":{"schema":{"type":"string"}}}},"400":{"description":"A field failed validation, or the model's task_type does not match this endpoint. Unparseable JSON is a 400 too, in plain text; an error relayed from a model is passed through in that model's own shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}},"text/plain":{"schema":{"type":"string"}}}},"401":{"description":"The API key is missing, malformed, expired, revoked or blocked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"402":{"description":"The wallet has no spendable balance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"No model with that id is active for API use, or it has no route that can serve it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"413":{"description":"The audio is over 25 MiB, or over the limit of every runtime on the route.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"415":{"description":"The upload was not recognised as audio.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"422":{"description":"The body is valid JSON but the wrong shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}},"text/plain":{"schema":{"type":"string"}}}},"429":{"description":"A per-key rate limit, or every backend for the model is busy. Read error.code and honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"The request could not be completed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"502":{"description":"Every backend on the route failed, or the model returned nothing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"503":{"description":"Temporarily unavailable: the model is cold-starting, the gateway is shedding load, or authentication is degraded. Read error.code and honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"504":{"description":"No backend answered in time. code is backend_timeout. Honour Retry-After, then retry or send a shorter input.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/chat/completions":{"post":{"summary":"Create a chat completion","description":"Generate a response from a text or vision model. Only the fields on ChatCompletionRequest are forwarded — any other field is accepted, returns 200 and is dropped before the model sees it, so tools, tool_choice, response_format and seed have no effect.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatCompletionRequest"}}},"required":true},"responses":{"200":{"description":"The completion.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatCompletionResponse"}},"text/event-stream":{"schema":{"$ref":"#/components/schemas/ChatCompletionChunk"}}}},"400":{"description":"A field failed validation, or the model's task_type does not match this endpoint. Unparseable JSON is a 400 too, in plain text; an error relayed from a model is passed through in that model's own shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}},"text/plain":{"schema":{"type":"string"}}}},"401":{"description":"The API key is missing, malformed, expired, revoked or blocked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"402":{"description":"The wallet has no spendable balance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"No model with that id is active for API use, or it has no route that can serve it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"413":{"description":"The request body is over 32 MiB, or a backend rejected it as too large.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}},"text/plain":{"schema":{"type":"string"}}}},"415":{"description":"A JSON endpoint was called without Content-Type: application/json.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}},"text/plain":{"schema":{"type":"string"}}}},"422":{"description":"The body is valid JSON but the wrong shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}},"text/plain":{"schema":{"type":"string"}}}},"429":{"description":"A per-key rate limit, or every backend for the model is busy. Read error.code and honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"The request could not be completed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"502":{"description":"Every backend on the route failed, or the model returned nothing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"503":{"description":"Temporarily unavailable: the model is cold-starting, the gateway is shedding load, or authentication is degraded. Read error.code and honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/classify":{"post":{"summary":"Classify a text input","description":"Run a classification or sentiment model on a text input.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClassifyRequest"}}},"required":true},"responses":{"200":{"description":"The label and the model's own score.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClassifyResponse"}}}},"400":{"description":"A field failed validation, or the model's task_type does not match this endpoint. Unparseable JSON is a 400 too, in plain text; an error relayed from a model is passed through in that model's own shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}},"text/plain":{"schema":{"type":"string"}}}},"401":{"description":"The API key is missing, malformed, expired, revoked or blocked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"402":{"description":"The wallet has no spendable balance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"No model with that id is active for API use, or it has no route that can serve it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"413":{"description":"The request body is over 32 MiB, or a backend rejected it as too large.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}},"text/plain":{"schema":{"type":"string"}}}},"415":{"description":"A JSON endpoint was called without Content-Type: application/json.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}},"text/plain":{"schema":{"type":"string"}}}},"422":{"description":"The body is valid JSON but the wrong shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}},"text/plain":{"schema":{"type":"string"}}}},"429":{"description":"A per-key rate limit, or every backend for the model is busy. Read error.code and honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"The request could not be completed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"502":{"description":"Every backend on the route failed, or the model returned nothing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"503":{"description":"Temporarily unavailable: the model is cold-starting, the gateway is shedding load, or authentication is degraded. Read error.code and honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"504":{"description":"No backend answered in time. code is backend_timeout. Honour Retry-After, then retry or send a shorter input.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/embeddings":{"post":{"summary":"Create embeddings","description":"Turn text into vectors. encoding_format base64 is accepted but the response is always read back as numbers, so it fails with 502 — send float.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmbeddingsRequest"}}},"required":true},"responses":{"200":{"description":"One entry per input, in order.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmbeddingsResponse"}}}},"400":{"description":"A field failed validation, or the model's task_type does not match this endpoint. Unparseable JSON is a 400 too, in plain text; an error relayed from a model is passed through in that model's own shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}},"text/plain":{"schema":{"type":"string"}}}},"401":{"description":"The API key is missing, malformed, expired, revoked or blocked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"402":{"description":"The wallet has no spendable balance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"No model with that id is active for API use, or it has no route that can serve it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"413":{"description":"The request body is over 32 MiB, or a backend rejected it as too large.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}},"text/plain":{"schema":{"type":"string"}}}},"415":{"description":"A JSON endpoint was called without Content-Type: application/json.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}},"text/plain":{"schema":{"type":"string"}}}},"422":{"description":"The body is valid JSON but the wrong shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}},"text/plain":{"schema":{"type":"string"}}}},"429":{"description":"A per-key rate limit, or every backend for the model is busy. Read error.code and honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"The request could not be completed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"502":{"description":"Every backend on the route failed, or the model returned nothing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"503":{"description":"Temporarily unavailable: the model is cold-starting, the gateway is shedding load, or authentication is degraded. Read error.code and honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/forecasts":{"post":{"summary":"Create a forecast","description":"Forecast future values from an observed time series.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ForecastRequest"}}},"required":true},"responses":{"200":{"description":"The forecast.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ForecastResponse"}}}},"400":{"description":"A field failed validation, or the model's task_type does not match this endpoint. Unparseable JSON is a 400 too, in plain text; an error relayed from a model is passed through in that model's own shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}},"text/plain":{"schema":{"type":"string"}}}},"401":{"description":"The API key is missing, malformed, expired, revoked or blocked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"402":{"description":"The wallet has no spendable balance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"No model with that id is active for API use, or it has no route that can serve it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"413":{"description":"The request body is over 32 MiB, or a backend rejected it as too large.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}},"text/plain":{"schema":{"type":"string"}}}},"415":{"description":"A JSON endpoint was called without Content-Type: application/json.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}},"text/plain":{"schema":{"type":"string"}}}},"422":{"description":"The body is valid JSON but the wrong shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}},"text/plain":{"schema":{"type":"string"}}}},"429":{"description":"A per-key rate limit, or every backend for the model is busy. Read error.code and honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"The request could not be completed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"502":{"description":"Every backend on the route failed, or the model returned nothing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"503":{"description":"Temporarily unavailable: the model is cold-starting, the gateway is shedding load, or authentication is degraded. Read error.code and honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"504":{"description":"No backend answered in time. code is backend_timeout. Honour Retry-After, then retry or send a shorter input.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/messages":{"post":{"summary":"Create a message","description":"The Anthropic Messages API, for clients such as Claude Code (set ANTHROPIC_BASE_URL to the gateway and ANTHROPIC_AUTH_TOKEN to an API key), served by the same models, billing and limits as chat completions. Client tools round-trip as tool_use / tool_result blocks; server tools are not offered to the model, and the model's reasoning is not returned as thinking blocks. With stream: true the body is Messages server-sent events ending in message_stop, or an error event. Errors use the Anthropic shape {type: error, error: {type, message}} with chat's status and headers; a used-up usage plan or an empty wallet is a billing_error with x-should-retry: false.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessagesRequest"}}},"required":true},"responses":{"200":{"description":"The message.","content":{"application/json":{"schema":{"type":"object","description":"An Anthropic Messages API message: id, content blocks, stop_reason and usage."}},"text/event-stream":{"schema":{"type":"object","description":"A Messages API stream event, named by its type."}}}},"400":{"description":"A field failed validation, or the model's task_type does not match this endpoint. Unparseable JSON is a 400 too, in plain text; an error relayed from a model is passed through in that model's own shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}},"text/plain":{"schema":{"type":"string"}}}},"401":{"description":"The API key is missing, malformed, expired, revoked or blocked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"402":{"description":"The wallet has no spendable balance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"No model with that id is active for API use, or it has no route that can serve it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"413":{"description":"The request body is over 32 MiB, or a backend rejected it as too large.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}},"text/plain":{"schema":{"type":"string"}}}},"415":{"description":"A JSON endpoint was called without Content-Type: application/json.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}},"text/plain":{"schema":{"type":"string"}}}},"422":{"description":"The body is valid JSON but the wrong shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}},"text/plain":{"schema":{"type":"string"}}}},"429":{"description":"A per-key rate limit, or every backend for the model is busy. Read error.code and honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"The request could not be completed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"502":{"description":"Every backend on the route failed, or the model returned nothing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"503":{"description":"Temporarily unavailable: the model is cold-starting, the gateway is shedding load, or authentication is degraded. Read error.code and honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/models":{"get":{"summary":"List models","description":"List the models this API accepts. The same list for every key.","responses":{"200":{"description":"The catalog.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpenAiModelList"}}}},"401":{"description":"The API key is missing, malformed, expired, revoked or blocked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"A per-key rate limit. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"The catalog could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"503":{"description":"Authentication is degraded. Honour Retry-After and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/v1/responses":{"post":{"summary":"Create a response","description":"The OpenAI Responses API, served by the same models, billing and limits as chat completions. Function, custom and namespace tools round-trip as function_call / custom_tool_call items; hosted tools (web_search and the like) are not offered to the model. Nothing is stored, so send the whole conversation in input. With stream: true the body is Responses server-sent events ending in response.completed, response.incomplete or response.failed. Errors are the same envelope chat returns.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponsesRequest"}}},"required":true},"responses":{"200":{"description":"The response.","content":{"application/json":{"schema":{"type":"object","description":"A Responses API response object: id, status, output items and usage."}},"text/event-stream":{"schema":{"type":"object","description":"A Responses API stream event, named by its type."}}}},"400":{"description":"A field failed validation, or the model's task_type does not match this endpoint. Unparseable JSON is a 400 too, in plain text; an error relayed from a model is passed through in that model's own shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}},"text/plain":{"schema":{"type":"string"}}}},"401":{"description":"The API key is missing, malformed, expired, revoked or blocked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"402":{"description":"The wallet has no spendable balance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"No model with that id is active for API use, or it has no route that can serve it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"413":{"description":"The request body is over 32 MiB, or a backend rejected it as too large.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}},"text/plain":{"schema":{"type":"string"}}}},"415":{"description":"A JSON endpoint was called without Content-Type: application/json.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}},"text/plain":{"schema":{"type":"string"}}}},"422":{"description":"The body is valid JSON but the wrong shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}},"text/plain":{"schema":{"type":"string"}}}},"429":{"description":"A per-key rate limit, or every backend for the model is busy. Read error.code and honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"The request could not be completed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"502":{"description":"Every backend on the route failed, or the model returned nothing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"503":{"description":"Temporarily unavailable: the model is cold-starting, the gateway is shedding load, or authentication is degraded. Read error.code and honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}}},"components":{"schemas":{"ApiError":{"type":"object","required":["error"],"properties":{"error":{"$ref":"#/components/schemas/ApiErrorBody"}}},"ApiErrorBody":{"type":"object","required":["message","type"],"properties":{"code":{"type":["string","null"]},"message":{"type":"string"},"recharge_url":{"type":["string","null"]},"type":{"type":"string"}}},"ChatCompletionChoice":{"type":"object","required":["index","message","finish_reason"],"properties":{"finish_reason":{"type":"string"},"index":{"type":"integer","format":"int32"},"message":{"$ref":"#/components/schemas/ChatMessage"}}},"ChatCompletionChunk":{"type":"object","required":["id","object","created","model","choices"],"properties":{"choices":{"type":"array","items":{"$ref":"#/components/schemas/ChatCompletionChunkChoice"}},"created":{"type":"integer","format":"int64"},"id":{"type":"string"},"model":{"type":"string"},"object":{"type":"string"},"usage":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Usage"}]}}},"ChatCompletionChunkChoice":{"type":"object","required":["index","delta"],"properties":{"delta":{"$ref":"#/components/schemas/ChatMessageDelta"},"finish_reason":{"type":["string","null"]},"index":{"type":"integer","format":"int32"}}},"ChatCompletionRequest":{"type":"object","description":"OpenAI-shaped request body. We parse it once on the way in, hand a\nborrowed copy to inference_config merging, then re-serialise it for\nthe downstream backend (vLLM, Lambda, external).\n\nOptional fields use `skip_serializing_if = \"Option::is_none\"` so the\nre-serialised body matches what the client actually sent. Without\nthis, every Option field is emitted as `null` — fine for vLLM\n(which ignores extras) but rejected by FastAPI/pydantic backends\nconfigured with `extra=\"forbid\"` (e.g. the tiny-llm Lambda shims).","required":["model","messages"],"properties":{"chat_template_kwargs":{},"enable_thinking":{"type":["boolean","null"]},"frequency_penalty":{"type":["number","null"],"format":"double"},"logprobs":{"type":["boolean","null"]},"max_completion_tokens":{"type":["integer","null"],"format":"int32","description":"OpenAI's current name for `max_tokens`. Newer SDKs send this one, and\nwithout it here they lost their output limit entirely — the same silent\ndrop, on the parameter that governs cost. Normalised into `max_tokens`\non ingest so the deployment cap and the truncation signal both see it."},"max_tokens":{"type":["integer","null"],"format":"int32","description":"Gateway-enforced ceiling; outside it is a 400. A route may cap it lower.","maximum":32768,"minimum":1},"messages":{"type":"array","items":{"$ref":"#/components/schemas/ChatMessage"}},"min_p":{"type":["number","null"],"format":"double"},"model":{"type":"string"},"n":{"type":["integer","null"],"format":"int32"},"parallel_tool_calls":{"type":["boolean","null"]},"presence_penalty":{"type":["number","null"],"format":"double"},"repetition_penalty":{"type":["number","null"],"format":"double"},"response_format":{},"seed":{"type":["integer","null"],"format":"int64"},"stop":{},"stream":{"type":"boolean"},"temperature":{"type":["number","null"],"format":"double","description":"Gateway-enforced range; outside it is a 400.","maximum":2,"minimum":0},"tool_choice":{},"tools":{},"top_k":{"type":["integer","null"],"format":"int32"},"top_logprobs":{"type":["integer","null"],"format":"int32"},"top_p":{"type":["number","null"],"format":"double"},"user":{"type":["string","null"]}}},"ChatCompletionResponse":{"type":"object","required":["id","object","created","model","choices","usage"],"properties":{"choices":{"type":"array","items":{"$ref":"#/components/schemas/ChatCompletionChoice"}},"created":{"type":"integer","format":"int64"},"id":{"type":"string"},"model":{"type":"string"},"object":{"type":"string"},"usage":{"$ref":"#/components/schemas/Usage"}}},"ChatImageUrl":{"type":"object","properties":{"detail":{"type":["string","null"]},"url":{"type":["string","null"]}}},"ChatMessage":{"type":"object","required":["role"],"properties":{"content":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/ChatMessageContent","description":"Optional because an assistant message that only calls tools carries\n`\"content\": null`. This struct previously required content, so a client\nreplaying its own tool-call turn back to us failed to deserialize and\nthe entire request was rejected — multi-turn tool use could not work\neven in principle."}]},"name":{"type":["string","null"],"description":"Tool/function name on a tool result message."},"reasoning":{"type":["string","null"],"description":"Newer vLLM releases spell the same field `reasoning`. It is carried as\nits own field rather than a serde `alias` of `reasoning_content`: an\nengine that emits both keys would make an alias fail with a\nduplicate-field error and turn every completion into a 500. Passing\neach through verbatim keeps the client's view identical to the engine's."},"reasoning_content":{"type":["string","null"],"description":"The model's chain of thought, when the engine runs a reasoning parser\n(vLLM `--reasoning-parser`) and splits it out of `content`. Without this\nfield the gateway deserialized a completion into this struct and\nre-serialized it, so the reasoning was generated and billed as\ncompletion tokens and then silently discarded before the client saw it.\n\nOutput only: [`ChatMessage::strip_reasoning`] removes it from inbound\nrequest messages, so a client replaying an assistant turn forwards\nexactly what it forwarded before this field existed."},"role":{"type":"string"},"tool_call_id":{"type":["string","null"],"description":"Set on a `role: \"tool\"` message, linking a result to its call."},"tool_calls":{"description":"Tool calls the model asked for. `Value` for the same reason as the\nrequest-side tool fields: pass the structure through rather than\nre-deriving it and risking a dropped sub-field."}}},"ChatMessageContent":{"oneOf":[{"type":"string"},{"type":"array","items":{"$ref":"#/components/schemas/ChatMessageContentPart"}}]},"ChatMessageContentPart":{"oneOf":[{"type":"object","required":["type"],"properties":{"text":{"type":["string","null"]},"type":{"type":"string","enum":["text"]}}},{"type":"object","required":["type"],"properties":{"image_url":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/ChatImageUrl"}]},"type":{"type":"string","enum":["image_url"]}}}]},"ChatMessageDelta":{"type":"object","properties":{"content":{"type":["string","null"]},"reasoning":{"type":["string","null"],"description":"`reasoning` spelling of the same, kept separate for the reason given on\n[`ChatMessage::reasoning`]."},"reasoning_content":{"type":["string","null"],"description":"Streamed reasoning fragments — the streaming twin of\n[`ChatMessage::reasoning_content`]. A reasoning model's first chunks\ncarry only this, so without it the client received a run of empty\ndeltas in place of the model's thinking."},"role":{"type":["string","null"]},"tool_calls":{"description":"Streamed tool-call fragments. Without this the streaming path stripped\ntool calls from every chunk, so streaming + tools was broken separately\nfrom the non-streaming case."}}},"ClassifyRequest":{"type":"object","description":"POST /v1/classify request body. `task` is optional and, when set,\nconstrains classification to a specific label space (e.g. \"sentiment\",\n\"topic\"). When omitted, the backend defaults to the model's primary\ntask as declared in the catalog.","required":["model","input"],"properties":{"input":{"type":"string","minLength":1},"model":{"type":"string","minLength":1},"task":{"type":["string","null"]}}},"ClassifyResponse":{"type":"object","required":["model","label","score","usage"],"properties":{"label":{"type":"string"},"model":{"type":"string"},"score":{"type":"number","format":"double"},"usage":{"$ref":"#/components/schemas/ClassifyUsage"}}},"ClassifyUsage":{"type":"object","required":["inference_calls"],"properties":{"inference_calls":{"type":"integer","format":"int32"}}},"EmbeddingData":{"type":"object","required":["object","embedding","index"],"properties":{"embedding":{"type":"array","items":{"type":"number","format":"double"}},"index":{"type":"integer","format":"int32"},"object":{"type":"string"}}},"EmbeddingsRequest":{"type":"object","description":"POST /v1/embeddings request body. `input` supports the OpenAI-compatible\nshape: either a single string or an array of strings/tokens.","required":["model","input"],"properties":{"dimensions":{"type":["integer","null"],"format":"int32","minimum":1},"encoding_format":{"type":["string","null"],"pattern":"^(float|base64)$"},"input":{},"model":{"type":"string"},"user":{"type":["string","null"]}}},"EmbeddingsResponse":{"type":"object","required":["object","data","model","usage"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/EmbeddingData"}},"model":{"type":"string"},"object":{"type":"string"},"usage":{"$ref":"#/components/schemas/EmbeddingsUsage"}}},"EmbeddingsUsage":{"type":"object","required":["prompt_tokens","total_tokens"],"properties":{"prompt_tokens":{"type":"integer","format":"int32"},"total_tokens":{"type":"integer","format":"int32"}}},"ForecastRequest":{"type":"object","description":"POST /v1/forecasts request body. `series` is the historical observation\nvector and `horizon` is the number of steps to predict. The backend\nmodel owns the forecast strategy; the gateway only enforces shape and\ntask_type guard before proxying.","required":["model","series","horizon"],"properties":{"horizon":{"type":"integer","format":"int32","minimum":1},"model":{"type":"string","minLength":1},"series":{"type":"array","items":{"type":"number","format":"double"},"description":"Every value must be finite; NaN and Infinity are a 400.","minItems":1}}},"ForecastResponse":{"type":"object","required":["model","forecast","usage"],"properties":{"forecast":{"type":"array","items":{"type":"number","format":"double"}},"model":{"type":"string"},"usage":{"$ref":"#/components/schemas/ForecastUsage"}}},"ForecastUsage":{"type":"object","description":"Per-request usage envelope for /v1/forecasts. `inference_calls` is the\nactive billing dimension (Phase 2, OPE-199). `series_points` is the\ncount of observations in the input series — kept in the response so we\ncan migrate to per-point pricing later without changing the client\ncontract (OPE-182 CEO decision).","required":["inference_calls","series_points"],"properties":{"inference_calls":{"type":"integer","format":"int32"},"series_points":{"type":"integer","format":"int32"}}},"MessagesRequest":{"type":"object","description":"The subset of a Messages request the gateway acts on. Unknown fields are\naccepted and ignored, as on chat.","required":["model"],"properties":{"max_tokens":{"type":["integer","null"],"format":"int32","description":"Required by the Messages API. Values above the gateway's output ceiling\nare reduced to it, as a deployment cap would."},"messages":{"type":"array","items":{"type":"object"},"description":"`user` / `assistant` turns whose content is a string or an array of\n`text`, `image`, `document`, `tool_use` and `tool_result` blocks."},"model":{"type":"string"},"stop_sequences":{"type":["array","null"],"items":{"type":"string"}},"stream":{"type":"boolean"},"system":{"type":["object","null"],"description":"A string, or an array of `text` blocks."},"temperature":{"type":["number","null"],"format":"double","maximum":1,"minimum":0},"tool_choice":{"type":["object","null"]},"tools":{"type":["array","null"],"items":{"type":"object"},"description":"Client tools (`name`, `description`, `input_schema`). Server tools are ignored."},"top_p":{"type":["number","null"],"format":"double"}}},"OpenAiModel":{"type":"object","required":["id","object","created","owned_by","task_type","input_modality","output_modality","endpoint","status","price_per_unit","billing_unit"],"properties":{"billing_unit":{"type":["string","null"],"description":"The unit `price_per_unit` is denominated in (\"audio_minute\",\n\"character_million\", …), straight from `llm_models.billing_unit`. `null`\nfor token-priced text models. Always present. Additive — OpenAI SDK\nclients ignore unknown keys."},"created":{"type":"integer","format":"int64"},"endpoint":{"type":"string","description":"Public /v1 endpoint for this model, derived from `task_type`.\nEmpty string when the task_type has no implemented endpoint yet —\nthe field is always present so SDK consumers can rely on its shape."},"id":{"type":"string"},"input_modality":{"type":"string","description":"Input representation accepted by the advertised endpoint, for example\n`text` or `image+text`."},"object":{"type":"string"},"output_modality":{"type":"string","description":"Output representation returned by the advertised endpoint, for example\n`text`, `labels`, or `audio`."},"owned_by":{"type":"string"},"price_per_unit":{"type":["string","null"],"description":"Advertised metered price for non-token modalities, as a full-precision\ndecimal string (e.g. `\"10\"` = $10) in the unit named by `billing_unit`:\nSTT is $/audio-minute, TTS is $/1M characters. A string (not a JSON\nnumber) so the rate is exposed without float rounding. `null` for\ntoken-priced text models, which advertise no per-unit rate. Always\npresent so SDK consumers can rely on the response shape. Sourced from\n`llm_models.price_per_unit`."},"status":{"type":"string","description":"Liveness label: `\"live\"` when at least one routable deployment\nbacks this model row, `\"inactive\"` otherwise. Lets clients filter\nthe catalog before issuing inference and avoid the\n`no active deployment for model ...` `server_error` (OPE-163).\nAdditive — OpenAI SDK clients ignore unknown keys."},"task_type":{"type":"string","description":"Catalog routing key (e.g. \"chat\", \"forecasting\", \"classification\").\nAdditive field; OpenAI SDK clients ignore unknown keys so this does\nnot break existing integrations."}}},"OpenAiModelList":{"type":"object","required":["object","data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/OpenAiModel"}},"object":{"type":"string"}}},"ResponsesRequest":{"type":"object","description":"The subset of a Responses request the gateway acts on. Unknown fields\n(`store`, `include`, `reasoning`, `prompt_cache_key`, metadata) are accepted\nand ignored, as on chat.","required":["model"],"properties":{"input":{"type":"object","description":"A string (one user message) or an array of input items: `message`,\n`function_call`, `function_call_output`, `custom_tool_call`,\n`custom_tool_call_output`."},"instructions":{"type":["string","null"],"description":"System instructions; sent to the model as the leading system message."},"max_output_tokens":{"type":["integer","null"],"format":"int32","maximum":32768,"minimum":1},"model":{"type":"string"},"parallel_tool_calls":{"type":["boolean","null"]},"stream":{"type":"boolean"},"temperature":{"type":["number","null"],"format":"double","maximum":2,"minimum":0},"text":{"type":["object","null"],"description":"`text.format` maps to chat `response_format`."},"tool_choice":{"type":["object","null"]},"tools":{"type":["array","null"],"items":{"type":"object"},"description":"`function`, `custom` and `namespace` tools. Hosted tools are ignored."},"top_p":{"type":["number","null"],"format":"double"},"user":{"type":["string","null"]}}},"SpeechRequest":{"type":"object","description":"POST /v1/audio/speech request body. OpenAI-compatible shape:\ntext input + voice selection, with optional output format and speed.","required":["model","input","voice"],"properties":{"emotion":{"type":["string","null"],"description":"Emotion controls (all optional), forwarded to the multilingual Chatterbox\nworker, where the model-agnostic emotion layer turns them into per-span\nsynthesis controls. `emotion` (a canonical name, e.g. \"happy\") + `intensity`\n(0–1) set one emotion for the whole text; `emotion_plan` carries an explicit\nlist of {text, emotion, intensity} spans (several emotions in one request);\ninline `[emotion]` markers in `input` are parsed worker-side. The Piper\nfallback parses only the fields it knows and ignores these, so they ride\nsafely on every backend. The worker validates them (a 400 for an unknown\nemotion), so the gateway stays model-agnostic and hard-codes no emotion list."},"emotion_plan":{},"input":{"type":"string","description":"Capped at `MAX_SPEECH_INPUT_CHARS`; longer is a 413, not a truncation.","maxLength":4096},"intensity":{"type":["number","null"],"format":"double"},"language":{"type":["string","null"],"description":"Target synthesis language (an ISO code the model accepts, e.g. \"hi\" for\nHindi). Accepted from the client and forwarded verbatim to the backend. The\nChatterbox MULTILINGUAL worker consumes it (its `generate` requires a\n`language_id`); the Piper fallback parses only the fields it knows and\nignores this one, so it rides safely on every backend. Absent -> the worker\ndefaults to English. The worker validates the code against its own supported\nset (a 400 for an unknown code), so the gateway stays model-agnostic and does\nnot hard-code any language list here."},"model":{"type":"string"},"performance":{"description":"OPTIONAL performance directive — `{speed, volume, pause_before, …}`. Forwarded\nverbatim to the Chatterbox worker, which honors what it can render (speed,\nvolume, lead-in pause) and IGNORES the rest (pitch/tension/breathiness/…), so a\nclient can send the full performance schema forward-compatibly. Kept as a raw\nJSON object so the gateway hard-codes no performance field list; the Piper\nfallback ignores it, so it rides safely on every backend."},"response_format":{"type":["string","null"],"pattern":"^(mp3|opus|aac|flac|wav|pcm)$"},"speed":{"type":["number","null"],"format":"double","maximum":4,"minimum":0.25},"voice":{"type":"string"}}},"TranscriptionResponse":{"type":"object","description":"OpenAI-compatible transcription JSON envelope.","required":["text"],"properties":{"text":{"type":"string"}}},"Usage":{"type":"object","required":["prompt_tokens","completion_tokens","total_tokens"],"properties":{"completion_tokens":{"type":"integer","format":"int32"},"prompt_tokens":{"type":"integer","format":"int32"},"total_tokens":{"type":"integer","format":"int32"}}}},"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Your API key. Keys start `PK-TADLY-` and are case-sensitive."}}},"security":[{"bearerAuth":[]}]}