Skip to content

Commit 4af9a34

Browse files
committed
fix: map ChatResponse usage metadata to LlmResponse
1 parent d37f6ee commit 4af9a34

2 files changed

Lines changed: 92 additions & 6 deletions

File tree

contrib/spring-ai/src/main/java/com/google/adk/models/springai/MessageConverter.java

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.google.adk.models.LlmResponse;
2323
import com.google.genai.types.Content;
2424
import com.google.genai.types.FunctionCall;
25+
import com.google.genai.types.GenerateContentResponseUsageMetadata;
2526
import com.google.genai.types.Part;
2627
import java.net.URI;
2728
import java.util.ArrayList;
@@ -32,6 +33,8 @@
3233
import org.springframework.ai.chat.messages.SystemMessage;
3334
import org.springframework.ai.chat.messages.ToolResponseMessage;
3435
import org.springframework.ai.chat.messages.UserMessage;
36+
import org.springframework.ai.chat.metadata.EmptyUsage;
37+
import org.springframework.ai.chat.metadata.Usage;
3538
import org.springframework.ai.chat.model.ChatResponse;
3639
import org.springframework.ai.chat.model.Generation;
3740
import org.springframework.ai.chat.prompt.ChatOptions;
@@ -318,11 +321,27 @@ public LlmResponse toLlmResponse(ChatResponse chatResponse, boolean isStreaming)
318321
boolean isPartial = isStreaming && isPartialResponse(assistantMessage);
319322
boolean isTurnComplete = !isStreaming || isTurnCompleteResponse(chatResponse);
320323

321-
return LlmResponse.builder()
322-
.content(content)
323-
.partial(isPartial)
324-
.turnComplete(isTurnComplete)
325-
.build();
324+
LlmResponse.Builder responseBuilder =
325+
LlmResponse.builder().content(content).partial(isPartial).turnComplete(isTurnComplete);
326+
327+
if (chatResponse.getMetadata() != null
328+
&& chatResponse.getMetadata().getUsage() != null
329+
&& !(chatResponse.getMetadata().getUsage() instanceof EmptyUsage)) {
330+
Usage springUsage = chatResponse.getMetadata().getUsage();
331+
332+
GenerateContentResponseUsageMetadata adkUsage =
333+
GenerateContentResponseUsageMetadata.builder()
334+
.promptTokenCount(nullSafeInt(springUsage.getPromptTokens()))
335+
.candidatesTokenCount(nullSafeInt(springUsage.getCompletionTokens()))
336+
.totalTokenCount(nullSafeInt(springUsage.getTotalTokens()))
337+
.build();
338+
responseBuilder.usageMetadata(adkUsage);
339+
}
340+
return responseBuilder.build();
341+
}
342+
343+
private int nullSafeInt(Integer value) {
344+
return value != null ? value.intValue() : 0;
326345
}
327346

328347
/** Determines if an assistant message represents a partial response in streaming. */

contrib/spring-ai/src/test/java/com/google/adk/models/springai/MessageConverterTest.java

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
package com.google.adk.models.springai;
1717

1818
import static org.assertj.core.api.Assertions.assertThat;
19-
import static org.junit.jupiter.api.Assertions.assertThrows;
19+
import static org.junit.jupiter.api.Assertions.*;
20+
import static org.junit.jupiter.api.Assertions.assertEquals;
2021

2122
import com.fasterxml.jackson.databind.ObjectMapper;
2223
import com.google.adk.models.LlmRequest;
@@ -33,6 +34,8 @@
3334
import org.springframework.ai.chat.messages.Message;
3435
import org.springframework.ai.chat.messages.SystemMessage;
3536
import org.springframework.ai.chat.messages.UserMessage;
37+
import org.springframework.ai.chat.metadata.ChatResponseMetadata;
38+
import org.springframework.ai.chat.metadata.DefaultUsage;
3639
import org.springframework.ai.chat.model.ChatResponse;
3740
import org.springframework.ai.chat.model.Generation;
3841
import org.springframework.ai.chat.prompt.Prompt;
@@ -237,6 +240,70 @@ void testToLlmResponseFromChatResponseWithToolCalls() {
237240
assertThat(functionCallPart.functionCall().get().id()).contains("call_123");
238241
}
239242

243+
@Test
244+
void testUsageMetadataShouldBeEmptyWhenSpringAiMetadataIsNull() {
245+
MessageConverter converter = new MessageConverter(new ObjectMapper());
246+
AssistantMessage assistantMessage = new AssistantMessage("intermediate chunk");
247+
Generation generation = new Generation(assistantMessage);
248+
249+
ChatResponse chatResponse = new ChatResponse(List.of(generation), null);
250+
251+
LlmResponse llmResponse = converter.toLlmResponse(chatResponse, true);
252+
253+
assertTrue(
254+
llmResponse.usageMetadata().isEmpty(),
255+
"Expected usageMetadata to be empty for intermediate stream chunks lacking metadata");
256+
}
257+
258+
@Test
259+
void testUsageMetadataShouldBeEmptyWhenSpringAiUsageIsNull() {
260+
MessageConverter converter = new MessageConverter(new ObjectMapper());
261+
AssistantMessage assistantMessage = new AssistantMessage("intermediate chunk");
262+
Generation generation = new Generation(assistantMessage);
263+
264+
ChatResponseMetadata metadata = ChatResponseMetadata.builder().id("resp-no-usage").build();
265+
266+
ChatResponse chatResponse = new ChatResponse(List.of(generation), metadata);
267+
268+
LlmResponse llmResponse = converter.toLlmResponse(chatResponse, true);
269+
270+
assertTrue(
271+
llmResponse.usageMetadata().isEmpty(),
272+
"Expected usageMetadata to be empty when metadata exists but usage is null");
273+
}
274+
275+
@Test
276+
void testUsageMetadataShouldDefaultToZeroWhenSpringAiTokensAreNull() {
277+
MessageConverter converter = new MessageConverter(new ObjectMapper());
278+
AssistantMessage assistantMessage = new AssistantMessage("final chunk");
279+
Generation generation = new Generation(assistantMessage);
280+
281+
// Anonymous implementation to simulate incomplete provider data where some token counts are
282+
// null
283+
DefaultUsage incompleteUsage = new DefaultUsage(null, null, 42);
284+
ChatResponseMetadata metadata =
285+
ChatResponseMetadata.builder().id("resp-partial-tokens").usage(incompleteUsage).build();
286+
287+
ChatResponse chatResponse = new ChatResponse(List.of(generation), metadata);
288+
289+
LlmResponse llmResponse = converter.toLlmResponse(chatResponse, false);
290+
291+
assertTrue(llmResponse.usageMetadata().isPresent(), "Expected usageMetadata to be present");
292+
293+
assertEquals(
294+
0,
295+
llmResponse.usageMetadata().get().promptTokenCount().orElse(-1),
296+
"Null prompt tokens should default to 0");
297+
assertEquals(
298+
0,
299+
llmResponse.usageMetadata().get().candidatesTokenCount().orElse(-1),
300+
"Null completion tokens should default to 0");
301+
assertEquals(
302+
42,
303+
llmResponse.usageMetadata().get().totalTokenCount().orElse(-1),
304+
"Total tokens should be mapped correctly");
305+
}
306+
240307
@Test
241308
void testToolCallIdPreservedInConversion() {
242309
// Create AssistantMessage with tool call including ID

0 commit comments

Comments
 (0)