Skip to content

Transform strict structured-output schemas on the OpenAI request path - #122

Merged
shibayan merged 2 commits into
masterfrom
issue-102
Aug 31, 2026
Merged

Transform strict structured-output schemas on the OpenAI request path#122
shibayan merged 2 commits into
masterfrom
issue-102

Conversation

@shibayan

@shibayan shibayan commented Aug 31, 2026

Copy link
Copy Markdown
Member

What this changes

Fixes #102. Structured output defaults strict to true, but the OpenAI request path sent the
JSON 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: false
or 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: false and a completed required, default moves into
the description, and a schema strict mode cannot express fails locally with its path instead of as
a service 400. A strict: false format is passed through structurally unchanged, and the caller's
schema object is never written to. An unnamed raw schema takes its format name from a string root
title; an explicit name wins.

Parity

  • Reference checked: Go's provider/openaiprovider/strict_schema.go, branch for branch — the root
    validation, the 27 unsupported keywords, the container-shape errors, the required ordering
    (caller's order, then the remaining names sorted), the default-into-description rule and the
    strict JSON schema at <path>: … message shape. Python's _chat_client.py supplies the
    title-as-format-name behaviour, which Go lacks. Python's raw-schema path patches
    additionalProperties at the root only, which is the shallow form this deliberately does not
    follow.
  • Wire format affected: yes
  • Public API affected: no
  • Breaking change: yes

A strict request now carries additionalProperties: false and a completed required on object
nodes, default becomes description text, and text.format.name may come from a schema title.
Requests that worked before are unchanged in shape, since the ones this affects were rejected by
the service. What breaks: strict: true with a schema strict mode cannot express now fails locally
with a ChatClientError naming the path, where it previously reached the service and returned a
400.

The transform is package-internal — exported from neither the root entry nor ./internal — so
there 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, title is not popped off the
schema: Go's 27-keyword unsupported list does not include it, so Go preserves it, and popping would
change what the caller declared.

Checklist

  • pnpm check passes (lint, typecheck, build, test)
  • Behaviour changes are covered by a test that fails without the change

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>
Copilot AI lite review requested due to automatic review settings August 31, 2026 03:34
@shibayan shibayan added bug Usage: [PRs], Target: bug fixes and regressions; issues use the Bug issue type breaking change Usage: [PRs], Target: changes that are not backward compatible labels Aug 31, 2026
@github-actions github-actions Bot added documentation Usage: [Issues, PRs], Target: documentation changes openai Usage: [Issues, PRs], Target: packages/openai core Usage: [Issues, PRs], Target: packages/core and removed breaking change Usage: [PRs], Target: changes that are not backward compatible labels Aug 31, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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), completes required, moves default into description, and rejects unsupported strict-mode shapes with path-aware errors.
  • Apply the transformer on the OpenAI request mapping path only when strict resolves to true; pass schemas through unchanged when strict: false.
  • Improve default response-format naming by deriving the format name from a string root title when 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.

@github-actions github-actions Bot added the breaking change Usage: [PRs], Target: changes that are not backward compatible label Aug 31, 2026
Copilot AI review requested due to automatic review settings August 31, 2026 03:57
@shibayan
shibayan merged commit f1d8849 into master Aug 31, 2026
9 checks passed
@shibayan
shibayan deleted the issue-102 branch August 31, 2026 03:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Review details
  • Files reviewed: 8/8 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking change Usage: [PRs], Target: changes that are not backward compatible bug Usage: [PRs], Target: bug fixes and regressions; issues use the Bug issue type core Usage: [Issues, PRs], Target: packages/core documentation Usage: [Issues, PRs], Target: documentation changes openai Usage: [Issues, PRs], Target: packages/openai

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OpenAI structured output sends strict: true without a strict-compatible schema transformation

2 participants