Skip to content

@tanstack/openai-base: coerceStrictSchema mangles tuple schemas (items as an array) into a numeric-keyed object, silently dropping element constraints #1208

Description

@jsve

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    has-prAn open PR references this issuewaiting-on: maintainerThe ball is in the maintainers’ court

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions