@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 小时
- 30 天内合并 PR
- 253
环境准备
- 没有 Dockerfile 或 Docker Compose 文件
- 有 Pull Request 模板
- 阅读贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 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 天内回复