TanStack AI version
0.45.1 (@tanstack/openai-base@0.9.14; unchanged on current main)
Framework/Library version
Tanstack Start
Describe the bug and the steps to reproduce it
Package: @tanstack/openai-base
Summary
coerceStrictSchema in packages/openai-base/src/utils/schema-converter.ts assumes an array schema's items is a single schema object. In draft-07 a positional tuple declares items as an array of per-position schemas (this is what e.g. z.tuple(...) produces via zod-to-json-schema). Both array branches pass that array straight into coerceStrictSchema, whose first line is const result = { ...schema }. Spreading an array produces a numeric-keyed object:
{ "type": "array", "items": [ { "type": "number", "minimum": -180 }, { "type": "number", "minimum": -90 } ] }
becomes
{ "type": "array", "items": { "0": { "type": "number", "minimum": -180 }, "1": { "type": "number", "minimum": -90 } } }
That items is not a valid schema, and every per-position constraint is silently dropped. The model receives a tool whose tuple parameter carries no element types or bounds at all.
Notably the strict-compatibility gate already understands the tuple form: containsTypelessSchema spreads Array.isArray(schema.items) ? schema.items : [schema.items]. So a tuple passes the gate as strict-compatible and is then mangled by the coercion. The two walks disagree about what items can be.
How we hit it
A tool parameter was a bbox tuple with per-edge bounds (west, south, east, north). The model kept receiving the parameter with no element constraints and produced malformed arguments. We worked around it by remodeling the tuple as a named object { west, south, east, north }, so we carry no patch for this.
Suggested fix
Handle Array.isArray(items) in both array branches: coerce each position schema individually and keep the array shape (plus additionalItems when present). The 2020-12 equivalent prefixItems deserves the same treatment while you are in there; today it passes through untouched, which at least does not corrupt the schema, but its position schemas also never get the strict-mode normalization.
Happy to PR the fix with a regression test.
TanStack AI version
0.45.1 (
@tanstack/openai-base@0.9.14; unchanged on current main)Framework/Library version
Tanstack Start
Describe the bug and the steps to reproduce it
Package:
@tanstack/openai-baseSummary
coerceStrictSchemainpackages/openai-base/src/utils/schema-converter.tsassumes an array schema'sitemsis a single schema object. In draft-07 a positional tuple declaresitemsas an array of per-position schemas (this is what e.g.z.tuple(...)produces via zod-to-json-schema). Both array branches pass that array straight intocoerceStrictSchema, whose first line isconst result = { ...schema }. Spreading an array produces a numeric-keyed object:{ "type": "array", "items": [ { "type": "number", "minimum": -180 }, { "type": "number", "minimum": -90 } ] }becomes
{ "type": "array", "items": { "0": { "type": "number", "minimum": -180 }, "1": { "type": "number", "minimum": -90 } } }That
itemsis not a valid schema, and every per-position constraint is silently dropped. The model receives a tool whose tuple parameter carries no element types or bounds at all.Notably the strict-compatibility gate already understands the tuple form:
containsTypelessSchemaspreadsArray.isArray(schema.items) ? schema.items : [schema.items]. So a tuple passes the gate as strict-compatible and is then mangled by the coercion. The two walks disagree about whatitemscan be.How we hit it
A tool parameter was a bbox tuple with per-edge bounds (west, south, east, north). The model kept receiving the parameter with no element constraints and produced malformed arguments. We worked around it by remodeling the tuple as a named object
{ west, south, east, north }, so we carry no patch for this.Suggested fix
Handle
Array.isArray(items)in both array branches: coerce each position schema individually and keep the array shape (plusadditionalItemswhen present). The 2020-12 equivalentprefixItemsdeserves the same treatment while you are in there; today it passes through untouched, which at least does not corrupt the schema, but its position schemas also never get the strict-mode normalization.Happy to PR the fix with a regression test.