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
Nobody has claimed this yet.
- 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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