Chat completions
The main endpoint: its parameters, its streaming protocol and its errors.
The endpoint
POST https://api.multigrid.ai/v1/chat/completions. The request body is OpenAI’s. We require exactly two things — model as a non-empty string and messages as an array with at least one entry — and pass everything else upstream untouched. temperature, top_p, stop, tools, response_format, seed, logprobs and anything a vendor shipped last week all travel through us to an OpenAI-format provider without being inspected.
Two qualifications, because “passed through” is a promise people plan against:
- Anthropic routes are built field by field, not forwarded, so a parameter the Messages API has no equivalent for cannot be honoured there. The ones that would change the answer —
response_format,seed,reasoning_effort,thinking,n— are refused with a 400 naming the limitation, or routed to a non-Anthropic fallback if you have one, and never silently dropped.frequency_penalty,presence_penalty,logit_biasandparallel_tool_callsare still dropped without comment on Anthropic; that is a real gap and it is listed here rather than discovered. - What comes back is passed through too.
logprobsandusage.completion_tokens_details— includingreasoning_tokens, which is most of what a reasoning model’s completion costs — are returned as the provider reported them, on both the streaming and the non-streaming path. They used to survive only on a stream, which meant the same model on the same prompt behaved differently depending on transport.
Parameters we add
Six optional top-level fields are ours, and three more are accepted for compatibility. Every one of them is removed before the request reaches a provider, so none of them can cause an upstream 400: models, provider, metadata, plugins, cache, retry, plus transforms, route and multigrid.
| Parameter | Type | Required | Description |
|---|---|---|---|
| models | string[] | Optional | Fallback chain. Tried in order after the primary model, and after every provider for it has been exhausted. See failover. |
| provider | object | Optional | order, ignore, allow_fallbacks, sort, weights. Full reference in provider preferences. |
| metadata | object | Optional | Up to 16 string labels stored on the request row and filterable in Activity and Analytics. Keys are up to 64 characters from A-Z a-z 0-9 . _ : -; values up to 256. Over a limit is an error, never a truncation — a tag silently cut short would merge two customers’ spend into one line. |
| plugins | object[] | Optional | Currently one: { "id": "web" }, which runs a web search and grounds the answer in the results. It is charged on top of the tokens and never cached. If the deployment has no search backend configured, the request is refused rather than answered without it. |
| cache | object | Optional | { "ttl": seconds }, up to 86,400. Opts this request into the exact-match response cache, which is off unless you ask for it. Identical to the X-Multigrid-Cache-Ttl header — use whichever your client makes easier. Your account id is inside the cache key, so a hit can only ever be your own earlier answer. |
| retry | object | Optional | { "max_attempts": 1..5 }, clamped to that range. Overrides how many times one provider is retried with backoff before we fail over to the next. 1 disables retries for a latency-sensitive call; the default suits most traffic. Also honoured on /embeddings, /images/generations, /video/generations and /rerank. |
{
"model": "meta/llama-3.3-70b-instruct",
"messages": [{"role": "user", "content": "Classify this ticket."}],
"models": ["openai/gpt-5-nano"],
"provider": { "sort": "price", "ignore": ["together"] },
"metadata": { "feature": "ticket-triage", "env": "prod" }
}The model id may also carry a routing suffix — :floor, :nitro or :free — which is read off the head of the chain and stripped before anything downstream sees it. Those are documented with the rest of the provider controls.
The multigrid object
Every successful response carries one. Fields that would only repeat what you already sent are left off rather than padded in.
| Field | Description |
|---|---|
| model | The model that answered. Differs from yours when a fallback fired or multigrid/auto chose. |
| provider | The provider slug that served it. |
| requested_model | Only present when it differs from model. |
| auto_reason | Only present on multigrid/auto: why that model was chosen. |
| attempts | Upstream calls made. Two or more means a retry or a failover happened. |
| cost_micros | What you were charged, as an integer number of millionths of a dollar. This is the authoritative figure. |
| cost_usd | The same number as a decimal. |
| byok | True when your own provider key paid for it, in which case the cost is zero. |
| saved_usd | Only present when something waived the charge — what it would otherwise have cost. |
| waived | "byok" or "zero_completion". Present exactly when the cost was waived, so a $0 line is never unexplained. |
| cost_breakdown | Where the charge came from — prompt, completion, cached prompt, and any plugin billed on top. Present when there is more than one component to it. |
| cache | Present on a cache hit: what was served from the cache and what it saved. |
| sources | Present when the web plugin ran: what it actually retrieved. Without this the answer is grounded in something you cannot see. |
| plugins | Which plugins ran and what each one cost, separately from the tokens. |
| unit / units / unit_price_micros | On the per-unit endpoints only — images, video, rerank. What one billable unit is, how many were billed, and the rate. Absent on token-priced responses. |
| request_id | Also the id of the completion, and the id GET /generation takes. |
Streaming
Send stream: true and you get StreamingThe answer arrives word by word as it is written, instead of all at once at the end. It costs the same; it just feels far faster. in OpenAI’s chunk format. Two things are ours:
- A terminal chunk before
[DONE]. It carriesusageand themultigridobject, because the cost is not known until the last token and so cannot be a response header. Itschoicesarray is empty when an earlier chunk already carried afinish_reason— repeating one makes SDK accumulators emit the message twice. - Failures after the first byte arrive in-band. The 200 is already on the wire, so an upstream error is delivered as
event: errorwith the same envelope a non-streamed error would have had. Tokens produced before the break are still delivered, still logged and still billed.
data: {"id":"req_…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"A "},"finish_reason":null}]}
data: {"id":"req_…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"gateway"},"finish_reason":null}]}
data: {"id":"req_…","object":"chat.completion.chunk","choices":[],"usage":{"prompt_tokens":21,"completion_tokens":48,"total_tokens":69,"prompt_tokens_details":{"cached_tokens":0}},"multigrid":{"model":"openai/gpt-5-nano","provider":"openai","attempts":1,"cost_usd":0.0000202,"cost_micros":20,"byok":false,"request_id":"req_…"}}
data: [DONE]Answers you are not charged for
If the model returns no text and calls no tools, the completion is waived: cost zero, waived: "zero_completion", and saved_usd showing what it would have cost. It happens for real — a content filter, a dropped completion, a context overflow reported as a clean stop — and from the outside it is indistinguishable from a model with nothing to say. A turn that only calls tools is not empty: that is the normal shape of a tool-using request and it cost real tokens to produce.
Errors
| Status | Code | Retry | Meaning |
|---|---|---|---|
| 400 | invalid_request | No | Malformed body, or an upstream 4xx passed back through. The message names the field. |
| 401 | unauthenticated | No | Missing, unrecognised or revoked key. |
| 402 | insufficient_credit | No | The balance cannot cover this request’s worst case. Nothing was sent upstream. |
| 402 | key_limit_exceeded | No | This key’s own spend cap would be breached. |
| 402 | monthly_limit_exceeded | No | The account’s monthly ceiling would be breached. You set this; it is not a payment failure. |
| 402 | no_free_route | No | A :free request found no zero-cost route. |
| 404 | model_not_found | No | No model in the catalogue has that id. |
| 409 | request_in_flight | Yes | Another request with the same Idempotency-Key is still upstream. |
| 413 | payload_too_large | No | An attachment or the request as a whole is over the size cap. |
| 422 | guardrail_blocked | No | One of your own guardrails refused it. The message names the rule. |
| 429 | rate_limited | Yes | Your key’s per-minute limit, or an upstream one. Honour Retry-After. |
| 502 | upstream_error | Yes | Every provider we could try failed. The failures are recorded on the request row. |
| 503 | no_provider_available | No | The model exists but nothing on this deployment can serve it — no adapter, or no key. |
| 504 | upstream_timeout | Yes | No provider responded inside the deadline. |
The full list, including the codes only other endpoints return, is in the error reference.
Something here disagrees with what the API actually did? That is a bug in this page, and worth reporting.
Report it