spring-projects / spring-projects/spring-ai

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

Open
#4,790 18 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
Java
Stars
9.5k
Forks
2.9k
Avg merge
1d 7h
Merged PRs (30d)
6

Description

When using Spring AI 1.0.3 with Qwen3 (e.g. qwen3-30b-a3b-instruct-2507) in streaming mode (stream=true),
an exception occurs during streaming tool calls:

java.lang.IllegalArgumentException: toolName cannot be null or empty
at org.springframework.util.Assert.hasText(Assert.java:253)
at org.springframework.ai.tool.resolution.DelegatingToolCallbackResolver.resolve(DelegatingToolCallbackResolver.java:45)

This happens because Qwen streams tool_calls incrementally —
only the first frame contains "function.name", while subsequent frames only contain "function.arguments".
Spring AI currently expects each frame to include the tool name,
so when a partial frame without "name" arrives, it fails with the above error.

This is not a model-side bug. Qwen follows the OpenAI-compatible streaming protocol
where tool call data is split across multiple chunks.

Environment

Spring AI version: 1.0.3

Java version: 17

Model: qwen3-30b-a3b-instruct-2507 (DashScope-compatible API)

Mode: stream=true

Error: toolName cannot be null or empty

Expected behavior

Spring AI should correctly aggregate tool_call fragments across streaming chunks,
retaining the last non-empty function.name and concatenating function.arguments until the stream completes.

No exception should be thrown, and the tool callback should execute once the tool call is fully assembled.

Qwen3 output:

data: {"model":"qwen3-30b-a3b-instruct-2507","id":"chatcmpl-6a13abdb-a3be-4557-8dc2-8381fcd83696","created":1762178257774,"object":"chat.completion.chunk","usage":null,"choices":[{"logprobs":null,"index":0,"delta":{"content":"","role":"assistant","tool_calls":[{"index":0,"id":"call_f7e76b4bdf8242b68b7124","type":"function","function":{"name":"init_work_status","arguments":""}}]}}]}

data: {"model":"qwen3-30b-a3b-instruct-2507","id":"chatcmpl-6a13abdb-a3be-4557-8dc2-8381fcd83696","choices":[{"delta":{"content":"","tool_calls":[{"index":0,"id":"call_f7e76b4bdf8242b68b7124","type":"function","function":{"name":"","arguments":"{"firstStep"}}],"role":null},"index":0}],"created":1762178257774,"object":"chat.completion.chunk","usage":null}

data: {"model":"qwen3-30b-a3b-instruct-2507","id":"chatcmpl-6a13abdb-a3be-4557-8dc2-8381fcd83696","choices":[{"delta":{"content":"","tool_calls":[{"index":0,"id":"","type":"function","function":{"name":"","arguments":"": "开始需求"}}],"role":null},"index":0}],"created":1762178257774,"object":"chat.completion.chunk","usage":null}

data: {"model":"qwen3-30b-a3b-instruct-2507","id":"chatcmpl-6a13abdb-a3be-4557-8dc2-8381fcd83696","choices":[{"delta":{"content":"","tool_calls":[{"index":0,"id":"","type":"function","function":{"name":"","arguments":"收集阶段", ""}}],"role":null},"index":0}],"created":1762178257774,"object":"chat.completion.chunk","usage":null}

data: {"model":"qwen3-30b-a3b-instruct-2507","id":"chatcmpl-6a13abdb-a3be-4557-8dc2-8381fcd83696","choices":[{"delta":{"content":"","tool_calls":[{"index":0,"id":"","type":"function","function":{"name":"","arguments":"reason": "用户"}}],"role":null},"index":0}],"created":1762178257774,"object":"chat.completion.chunk","usage":null}

data: {"model":"qwen3-30b-a3b-instruct-2507","id":"chatcmpl-6a13abdb-a3be-4557-8dc2-8381fcd83696","choices":[{"delta":{"content":"","tool_calls":[{"index":0,"id":"","type":"function","function":{"name":"","arguments":"提出需要撰写2"}}],"role":null},"index":0}],"created":1762178257774,"object":"chat.completion.chunk","usage":null}

data: {"model":"qwen3-30b-a3b-instruct-2507","id":"chatcmpl-6a13abdb-a3be-4557-8dc2-8381fcd83696","choices":[{"delta":{"content":"","tool_calls":[{"index":0,"id":"","type":"function","function":{"name":"","arguments":"025年上半年"}}],"role":null},"index":0}],"created":1762178257774,"object":"chat.completion.chunk","usage":null}

data: {"model":"qwen3-30b-a3b-instruct-2507","id":"chatcmpl-6a13abdb-a3be-4557-8dc2-8381fcd83696","choices":[{"delta":{"content":"","tool_calls":[{"index":0,"id":"","type":"function","function":{"name":"","arguments":"区教育局的工作"}}],"role":null},"index":0}],"created":1762178257774,"object":"chat.completion.chunk","usage":null}

data: {"model":"qwen3-30b-a3b-instruct-2507","id":"chatcmpl-6a13abdb-a3be-4557-8dc2-8381fcd83696","choices":[{"delta":{"content":"","tool_calls":[{"index":0,"id":"","type":"function","function":{"name":"","arguments":"总结,这是一个新的"}}],"role":null},"index":0}],"created":1762178257774,"object":"chat.completion.chunk","usage":null}

data: {"model":"qwen3-30b-a3b-instruct-2507","id":"chatcmpl-6a13abdb-a3be-4557-8dc2-8381fcd83696","choices":[{"delta":{"content":"","tool_calls":[{"index":0,"id":"","type":"function","function":{"name":"","arguments":"文档编写任务,"}}],"role":null},"index":0}],"created":1762178257774,"object":"chat.completion.chunk","usage":null}

data: {"model":"qwen3-30b-a3b-instruct-2507","id":"chatcmpl-6a13abdb-a3be-4557-8dc2-8381fcd83696","choices":[{"delta":{"content":"","tool_calls":[{"index":0,"id":"","type":"function","function":{"name":"","arguments":"需启动需求收集"}}],"role":null},"index":0}],"created":1762178257774,"object":"chat.completion.chunk","usage":null}

data: {"model":"qwen3-30b-a3b-instruct-2507","id":"chatcmpl-6a13abdb-a3be-4557-8dc2-8381fcd83696","choices":[{"delta":{"content":"","tool_calls":[{"index":0,"id":"","type":"function","function":{"name":"","arguments":"流程。"}"}}],"role":null},"index":0}],"created":1762178257774,"object":"chat.completion.chunk","usage":null}

data: {"model":"qwen3-30b-a3b-instruct-2507","id":"chatcmpl-6a13abdb-a3be-4557-8dc2-8381fcd83696","choices":[{"delta":{"tool_calls":[{"function":{"arguments":null},"index":0,"id":"","type":"function"}],"content":""}}],"created":1762178257774,"object":"chat.completion.chunk","usage":null}

data: {"model":"qwen3-30b-a3b-instruct-2507","id":"chatcmpl-6a13abdb-a3be-4557-8dc2-8381fcd83696","choices":[{"delta":{},"index":0,"finish_reason":"tool_calls"}],"created":1762178257774,"object":"chat.completion.chunk","usage":null}

data: [DONE]

exception stack:

2025-11-03 22:29:45.777 [boundedElastic-1] ERROR o.s.ai.chat.model.MessageAggregator - Aggregation Error
java.lang.IllegalArgumentException: toolName cannot be null or empty
at org.springframework.util.Assert.hasText(Assert.java:253)
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Assembly trace from producer [reactor.core.publisher.FluxDefer] :
reactor.core.publisher.Flux.defer(Flux.java:810)
org.springframework.ai.openai.OpenAiChatModel.lambda$internalStream$10(OpenAiChatModel.java:366)
Error has been observed at the following site(s):
*_________Flux.defer ⇢ at org.springframework.ai.openai.OpenAiChatModel.lambda$internalStream$10(OpenAiChatModel.java:366)
|
Flux.subscribeOn ⇢ at org.springframework.ai.openai.OpenAiChatModel.lambda$internalStream$10(OpenAiChatModel.java:381)
*__Flux.flatMap ⇢ at org.springframework.ai.openai.OpenAiChatModel.lambda$internalStream$13(OpenAiChatModel.java:364)
|
Flux.doOnError ⇢ at org.springframework.ai.openai.OpenAiChatModel.lambda$internalStream$13(OpenAiChatModel.java:387)
|
Flux.doFinally ⇢ at org.springframework.ai.openai.OpenAiChatModel.lambda$internalStream$13(OpenAiChatModel.java:388)
|
Flux.contextWrite ⇢ at org.springframework.ai.openai.OpenAiChatModel.lambda$internalStream$13(OpenAiChatModel.java:389)
|
Flux.doOnSubscribe ⇢ at org.springframework.ai.chat.model.MessageAggregator.aggregate(MessageAggregator.java:79)
|
Flux.doOnNext ⇢ at org.springframework.ai.chat.model.MessageAggregator.aggregate(MessageAggregator.java:91)
|
Flux.doOnComplete ⇢ at org.springframework.ai.chat.model.MessageAggregator.aggregate(MessageAggregator.java:142)
Original Stack Trace:
at org.springframework.util.Assert.hasText(Assert.java:253)
at org.springframework.ai.tool.resolution.DelegatingToolCallbackResolver.resolve(DelegatingToolCallbackResolver.java:45)
at org.springframework.ai.model.tool.DefaultToolCallingManager.lambda$executeToolCall$3(DefaultToolCallingManager.java:207)
at java.base/java.util.Optional.orElseGet(Optional.java:364)
at org.springframework.ai.model.tool.DefaultToolCallingManager.executeToolCall(DefaultToolCallingManager.java:207)
at org.springframework.ai.model.tool.DefaultToolCallingManager.executeToolCalls(DefaultToolCallingManager.java:138)
at com.lx.ai.plugin.service.impl.MyToolCallingManager.executeToolCalls(MyToolCallingManager.java:85)
at org.springframework.ai.openai.OpenAiChatModel.lambda$internalStream$9(OpenAiChatModel.java:369)
at reactor.core.publisher.FluxDefer.subscribe(FluxDefer.java:46)
at reactor.core.publisher.InternalFluxOperator.subscribe(InternalFluxOperator.java:68)
at reactor.core.publisher.FluxSubscribeOn$SubscribeOnSubscriber.run(FluxSubscribeOn.java:194)
at reactor.core.scheduler.WorkerTask.call(WorkerTask.java:84)
at reactor.core.scheduler.WorkerTask.call(WorkerTask.java:37)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
at java.base/java.lang.Thread.run(Thread.java:1575)
2025-11-03 22:29:45.790 [boundedElastic-2] ERROR o.s.ai.chat.model.MessageAggregator - Aggregation Error
java.lang.IllegalArgumentException: toolName cannot be null or empty
at org.springframework.util.Assert.hasText(Assert.java:253)
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Assembly trace from producer [reactor.core.publisher.FluxDefer] :
reactor.core.publisher.Flux.defer(Flux.java:810)
org.springframework.ai.openai.OpenAiChatModel.lambda$internalStream$10(OpenAiChatModel.java:366)
Error has been observed at the following site(s):
*___________Flux.defer ⇢ at org.springframework.ai.openai.OpenAiChatModel.lambda$internalStream$10(OpenAiChatModel.java:366)
|
Flux.subscribeOn ⇢ at org.springframework.ai.openai.OpenAiChatModel.lambda$internalStream$10(OpenAiChatModel.java:381)
*Flux.flatMap ⇢ at org.springframework.ai.openai.OpenAiChatModel.lambda$internalStream$13(OpenAiChatModel.java:364)
|
Flux.doOnError ⇢ at org.springframework.ai.openai.OpenAiChatModel.lambda$internalStream$13(OpenAiChatModel.java:387)
|
Flux.doFinally ⇢ at org.springframework.ai.openai.OpenAiChatModel.lambda$internalStream$13(OpenAiChatModel.java:388)
|
Flux.contextWrite ⇢ at org.springframework.ai.openai.OpenAiChatModel.lambda$internalStream$13(OpenAiChatModel.java:389)
|
Flux.doOnSubscribe ⇢ at org.springframework.ai.chat.model.MessageAggregator.aggregate(MessageAggregator.java:79)
|
Flux.doOnNext ⇢ at org.springframework.ai.chat.model.MessageAggregator.aggregate(MessageAggregator.java:91)
|
Flux.doOnComplete ⇢ at org.springframework.ai.chat.model.MessageAggregator.aggregate(MessageAggregator.java:142)
|
Flux.doOnError ⇢ at org.springframework.ai.chat.model.MessageAggregator.aggregate(MessageAggregator.java:182)
*Flux.deferContextual ⇢ at org.springframework.ai.openai.OpenAiChatModel.internalStream(OpenAiChatModel.java:271)
|
Flux.map ⇢ at org.springframework.ai.chat.client.advisor.ChatModelStreamAdvisor.adviseStream(ChatModelStreamAdvisor.java:53)
|
Flux.publishOn ⇢ at org.springframework.ai.chat.client.advisor.ChatModelStreamAdvisor.adviseStream(ChatModelStreamAdvisor.java:57)
|
Flux.doOnError ⇢ at org.springframework.ai.chat.client.advisor.DefaultAroundAdvisorChain.lambda$nextStream$5(DefaultAroundAdvisorChain.java:137)
|
Flux.doFinally ⇢ at org.springframework.ai.chat.client.advisor.DefaultAroundAdvisorChain.lambda$nextStream$5(DefaultAroundAdvisorChain.java:138)
|
Flux.contextWrite ⇢ at org.springframework.ai.chat.client.advisor.DefaultAroundAdvisorChain.lambda$nextStream$5(DefaultAroundAdvisorChain.java:139)
*____________Flux.defer ⇢ at org.springframework.ai.chat.client.advisor.DefaultAroundAdvisorChain.lambda$nextStream$6(DefaultAroundAdvisorChain.java:136)
*__Flux.deferContextual ⇢ at org.springframework.ai.chat.client.advisor.DefaultAroundAdvisorChain.nextStream(DefaultAroundAdvisorChain.java:117)
*__Mono.flatMapMany ⇢ at com.lx.ai.plugin.service.impl.MyMessageAdvisor.adviseStream(MyMessageAdvisor.java:258)
|
Flux.mapNotNull ⇢ at org.springframework.ai.chat.client.ChatClientMessageAggregator.aggregateChatClientResponse(ChatClientMessageAggregator.java:48)
|
Flux.doOnSubscribe ⇢ at org.springframework.ai.chat.model.MessageAggregator.aggregate(MessageAggregator.java:79)
|
Flux.doOnNext ⇢ at org.springframework.ai.chat.model.MessageAggregator.aggregate(MessageAggregator.java:91)
|
Flux.doOnComplete ⇢ at org.springframework.ai.chat.model.MessageAggregator.aggregate(MessageAggregator.java:142)
Original Stack Trace:
at org.springframework.util.Assert.hasText(Assert.java:253)
at org.springframework.ai.tool.resolution.DelegatingToolCallbackResolver.resolve(DelegatingToolCallbackResolver.java:45)
at org.springframework.ai.model.tool.DefaultToolCallingManager.lambda$executeToolCall$3(DefaultToolCallingManager.java:207)
at java.base/java.util.Optional.orElseGet(Optional.java:364)
at org.springframework.ai.model.tool.DefaultToolCallingManager.executeToolCall(DefaultToolCallingManager.java:207)
at org.springframework.ai.model.tool.DefaultToolCallingManager.executeToolCalls(DefaultToolCallingManager.java:138)
at com.lx.ai.plugin.service.impl.MyToolCallingManager.executeToolCalls(MyToolCallingManager.java:85)
at org.springframework.ai.openai.OpenAiChatModel.lambda$internalStream$9(OpenAiChatModel.java:369)
at reactor.core.publisher.FluxDefer.subscribe(FluxDefer.java:46)
at reactor.core.publisher.InternalFluxOperator.subscribe(InternalFluxOperator.java:68)
at reactor.core.publisher.FluxSubscribeOn$SubscribeOnSubscriber.run(FluxSubscribeOn.java:194)
at reactor.core.scheduler.WorkerTask.call(WorkerTask.java:84)
at reactor.core.scheduler.WorkerTask.call(WorkerTask.java:37)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
at java.base/java.lang.Thread.run(Thread.java:1575)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Trace streaming tool-call handling from OpenAiChatModel through MessageAggregator and DefaultToolCallingManager to DelegatingToolCallbackResolver. Compare the Qwen chunks in the report with the aggregation path, then verify that fragments retain the initial function name, concatenate arguments, avoid the empty-name exception, and execute the completed callback once.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
ai, api
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
50/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.