{"record":{"id":"bf7a3756d7c9af52","repo":"spring-projects/spring-ai","slug":"currently-only-one-tool-call-is-supported-per-mess","errorCode":null,"errorMessage":"Currently only one tool call is supported per message!","messagePattern":"Currently only one tool call is supported per message!","errorType":"exception","errorClass":"java.lang.IllegalStateException","httpStatus":null,"severity":"error","filePath":"models/spring-ai-deepseek/src/main/java/org/springframework/ai/deepseek/api/DeepSeekStreamFunctionCallingHelper.java","lineNumber":103,"sourceCode":"\t\tString name = (current.name() != null ? current.name() : (previous != null ? previous.name() : null));\n\t\tString toolCallId = (current.toolCallId() != null ? current.toolCallId()\n\t\t\t\t: (previous != null ? previous.toolCallId() : null));\n\n\t\tBoolean prefix = (current.prefix() != null ? current.prefix() : (previous != null ? previous.prefix() : null));\n\t\tString reasoningContent = (current.reasoningContent() != null ? current.reasoningContent()\n\t\t\t\t: (previous != null ? previous.reasoningContent() : null));\n\n\t\tList<ToolCall> toolCalls = new ArrayList<>();\n\t\tToolCall lastPreviousTooCall = null;\n\t\tif (previous != null && !CollectionUtils.isEmpty(previous.toolCalls())) {\n\t\t\tlastPreviousTooCall = previous.toolCalls().get(previous.toolCalls().size() - 1);\n\t\t\tif (previous.toolCalls().size() > 1) {\n\t\t\t\ttoolCalls.addAll(previous.toolCalls().subList(0, previous.toolCalls().size() - 1));\n\t\t\t}\n\t\t}\n\t\tif (!CollectionUtils.isEmpty(current.toolCalls())) {\n\t\t\tif (current.toolCalls().size() > 1) {\n\t\t\t\tthrow new IllegalStateException(\"Currently only one tool call is supported per message!\");\n\t\t\t}\n\t\t\tvar currentToolCall = current.toolCalls().iterator().next();\n\t\t\tif (StringUtils.hasText(currentToolCall.id())) {\n\t\t\t\tif (lastPreviousTooCall != null) {\n\t\t\t\t\ttoolCalls.add(lastPreviousTooCall);\n\t\t\t\t}\n\t\t\t\ttoolCalls.add(currentToolCall);\n\t\t\t}\n\t\t\telse {\n\t\t\t\ttoolCalls.add(merge(lastPreviousTooCall, currentToolCall));\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\tif (lastPreviousTooCall != null) {\n\t\t\t\ttoolCalls.add(lastPreviousTooCall);\n\t\t\t}\n\t\t}\n\t\treturn new ChatCompletionMessage(content, role, name, toolCallId, toolCalls, prefix, reasoningContent);","sourceCodeStart":85,"sourceCodeEnd":121,"githubUrl":"https://github.com/spring-projects/spring-ai/blob/98a7beda4f29d80a71c5837eb4053b03a93a46f7/models/spring-ai-deepseek/src/main/java/org/springframework/ai/deepseek/api/DeepSeekStreamFunctionCallingHelper.java#L85-L121","documentation":"DeepSeekStreamFunctionCallingHelper merges successive streaming chunks that carry partial tool-call deltas. Because DeepSeek streams one tool call per chunk, the merge logic assumes at most one tool call per message; receiving a chunk message containing more than one tool call breaks the incremental assembly and triggers this IllegalStateException.","triggerScenarios":"Processing a DeepSeek chat-completion stream where a ChatCompletionChunk message contains toolCalls().size() > 1 — e.g. the model emits multiple parallel tool calls inside a single streamed message while the helper merges it with previous state.","commonSituations":"Prompts that request multiple tool/function invocations in one turn; server-side changes in DeepSeek streaming format emitting batched tool calls; non-DeepSeek-compatible proxies returning OpenAI-style multi-tool chunks.","solutions":["Restrict the request to a single tool call per turn (set tool_choice to a specific function or parallel_tool_calls=false if supported)","Upgrade spring-ai-deepseek to a version whose stream helper handles multiple tool-call deltas","Aggregate tool calls manually from the raw chunks instead of relying on the merge helper"],"exampleFix":"// before\nChatCompletionRequest req = ChatCompletionRequest.builder().tools(tools).build(); // model may emit multiple calls\n// after\nChatCompletionRequest req = ChatCompletionRequest.builder()\n    .tools(tools)\n    .toolChoice(ChatCompletionRequest.ToolChoice.of(\"my_function\"))\n    .build();","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try { return streamHelper.merge(previous, current); } catch (IllegalStateException e) { if (e.getMessage().contains(\"only one tool call\")) { /* handle chunk with multiple tool calls separately */ } throw e; }","preventionTips":["Limit DeepSeek requests to one tool call per turn (specific tool_choice)","Avoid prompts that encourage parallel tool invocations when streaming","Keep spring-ai-deepseek updated for streaming format changes"],"tags":["deepseek","streaming","tool-calling","illegal-state"],"backgroundTag":"unexpected-response-shape","analyzedSha":"98a7beda4f29d80a71c5837eb4053b03a93a46f7","analyzedAt":"2026-09-11T14:15:49.441Z","contentChangedAt":"2026-09-11T14:15:49.441Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}