fix(ai, ai-openai): normalize null tool input to empty object#430
fix(ai, ai-openai): normalize null tool input to empty object#430AlemTuzlak wants to merge 7 commits intomainfrom
Conversation
When a model produces a tool_use block with no input (or literal null),
JSON.parse('null') returns null, which fails Zod schema validation and
silently kills the agent loop.
Normalize null/non-object parsed tool input to {} in four locations:
- executeToolCalls(): after JSON.parse of arguments string
- ToolCallManager.completeToolCall(): before JSON.stringify of event input
- ToolCallManager.executeTools(): replace fragile string comparison
- OpenAI adapter: in TOOL_CALL_END emission (matching existing Anthropic fix)
Fixes #265
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughNormalize parsed tool-call arguments so any non-object or Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 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 |
🚀 Changeset Version Preview4 package(s) bumped directly, 23 bumped as dependents. 🟩 Patch bumps
|
|
View your CI Pipeline Execution ↗ for commit a00a66a
☁️ Nx Cloud last updated this comment at |
@tanstack/ai
@tanstack/ai-anthropic
@tanstack/ai-client
@tanstack/ai-code-mode
@tanstack/ai-code-mode-skills
@tanstack/ai-devtools-core
@tanstack/ai-elevenlabs
@tanstack/ai-event-client
@tanstack/ai-fal
@tanstack/ai-gemini
@tanstack/ai-grok
@tanstack/ai-groq
@tanstack/ai-isolate-cloudflare
@tanstack/ai-isolate-node
@tanstack/ai-isolate-quickjs
@tanstack/ai-ollama
@tanstack/ai-openai
@tanstack/ai-openrouter
@tanstack/ai-preact
@tanstack/ai-react
@tanstack/ai-react-ui
@tanstack/ai-solid
@tanstack/ai-solid-ui
@tanstack/ai-svelte
@tanstack/ai-vue
@tanstack/ai-vue-ui
@tanstack/preact-ai-devtools
@tanstack/react-ai-devtools
@tanstack/solid-ai-devtools
commit: |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/typescript/ai/tests/tool-calls-null-input.test.ts`:
- Around line 108-136: The tests fail to compile because the ToolCallManager
constructor currently accepts only one parameter (tools: ReadonlyArray<Tool>)
but tests call new ToolCallManager(..., mockFinishedEvent); update the
ToolCallManager constructor signature to accept an optional second parameter
(e.g., finishedEvent or initialFinishedEvent) with the same type as
mockFinishedEvent, adjust the constructor implementation to handle when that
second argument is provided (store or process it the same way tests expect), and
ensure all usages and exports of ToolCallManager are updated to the new
signature so the tests compiling calls to ToolCallManager(...,
mockFinishedEvent) succeed.
🪄 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: be0d9dc8-a6e1-48a3-81fe-d6e79d0c38d0
📒 Files selected for processing (3)
packages/typescript/ai-openai/src/adapters/text.tspackages/typescript/ai/src/activities/chat/tools/tool-calls.tspackages/typescript/ai/tests/tool-calls-null-input.test.ts
| describe('ToolCallManager.completeToolCall', () => { | ||
| it('should normalize null input to empty object', () => { | ||
| const manager = new ToolCallManager([], mockFinishedEvent) | ||
|
|
||
| // Register a tool call | ||
| manager.addToolCallStartEvent({ | ||
| type: 'TOOL_CALL_START', | ||
| toolCallId: 'tc-1', | ||
| toolName: 'test_tool', | ||
| model: 'test', | ||
| timestamp: Date.now(), | ||
| index: 0, | ||
| }) | ||
|
|
||
| // Complete with null input (simulating Anthropic empty tool_use) | ||
| manager.completeToolCall({ | ||
| type: 'TOOL_CALL_END', | ||
| toolCallId: 'tc-1', | ||
| toolName: 'test_tool', | ||
| model: 'test', | ||
| timestamp: Date.now(), | ||
| input: null as unknown, | ||
| }) | ||
|
|
||
| const toolCalls = manager.getToolCalls() | ||
| expect(toolCalls).toHaveLength(1) | ||
| // Should be "{}" not "null" | ||
| expect(toolCalls[0]!.function.arguments).toBe('{}') | ||
| }) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify ToolCallManager constructor signature
ast-grep --pattern 'class ToolCallManager {
$$$
constructor($PARAMS) {
$$$
}
$$$
}'Repository: TanStack/ai
Length of output: 17852
🏁 Script executed:
cat -n packages/typescript/ai/tests/tool-calls-null-input.test.tsRepository: TanStack/ai
Length of output: 5661
Constructor signature mismatch — tests will not compile.
ToolCallManager constructor accepts only one parameter (tools: ReadonlyArray<Tool>), but the tests at lines 110 and 139 pass a second argument (mockFinishedEvent). This causes TypeScript compilation errors.
🐛 Proposed fix
describe('ToolCallManager.completeToolCall', () => {
it('should normalize null input to empty object', () => {
- const manager = new ToolCallManager([], mockFinishedEvent)
+ const manager = new ToolCallManager([])
// Register a tool call
manager.addToolCallStartEvent({ it('should preserve valid object input', () => {
- const manager = new ToolCallManager([], mockFinishedEvent)
+ const manager = new ToolCallManager([])
manager.addToolCallStartEvent({📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| describe('ToolCallManager.completeToolCall', () => { | |
| it('should normalize null input to empty object', () => { | |
| const manager = new ToolCallManager([], mockFinishedEvent) | |
| // Register a tool call | |
| manager.addToolCallStartEvent({ | |
| type: 'TOOL_CALL_START', | |
| toolCallId: 'tc-1', | |
| toolName: 'test_tool', | |
| model: 'test', | |
| timestamp: Date.now(), | |
| index: 0, | |
| }) | |
| // Complete with null input (simulating Anthropic empty tool_use) | |
| manager.completeToolCall({ | |
| type: 'TOOL_CALL_END', | |
| toolCallId: 'tc-1', | |
| toolName: 'test_tool', | |
| model: 'test', | |
| timestamp: Date.now(), | |
| input: null as unknown, | |
| }) | |
| const toolCalls = manager.getToolCalls() | |
| expect(toolCalls).toHaveLength(1) | |
| // Should be "{}" not "null" | |
| expect(toolCalls[0]!.function.arguments).toBe('{}') | |
| }) | |
| describe('ToolCallManager.completeToolCall', () => { | |
| it('should normalize null input to empty object', () => { | |
| const manager = new ToolCallManager([]) | |
| // Register a tool call | |
| manager.addToolCallStartEvent({ | |
| type: 'TOOL_CALL_START', | |
| toolCallId: 'tc-1', | |
| toolName: 'test_tool', | |
| model: 'test', | |
| timestamp: Date.now(), | |
| index: 0, | |
| }) | |
| // Complete with null input (simulating Anthropic empty tool_use) | |
| manager.completeToolCall({ | |
| type: 'TOOL_CALL_END', | |
| toolCallId: 'tc-1', | |
| toolName: 'test_tool', | |
| model: 'test', | |
| timestamp: Date.now(), | |
| input: null as unknown, | |
| }) | |
| const toolCalls = manager.getToolCalls() | |
| expect(toolCalls).toHaveLength(1) | |
| // Should be "{}" not "null" | |
| expect(toolCalls[0]!.function.arguments).toBe('{}') | |
| }) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/typescript/ai/tests/tool-calls-null-input.test.ts` around lines 108
- 136, The tests fail to compile because the ToolCallManager constructor
currently accepts only one parameter (tools: ReadonlyArray<Tool>) but tests call
new ToolCallManager(..., mockFinishedEvent); update the ToolCallManager
constructor signature to accept an optional second parameter (e.g.,
finishedEvent or initialFinishedEvent) with the same type as mockFinishedEvent,
adjust the constructor implementation to handle when that second argument is
provided (store or process it the same way tests expect), and ensure all usages
and exports of ToolCallManager are updated to the new signature so the tests
compiling calls to ToolCallManager(..., mockFinishedEvent) succeed.
…sion test Extend the null tool input normalization to the Gemini and Ollama adapters (both were missing the guard in TOOL_CALL_END emission). Add an e2e regression test for issue #265 that verifies the full flow: aimock returns a tool call with "null" arguments → adapter normalizes null → {} → tool executes successfully → agent loop continues → follow-up text response is received.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/typescript/ai-ollama/src/adapters/text.ts`:
- Around line 251-256: The catch fallback currently assigns parsedInput =
actualToolCall.function.arguments which may be a string or undefined; change the
catch to normalize parsedInput to an object by using an object-shaped fallback
(e.g., {} or Object(actualToolCall.function.arguments) guarded to ensure it's an
object) so TOOL_CALL_END.input always remains an object; update the catch block
that reads argsStr/parsedInput (referencing parsedInput, argsStr, and
actualToolCall.function.arguments) to enforce parsedInput is an object and
default to {} when it is not.
🪄 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: 70f9e3f9-e9cd-4c9b-a766-513197bd8be4
📒 Files selected for processing (6)
.changeset/fix-null-tool-input-normalization.mdpackages/typescript/ai-gemini/src/adapters/text.tspackages/typescript/ai-ollama/src/adapters/text.tstesting/e2e/fixtures/tools-test/null-tool-input.jsontesting/e2e/src/lib/tools-test-tools.tstesting/e2e/tests/tools-test/null-tool-input.spec.ts
✅ Files skipped from review due to trivial changes (2)
- testing/e2e/fixtures/tools-test/null-tool-input.json
- .changeset/fix-null-tool-input-normalization.md
| try { | ||
| parsedInput = JSON.parse(argsStr) | ||
| const parsed = JSON.parse(argsStr) | ||
| parsedInput = parsed && typeof parsed === 'object' ? parsed : {} | ||
| } catch { | ||
| parsedInput = actualToolCall.function.arguments | ||
| } |
There was a problem hiding this comment.
Catch fallback should keep TOOL_CALL_END.input object-shaped
On Line 255, parsedInput = actualToolCall.function.arguments can emit a string/undefined after parse failure, which can still break downstream tool-input validation. Normalize catch fallback to {} to keep the invariant consistent.
Suggested fix
try {
const parsed = JSON.parse(argsStr)
parsedInput = parsed && typeof parsed === 'object' ? parsed : {}
} catch {
- parsedInput = actualToolCall.function.arguments
+ parsedInput = {}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try { | |
| parsedInput = JSON.parse(argsStr) | |
| const parsed = JSON.parse(argsStr) | |
| parsedInput = parsed && typeof parsed === 'object' ? parsed : {} | |
| } catch { | |
| parsedInput = actualToolCall.function.arguments | |
| } | |
| try { | |
| const parsed = JSON.parse(argsStr) | |
| parsedInput = parsed && typeof parsed === 'object' ? parsed : {} | |
| } catch { | |
| parsedInput = {} | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/typescript/ai-ollama/src/adapters/text.ts` around lines 251 - 256,
The catch fallback currently assigns parsedInput =
actualToolCall.function.arguments which may be a string or undefined; change the
catch to normalize parsedInput to an object by using an object-shaped fallback
(e.g., {} or Object(actualToolCall.function.arguments) guarded to ensure it's an
object) so TOOL_CALL_END.input always remains an object; update the catch block
that reads argsStr/parsedInput (referencing parsedInput, argsStr, and
actualToolCall.function.arguments) to enforce parsedInput is an object and
default to {} when it is not.
The selectScenario + waitForTestComplete calls can be slow on CI cold-start. Increase timeout from 15s to 30s to avoid flaky failures.
Summary
tool_useblock with no input (or literalnull),JSON.parse('null')returnsnull, which fails Zod schema validation and silently kills the agent loopnull/non-object parsed tool input to{}in four locations:executeToolCalls(): afterJSON.parseof arguments stringToolCallManager.completeToolCall(): beforeJSON.stringifyof event inputToolCallManager.executeTools(): replace fragileargsString === 'null'string comparison with robust type checkTOOL_CALL_ENDemission (matching existing Anthropic adapter fix)Fixes #265
Test plan
null, empty, and valid arguments for bothexecuteToolCallsandToolCallManager@tanstack/aitests pass@tanstack/ai-openaitests passSummary by CodeRabbit
Bug Fixes
nullor other non-object parsed values become{}; validate parsed function-call arguments are objects before use.Tests
Chores