Fix tool call streaming bug with Qwen3 inconsistent indices

Co-authored-by: mdrxy <61371264+mdrxy@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-07-22 01:03:54 +00:00
parent b07d2d5ee8
commit 7f366d7d3f
2 changed files with 92 additions and 1 deletions

View File

@ -109,7 +109,36 @@ def merge_lists(left: Optional[list], *others: Optional[list]) -> Optional[list]
) )
merged[to_merge[0]] = merge_dicts(merged[to_merge[0]], new_e) merged[to_merge[0]] = merge_dicts(merged[to_merge[0]], new_e)
else: else:
merged.append(e) # Special handling for tool call chunks: if this chunk appears to be
# a continuation (has None name/id) and no matching index was found,
# try to merge with the most recent tool call chunk that has a name/id.
# This fixes issues with models like Qwen3 that send inconsistent indices.
if (
e.get("type") == "tool_call_chunk"
and e.get("name") is None
and e.get("id") is None
and merged
):
# Find the most recent tool call chunk with a valid name or id
for i in reversed(range(len(merged))):
if (
isinstance(merged[i], dict)
and merged[i].get("type") == "tool_call_chunk"
and (merged[i].get("name") is not None or merged[i].get("id") is not None)
):
# Merge with this chunk
new_e = (
{k: v for k, v in e.items() if k != "type"}
if "type" in e
else e
)
merged[i] = merge_dicts(merged[i], new_e)
break
else:
# No suitable chunk found, append as new
merged.append(e)
else:
merged.append(e)
else: else:
merged.append(e) merged.append(e)
return merged return merged

View File

@ -1197,3 +1197,65 @@ def test_convert_to_openai_image_block() -> None:
} }
result = convert_to_openai_image_block(input_block) result = convert_to_openai_image_block(input_block)
assert result == expected assert result == expected
def test_tool_call_streaming_different_indices():
"""
Test that tool call chunks with different indices but logically part of the same
tool call are merged correctly. This addresses issues with models like Qwen3 that
send inconsistent indices during streaming.
See: https://github.com/langchain-ai/langchain/issues/31511
"""
# Create chunks that simulate Qwen3 behavior:
# First chunk has index=1, subsequent chunks have index=0 with name=None, id=None
chunk1 = AIMessageChunk(
content="",
tool_call_chunks=[
create_tool_call_chunk(
name="search_function",
args='{"query": "langchain',
id="call_123",
index=1 # Initial index
)
]
)
chunk2 = AIMessageChunk(
content="",
tool_call_chunks=[
create_tool_call_chunk(
name=None, # Continuation chunk
args=' tutorial"}',
id=None, # Continuation chunk
index=0 # Different index
)
]
)
# Merge chunks as happens during streaming
merged_chunk = chunk1 + chunk2
# Should result in a single merged tool call chunk
assert len(merged_chunk.tool_call_chunks) == 1
assert merged_chunk.tool_call_chunks[0]["name"] == "search_function"
assert merged_chunk.tool_call_chunks[0]["args"] == '{"query": "langchain tutorial"}'
assert merged_chunk.tool_call_chunks[0]["id"] == "call_123"
# Should result in a single valid tool call
assert len(merged_chunk.tool_calls) == 1
assert len(merged_chunk.invalid_tool_calls) == 0
# Verify the final tool call is correct
tool_call = merged_chunk.tool_calls[0]
assert tool_call["name"] == "search_function"
assert tool_call["args"] == {"query": "langchain tutorial"}
assert tool_call["id"] == "call_123"
# Test with message_chunk_to_message (core functionality)
message = message_chunk_to_message(merged_chunk)
assert len(message.tool_calls) == 1
assert len(message.invalid_tool_calls) == 0
assert message.tool_calls[0]["name"] == "search_function"
assert message.tool_calls[0]["args"] == {"query": "langchain tutorial"}