fix(core): prevent premature tool_call parsing on empty SSE args#35634
fix(core): prevent premature tool_call parsing on empty SSE args#35634Giulio Leone (giulio-leone) wants to merge 3 commits intolangchain-ai:masterfrom
Conversation
Merging this PR will degrade performance by 10.56%
|
| 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)
Footnotes
-
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. ↩
ce3ed30 to
27d512b
Compare
…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.
27d512b to
19355f7
Compare
|
Friendly ping — CI is green, tests pass, rebased on latest. Ready for review whenever convenient. Happy to address any feedback. 🙏 |
|
Friendly ping — rebased on latest and ready for review. Happy to address any feedback! |
Description
Fixes #35514
When streaming tool calls via SSE, some LLM providers (OpenRouter, DeepSeek) split
tool_callarguments across multiple chunks. The first chunk carriesname+idbut empty args (""). Previously, the empty string fell through toelse {}ininit_tool_calls, producing a validtool_callsentry withargs={}. 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):chunk["args"]is""(empty string) → falsy →args_ = {}→ creates a validtool_callwith empty args.Fix
Skip
tool_call_chunkswhoseargsis 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_listsconcatenation across all SSE fragments) carries the full args string and parses correctly.Verification
Tests
test_sse_fragmented_tool_calls_no_premature_parse— verifies individual chunks don't produce premature tool_calls but accumulated result is correcttest_no_arg_tool_call_still_works— verifies tools withargs="{}"still workIssue link
Fixes #35514