ADR-128: Provider-native structured output
- Status
-
Accepted (callers consume a typed response, not the decoded array — see ADR-211)
- Date
-
2026-08-05
- Amended
-
2026-09-28 by ADR-211
Context
ADR-082 built structured completions on a prompt instruction plus local
validation and one repair round-trip, and named native, per-provider
enforcement as a follow-up behind a schema-compatibility normaliser.
ADR-126 delivered the missing precondition: a named, pre-flighted schema
subset. What remained was the transport — the recon found that only the
OpenAI adapter emitted any response_ at all; the other six
adapters silently dropped the option, so even plain JSON mode was
prompt-only on six of seven providers.
Decision
Chat gains with (declared outside the
constructor, like suppress — the
@phpstan-/Tool constraint). Both
complete methods attach the already-pre-flighted schema;
it reaches the adapter as response_ in the flat options array.
Each adapter emits its provider's dialect:
- OpenAI, Groq, Mistral, OpenRouter share
Open. A schema that qualifies for OpenAI's strict mode (conservative profile: object root, every objectAi Response Format Trait additionalwith all properties required, allowlisted keywords only) is sent as `Properties: false response_` — strict mode 400s on schemas outside its rules, and a provider error for a valid ADR-126 schema is the failure mode this profile exists to prevent. Plainformat: {type: json_ schema, strict: true}``; any other schema degrades to `` {type: json_ object} response_now emits JSON mode on all four (previously: OpenAI only).format: 'json' - Gemini sets
generationand, when the root is expressible,Config. response Mime Type: application/ json responsein Gemini's dialect. The dialect conversion only ever widens (drops keywords it cannot express — a partially-convertedSchema enumwould narrow below the real schema and block valid values, so inexpressible keywords are dropped whole). A union of one type andnullbecomes that type withnullable: true. A union of several non-null types is left out: it would need ananynode withoutOf type, which Gemini's Schema reference marks as required. An object without expressible properties and an array without expressible items are left out too, because Gemini rejects anOBJECTwithout properties. - Ollama sends the schema verbatim as the top-level
formatfield (likethink), orformat: 'json'for plain JSON mode. - Claude has no response-format parameter; the native idiom is a single
forced tool whose
input_is the schema, and whoseschema tool_input is returned as the JSON string every other provider returns. Object-root schemas only (Claude's requirement); anything else stays prompt-only.use
The invariant that makes all of this safe: native enforcement narrows what the model can emit; the local strict validation (ADR-126) remains authoritative on the response. Native emission may be weaker than the schema (Gemini dialect, json_object fallback) but must never be stronger; the prompt instruction and the repair round-trip stay untouched.
Consequences
completenow gets real JSON mode on Groq, Mistral, OpenRouter, Ollama and Gemini instead of relying on the prompt alone — fewer "Failed to decode JSON response" failures, no API change.Json () - Streaming is deliberately out of scope: structured completions never
stream (ADR-062), so
streamdoes not emit schemas.Chat Completion () - The strict-mode profile and the Gemini dialect are conservative by design; widening them (more keywords, union types in the strict-mode profile) is additive and needs no API change.
- Ollama receives the full subset schema; its grammar conversion handles the ADR-126 keyword set, but an exotic schema a future Ollama version rejects would surface as a provider error — accepted, since Ollama is the local reference provider and the schema was pre-flighted.
- On Claude, a structured completion's
finishreflects the forced tool call (Reason tool_) rather thancalls stop; callers ofcompleteconsume the decoded array, not the reason.Structured* () - ADR-082's "no native parameter at all" statement about Claude is superseded by the tool-forcing idiom; its prompt+repair architecture stands.