Skip to content

Conversation

@ultramancode
Copy link

@ultramancode ultramancode commented Nov 3, 2025

Fixes #4790

Problem

When using OpenAI-compatible APIs like Qwen with streaming tool calls, subsequent chunks may not include the tool call ID. The current MessageAggregator uses addAll()which creates separate, incomplete ToolCall objects for each chunk instead of merging them. This results in ToolCall objects with empty name fields, causing:
IllegalArgumentException: toolName cannot be null or empty

Root Cause

Some OpenAI-compatible APIs (e.g., Qwen via OpenRouter) follow a streaming pattern where:

  • First chunk: Contains both id and function.name
  • Subsequent chunks: Contain only function.arguments without id

Example:

Chunk 1: ToolCall(id="tool-123", name="getCurrentWeather", args="")
Chunk 2: ToolCall(id="",        name="",                  args="{\"location\": \"")
Chunk 3: ToolCall(id="",        name="",                  args="Seoul\"}")

Solution

Added mergeToolCalls() method in MessageAggregator as a safety net to handle tool call fragments that may not be properly merged at the API layer (e.g., OpenAiStreamFunctionCallingHelper).

This ensures that even when API-layer merging is incomplete or providers behave slightly differently, the aggregation layer can properly merge streaming tool call fragments.

This handles:

  • Standard ID-based matching (existing behavior)
  • ID-less streaming chunks
  • Multiple simultaneous tool calls
  • Mixed ID/no-ID scenarios

Changes

  • Replaced addAll() with new mergeToolCalls() method to properly handle streaming tool call fragments
  • Added mergeToolCall() helper method for null-safe property merging
  • Added comprehensive tests in MessageAggregatorTests
    • shouldMergeToolCallsWithoutIds: Verifies Qwen streaming pattern
    • shouldMergeMultipleToolCallsWithMixedIds: Multiple tool calls
    • shouldMergeToolCallsById: ID-based matching still works

Testing

All tests pass with actual Qwen streaming response pattern verified via OpenRouter API.

Example:

// Input: Streaming chunks
Chunk 1: ToolCall(id="tool-123", name="getCurrentWeather", args="")
Chunk 2: ToolCall(id="",        name="",                  args="{\"location\": \"")
Chunk 3: ToolCall(id="",        name="",                  args="Seoul\"}")

// Output: Merged result
ToolCall(id="tool-123", name="getCurrentWeather", args="{\"location\": \"Seoul\"}")

- Update MessageAggregator to handle tool calls without IDs
- When tool call has no ID, merge with last tool call
- Add comprehensive tests for streaming patterns

Signed-off-by: ultramancode <[email protected]>
@ultramancode ultramancode force-pushed the fix/streaming-tool-calls-merge branch from bf2c9ce to d2057f3 Compare November 4, 2025 06:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Streaming tool_calls with Qwen models cause toolName cannot be null or empty in Spring AI 1.0.3

4 participants