Skip to content

@tanstack/openai-base: tools silently lose strict mode when their schema cannot be coerced #1213

Description

@jsve

TanStack AI version

0.45.1 (@tanstack/openai-base@0.9.14; behavior unchanged on current main)

Framework/Library version

Tanstack Start

Describe the bug and the steps to reproduce it

Package: @tanstack/openai-base

Summary

When the input schema of a tool cannot be rewritten into OpenAI's strict Structured Outputs subset, the tool converters send the tool with strict: false and the schema as-authored. That fallback is the right call (the alternative is the API rejecting the entire request). However, I believe exercising this fallback should be made clear to devs. currently the converters have no logger access and emit nothing. The developer keeps a working tool and loses strict without any signal that it happened.

What triggers the fallback today

isStrictModeCompatible sends a tool non-strict when the schema contains any of:

  • oneOf, allOf, not, $ref, $defs, definitions anywhere in the tree (note that schema converters routinely emit $ref/definitions for reused sub-schemas, so ordinary zod-derived schemas can hit this),
  • a typeless schema node, e.g. the {} that z.any() / z.unknown() produce, or a bare boolean schema,
  • an open object (additionalProperties not false, or no properties), e.g. z.record(),
  • an optional field inside an anyOf variant (untrackable null widening).

A single z.unknown() field anywhere de-stricts the whole tool. The source comments note MCP servers (e.g. Notion) routinely emit these, so the whole MCP tool category runs non-strict today. The list may also grow (e.g. tuple forms, see #1208 and #1210), which makes discoverability worse over time.

Why developers cannot find this out on their own

The only way to observe the downgrade is to capture the actual wire request (injected fetch, proxy, provider request logs) and notice "strict": false on the tool definition. Nothing in the library's logs, types, return values, or docs says it happened. The observable symptom is just a slightly higher rate of malformed tool arguments, which is easy to misattribute to the model or the prompt. A developer who chose their provider and schema specifically to get guaranteed-valid arguments has no way to learn that the guarantee is not in effect.

For comparison, both official OpenAI SDKs make this state visible in their own ways: openai-node's strict transform throws ("cannot be represented in strict Structured Outputs"), and openai-python rewrites the schema into the strict subset. TanStack's keep-the-tool-callable behavior is friendlier than either, but it is the only one of the three that says nothing.

Suggested fix

Log once per tool when the fallback triggers, naming the tool and the specific reason (which keyword or condition failed the gate), e.g.:

tool "lookup_weather" sent with strict: false: schema uses $ref, which cannot be represented in strict Structured Outputs

The converters are pure functions today; the adapters already carry a logger channel, so the plumbing exists one level up.

The level is a genuine judgment call and I would leave it to maintainers: warn because the developer probably believes they have a guarantee they do not have, and it is actionable (restructure the schema and strict mode comes back); info because the tool still works as documented; debug because it is wire-level detail. My own preference is warn, at most once per tool per process, but any level is a large improvement over silence.

Happy to PR this.

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-reproNeeds a minimal reproduction before it can be worked onwaiting-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