@tanstack/openai-base: strict structured output skips $defs/definitions, so recursive zod schemas are rejected
メンテナーはふだん 1 日以内に返信
評価
この issue はまだ評価されていません。
説明
TanStack AI version
@tanstack/[email protected] (same code on main; we hit it on 1.0.0 with @tanstack/[email protected])
Framework/Library version
Node 24, zod 4.6.7
Describe the bug and the steps to reproduce it
coerceStrictSchema (behind makeStructuredOutputCompatible / makeStructuredOutputCompatibleWithMap) applies the strict Structured Outputs rules to properties, items and anyOf. It never goes into $defs or definitions. Every object schema in a definition pool reaches the API unchanged: optional properties are left out of required and are not widened to null.
zod 4 puts every recursive schema into a definition pool and points to it with $ref. A recursive outputSchema is common in practice, for example a filter tree where and/or hold more filters. With such a schema, strict response_format is built from a half-converted schema. OpenAI and Azure OpenAI reject it, because strict mode requires required to list every key in properties.
This differs from #1213. That issue covers tools, which fall back to strict: false when they meet $ref/$defs. Structured output has no such fallback. The request is sent strict and fails.
Repro (no API key needed; it shows the schema that goes on the wire):
import { makeStructuredOutputCompatible } from "@tanstack/openai-base";
import { z } from "zod";
const node = z.object({
label: z.string(),
note: z.string().optional(),
get children() { return z.array(node); },
});
const json = z.toJSONSchema(z.object({ root: node }), { target: "draft-7" });
console.log(JSON.stringify(makeStructuredOutputCompatible(json, json.required), null, 2));
Output (trimmed):
{
"type": "object",
"properties": { "root": { "$ref": "#/definitions/__schema0" } },
"required": ["root"],
"additionalProperties": false,
"definitions": {
"__schema0": {
"type": "object",
"properties": {
"label": { "type": "string" },
"note": { "type": "string" },
"children": { "type": "array", "items": { "$ref": "#/definitions/__schema0" } }
},
"required": ["label", "children"],
"additionalProperties": false
}
}
}
The top level is converted. Inside __schema0, note is still optional and not nullable.
Expected: each schema in $defs/definitions goes through the same strict conversion. In this example that means "note": { "type": ["string", "null"] } and "required": ["label", "note", "children"].
Fix we run as a pnpm patch (on the built dist/esm/utils/schema-converter.js, at the end of coerceStrictSchema, just before the return):
for (const defsKey of ["$defs", "definitions"]) {
if (!isSchemaObject(result[defsKey])) continue;
result[defsKey] = Object.fromEntries(Object.entries(result[defsKey]).map(([name, def]) => {
if (!isSchemaObject(def)) return [name, def];
const nested = coerceStrictSchema(def, def.required || []);
hasUntrackableAnyOfWidening ||= nested.nullWideningMap !== undefined || nested.hasUntrackableAnyOfWidening;
return [name, nested.schema];
}));
}
Open point: the null-widening map is keyed by position in the tree, so it cannot follow a $ref. With the patch, a field the conversion widened to null inside a definition comes back as null, not as an omitted key. A schema that uses .optional() there then fails validation on null. We work around this with .nullish() inside recursive schemas. A full fix would carry a map per definition and resolve it at each $ref.
Your Minimal, Reproducible Example - (Sandbox Highly Recommended)
The snippet above runs as-is with @tanstack/openai-base and zod installed.
Terms & Code of Conduct
- I agree to follow this project's Code of Conduct
- I understand that if my bug cannot be reliable reproduced in a debuggable environment, it will probably not be fixed and this issue may even be closed.
- 主要言語
- TypeScript
- スター
- 3.3k
- フォーク
- 378
- 平均マージ
- 1日 3時間
- マージ済み PR(30日)
- 253
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
TanStack/ai のほかの issue
-
openai-base: a Responses max_output_tokens cutoff ends as RUN_ERROR without usage instead of finishReason 'length'対応中かも @tombeckenham が 9 日前に担当しました。 オープンhas-pr waiting-on: maintainer
難易度 2/5 1〜3時間 初心者へのやさしさ 74/100
TanStack/ai#1916 · コメント 1 件 · 担当者 1 名 ·
メンテナーはふだん 1 日以内に返信
-
feat(ai-anthropic): support native on-demand compaction対応中かも @tombeckenham が今日担当しました。 オープンwaiting-on: maintainer
メンテナーはふだん 1 日以内に返信
-
Approval resume validates null-widened history args, so approved tools fail with repairToolInput: false対応中かも @tombeckenham が今日担当しました。 オープンwaiting-on: maintainer
メンテナーはふだん 1 日以内に返信
-
ai-sandbox: encrypt snapshot blobs at rest with an encrypting blob store wrapper対応中かも @AlemTuzlak が 1 日前に担当しました。 オープンwaiting-on: maintainer
メンテナーはふだん 1 日以内に返信
-
Sandbox takeover: the saved transcript loses everything the first host delivered before the restart対応中かも @tombeckenham が 1 日前に担当しました。 オープンwaiting-on: maintainer
メンテナーはふだん 1 日以内に返信
似ている issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
メンテナーはふだん 1 日以内に返信
-
binding:python bug
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
メンテナーはふだん 1 日以内に返信
-
enhancement
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
coollabsio/orbit#72 ·
メンテナーはふだん 1 日以内に返信
-
Bug
難易度 2/5 1〜3時間 初心者へのやさしさ 67/100
メンテナーはふだん 1 日以内に返信
-
bug needs-triage potential-regression
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
aws/aws-sdk-js-v3#8408 ·
メンテナーはふだん 1 日以内に返信