From 54a884df6d7f3c642dae6e2b90cd1e39065f70d5 Mon Sep 17 00:00:00 2001 From: liugddx Date: Sun, 19 Oct 2025 23:02:21 +0800 Subject: [PATCH] fix: enhance content extraction from chat response to handle multiple generations with null content Signed-off-by: liugddx --- .../ai/chat/client/DefaultChatClient.java | 15 +++++++-- .../chat/client/DefaultChatClientTests.java | 31 +++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/DefaultChatClient.java b/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/DefaultChatClient.java index 20b207d5c5c..197b00f7274 100644 --- a/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/DefaultChatClient.java +++ b/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/DefaultChatClient.java @@ -521,10 +521,21 @@ private ChatClientResponse doGetObservableChatClientResponse(ChatClientRequest c @Nullable private static String getContentFromChatResponse(@Nullable ChatResponse chatResponse) { - return Optional.ofNullable(chatResponse) - .map(ChatResponse::getResult) + if (chatResponse == null || CollectionUtils.isEmpty(chatResponse.getResults())) { + return null; + } + // Iterate through all generations to find the first one with non-null content + // This handles cases where models return multiple generations (e.g., Bedrock + // Converse API + // with openai.gpt-oss models may return reasoning output first with null + // content, + // followed by the actual response) + return chatResponse.getResults() + .stream() .map(Generation::getOutput) .map(AbstractMessage::getText) + .filter(text -> text != null) + .findFirst() .orElse(null); } diff --git a/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/DefaultChatClientTests.java b/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/DefaultChatClientTests.java index 07adcf72b48..77283137ff4 100644 --- a/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/DefaultChatClientTests.java +++ b/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/DefaultChatClientTests.java @@ -969,6 +969,37 @@ void whenChatResponseContentIsNull() { assertThat(content).isNull(); } + @Test + void whenMultipleGenerationsWithFirstContentNull() { + // Test case for Bedrock Converse API with openai.gpt-oss models + // which return multiple generations where the first one has null content + // (reasoning output) + // and the second one contains the actual response + ChatModel chatModel = mock(ChatModel.class); + ArgumentCaptor promptCaptor = ArgumentCaptor.forClass(Prompt.class); + given(chatModel.call(promptCaptor.capture())) + .willReturn(new ChatResponse(List.of(new Generation(new AssistantMessage(null)), // First + // generation + // with + // null + // content + new Generation(new AssistantMessage("Hello! How can I help you today?")) // Second + // generation + // with + // actual + // content + ))); + + ChatClient chatClient = new DefaultChatClientBuilder(chatModel).build(); + DefaultChatClient.DefaultChatClientRequestSpec chatClientRequestSpec = (DefaultChatClient.DefaultChatClientRequestSpec) chatClient + .prompt("Hello"); + DefaultChatClient.DefaultCallResponseSpec spec = (DefaultChatClient.DefaultCallResponseSpec) chatClientRequestSpec + .call(); + + String content = spec.content(); + assertThat(content).isEqualTo("Hello! How can I help you today?"); + } + @Test void whenResponseEntityWithParameterizedTypeIsNull() { ChatClient chatClient = new DefaultChatClientBuilder(mock(ChatModel.class)).build();