Skip to content

Neutral IR does not record structured-output enforcement #467

Description

@ting-hong-shieh

Summary

The neutral request IR records what schema a caller wants but not whether the provider must enforce it. Enforcement is a first-class part of the contract on both providers we translate between, and it is expressed differently on each side, so it is lost or invented at the boundary.

Split out of #462 at a maintainer's request to keep that PR in limited scope.

The gap

OutputParams::response_format holds an OpenAI-shaped response_format value. Enforcement lives inside that value as json_schema.strict, which makes it a property of one provider's wire encoding rather than something the IR states.

The two providers do not agree on how enforcement is expressed:

  • Anthropicoutput_config.format with type: json_schema is enforced. There is no flag; supplying a schema is the request for enforcement.
  • OpenAI Chat — a schema is advisory unless strict: true is set. Their guide is explicit that "only Structured Outputs ensure schema adherence"; without the flag you get valid JSON with no guarantee it matches.

So the two directions fail differently:

  • Anthropic → OpenAI Chat loses enforcement. The schema is forwarded, the guarantee is not, and the caller can receive prose where they asked for JSON. This is the case reported in [bug]: /v1/messages drops output_format / json_schema #452.
  • OpenAI Chat → Anthropic gains it. encode_anthropic_output_format keeps only the schema, so a request that arrived with strict absent is translated into an Anthropic request that is enforced anyway.

Neither is visible to the caller, and neither produces a diagnostic today.

Why it is not a one-line fix

Setting strict: true whenever a schema is present would trade a quiet degradation for a loud regression, because the accepted schema subsets differ:

Anthropic output_config.format OpenAI strict: true
every property listed in required not required — optional properties are allowed required
additionalProperties: false required required
allOf supported, with limits not allowed
string format, minItems supported restricted for fine-tuned models

The first row is the one that bites. An Anthropic schema with any optional property is legal today, and OpenAI rejects an out-of-subset schema at request time when strict is on — so a request that currently returns unenforced output would start returning 400 instead.

This codec already handles the mirror-image problem rather than passing schemas through and hoping: strip_anthropic_unsupported_constraints narrows an OpenAI schema to what Anthropic accepts when translating the other way.

Possible directions

Listed for discussion, not as a proposal:

  1. Record enforcement in the IR — an explicit field on OutputParams so each codec decides how to express it, rather than encoding one provider's spelling and reinterpreting it elsewhere. Largest change, and the one that actually removes the ambiguity.
  2. Derive strict from the schema — emit strict: true only when the schema provably satisfies the strict subset, and omit it otherwise. No IR change, enforcement where it is safe, and never turns a working request into a 400. Does not help the schemas that fall outside the subset.
  3. Diagnose the downgrade — leave the behavior alone and emit a lossiness diagnostic when enforcement is dropped. Note that under LossyConversionPolicy::Reject this would fail every affected request, so it likely needs its own severity rather than reusing push_lossy.

(1) and (2) are not exclusive; (2) is a reasonable step even if (1) lands later.

Related

Environment

main at f9b2df61.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions