Structured output
response_format reaches OpenAI-format providers untouched, and is translated into a forced tool call on Anthropic ones.
How it works here
Multigrid does not implement schema enforcement itself. It is a passthrough: response_format is a field we do not model, so on any provider that speaks the OpenAI format (OpenAI, Groq, Together, DeepInfra) it reaches them exactly as you wrote it, and whatever guarantee that vendor offers is the guarantee you get.
{
"model": "openai/gpt-5-mini",
"messages": [{"role": "user", "content": "Extract the invoice total."}],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "invoice",
"strict": true,
"schema": {
"type": "object",
"properties": { "total_cents": { "type": "integer" } },
"required": ["total_cents"],
"additionalProperties": false
}
}
}
}On Anthropic routes
The Messages API has no response_format field at all. What it has is tool use: a tool declares a JSON Schema for its arguments, and tool_choice can require the model to call it. So a schema sent to a Claude route is declared as a single tool, that tool is forced, and its arguments come back to you as ordinary content with finish_reason: "stop". Nothing about the response says a tool was involved, because from where you are sitting one was not.
strict: true, the schema is a promise about the decoder. On a Claude route it is Anthropic’s tool-input handling, which is schema-directed and reliable and is not the same sentence. If you need the guarantee specifically, pin the route with provider: { ignore: ["anthropic"] } rather than assuming the two are interchangeable.The Messages API forces one tool per turn, so three requests are refused rather than half-served each with a 400 naming which two things collided:
- A schema and your own
tools. Both need the forced call. Drop one, or send this to an OpenAI-format provider whereresponse_formatis a field of its own. - A schema and reasoning. Anthropic’s extended thinking cannot be combined with a forced tool choice.
json_objectwith no schema. A forced tool needs a shape to force, and the only schema meaning “any object” is an empty one which does not ask for JSON, it asks for{}. Sendjson_schemawith the shape you actually want.
A refusal filters the route before it fails the request: if your fallback chain contains an OpenAI-format provider, the request goes there and succeeds, and you only see the 400 when every candidate is Anthropic. multigrid_structured_output is the tool name reserved for this, and a request declaring its own tool by that name is refused rather than quietly merged.
Checking you got JSON
There is a guardrail of kind json_output that inspects the response and flags anything that does not parse. It is an output-stage rule, so the tokens have already been spent by the time it fires. It is there to tell you a model or a route has started misbehaving, not to prevent the charge.
Something here disagrees with what the API actually did? That is a bug in this page, and worth reporting.
Report it