Chat completions
/v1/chat/completionsThe main text route: an OpenAI-shaped conversation.
Request
modelstringbodyrequiredmessagesarraybodyrequiredstreambooleanbodystream_optionsobjectbodymax_tokensintegerbodyResponses
Response
{
"id": "chatcmpl-…",
"object": "chat.completion",
"model": "gpt-5.6-sol",
"choices": [
{ "index": 0, "message": { "role": "assistant", "content": "Hello!" }, "finish_reason": "stop" }
],
"usage": { "prompt_tokens": 9, "completion_tokens": 12, "cost_usd": "0.0000465" }
}Details
Needs a key in Authorization: Bearer or x-api-key.
The gateway forwards the request to an OpenAI-compatible upstream. Only the fields it looks at itself are listed; every other body field (tools, tool_choice, response_format, temperature, seed, logprobs and the rest) is passed through untouched — their meaning is the OpenAI schema's, not ours.
The output ceiling (max_tokens or max_completion_tokens) sets the worst-case price the balance check works from: the bigger it is, the likelier a 402 on a thin balance — lowering it is a valid answer to a refusal. Name neither and the ceiling comes from the model's catalog row, so the request is still not priced at zero.
This request's cost arrives inside the answer: the x-teamtoken-cost-usd header and usage.cost_usd in the body. The value is a decimal string ("0.0000465"), not a number: a number would be re-displayed by the client language's own float rules (Python would show 4.65e-05), while a string reaches your code exactly as written. The cost can also fail to arrive at all — then it is in neither the header nor the field: on a non-streaming answer when the upstream did not report it, on a stream when the model has no catalog tariff. On a stream the headers leave before the cost is knowable, so it is written into the final usage frame — ask for it with "stream_options": {"include_usage": true}. Without include_usage the stream is relayed byte-for-byte with no frame parsing at all, and carries no cost.
A byte-identical request with the same key inside a short TTL (60 s by default) does not reach the model twice — the gateway returns the first answer's body and charges nothing for the second. Only a successful answer of at most 256 KB is cached; anything else goes upstream again. A replay from the cache carries no x-teamtoken-cost-usd header (nothing was charged), and the cost_usd in its body belongs to the first, paid answer. Hence the corollary: an already-paid answer is served even on an empty wallet — the balance check sits AFTER idempotency. The order of pre-flight checks: disabled model → moderation → balance. An answer with no substance at all (no text, no reasoning, no refusal, no tool call) is treated as a failure and retried before it is handed to you, provided there is somewhere left to switch to; an answer cut short by policy or by the output ceiling does not count as empty, and a stream is excluded from the rule entirely, because the 200 has already started. Every error arrives in one envelope (the code field is not on every status): { "error": { "message": "...", "type": "...", "code": "PROVIDER_CODE" } }.
Moderation, when it is enabled, reads the request text and blocks only on a real verdict: a moderator that is down or erroring lets the request through. The answer always carries back the logical model name you asked for. If it does not fit the gateway's wait (300 s read by default, 60 s without bytes on a stream) you get a 504; the gateway marks that non-streaming attempt, and if the upstream finished and billed anyway, the reconciler credits the amount back to your balance as a compensating grant. Upstream host names are scrubbed out of error bodies, so an error's text can differ from what the upstream sent.
This path has no route of its own: one gateway handler takes every POST /v1/*, and its list of accepted paths is closed — any other /v1/* answers 404.