Skip to content

Chat completions

The main endpoint: its parameters, its streaming protocol and its errors.

9 min read

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.

Why validation is thin on purpose
A gateway that rejected unfamiliar parameters would break clients that work fine against the provider directly, and it would do so on the day the provider adds a feature — the worst possible day to be the reason it does not work.

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_bias and parallel_tool_calls are 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. logprobs and usage.completion_tokens_details — including reasoning_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.

ParameterTypeRequiredDescription
modelsstring[]OptionalFallback chain. Tried in order after the primary model, and after every provider for it has been exhausted. See failover.
providerobjectOptionalorder, ignore, allow_fallbacks, sort, weights. Full reference in provider preferences.
metadataobjectOptionalUp 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.
pluginsobject[]OptionalCurrently 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.
cacheobjectOptional{ "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.
retryobjectOptional{ "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.
Request body
{
  "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.

FieldDescription
modelThe model that answered. Differs from yours when a fallback fired or multigrid/auto chose.
providerThe provider slug that served it.
requested_modelOnly present when it differs from model.
auto_reasonOnly present on multigrid/auto: why that model was chosen.
attemptsUpstream calls made. Two or more means a retry or a failover happened.
cost_microsWhat you were charged, as an integer number of millionths of a dollar. This is the authoritative figure.
cost_usdThe same number as a decimal.
byokTrue when your own provider key paid for it, in which case the cost is zero.
saved_usdOnly 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_breakdownWhere the charge came from — prompt, completion, cached prompt, and any plugin billed on top. Present when there is more than one component to it.
cachePresent on a cache hit: what was served from the cache and what it saved.
sourcesPresent when the web plugin ran: what it actually retrieved. Without this the answer is grounded in something you cannot see.
pluginsWhich plugins ran and what each one cost, separately from the tokens.
unit / units / unit_price_microsOn 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_idAlso 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 carries usage and the multigrid object, because the cost is not known until the last token and so cannot be a response header. Its choices array is empty when an earlier chunk already carried a finish_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: error with the same envelope a non-streamed error would have had. Tokens produced before the break are still delivered, still logged and still billed.
text/event-stream
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]
A stream cannot fail over mid-answer
Once the status line is committed we do not silently switch providers and continue. Splicing two models’ output into one answer would be undetectable from the outside, which is exactly why it is not done. Retry and failover apply up to the moment the first byte is sent.

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

StatusCodeRetryMeaning
400invalid_requestNoMalformed body, or an upstream 4xx passed back through. The message names the field.
401unauthenticatedNoMissing, unrecognised or revoked key.
402insufficient_creditNoThe balance cannot cover this request’s worst case. Nothing was sent upstream.
402key_limit_exceededNoThis key’s own spend cap would be breached.
402monthly_limit_exceededNoThe account’s monthly ceiling would be breached. You set this; it is not a payment failure.
402no_free_routeNoA :free request found no zero-cost route.
404model_not_foundNoNo model in the catalogue has that id.
409request_in_flightYesAnother request with the same Idempotency-Key is still upstream.
413payload_too_largeNoAn attachment or the request as a whole is over the size cap.
422guardrail_blockedNoOne of your own guardrails refused it. The message names the rule.
429rate_limitedYesYour key’s per-minute limit, or an upstream one. Honour Retry-After.
502upstream_errorYesEvery provider we could try failed. The failures are recorded on the request row.
503no_provider_availableNoThe model exists but nothing on this deployment can serve it — no adapter, or no key.
504upstream_timeoutYesNo 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