Skip to content

fix(core): prevent premature tool_call parsing on empty SSE args#35634

Open
Giulio Leone (giulio-leone) wants to merge 3 commits intolangchain-ai:masterfrom
giulio-leone:fix/streaming-tool-call-empty-args-35514
Open

fix(core): prevent premature tool_call parsing on empty SSE args#35634
Giulio Leone (giulio-leone) wants to merge 3 commits intolangchain-ai:masterfrom
giulio-leone:fix/streaming-tool-call-empty-args-35514

Conversation

@giulio-leone
Copy link
Contributor

Description

Fixes #35514

When streaming tool calls via SSE, some LLM providers (OpenRouter, DeepSeek) split tool_call arguments across multiple chunks. The first chunk carries name + id but empty args (""). Previously, the empty string fell through to else {} in init_tool_calls, producing a valid tool_calls entry with args={}. Downstream code (ToolNode, user streaming handlers) treated this as a complete tool call and executed the tool prematurely with empty arguments.

Root Cause

In AIMessageChunk.init_tool_calls() (line 543):

args_ = parse_partial_json(chunk["args"]) if chunk["args"] else {}

chunk["args"] is "" (empty string) → falsy → args_ = {} → creates a valid tool_call with empty args.

Fix

Skip tool_call_chunks whose args is explicitly the empty string "":

  • args="" (empty string): streaming not started yet → skip (no premature tool_call)
  • args=None: no args info (backward compat) → treat as {} (unchanged)
  • args="{}" (explicit empty dict): valid no-arg tool call → parse normally (unchanged)

The final accumulated chunk (after merge_lists concatenation across all SSE fragments) carries the full args string and parses correctly.

Verification

# Before fix: chunk1.tool_calls = [{name: "my_tool", args: {}}]  ← WRONG
# After fix:  chunk1.tool_calls = []                              ← CORRECT

# Accumulated (chunk1 + chunk2 + chunk3 + chunk4):
# tool_calls = [{name: "my_tool", args: {"url": "http://example.com"}}]  ← CORRECT (unchanged)

Tests

  • Updated existing test expectation for empty-string args behavior
  • Added regression test test_sse_fragmented_tool_calls_no_premature_parse — verifies individual chunks don't produce premature tool_calls but accumulated result is correct
  • Added test_no_arg_tool_call_still_works — verifies tools with args="{}" still work
  • All 1703 core unit tests pass
  • All 308 OpenAI partner unit tests pass

Issue link

Fixes #35514

@github-actions github-actions bot added core `langchain-core` package issues & PRs fix For PRs that implement a fix external labels Mar 7, 2026
@codspeed-hq
Copy link

codspeed-hq bot commented Mar 7, 2026

Merging this PR will degrade performance by 10.56%

⚠️ Unknown Walltime execution environment detected

Using the Walltime instrument on standard Hosted Runners will lead to inconsistent data.

For the most accurate results, we recommend using CodSpeed Macro Runners: bare-metal machines fine-tuned for performance measurement consistency.

❌ 1 regressed benchmark
✅ 12 untouched benchmarks
⏩ 23 skipped benchmarks1

⚠️ Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
WallTime test_import_time[InMemoryVectorStore] 637.1 ms 712.3 ms -10.56%

Comparing giulio-leone:fix/streaming-tool-call-empty-args-35514 (19355f7) with master (527fc02)

Open in CodSpeed

Footnotes

  1. 23 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

giulio-leone added 3 commits March 9, 2026 15:18
…gchain-ai#35514)

When streaming tool calls via SSE, some providers (OpenRouter, DeepSeek)
split arguments across multiple chunks. The first chunk carries name + id
but empty args (""). Previously parse_partial_json fell through to the
else branch producing args={}, which downstream code treated as a valid
tool call with empty arguments — causing premature execution.

Fix: skip tool_call_chunks whose args is explicitly the empty string.
- args="" (empty string): streaming not started yet → skip
- args=None: no args info (backward compat) → treat as {}
- args="{}" (explicit empty dict): valid no-arg tool → parse normally

The final accumulated chunk (after merge_lists concatenation) carries the
full args string and parses correctly.
@giulio-leone Giulio Leone (giulio-leone) force-pushed the fix/streaming-tool-call-empty-args-35514 branch from 27d512b to 19355f7 Compare March 9, 2026 14:19
@github-actions github-actions bot added the size: S 50-199 LOC label Mar 9, 2026
@giulio-leone
Copy link
Contributor Author

Friendly ping — CI is green, tests pass, rebased on latest. Ready for review whenever convenient. Happy to address any feedback. 🙏

@giulio-leone
Copy link
Contributor Author

Friendly ping — rebased on latest and ready for review. Happy to address any feedback!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core `langchain-core` package issues & PRs external fix For PRs that implement a fix size: S 50-199 LOC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Streaming tool call executed with empty args {} due to SSE fragmentation of tool call arguments

1 participant