spring-projects / spring-projects/spring-ai

Streaming tool_calls with Qwen models cause function.arguments Field required in Spring AI 2.0.0

Open
#5,116 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

status: waiting-for-triage
Dominant language
Java
Stars
9.5k
Forks
2.9k
Avg merge
1d 7h
Merged PRs (30d)
6

Description

Qwen follows the OpenAI-compatible streaming protocol

Environment
Spring AI version: 2.0.0
Java version: 17
Model: qwen3
Mode: stream=true

// //ok
// ChatClient chatClient = ChatClient.builder(chatModel)
// .defaultToolCallbacks(MethodToolCallbackProvider.builder().toolObjects(new MyTools()).build())
// .build();
// var answer = chatClient
// .prompt("What day is tomorrow?")
// .call().content();
// System.out.println(answer);

// error
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultToolCallbacks(MethodToolCallbackProvider.builder().toolObjects(new MyTools()).build())
.build();
var answer = chatClient
.prompt("What day is tomorrow?")
.stream().content();
String content = answer.collectList().block().stream().collect(Collectors.joining());
System.out.println(content);

static class MyTools {

@Tool(description = "Get the current date and time in the user's timezone")
String getCurrentDateTime() {
    return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();
}

}

logs:

2025-12-18T10:11:40.888+08:00 DEBUG 27332 --- [reactor-http-nio-2] reactor.netty.http.client.HttpClient : [e2db0bd5-1, READ COMPLETE
2025-12-18T10:11:40.966+08:00 DEBUG 27332 --- [reactor-http-nio-2] reactor.netty.http.client.HttpClient : [e2db0bd5-1, READ: 607B c4
data: {"id":"chatcmpl-b3d783b2b520495fb6e3f4f8f4421e3f","object":"chat.completion.chunk","created":1766023897,"model":"Qwen3","choices":[{"index":0,"delta":{"content":" call","tool_calls":[]}}]}

c2
data: {"id":"chatcmpl-b3d783b2b520495fb6e3f4f8f4421e3f","object":"chat.completion.chunk","created":1766023897,"model":"Qwen3","choices":[{"index":0,"delta":{"content":".\n","tool_calls":[]}}]}

c7
data: {"id":"chatcmpl-b3d783b2b520495fb6e3f4f8f4421e3f","object":"chat.completion.chunk","created":1766023897,"model":"Qwen3","choices":[{"index":0,"delta":{"content":"","tool_calls":[]}}]}

2025-12-18T10:11:40.969+08:00 DEBUG 27332 --- [reactor-http-nio-2] reactor.netty.http.client.HttpClient : [e2db0bd5-1, READ COMPLETE
2025-12-18T10:11:41.043+08:00 DEBUG 27332 --- [reactor-http-nio-2] reactor.netty.http.client.HttpClient : [e2db0bd5-1, READ: 201B c3
data: {"id":"chatcmpl-b3d783b2b520495fb6e3f4f8f4421e3f","object":"chat.completion.chunk","created":1766023897,"model":"Qwen3","choices":[{"index":0,"delta":{"content":"\n\n","tool_calls":[]}}]}

2025-12-18T10:11:41.044+08:00 DEBUG 27332 --- [reactor-http-nio-2] reactor.netty.http.client.HttpClient : [e2db0bd5-1, READ COMPLETE
2025-12-18T10:11:41.206+08:00 DEBUG 27332 --- [reactor-http-nio-2] reactor.netty.http.client.HttpClient : [e2db0bd5-1, READ: 309B 12e
data: {"id":"chatcmpl-b3d783b2b520495fb6e3f4f8f4421e3f","object":"chat.completion.chunk","created":1766023897,"model":"Qwen3","choices":[{"index":0,"delta":{"tool_calls":[{"id":"chatcmpl-tool-1a81528ba7854bd5af87bcf660b5607a","type":"function","index":0,"function":{"name":"getCurrentDateTime"}}]}}]}

2025-12-18T10:11:41.208+08:00 DEBUG 27332 --- [reactor-http-nio-2] reactor.netty.http.client.HttpClient : [e2db0bd5-1, READ COMPLETE
2025-12-18T10:11:41.387+08:00 DEBUG 27332 --- [reactor-http-nio-2] reactor.netty.http.client.HttpClient : [e2db0bd5-1, READ: 250B dc
data: {"id":"chatcmpl-b3d783b2b520495fb6e3f4f8f4421e3f","object":"chat.completion.chunk","created":1766023897,"model":"Qwen3","choices":[{"index":0,"delta":{"content":"","tool_calls":[]},"finish_reason":"tool_calls"}]}

e
data: [DONE]

0

2025-12-18T10:11:41.388+08:00 DEBUG 27332 --- [reactor-http-nio-2] r.n.http.client.HttpClientOperations : [e2db0bd5-1, Http client inbound receiver cancelled, closing channel.
2025-12-18T10:11:41.389+08:00 DEBUG 27332 --- [reactor-http-nio-2] reactor.netty.http.client.HttpClient : [e2db0bd5-1, CLOSE
2025-12-18T10:11:41.392+08:00 DEBUG 27332 --- [reactor-http-nio-2] r.n.http.client.HttpClientOperations : [e2db0bd5-1, ] Received last HTTP packet
2025-12-18T10:11:41.393+08:00 WARN 27332 --- [boundedElastic-2] o.s.a.m.tool.DefaultToolCallingManager : Tool call arguments are null or empty for tool: getCurrentDateTime. Using empty JSON object as default.
2025-12-18T10:11:41.393+08:00 DEBUG 27332 --- [reactor-http-nio-2] reactor.netty.http.client.HttpClient : [e2db0bd5, ] READ COMPLETE
2025-12-18T10:11:41.393+08:00 DEBUG 27332 --- [reactor-http-nio-2] reactor.netty.http.client.HttpClient : [e2db0bd5, ] INACTIVE
2025-12-18T10:11:41.395+08:00 DEBUG 27332 --- [reactor-http-nio-2] reactor.netty.http.client.HttpClient : [e2db0bd5, ] UNREGISTERED
2025-12-18T10:11:41.404+08:00 DEBUG 27332 --- [reactor-http-nio-3] reactor.netty.http.client.HttpClient : [40d6b05a] REGISTERED
2025-12-18T10:11:41.405+08:00 DEBUG 27332 --- [reactor-http-nio-3] reactor.netty.http.client.HttpClient : [40d6b05a] CONNECT: /
2025-12-18T10:11:41.434+08:00 DEBUG 27332 --- [reactor-http-nio-3] reactor.netty.http.client.HttpClient : [40d6b05a, ] ACTIVE
2025-12-18T10:11:41.434+08:00 DEBUG 27332 --- [reactor-http-nio-3] r.netty.http.client.HttpClientConnect : [40d6b05a-1, ] Handler is being applied: {uri=http:///ai/v1/chat/completions, method=POST}
2025-12-18T10:11:41.437+08:00 DEBUG 27332 --- [reactor-http-nio-3] reactor.netty.http.client.HttpClient : [40d6b05a-1, ] WRITE: 194B POST /ai/v1/chat/completions HTTP/1.1
host:
accept: /
Content-Type: application/json
User-Agent: spring-ai
Authorization: Bearer vrv-ai-client
content-length: 739

2025-12-18T10:11:41.438+08:00 DEBUG 27332 --- [reactor-http-nio-3] reactor.netty.http.client.HttpClient : [40d6b05a-1, ] WRITE: 739B {"messages":[{"content":"What day is tomorrow?","role":"user"},{"content":"","role":"assistant","tool_calls":[{"id":"chatcmpl-tool-1a81528ba7854bd5af87bcf660b5607a","type":"function","function":{"name":"getCurrentDateTime"}}]},{"content":""2025-12-18T10:11:41.397078900+08:00[Asia/Shanghai]"","role":"tool","name":"getCurrentDateTime","tool_call_id":"chatcmpl-tool-1a81528ba7854bd5af87bcf660b5607a"}],"model":"Qwen3","max_tokens":40960,"stream":true,"tools":[{"function":{"description":"Get the current date and time in the user's timezone","name":"getCurrentDateTime","parameters":{"$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"type":"object","properties":{},"required":[]}},"type":"function"}]}
2025-12-18T10:11:41.438+08:00 DEBUG 27332 --- [reactor-http-nio-3] reactor.netty.http.client.HttpClient : [40d6b05a-1, ] FLUSH
2025-12-18T10:11:41.480+08:00 DEBUG 27332 --- [reactor-http-nio-3] reactor.netty.http.client.HttpClient : [40d6b05a-1, ] READ: 483B HTTP/1.1 400 Bad Request
Server: nginx/1.27.0
Date: Thu, 18 Dec 2025 02:11:41 GMT
Content-Type: application/json
Content-Length: 319
Connection: keep-alive

{"object":"error","message":"1 validation error for ValidatorIterator\n0.function.arguments\n Field required [type=missing, input_value={'name': 'getCurrentDateTime'}, input_type=dict]\n For further information visit https://errors.pydantic.dev/2.11/v/missing None","type":"BadRequestError","param":null,"code":400}
2025-12-18T10:11:41.481+08:00 DEBUG 27332 --- [reactor-http-nio-3] r.n.http.client.HttpClientOperations : [40d6b05a-1, ] Received response (auto-read:false) : RESPONSE(decodeResult: success, version: HTTP/1.1)
HTTP/1.1 400 Bad Request
Server:
Date:
Content-Type:
Content-Length:
Connection:
2025-12-18T10:11:41.486+08:00 DEBUG 27332 --- [reactor-http-nio-3] r.n.http.client.HttpClientOperations : [40d6b05a-1, ] Received last HTTP packet
2025-12-18T10:11:41.500+08:00 ERROR 27332 --- [reactor-http-nio-3] o.s.ai.chat.model.MessageAggregator : Aggregation Error

org.springframework.web.reactive.function.client.WebClientResponseException$BadRequest: 400 Bad Request from POST http:///ai/v1/chat/completions
at org.springframework.web.reactive.function.client.WebClientResponseException.create(WebClientResponseException.java:307) ~[spring-webflux-7.0.1.jar:7.0.1]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
*__checkpoint ⇢ 400 BAD_REQUEST from POST http:///ai/v1/chat/completions [DefaultWebClient]
Original Stack Trace:
at org.springframework.web.reactive.function.client.WebClientResponseException.create(WebClientResponseException.java:307) ~[spring-webflux-7.0.1.jar:7.0.1]
at org.springframework.web.reactive.function.client.DefaultClientResponse.lambda$createException$1(DefaultClientResponse.java:214) ~[spring-webflux-7.0.1.jar:7.0.1]
at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:106) ~[reactor-core-3.8.0.jar:3.8.0]
at reactor.core.publisher.FluxOnErrorReturn$ReturnSubscriber.onNext(FluxOnErrorReturn.java:159) ~[reactor-core-3.8.0.jar:3.8.0]
at reactor.core.publisher.FluxDefaultIfEmpty$DefaultIfEmptySubscriber.onNext(FluxDefaultIfEmpty.java:123) ~[reactor-core-3.8.0.jar:3.8.0]
at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:130) ~[reactor-core-3.8.0.jar:3.8.0]
at reactor.core.publisher.FluxContextWrite$ContextWriteSubscriber.onNext(FluxContextWrite.java:109) ~[reactor-core-3.8.0.jar:3.8.0]
at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.onNext(FluxMapFuseable.java:300) ~[reactor-core-3.8.0.jar:3.8.0]
at reactor.core.publisher.FluxFilterFuseable$FilterFuseableConditionalSubscriber.onNext(FluxFilterFuseable.java:338) ~[reactor-core-3.8.0.jar:3.8.0]
at reactor.core.publisher.Operators$BaseFluxToMonoOperator.completePossiblyEmpty(Operators.java:2093) ~[reactor-core-3.8.0.jar:3.8.0]
at reactor.core.publisher.MonoCollect$CollectSubscriber.onComplete(MonoCollect.java:144) ~[reactor-core-3.8.0.jar:3.8.0]
at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:144) ~[reactor-core-3.8.0.jar:3.8.0]
at reactor.core.publisher.FluxPeek$PeekSubscriber.onComplete(FluxPeek.java:263) ~[reactor-core-3.8.0.jar:3.8.0]
at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:144) ~[reactor-core-3.8.0.jar:3.8.0]
at reactor.netty.channel.FluxReceive.onInboundComplete(FluxReceive.java:419) ~[reactor-netty-core-1.3.0.jar:1.3.0]
at reactor.netty.channel.ChannelOperations.onInboundComplete(ChannelOperations.java:465) ~[reactor-netty-core-1.3.0.jar:1.3.0]
at reactor.netty.channel.ChannelOperations.terminate(ChannelOperations.java:519) ~[reactor-netty-core-1.3.0.jar:1.3.0]
at reactor.netty.http.client.HttpClientOperations.onInboundNext(HttpClientOperations.java:924) ~[reactor-netty-http-1.3.0.jar:1.3.0]
at reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:115) ~[reactor-netty-core-1.3.0.jar:1.3.0]
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:356) ~[netty-transport-4.2.7.Final.jar:4.2.7.Final]
at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:434) ~[netty-transport-4.2.7.Final.jar:4.2.7.Final]
at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:346) ~[netty-codec-base-4.2.7.Final.jar:4.2.7.Final]
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:318) ~[netty-codec-base-4.2.7.Final.jar:4.2.7.Final]
at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:249) ~[netty-transport-4.2.7.Final.jar:4.2.7.Final]
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:354) ~[netty-transport-4.2.7.Final.jar:4.2.7.Final]
at io.netty.handler.logging.LoggingHandler.channelRead(LoggingHandler.java:280) ~[netty-handler-4.2.7.Final.jar:4.2.7.Final]
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:354) ~[netty-transport-4.2.7.Final.jar:4.2.7.Final]
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1429) ~[netty-transport-4.2.7.Final.jar:4.2.7.Final]
at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:918) ~[netty-transport-4.2.7.Final.jar:4.2.7.Final]
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:168) ~[netty-transport-4.2.7.Final.jar:4.2.7.Final]
at io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe.handle(AbstractNioChannel.java:445) ~[netty-transport-4.2.7.Final.jar:4.2.7.Final]
at io.netty.channel.nio.NioIoHandler$DefaultNioRegistration.handle(NioIoHandler.java:388) ~[netty-transport-4.2.7.Final.jar:4.2.7.Final]
at io.netty.channel.nio.NioIoHandler.processSelectedKey(NioIoHandler.java:596) ~[netty-transport-4.2.7.Final.jar:4.2.7.Final]
at io.netty.channel.nio.NioIoHandler.processSelectedKeysOptimized(NioIoHandler.java:571) ~[netty-transport-4.2.7.Final.jar:4.2.7.Final]
at io.netty.channel.nio.NioIoHandler.processSelectedKeys(NioIoHandler.java:512) ~[netty-transport-4.2.7.Final.jar:4.2.7.Final]
at io.netty.channel.nio.NioIoHandler.run(NioIoHandler.java:484) ~[netty-transport-4.2.7.Final.jar:4.2.7.Final]
at io.netty.channel.SingleThreadIoEventLoop.runIo(SingleThreadIoEventLoop.java:225) ~[netty-transport-4.2.7.Final.jar:4.2.7.Final]
at io.netty.channel.SingleThreadIoEventLoop.run(SingleThreadIoEventLoop.java:196) ~[netty-transport-4.2.7.Final.jar:4.2.7.Final]
at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:1193) ~[netty-common-4.2.7.Final.jar:4.2.7.Final]
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.2.7.Final.jar:4.2.7.Final]
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.2.7.Final.jar:4.2.7.Final]
at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]

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

Start at the streaming tool-call flow used by ChatClient, then inspect the DefaultToolCallingManager warning and MessageAggregator stack trace shown in the report. Reproduce the Qwen3 stream=true example and compare the emitted tool-call data with the subsequent request; done means the documented reproduction no longer produces the missing function.arguments validation error.

Written by the indexing model from the issue text.

Assessment

Tech stack
java, spring
Domain
api, backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.