Skip to content

docs: document Anthropic structured-output schema complexity limits (#682)#686

Open
tombeckenham wants to merge 4 commits into
mainfrom
682-claude-structured-output-hard-fails-on-large-schemas-instead-of-falling-back-compiled-grammar-is-too-large
Open

docs: document Anthropic structured-output schema complexity limits (#682)#686
tombeckenham wants to merge 4 commits into
mainfrom
682-claude-structured-output-hard-fails-on-large-schemas-instead-of-falling-back-compiled-grammar-is-too-large

Conversation

@tombeckenham
Copy link
Copy Markdown
Contributor

@tombeckenham tombeckenham commented Jun 2, 2026

🎯 Changes

Closes #682.

chat({ outputSchema }) routes Claude 4.5+ structured output through Anthropic's native output_config; a large/complex schema is rejected with "The compiled grammar is too large" / "Schema is too complex for compilation" (also reproducible for anthropic/* models via OpenRouter).

This PR originally added a structuredOutput: 'auto' | 'native' | 'tool' option that transparently re-ran the request through a lenient forced-tool path on schema rejection. That fallback has been removed — the PR is now docs-only: a new "Anthropic schema complexity limits" section in the structured-outputs overview that documents both rejection messages, the schema constructs that commonly trigger them (.optional() fields, .catch()/.default() wrappers, unions, deep nesting, unconstrained optional strings), and links to Anthropic's docs for the current limits.

Why not a fallback?

  • It hides a real problem. The grammar rejection is a signal that the schema is too complex — silently degrading to a non-strict tool path means users never find out and never fix it. In practice the fix is straightforward (the schema that motivated this issue was repaired by removing .catch() wrappers and unneeded optional strings).
  • It silently trades away guarantees. The forced-tool path uses a non-strict input_schema, so the "schema-constrained output" the user asked for is quietly replaced with a weaker contract — different failure modes, no grammar enforcement — without the user opting in.
  • It carried a lot of machinery for one provider's limit. A new public API option, an isStructuredOutputSchemaError adapter predicate, error-message sniffing in two adapters, a forced-tool mode for OpenRouter, and a retry/replay engine path — all keyed off matching provider error strings that can change at any time.

Surfacing the provider error unchanged plus documenting how to simplify the schema is the better trade.

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm run test:pr.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

🤖 Generated with Claude Code

Add `structuredOutput: 'auto' | 'native' | 'tool'` to `chat()` (default
'auto'). When a provider rejects a large structured-output schema
(Anthropic "compiled grammar is too large", directly or for anthropic/*
via OpenRouter), the engine transparently retries via the lenient
forced-tool path instead of hard-failing. Detection is delegated to a new
optional adapter predicate `isStructuredOutputSchemaError`; @tanstack/ai-anthropic
and @tanstack/ai-openrouter implement it, and OpenRouter gains a forced-tool
structured-output mode.

Closes #682.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 2, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

Added a new "Anthropic schema complexity limits" subsection to the structured outputs overview documentation. The subsection describes how Anthropic rejects oversized schemas with compilation-size errors, lists common problematic schema constructs, and directs users to simplification strategies and official documentation.

Changes

Anthropic schema complexity limits documentation

Layer / File(s) Summary
Anthropic schema complexity limits subsection
docs/structured-outputs/overview.md
New subsection documenting Anthropic's HTTP 400 schema compilation limits, error patterns from complex schemas, and links to official documentation for current constraints and reduction guidance.

🎯 1 (Trivial) | ⏱️ ~3 minutes

Suggested reviewers

  • AlemTuzlak

Poem

🐰 A schema grew too large to compile,
Anthropic said "simplify, be concise, smile!"
Now docs show the way—unions trimmed, optionals spared—
That oversized grammar? No more unprepared! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning Issue #682 requires implementing a structuredOutput strategy with three modes (auto/native/tool), fallback detection via isStructuredOutputSchemaError predicate, and both Anthropic and OpenRouter adapter updates. The PR only adds documentation without implementing the required feature. Implement the structuredOutput strategy in @tanstack/ai core, @tanstack/ai-anthropic, and @tanstack/ai-openrouter as specified in #682, or clarify why the feature is deferred.
Docstring Coverage ⚠️ Warning Docstring coverage is 38.46% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive The documentation addition describing Anthropic schema complexity limits is in scope for issue #682's documentation requirements, but represents only a partial solution that does not address the core feature request for fallback logic. Clarify whether this PR is an interim documentation-only change pending feature implementation, or if the feature implementation is expected in a separate PR.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately describes the primary change: adding documentation for Anthropic structured-output schema complexity limits, which is precisely what the changeset does.
Description check ✅ Passed The PR description is comprehensive and complete, covering the motivation, the decision to go docs-only, detailed reasoning for rejecting a fallback approach, and properly filled checklist items matching the template structure.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 682-claude-structured-output-hard-fails-on-large-schemas-instead-of-falling-back-compiled-grammar-is-too-large

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 2, 2026

🚀 Changeset Version Preview

No changeset entries found. Merging this PR will not cause a version bump for any packages.

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented Jun 2, 2026

View your CI Pipeline Execution ↗ for commit dcbc2ac

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 10s View ↗
nx run-many --targets=build --exclude=examples/... ✅ Succeeded 2s View ↗

☁️ Nx Cloud last updated this comment at 2026-06-04 00:09:24 UTC

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Jun 2, 2026

Open in StackBlitz

@tanstack/ai

npm i https://pkg.pr.new/@tanstack/ai@686

@tanstack/ai-anthropic

npm i https://pkg.pr.new/@tanstack/ai-anthropic@686

@tanstack/ai-client

npm i https://pkg.pr.new/@tanstack/ai-client@686

@tanstack/ai-code-mode

npm i https://pkg.pr.new/@tanstack/ai-code-mode@686

@tanstack/ai-code-mode-skills

npm i https://pkg.pr.new/@tanstack/ai-code-mode-skills@686

@tanstack/ai-devtools-core

npm i https://pkg.pr.new/@tanstack/ai-devtools-core@686

@tanstack/ai-elevenlabs

npm i https://pkg.pr.new/@tanstack/ai-elevenlabs@686

@tanstack/ai-event-client

npm i https://pkg.pr.new/@tanstack/ai-event-client@686

@tanstack/ai-fal

npm i https://pkg.pr.new/@tanstack/ai-fal@686

@tanstack/ai-gemini

npm i https://pkg.pr.new/@tanstack/ai-gemini@686

@tanstack/ai-grok

npm i https://pkg.pr.new/@tanstack/ai-grok@686

@tanstack/ai-groq

npm i https://pkg.pr.new/@tanstack/ai-groq@686

@tanstack/ai-isolate-cloudflare

npm i https://pkg.pr.new/@tanstack/ai-isolate-cloudflare@686

@tanstack/ai-isolate-node

npm i https://pkg.pr.new/@tanstack/ai-isolate-node@686

@tanstack/ai-isolate-quickjs

npm i https://pkg.pr.new/@tanstack/ai-isolate-quickjs@686

@tanstack/ai-ollama

npm i https://pkg.pr.new/@tanstack/ai-ollama@686

@tanstack/ai-openai

npm i https://pkg.pr.new/@tanstack/ai-openai@686

@tanstack/ai-openrouter

npm i https://pkg.pr.new/@tanstack/ai-openrouter@686

@tanstack/ai-preact

npm i https://pkg.pr.new/@tanstack/ai-preact@686

@tanstack/ai-react

npm i https://pkg.pr.new/@tanstack/ai-react@686

@tanstack/ai-react-ui

npm i https://pkg.pr.new/@tanstack/ai-react-ui@686

@tanstack/ai-solid

npm i https://pkg.pr.new/@tanstack/ai-solid@686

@tanstack/ai-solid-ui

npm i https://pkg.pr.new/@tanstack/ai-solid-ui@686

@tanstack/ai-svelte

npm i https://pkg.pr.new/@tanstack/ai-svelte@686

@tanstack/ai-utils

npm i https://pkg.pr.new/@tanstack/ai-utils@686

@tanstack/ai-vue

npm i https://pkg.pr.new/@tanstack/ai-vue@686

@tanstack/ai-vue-ui

npm i https://pkg.pr.new/@tanstack/ai-vue-ui@686

@tanstack/openai-base

npm i https://pkg.pr.new/@tanstack/openai-base@686

@tanstack/preact-ai-devtools

npm i https://pkg.pr.new/@tanstack/preact-ai-devtools@686

@tanstack/react-ai-devtools

npm i https://pkg.pr.new/@tanstack/react-ai-devtools@686

@tanstack/solid-ai-devtools

npm i https://pkg.pr.new/@tanstack/solid-ai-devtools@686

commit: dcbc2ac

@tombeckenham tombeckenham marked this pull request as ready for review June 2, 2026 09:03
@tombeckenham tombeckenham requested a review from a team June 2, 2026 09:04
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/ai-openrouter/tests/openrouter-adapter.test.ts (1)

2563-2568: 💤 Low value

Consider adding undefined to negative test cases for consistency.

The Anthropic adapter tests check both null and undefined (line 1379-1380 in anthropic-adapter.test.ts), but this test only checks null. For completeness, consider adding:

expect(adapter.isStructuredOutputSchemaError(undefined)).toBe(false)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-openrouter/tests/openrouter-adapter.test.ts` around lines 2563 -
2568, Add a negative test asserting undefined is treated like null: update the
test that calls adapter.isStructuredOutputSchemaError(...) to also include
expect(adapter.isStructuredOutputSchemaError(undefined)).toBe(false) so the spec
covers both null and undefined cases for the isStructuredOutputSchemaError
function.
packages/ai-openrouter/src/adapters/text.ts (1)

787-793: 💤 Low value

Missing content field in TEXT_MESSAGE_CONTENT event.

The TEXT_MESSAGE_CONTENT event typically includes both delta (the incremental content) and content (the accumulated content). Compare with line 502 in structuredOutputStream which includes both fields. Since this is a single-shot emission, they would be identical, but omitting content may cause consumers expecting the accumulated field to see undefined.

Suggested fix
     yield {
       type: EventType.TEXT_MESSAGE_CONTENT,
       messageId,
       delta: result.rawText,
+      content: result.rawText,
       model,
       timestamp,
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-openrouter/src/adapters/text.ts` around lines 787 - 793, The
TEXT_MESSAGE_CONTENT event emission is missing the accumulated "content" field;
update the yield that returns type EventType.TEXT_MESSAGE_CONTENT (the block
using messageId, delta: result.rawText, model, timestamp) to include content
(set it to the same accumulated text, e.g., result.rawText for this single-shot
case) so consumers expecting both delta and content receive a defined value
(follow the same pattern used in structuredOutputStream where both delta and
content are provided).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@testing/e2e/global-setup.ts`:
- Around line 107-130: The comment claiming a malformed body is treated as a
native attempt is incorrect because on JSON.parse failure `body` stays {} so
`body.output_config` is undefined and the request is not rejected; update either
the comment or the behavior: either change the comment above the try/catch to
note that parse failures fall through (so the `if (body.output_config != null)`
branch only triggers when output_config exists), or explicitly reject on parse
failure by detecting the parse error and returning the same 400 JSON error (use
the existing GRAMMAR_TOO_LARGE_MESSAGE and the same response logic used in the
`if (body.output_config != null)` block); refer to readBody, the parsed variable
`body`/`body.output_config`, and GRAMMAR_TOO_LARGE_MESSAGE to implement the
chosen fix.

---

Nitpick comments:
In `@packages/ai-openrouter/src/adapters/text.ts`:
- Around line 787-793: The TEXT_MESSAGE_CONTENT event emission is missing the
accumulated "content" field; update the yield that returns type
EventType.TEXT_MESSAGE_CONTENT (the block using messageId, delta:
result.rawText, model, timestamp) to include content (set it to the same
accumulated text, e.g., result.rawText for this single-shot case) so consumers
expecting both delta and content receive a defined value (follow the same
pattern used in structuredOutputStream where both delta and content are
provided).

In `@packages/ai-openrouter/tests/openrouter-adapter.test.ts`:
- Around line 2563-2568: Add a negative test asserting undefined is treated like
null: update the test that calls adapter.isStructuredOutputSchemaError(...) to
also include
expect(adapter.isStructuredOutputSchemaError(undefined)).toBe(false) so the spec
covers both null and undefined cases for the isStructuredOutputSchemaError
function.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9504d444-b210-4ea1-80a8-a39d0f5244ac

📥 Commits

Reviewing files that changed from the base of the PR and between 8aabeec and a07718d.

📒 Files selected for processing (18)
  • .changeset/structured-output-schema-fallback.md
  • docs/structured-outputs/overview.md
  • docs/structured-outputs/streaming.md
  • packages/ai-anthropic/src/adapters/text.ts
  • packages/ai-anthropic/tests/anthropic-adapter.test.ts
  • packages/ai-openrouter/src/adapters/text.ts
  • packages/ai-openrouter/tests/openrouter-adapter.test.ts
  • packages/ai/skills/ai-core/adapter-configuration/SKILL.md
  • packages/ai/skills/ai-core/structured-outputs/SKILL.md
  • packages/ai/src/activities/chat/adapter.ts
  • packages/ai/src/activities/chat/index.ts
  • packages/ai/tests/chat-structured-output-fallback.test.ts
  • packages/ai/tests/test-utils.ts
  • testing/e2e/fixtures/structured-output-fallback/openrouter-tool.json
  • testing/e2e/global-setup.ts
  • testing/e2e/src/routeTree.gen.ts
  • testing/e2e/src/routes/api.structured-output-fallback.ts
  • testing/e2e/tests/structured-output-fallback.spec.ts

Comment thread testing/e2e/global-setup.ts Outdated
Copy link
Copy Markdown
Contributor Author

@tombeckenham tombeckenham left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

…tion

Widen the structured-output schema-rejection predicate beyond the observed
"compiled grammar is too large" message to also match Anthropic's canonical
400 message "Schema is too complex for compilation", so `structuredOutput:
'auto'` falls back on the documented error too. Applies to both
@tanstack/ai-anthropic and @tanstack/ai-openrouter.

Document the boundary in the structured-outputs overview and skill: detection
is reactive (we never pre-check schemas against Anthropic's caps), `'auto'`
recovers from grammar-size rejections only, and the numeric limits are linked
out rather than copied (they change and aren't all published).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/ai-openrouter/src/adapters/text.ts (1)

1602-1608: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Narrow the generic schema-path match.

Line 1607 currently treats any error mentioning output_config.format.schema as retryable. That will also catch non-recoverable request-validation errors for an invalid schema, so structuredOutput: 'auto' can silently rerun through the lenient tool path instead of surfacing the real caller bug. The core contract in packages/ai/src/activities/chat/adapter.ts only allows true for recoverable schema-compilation rejections.

Suggested fix
 function isOpenRouterStructuredOutputSchemaError(error: unknown): boolean {
   const text = collectErrorText(error).toLowerCase()
   return (
     text.includes('compiled grammar is too large') ||
-    text.includes('schema is too complex for compilation') ||
-    text.includes('output_config.format.schema')
+    text.includes('schema is too complex for compilation')
   )
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-openrouter/src/adapters/text.ts` around lines 1602 - 1608, The
function isOpenRouterStructuredOutputSchemaError currently treats any error
mentioning "output_config.format.schema" as recoverable; narrow this by only
returning true when the error text contains "output_config.format.schema"
together with compilation-related terms (e.g., "compile", "compilation",
"compiled", "too large", "too complex", or similar) to ensure only
schema-compilation failures are considered retryable; update the logic in
isOpenRouterStructuredOutputSchemaError to check both the presence of
"output_config.format.schema" AND at least one compilation-related keyword
(instead of matching "output_config.format.schema" alone).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/ai-openrouter/src/adapters/text.ts`:
- Around line 1602-1608: The function isOpenRouterStructuredOutputSchemaError
currently treats any error mentioning "output_config.format.schema" as
recoverable; narrow this by only returning true when the error text contains
"output_config.format.schema" together with compilation-related terms (e.g.,
"compile", "compilation", "compiled", "too large", "too complex", or similar) to
ensure only schema-compilation failures are considered retryable; update the
logic in isOpenRouterStructuredOutputSchemaError to check both the presence of
"output_config.format.schema" AND at least one compilation-related keyword
(instead of matching "output_config.format.schema" alone).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3253bff2-76b6-4daa-8f9c-f2494ac93f36

📥 Commits

Reviewing files that changed from the base of the PR and between a07718d and bc43c30.

📒 Files selected for processing (7)
  • .changeset/structured-output-schema-fallback.md
  • docs/structured-outputs/overview.md
  • packages/ai-anthropic/src/adapters/text.ts
  • packages/ai-anthropic/tests/anthropic-adapter.test.ts
  • packages/ai-openrouter/src/adapters/text.ts
  • packages/ai-openrouter/tests/openrouter-adapter.test.ts
  • packages/ai/skills/ai-core/structured-outputs/SKILL.md
✅ Files skipped from review due to trivial changes (4)
  • docs/structured-outputs/overview.md
  • packages/ai/skills/ai-core/structured-outputs/SKILL.md
  • .changeset/structured-output-schema-fallback.md
  • packages/ai-anthropic/tests/anthropic-adapter.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/ai-anthropic/src/adapters/text.ts
  • packages/ai-openrouter/tests/openrouter-adapter.test.ts

@tombeckenham tombeckenham requested a review from AlemTuzlak June 3, 2026 05:08
tombeckenham and others added 2 commits June 4, 2026 10:06
Reverts the structuredOutput 'auto'/'native'/'tool' strategy option, the
forced-tool fallback retry, the isStructuredOutputSchemaError adapter
predicate (Anthropic + OpenRouter grammar-message detection), and
OpenRouter's forced-tool structured-output mode.

Transparently falling back to the lenient tool path hides schema problems
users should fix at the source — Anthropic's grammar limits are a signal
the schema is too complex, and simplifying it (fewer optionals, no
catch/default wrappers) is the right fix. A docs note replaces the
machinery (#682).

This reverts commits a07718d and bc43c30.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…682)

Large/complex schemas are rejected by Anthropic's grammar compiler
("Schema is too complex for compilation" / "compiled grammar is too
large"), directly and for anthropic/* models via OpenRouter. Document
the errors, the common schema constructs that trigger them (optionals,
catch/default wrappers, unions, unconstrained strings), and link to
Anthropic's docs for current limits rather than copying numbers that
change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tombeckenham tombeckenham changed the title feat(ai): structuredOutput strategy + schema-rejection fallback (#682) docs: document Anthropic structured-output schema complexity limits (#682) Jun 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Claude structured output hard-fails on large schemas instead of falling back ("compiled grammar is too large")

1 participant