From ae2500bd9d7e17c509a2d2191a39713b7a7de695 Mon Sep 17 00:00:00 2001 From: Dongha Koo Date: Mon, 14 Jul 2025 20:47:50 +0900 Subject: [PATCH 1/3] GH-3657: Fix DeepSeek tool call content null issue Closes #3657 * Add content fallback for DeepSeek when only tool calls are present * Ensures AssistantMessage has proper output for downstream processing * Add test case for AssistantMessage with tool calls only Signed-off-by: Dongha Koo --- .../ai/deepseek/DeepSeekChatModel.java | 5 ++++- .../ai/chat/client/ChatClientResponseTests.java | 15 +++++++++++++++ .../ai/chat/messages/AssistantMessage.java | 13 +++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/models/spring-ai-deepseek/src/main/java/org/springframework/ai/deepseek/DeepSeekChatModel.java b/models/spring-ai-deepseek/src/main/java/org/springframework/ai/deepseek/DeepSeekChatModel.java index 6295666e07f..dc52688706b 100644 --- a/models/spring-ai-deepseek/src/main/java/org/springframework/ai/deepseek/DeepSeekChatModel.java +++ b/models/spring-ai-deepseek/src/main/java/org/springframework/ai/deepseek/DeepSeekChatModel.java @@ -76,6 +76,7 @@ * backed by {@link DeepSeekApi}. * * @author Geng Rong + * @author Dongha Koo */ public class DeepSeekChatModel implements ChatModel { @@ -338,7 +339,9 @@ private Generation buildGeneration(Choice choice, Map metadata) String textContent = choice.message().content(); String reasoningContent = choice.message().reasoningContent(); - + if (textContent == null && !toolCalls.isEmpty()) { + textContent = "__tool_call__"; + } DeepSeekAssistantMessage assistantMessage = new DeepSeekAssistantMessage(textContent, reasoningContent, metadata, toolCalls); return new Generation(assistantMessage, generationMetadataBuilder.build()); diff --git a/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/ChatClientResponseTests.java b/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/ChatClientResponseTests.java index 234309a02c8..875a5a33c60 100644 --- a/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/ChatClientResponseTests.java +++ b/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/ChatClientResponseTests.java @@ -17,9 +17,12 @@ package org.springframework.ai.chat.client; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; +import org.springframework.ai.chat.messages.AssistantMessage; +import org.springframework.ai.chat.model.Generation; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -28,6 +31,7 @@ * Unit tests for {@link ChatClientResponse}. * * @author Thomas Vitale + * @author Dongha Koo */ class ChatClientResponseTests { @@ -82,4 +86,15 @@ void whenMutateThenImmutableContext() { assertThat(response.context()).containsEntry("key", "value"); } + @Test + void whenAssistantMessageHasOnlyToolCalls_thenContentIsToolCallMarker() { + var toolCall = new AssistantMessage.ToolCall("tool-1", "function", "doSomething", "{\"foo\":\"bar\"}"); + var assistantMessage = new AssistantMessage(null, Map.of(), List.of(toolCall), List.of()); + + assertThat(assistantMessage.getDisplayText()).isEqualTo("__tool_call__"); + + var generation = new Generation(assistantMessage); + assertThat(generation.getOutput().getDisplayText()).isEqualTo("__tool_call__"); + } + } diff --git a/spring-ai-model/src/main/java/org/springframework/ai/chat/messages/AssistantMessage.java b/spring-ai-model/src/main/java/org/springframework/ai/chat/messages/AssistantMessage.java index b092de2d6da..09f23e4bb75 100644 --- a/spring-ai-model/src/main/java/org/springframework/ai/chat/messages/AssistantMessage.java +++ b/spring-ai-model/src/main/java/org/springframework/ai/chat/messages/AssistantMessage.java @@ -33,6 +33,7 @@ * * @author Mark Pollack * @author Christian Tzolov + * @author Dongha Koo * @since 1.0.0 */ public class AssistantMessage extends AbstractMessage implements MediaContent { @@ -104,4 +105,16 @@ public record ToolCall(String id, String type, String name, String arguments) { } + /** + * Returns a safe, non-null text representation. If text is null or empty and + * toolCalls exist, returns "__tool_call__". + */ + public String getDisplayText() { + String text = super.getText(); + if ((text == null || text.trim().isEmpty()) && this.hasToolCalls()) { + return "__tool_call__"; + } + return (text != null) ? text : ""; + } + } From 47f2ba82ec38250ad0ce41a60c80406135d522a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B5=AC=EB=8F=99=ED=95=98?= <52133649+Acacian@users.noreply.github.com> Date: Mon, 14 Jul 2025 22:20:42 +0900 Subject: [PATCH 2/3] Update spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/ChatClientResponseTests.java MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: jonghoonpark Signed-off-by: 구동하 <52133649+Acacian@users.noreply.github.com> --- .../springframework/ai/chat/client/ChatClientResponseTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/ChatClientResponseTests.java b/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/ChatClientResponseTests.java index 875a5a33c60..e3f5a5e3f76 100644 --- a/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/ChatClientResponseTests.java +++ b/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/ChatClientResponseTests.java @@ -21,6 +21,7 @@ import java.util.Map; import org.junit.jupiter.api.Test; + import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.model.Generation; From 4b054b099bf2109c7020653818b41ab0b3e06617 Mon Sep 17 00:00:00 2001 From: Dongha Koo Date: Mon, 14 Jul 2025 23:03:10 +0900 Subject: [PATCH 3/3] GH-3657: Fix DeepSeek tool call content null issue Closes #3657 * Set content = "__tool_call__" when content is null and toolCalls are present * Prevents schema validation errors in downstream clients * Add unit test: ChatClientResponseTests.whenAssistantMessageHasOnlyToolCalls_thenContentIsToolCallMarker * Apply code style: import formatting, copyright year Signed-off-by: Dongha Koo --- .../java/org/springframework/ai/deepseek/DeepSeekChatModel.java | 2 +- .../org/springframework/ai/chat/messages/AssistantMessage.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/models/spring-ai-deepseek/src/main/java/org/springframework/ai/deepseek/DeepSeekChatModel.java b/models/spring-ai-deepseek/src/main/java/org/springframework/ai/deepseek/DeepSeekChatModel.java index dc52688706b..3b762a6d58d 100644 --- a/models/spring-ai-deepseek/src/main/java/org/springframework/ai/deepseek/DeepSeekChatModel.java +++ b/models/spring-ai-deepseek/src/main/java/org/springframework/ai/deepseek/DeepSeekChatModel.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-ai-model/src/main/java/org/springframework/ai/chat/messages/AssistantMessage.java b/spring-ai-model/src/main/java/org/springframework/ai/chat/messages/AssistantMessage.java index 09f23e4bb75..c63ad1dba46 100644 --- a/spring-ai-model/src/main/java/org/springframework/ai/chat/messages/AssistantMessage.java +++ b/spring-ai-model/src/main/java/org/springframework/ai/chat/messages/AssistantMessage.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License.