fix: normalize null tool input from Anthropic adapter#266
fix: normalize null tool input from Anthropic adapter#266KrunchMuffin wants to merge 3 commits intoTanStack:mainfrom
Conversation
When Claude occasionally produces a tool_use content block with no
input_json_delta events (or with "null" as the partial JSON),
JSON.parse("null") returns JavaScript null — which passes the truthy
check but isn't {}. This causes downstream Zod schema validation to
fail, and the agent loop stalls silently.
Changes:
- Anthropic adapter (content_block_stop): normalize parsed input to {}
when JSON.parse produces null or a non-object value
- Anthropic adapter (formatMessages): same normalization when
reconstructing tool calls for conversation history
- executeToolCalls + ToolCallManager.executeTools: normalize "null"
argument strings to "{}" before parsing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
📝 WalkthroughWalkthroughValidated and normalized JSON inputs for tool-handling: the anthropic text adapter now ensures parsed JSON is an object before assignment, and the tool-call handlers trim input, convert the literal "null" to "{}", and parse safely to avoid non-object or empty inputs being used. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
|
View your CI Pipeline Execution ↗ for commit f0c28a9 ☁️ Nx Cloud last updated this comment at |
#265
Summary
When Claude occasionally produces a
tool_usecontent block with noinput_json_deltaevents (or with"null"as the partial JSON), the Anthropic adapter'scontent_block_stophandler passesnullas the parsed input instead of defaulting to{}.This causes
executeToolCallsto fail Zod schema validation (sincenullisn't an object). The error result is correctly captured, but the agent loop stalls after the failed tool execution — the model either doesn't produce a follow-up text response, or the follow-up text isn't emitted through the stream.The end result for users: the assistant says "Let me search for that..." then silence. No error, no follow-up.
Root Cause
In
@tanstack/ai-anthropic—adapters/text.tsThe
processAnthropicStreammethod, inside thecontent_block_stophandler:When
existing.inputis the string"null"(from the model streamingpartial_json: "null"),JSON.parse("null")returns JavaScriptnull— which passes the truthy check but isn't{}. The resultingTOOL_CALL_ENDevent hasinput: null.In
@tanstack/ai—tools/tool-calls.tsDownstream in
executeToolCalls:toolCall.function.argumentsis"null"(set bycompleteToolCallviaJSON.stringify(null))argsStr = "null".trim() || "{}"→"null"(truthy, so no fallback)JSON.parse("null")→nullparseWithStandardSchema(tool.inputSchema, null)→ Zod throws (null is not an object)Changes
Anthropic adapter (
content_block_stop): Normalize parsed input to{}whenJSON.parseproducesnullor any non-object value:Anthropic adapter (
formatMessages): Same normalization when reconstructing tool calls for conversation history (preventsinput: nullbeing sent back to the Anthropic API on subsequent turns).executeToolCalls+ToolCallManager.executeTools: Normalize"null"argument strings to"{}"before JSON parsing — a defense-in-depth fix for the same issue at the tool execution layer.How to Reproduce
This is intermittent and depends on model behavior. We've observed it with
claude-sonnet-4-5when the model generates atool_useblock with no meaningful arguments (e.g., after a first tool call that returned zero results). The model seems to "start" a follow-up tool call but doesn't commit to arguments.Typical pattern:
tool_use(with valid arguments) + secondtool_use(with null/empty arguments)Example Session Data
{ "role": "assistant", "content": "Let me search for disc mowers near you in Saskatoon:", "toolCalls": [ { "id": "toolu_1770336584303_0", "type": "function", "function": { "name": "search_equipment", "arguments": "null" } } ] }Test Plan
JSON.parse("null")→ normalizes to{}existing.input→ defaults to{}Summary by CodeRabbit