fix(openai): reject oneOf/allOf and validate $defs/$ref before enabling strict mode (#4106) - #4155
Merged
Merged
Conversation
…ng strict mode (#4106) OpenAI Structured Outputs strict mode rejects oneOf/allOf/not/if-then-else/ dependent* composition keywords and $ref nodes with sibling keywords, but isStrictCompatible only checked additionalProperties and never traversed $defs/definitions. The pipeline also injected a stray 'type: object' onto $ref nodes, which downstream normalization then widened with additionalProperties/nullable — corrupting an otherwise-valid $ref. - Factor a shared childSchemas iterator used by both the compatibility check and the normalization walker, covering properties, patternProperties, $defs/definitions, anyOf/oneOf/allOf/prefixItems, items, additionalProperties, and the remaining single-schema keywords. - hasIncompatibleNode now rejects oneOf/allOf/not/if/then/else/ dependentRequired/dependentSchemas outright, and validates $ref nodes via a small local JSON-pointer resolver (string, local, resolvable, no sibling keywords). - makeAllRequired/ensureTypeFields treat $ref nodes as leaves; a newly-required $ref property is wrapped as {anyOf: [<$ref>, {type: null}]}, OpenAI's documented optional-ref pattern. - pkg/tools/schema.go's ensurePropertyTypes skips $ref nodes so no provider gets a polluted $ref. Adds T1-T15 coverage per the design doc, including an invariant test that normalization never turns a strict-compatible schema incompatible.
aheritier
marked this pull request as ready for review
September 3, 2026 18:58
dgageot
approved these changes
Sep 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🤖 Automated implementer agent — this comment was posted by the implementer bot from Docker Agentic Platform, not by a human developer
Closes #4106.
Problem
isStrictCompatible/hasIncompatibleNodeonly checkedadditionalProperties, never rejectedoneOf/allOf/other composition keywords OpenAI Structured Outputs doesn't support, and neither it nor the schema normalizer (walkSchema) traversed$defs/definitions. A tool whose schema usedoneOf(the issue's repro) or had an unnormalized$defsentry was sent to OpenAI withstrict: trueand rejected with HTTP 400, killing the whole request.A third, related defect found during design: the shared
pkg/tools/schema.goensurePropertyTypeshelper injectedtype: "object"onto$refnodes, which downstream normalization then widened withadditionalProperties/nullable-type — producing a$refnode with sibling keywords, which OpenAI strict mode rejects.Fix
pkg/model/provider/openai/schema.go: sharedchildSchemasiterator (iter.Seq) used by both the compatibility check and the normalizer, coveringproperties/patternProperties/$defs/definitions/anyOf/oneOf/allOf/prefixItems/items/additionalProperties/not/if/then/else/contains/propertyNames.hasIncompatibleNoderejectsoneOf, allOf, not, if, then, else, dependentRequired, dependentSchemasoutright, and validates$refnodes via a small local JSON-pointer resolver (string, local#/#/..., resolvable, no sibling keywords).$refnodes are treated as leaves bymakeAllRequired/ensureTypeFields(notype/additionalPropertiesinjection). A newly-required (optional)$refproperty is wrapped as{"anyOf": [<$ref>, {"type": "null"}]}, OpenAI's documented optional-reference pattern — withensureTypeFieldsalso skippinganyOf/oneOf/allOfnodes so this wrapper doesn't get a conflictingtypesibling (a real bug an earlier draft of this PR had, caught in review).pkg/tools/schema.go'sensurePropertyTypesskips$refnodes (the root cause of the pollution above).Testing
pkg/model/provider/openai/schema_test.go(T1–T14 from the design doc, plus an invariant test: normalizing a strict-compatible schema must never make it incompatible) andpkg/tools/schema_test.go.task build,task test,task lintall green (full suite, includinggo mod tidy --diffand the project's customgo run ./lint .cops).$refnode with adescriptionsibling understrict: true(would relax the current conservative "no sibling keywords" rule to "no siblings other thandescription", per theopenai-pythonSDK precedent this design follows). No OpenAI API key was available in this environment. Documented as a follow-up rather than blocking this fix.Out of scope
Structured Outputs size limits, root-level
anyOfrejection,anyOf-typed optional property nullable-widening (pre-existing, unrelated gap), and inlining$ref(rejected approach — would break recursive schemas).