Show notes
A contract is more than "return JSON"#
A real structured output contract defines shape, types, required fields, nullability, enums, ordering where it matters, semantic constraints, error behavior, and version compatibility. The gateway's job is to make that contract mean the same thing across providers with very different levels of support.
Start with a registry: each schema gets a stable name, version, owner, purpose, compatibility policy, and test suite, with canonical schema bytes and a digest. A mutable prompt template or an undocumented example is not a durable contract. Keep provider-specific wrappers, tool-call envelopes, streaming events, and usage metadata in an adapter layer, and expose stable domain fields to clients.
Compatibility rules and version pinning#
Adding an optional response field is often backward compatible. Removing a required field, changing a type, narrowing an enum, or changing units may not be. Input and output compatibility run in opposite directions, so test old clients against new responses and, where migration requires it, new clients against old responses.
Pin what each request used: contract name, version, digest, model route, provider adapter, and policy revision. Prompts and adapters belong in that identity too. A prompt change that alters what a field means is a contract change even if the JSON shape is identical, and adapters can change coercion, defaults, or error mapping. Consumer-driven tests close the loop: clients publish the fields and semantics they depend on, and the gateway verifies adapters against them.
Route by capability and enforce in layers#
Some models support strict JSON Schema, some offer JSON mode, some only follow instructions probabilistically, and some providers implement only a subset of schema keywords. Build a capability matrix covering required and additional properties, unions, recursion, formats, numeric bounds, patterns, arrays, streaming fragments, and tool schemas, and validate it against real behavior rather than documentation alone.
Enforcement then works as a ladder:
- Provider-native strict constraints are strongest.
- Gateway-side parsing and validation come next.
- Repair can fix bounded syntax issues if it is explicit, limited, and observable.
- Prompt-only instructions must never be presented as a strict guarantee.
Record every repair with the original classification, algorithm version, input and output digests, and reason. Never invent a missing field or coerce a dangerous type just to pass validation. On retries and fallbacks, re-check support on each route; if a fallback cannot honor strictness, fail or downgrade explicitly rather than returning a best-effort object under a strict header.
Validation beyond the schema#
Schema validation cannot prove that a currency is correct, an identifier belongs to the tenant, a date is plausible, or a tool action is authorized. Domain checks still have to run afterwards.
Several details deserve explicit rules in the contract. Unexpected properties should be rejected or quarantined in strict contracts, since they can hide prompt injection or provider drift. A missing field, explicit null, empty string, empty array, and default value can mean different things, so don't let a serializer erase the distinction. Specify units, precision, ranges, rounding, Unicode normalization, maximum lengths, and case sensitivity; syntactically valid JSON can still carry an unsafe URL, an overlong value, or a number that loses precision in JavaScript.
Streaming, tool calls, and structured errors#
Decide whether streaming clients receive complete JSON, JSON patches, typed events, or text that only becomes structured at the end. Every fragment needs sequence and completion semantics, and a disconnected stream must not be assumed to have produced a valid object.
A tool's argument schema is part of the model interaction contract, while its result may have a separate client contract. Version both, validate arguments before execution, and keep tool errors distinct from normal output; a schema-valid tool call can still violate authorization.
Errors should be structured too, with stable codes for unsupported schema, incompatible version, capability mismatch, malformed output, semantic violation, repair limit exceeded, incomplete stream, and contract timeout, plus field paths and safe remediation.
Testing, caching, observability, and deprecation#
Generate valid, boundary, invalid, adversarial, deeply nested, huge, Unicode, numeric, null, extra-field, and truncated examples, and test migrations, provider subsets, stream disconnects, repairs, retries, cache hits, and multi-region rollout. Treat schema descriptions, examples, and enum labels as configuration, and bound schema size, depth, regex complexity, and recursion so clients cannot smuggle instructions into privileged prompts.
Cache keys need the schema digest, adapter, model, prompt, policy, and transformation versions; a version-one response must not satisfy a version-two request just because fields overlap. Track strict-provider coverage, parse failures, per-field violations, repair rate, fallback downgrades, and version adoption. When retiring a version, publish migration windows and a removal date, and make the sunset return a typed error rather than an unexplained 500.
