@tanstack/openai-base: strict structured output skips $defs/definitions, so recursive zod schemas are rejected

未关闭
#2,076 0 条评论 0 个 reaction 已指派 1 人 在 GitHub 查看

维护者通常 1 天内回复

@jherr 已经在做这个了。

开始于 2026年11月6日。

  • #2079 来自 @pcbeingused333 —— 未关闭

评估

这个 Issue 还没有评估数据。

描述

has-pr waiting-on: maintainer
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

环境准备

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

TanStack/ai 的其他 Issue

查看 TanStack/ai 的全部 Issue

相似的 Issue

更多 TypeScript Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。