Skip to content

fix(openai-base): preserve malformed tool arguments for error recovery - #1602

Merged
tombeckenham merged 4 commits into
TanStack:mainfrom
L-1ngg:fix/openai-malformed-tool-arguments
Oct 4, 2026
Merged

tombeckenham merged 4 commits into
TanStack:mainfrom
L-1ngg:fix/openai-malformed-tool-arguments

Conversation

@L-1ngg

@L-1ngg L-1ngg commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Chat Completions can run a tool after streamed arguments fail JSON parsing. The adapter replaces those arguments with {}. This PR keeps the original argument string. The core then returns a tool error, and the tool does not run.

🎯 Changes

processStreamChunks sets parsedInput to undefined when JSON parsing fails. This applies on the finish_reason path and on the end-of-body drain path. completeToolCall keeps the raw argument string when that input is undefined. The core parses the original string, returns a tool error, and does not call the tool. Valid JSON still uses normalizeToolInput.

The docs page states that this also applies to Chat Completions tools with no required input fields. The changeset is a patch for @tanstack/openai-base.

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested code changes locally with pnpm run test:pr, or these tests do not apply to this pull request.
  • I fully understand the code in this pull request, including any code generated with AI assistance.
  • Docs: I updated docs/ for this change, or this change is not user-facing.
  • Changeset: I added a changeset (pnpm changeset), or this PR does not change a published package.

🚀 Release Impact

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

Root cause

Issue. A tool with z.object({ path: z.string().optional() }) runs with {} when the model streams {"path":. This happens when finish_reason is tool_calls. It also happens when the body ends with no finish reason.

Cause. OpenAIBaseChatCompletionsTextAdapter.processStreamChunks assigns {} in the JSON parse catch. completeToolCall writes JSON.stringify of that value over the raw argument string. executeToolCalls then parses "{}". The optional schema accepts that object, so the tool runs.

Fix. Both catch paths assign undefined. completeToolCall returns early when input is undefined, so the raw string stays in place. The core returns Failed to parse tool arguments as JSON and does not call the tool. The agent loop can continue.

Possible alternatives

  • Abort the run. A RUN_ERROR or a thrown error stops the tool. It also stops the agent loop, so the model cannot repair the call.
  • Re-parse the raw arguments in the core. If that parse fails, the core ignores the completed input. This discards valid adapter normalization. Strict-null cleanup is one example. The two assignments remove the bad {} at the source.

Testing

Commands run. I ran one agent-written chat() command on clean main f35fec4d4 and on this branch 8c3fc5e52. Main exited 1. This branch exited 0. The command calls the Chat Completions adapter with arguments {"path":. pnpm test:pr did not run after merge commit 8c3fc5e52. This branch includes origin/main at f35fec4d4. That commit adds the afterModel continuation fixture.

Clean main:

[
  {
    "withFinishReason": true,
    "executed": [{}],
    "requestCount": 2,
    "toolContent": "tool ran",
    "runErrors": []
  },
  {
    "withFinishReason": false,
    "executed": [{}],
    "requestCount": 2,
    "toolContent": "tool ran",
    "runErrors": []
  }
]
MAIN_EXIT=1

This branch:

[
  {
    "withFinishReason": true,
    "executed": [],
    "requestCount": 2,
    "toolContent": "{\"error\":\"Failed to parse tool arguments as JSON: {\\\"path\\\":\"}",
    "runErrors": []
  },
  {
    "withFinishReason": false,
    "executed": [],
    "requestCount": 2,
    "toolContent": "{\"error\":\"Failed to parse tool arguments as JSON: {\\\"path\\\":\"}",
    "runErrors": []
  }
]
PR_EXIT=0

Manual test.

  1. On main, call chat() with tool schema z.object({ path: z.string().optional() }) and arguments {"path":. The tool runs with {}.
  2. On this branch, repeat that call with finish_reason: "tool_calls".
  3. Repeat that call with no finish reason. The tool does not run. The tool message is Failed to parse tool arguments as JSON: {"path":.
  4. Run pnpm --filter @tanstack/ai-e2e test:e2e -- tests/openai-malformed-tool-arguments.spec.ts.

How this PR makes testing easy. testing/e2e/tests/openai-malformed-tool-arguments.spec.ts covers both endings through the real adapter and the core tool loop. packages/openai-base/tests/chat-completions-text.test.ts asserts that TOOL_CALL_END.input stays undefined. These tests need no provider key.

Linked issues

Closes #1601

Risk / rollback

Callers that treated a broken JSON tool call as an empty-object call now get a tool error. Revert this PR to restore the previous {} fallback. The Responses adapter still assigns {} on a parse failure. That path is outside this PR.

Summary by CodeRabbit

  • Bug Fixes

    • Malformed Chat Completions tool arguments are preserved and reported as a tool error instead of being replaced with an empty object. The affected tool is not executed with invalid input, and the conversation can continue with a recovery response.
    • This behavior applies whether the response ends with a tool-call finish reason or the stream ends without one.
  • Documentation

    • Clarified that malformed arguments and schema validation errors can also occur with Chat Completions tools that have no required input fields.

@coderabbitai

coderabbitai Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: TanStack/ai/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 78fb879e-c1aa-403c-b6e1-e1cb041fb705
📥 Commits

Reviewing files that changed from the base of the PR and between 53a1a2e and 8c3fc5e.

📒 Files selected for processing (1)
  • testing/e2e/src/routeTree.gen.ts

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 6 remain after this review.


📝 Walkthrough

Walkthrough

The Chat Completions adapter leaves parsed input undefined when tool arguments are malformed. Unit and end-to-end tests cover streams with and without a tool-call finish reason.

Changes

Malformed tool arguments

Layer / File(s) Summary
Preserve malformed arguments
packages/openai-base/src/adapters/chat-completions-text.ts, packages/openai-base/tests/chat-completions-text.test.ts, docs/tools/server-tools.md, .changeset/fix-malformed-chat-completions-tool-arguments.md
Both adapter paths leave parsed input undefined when argument parsing fails. Tests cover both stream endings and logged parse-error details. The documentation and changeset describe the handling.
Exercise malformed arguments end to end
testing/e2e/src/routes/api.openai-malformed-tool-arguments.ts, testing/e2e/src/routeTree.gen.ts, testing/e2e/tests/openai-malformed-tool-arguments.spec.ts
The route mocks malformed arguments followed by recovery text. The test checks both finish-reason cases, confirms no tool inputs were executed, and checks the tool error and recovered text.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix · Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to 8c3fc

Malformed arguments now produce a tool error instead of executing the tool, allowing recovery in both stream-ending paths. No actionable merge risk is identified; test-run results were not supplied.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 8c3fc

The change prevents malformed, nonempty tool arguments from being replaced with executable empty input. The new test endpoint uses simulated responses and a local tool without external side effects. No introduced security concern was identified, but deployed exposure and runtime test results were not established.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The published adapter change affects applications consuming streamed Chat Completions tool calls; tool authority remains determined by each application's configured tools. The new endpoint itself supplies no real provider credential or privileged tool: its transport and recording tool are request-local doubles.

Trust Boundaries and Controls

  • observed — For nonempty malformed provider argument text, the adapter now preserves the untrusted representation across the completion boundary. Core strictly parses it and returns an output-error tied to the call identity before schema validation or execution, preventing optional-field schemas from accepting a substituted empty object on this path.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: preserving malformed Chat Completions tool arguments so the error can be handled without running the tool.
Description check ✅ Passed The description includes the required Changes, Checklist, and Release Impact sections. It explains the root cause, fix, alternatives, testing, linked issue, and rollback risk. It also notes that the f…
Linked Issues check ✅ Passed The PR meets [#1601]. The Chat Completions adapter sets parsed input to undefined after JSON parse failures on both the tool_calls finish path and the EOF-drain path. This preserves the raw argume…
Out of Scope Changes check ✅ Passed The whole-PR summary lists adapter changes, related unit and E2E tests, the route fixture, documentation, and a changeset. These changes support [#1601]. It does not identify unrelated Code Mode chang…
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 5…
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


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.

@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Thanks for the PR, @L-1ngg! 🙌 @AlemTuzlak will take a look.

Automated pre-review checks

  • ✅ CI passing
  • ✅ No merge conflicts
  • ✅ Changeset present
  • ✅ E2E test changes included

Automated triage — a human review follows.

@github-actions github-actions Bot added the waiting-on: maintainer The ball is in the maintainers’ court label Oct 2, 2026
@tombeckenham
tombeckenham self-requested a review October 3, 2026 06:36
@changeset-bot

changeset-bot Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8c3fc5e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@tanstack/openai-base Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added merge-conflicts Conflicts with the base branch — needs a rebase waiting-on: author Waiting for the author to respond or update and removed waiting-on: maintainer The ball is in the maintainers’ court labels Oct 3, 2026
@nx-cloud

nx-cloud Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 8c3fc5e

Command Status Duration Result
nx run-many --targets=build --exclude=examples/... ✅ Succeeded 24s View ↗

☁️ Nx Cloud last updated this comment at 2026-10-04 12:10:09 UTC

@pkg-pr-new

pkg-pr-new Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@tanstack/ai

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

@tanstack/ai-acp

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-acp@1602

@tanstack/ai-angular

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-angular@1602

@tanstack/ai-anthropic

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

@tanstack/ai-bedrock

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-bedrock@1602

@tanstack/ai-byteplus

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-byteplus@1602

@tanstack/ai-claude-code

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-claude-code@1602

@tanstack/ai-client

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

@tanstack/ai-cloudflare

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-cloudflare@1602

@tanstack/ai-code-mode

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

@tanstack/ai-code-mode-snippets

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-code-mode-snippets@1602

@tanstack/ai-codex

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-codex@1602

@tanstack/ai-cohere

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-cohere@1602

@tanstack/ai-compaction

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-compaction@1602

@tanstack/ai-devtools-core

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

@tanstack/ai-durable-stream

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-durable-stream@1602

@tanstack/ai-elevenlabs

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

@tanstack/ai-event-client

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

@tanstack/ai-fal

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

@tanstack/ai-gemini

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

@tanstack/ai-grok

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

@tanstack/ai-grok-build

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-grok-build@1602

@tanstack/ai-groq

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

@tanstack/ai-isolate-cloudflare

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

@tanstack/ai-isolate-daytona

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-daytona@1602

@tanstack/ai-isolate-node

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

@tanstack/ai-isolate-quickjs

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

@tanstack/ai-isolate-quickjs-bun

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-quickjs-bun@1602

@tanstack/ai-llmgateway

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-llmgateway@1602

@tanstack/ai-lovable

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-lovable@1602

@tanstack/ai-mcp

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-mcp@1602

@tanstack/ai-memory

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-memory@1602

@tanstack/ai-mistral

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-mistral@1602

@tanstack/ai-octane

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-octane@1602

@tanstack/ai-ollama

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

@tanstack/ai-ollaya

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-ollaya@1602

@tanstack/ai-openai

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

@tanstack/ai-opencode

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-opencode@1602

@tanstack/ai-openrouter

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

@tanstack/ai-perplexity

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-perplexity@1602

@tanstack/ai-persistence

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-persistence@1602

@tanstack/ai-preact

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

@tanstack/ai-react

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

@tanstack/ai-react-ui

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

@tanstack/ai-reactor

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-reactor@1602

@tanstack/ai-remix

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-remix@1602

@tanstack/ai-sandbox

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox@1602

@tanstack/ai-sandbox-blaxel

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-blaxel@1602

@tanstack/ai-sandbox-boxd

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-boxd@1602

@tanstack/ai-sandbox-cloudflare

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-cloudflare@1602

@tanstack/ai-sandbox-daytona

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-daytona@1602

@tanstack/ai-sandbox-docker

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-docker@1602

@tanstack/ai-sandbox-e2b

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-e2b@1602

@tanstack/ai-sandbox-local-process

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-local-process@1602

@tanstack/ai-sandbox-sprites

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-sprites@1602

@tanstack/ai-sandbox-upstash-box

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-upstash-box@1602

@tanstack/ai-sandbox-vercel

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-vercel@1602

@tanstack/ai-skills

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-skills@1602

@tanstack/ai-solid

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

@tanstack/ai-solid-ui

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

@tanstack/ai-svelte

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

@tanstack/ai-typesafe

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-typesafe@1602

@tanstack/ai-utils

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

@tanstack/ai-vercel-gateway

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vercel-gateway@1602

@tanstack/ai-vertex

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vertex@1602

@tanstack/ai-vue

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

@tanstack/ai-vue-ui

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

@tanstack/ai-worldlabs

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-worldlabs@1602

@tanstack/openai-base

npm i https://pkg.pr.new/TanStack/ai/@tanstack/openai-base@1602

@tanstack/preact-ai-devtools

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

@tanstack/react-ai-devtools

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

@tanstack/solid-ai-devtools

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

@tanstack/svelte-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/svelte-ai-devtools@1602

commit: 8c3fc5e

…tool-arguments

# Conflicts:
#	testing/e2e/src/routeTree.gen.ts
@tombeckenham
tombeckenham enabled auto-merge (squash) October 4, 2026 09:59
@tombeckenham

Copy link
Copy Markdown
Contributor

Thank you for this!

@github-actions github-actions Bot removed the merge-conflicts Conflicts with the base branch — needs a rebase label Oct 4, 2026
@tombeckenham
tombeckenham merged commit 6d8e648 into TanStack:main Oct 4, 2026
10 of 11 checks passed
@github-actions github-actions Bot mentioned this pull request Oct 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

waiting-on: author Waiting for the author to respond or update

Projects

None yet

Development

Successfully merging this pull request may close these issues.

openai-base: malformed Chat Completions tool arguments execute as an empty object

3 participants