Conversation
Structured output defaults `strict` to `true`, but the OpenAI request path sent the caller's JSON Schema untouched, so a valid framework input — a raw object schema without `additionalProperties: false`, or with an incomplete `required` list — reached the Responses API as a combination strict mode rejects. `toResponsesTextFormat` now rewrites a deep clone of the schema whenever the effective `strict` value is `true`, mirroring the contract Go's `strictSchemaToMap` applies rather than patching the root alone: an object root is required, `properties` / `items` / `anyOf` / `oneOf` / `$defs` / `definitions` are walked recursively, every object with declared properties gains `additionalProperties: false` and a deterministic `required` list naming all of them, and `default` moves into the node's description. A schema strict mode cannot express — an explicitly open object, a non-object root, a root `anyOf`, a boolean subschema, an object that declares nothing, an undeclared `required` entry, or a keyword outside the strict subset — now fails locally with the offending schema path instead of producing an opaque service 400, and is never silently narrowed. `strict: false` still goes out untouched, and the caller's schema object is never mutated. A `responseFormat` given without a name takes its name from a string root `title`, as Python does; an explicit name still wins and the keyword stays on the schema. Fixes #102 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
🟢 Approval recommended
Pull request overview
This PR fixes OpenAI structured-output failures when strict is effectively true by transforming JSON Schemas into the strict-compatible “closed” form before sending them to the Responses API, while preserving the caller’s original schema object.
Changes:
- Add a strict-schema transformer (
toStrictJsonSchema) that deep-clones, recursively closes object schemas (additionalProperties: false), completesrequired, movesdefaultintodescription, and rejects unsupported strict-mode shapes with path-aware errors. - Apply the transformer on the OpenAI request mapping path only when
strictresolves totrue; pass schemas through unchanged whenstrict: false. - Improve default response-format naming by deriving the format name from a string root
titlewhen the caller didn’t provide a name, and add/adjust tests + changelog.
File summaries
| File | Description |
|---|---|
| packages/openai/src/to-openai.ts | Applies strict-schema transformation on the OpenAI Responses text.format path when strict is true. |
| packages/openai/src/strict-schema.ts | Implements deep-clone + recursive strict-schema transform and path-aware local validation errors. |
| packages/openai/src/strict-schema.test.ts | Adds comprehensive transformer tests plus toResponsesTextFormat strict/non-strict behavioral tests. |
| packages/openai/src/chat-client.test.ts | Extends request-mapping tests to cover strict transformation, pass-through for strict:false, immutability, and title-based naming. |
| packages/openai/src/to-openai.wire-fallbacks.test.ts | Updates an existing wire test to use a strict-compatible object schema shape. |
| packages/core/src/client/structured-output.ts | Derives default response-format names from a usable root title instead of always "response". |
| packages/core/src/client/structured-output.test.ts | Adds tests for strict defaulting and title-based naming behavior in resolveResponseFormat. |
| CHANGELOG.md | Documents the breaking behavior change and its rationale in the OpenAI package. |
Review details
- Files reviewed: 8/8 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
# Conflicts: # CHANGELOG.md
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.
What this changes
Fixes #102. Structured output defaults
stricttotrue, but the OpenAI request path sent theJSON Schema unchanged, so a raw schema that is a valid framework input could reach the Responses
API in a form strict mode rejects — commonly an object node without
additionalProperties: falseor without a complete
required.A strict format's schema now goes through a recursive transform on a deep clone before it is sent.
Object nodes gain
additionalProperties: falseand a completedrequired,defaultmoves intothe description, and a schema strict mode cannot express fails locally with its path instead of as
a service 400. A
strict: falseformat is passed through structurally unchanged, and the caller'sschema object is never written to. An unnamed raw schema takes its format name from a string root
title; an explicit name wins.Parity
provider/openaiprovider/strict_schema.go, branch for branch — the rootvalidation, the 27 unsupported keywords, the container-shape errors, the
requiredordering(caller's order, then the remaining names sorted), the
default-into-description rule and thestrict JSON schema at <path>: …message shape. Python's_chat_client.pysupplies thetitle-as-format-name behaviour, which Go lacks. Python's raw-schema path patchesadditionalPropertiesat the root only, which is the shallow form this deliberately does notfollow.
A strict request now carries
additionalProperties: falseand a completedrequiredon objectnodes,
defaultbecomes description text, andtext.format.namemay come from a schematitle.Requests that worked before are unchanged in shape, since the ones this affects were rejected by
the service. What breaks:
strict: truewith a schema strict mode cannot express now fails locallywith a
ChatClientErrornaming the path, where it previously reached the service and returned a400.
The transform is package-internal — exported from neither the root entry nor
./internal— sothere is no public API addition.
Two deliberate divergences: Go's transform cache is not ported, because the clone here is a
plain-object round trip and a content-keyed cache would only reintroduce the shared-mutable-result
hazard Go spends three of its tests guarding. And unlike Python,
titleis not popped off theschema: Go's 27-keyword unsupported list does not include it, so Go preserves it, and popping would
change what the caller declared.
Checklist
pnpm checkpasses (lint, typecheck, build, test)