From c75ca066f254c386971acf0d76b6fcbffa580a64 Mon Sep 17 00:00:00 2001 From: Javier Aliaga Date: Thu, 5 Feb 2026 10:50:44 +0100 Subject: [PATCH 1/2] feat: Add new fields to conversation api Signed-off-by: Javier Aliaga --- .../java/io/dapr/client/DaprClientImpl.java | 59 ++++++++++- .../io/dapr/client/DaprPreviewClient.java | 5 - .../client/domain/ConversationOutput.java | 1 - .../domain/ConversationRequestAlpha2.java | 21 ++++ .../client/domain/ConversationResponse.java | 1 - .../domain/ConversationResultAlpha2.java | 28 +++++- .../ConversationResultCompletionUsage.java | 99 +++++++++++++++++++ ...versationResultCompletionUsageDetails.java | 73 ++++++++++++++ .../ConversationResultPromptUsageDetails.java | 48 +++++++++ .../client/DaprPreviewClientGrpcTest.java | 45 +++++++++ .../java/io/dapr/client/domain/TestData.java | 27 +++++ 11 files changed, 397 insertions(+), 10 deletions(-) create mode 100644 sdk/src/main/java/io/dapr/client/domain/ConversationResultCompletionUsage.java create mode 100644 sdk/src/main/java/io/dapr/client/domain/ConversationResultCompletionUsageDetails.java create mode 100644 sdk/src/main/java/io/dapr/client/domain/ConversationResultPromptUsageDetails.java create mode 100644 sdk/src/test/java/io/dapr/client/domain/TestData.java diff --git a/sdk/src/main/java/io/dapr/client/DaprClientImpl.java b/sdk/src/main/java/io/dapr/client/DaprClientImpl.java index f6213bf300..cc7d4b890c 100644 --- a/sdk/src/main/java/io/dapr/client/DaprClientImpl.java +++ b/sdk/src/main/java/io/dapr/client/DaprClientImpl.java @@ -42,7 +42,10 @@ import io.dapr.client.domain.ConversationResponseAlpha2; import io.dapr.client.domain.ConversationResultAlpha2; import io.dapr.client.domain.ConversationResultChoices; +import io.dapr.client.domain.ConversationResultCompletionUsage; +import io.dapr.client.domain.ConversationResultCompletionUsageDetails; import io.dapr.client.domain.ConversationResultMessage; +import io.dapr.client.domain.ConversationResultPromptUsageDetails; import io.dapr.client.domain.ConversationToolCalls; import io.dapr.client.domain.ConversationToolCallsOfFunction; import io.dapr.client.domain.ConversationTools; @@ -1793,6 +1796,7 @@ public Mono converseAlpha2(ConversationRequestAlpha2 DaprAiProtos.ConversationResponseAlpha2 conversationResponse = conversationResponseMono.block(); assert conversationResponse != null; + List results = buildConversationResults(conversationResponse.getOutputsList()); return Mono.just(new ConversationResponseAlpha2(conversationResponse.getContextId(), results)); } catch (Exception ex) { @@ -1857,6 +1861,33 @@ private DaprAiProtos.ConversationRequestAlpha2 buildConversationRequestProto(Con builder.addInputs(inputBuilder.build()); } + + if (request.getResponseFormat() != null) { + Map responseParams = request.getResponseFormat() + .entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + e -> { + try { + return ProtobufValueHelper.toProtobufValue(e.getValue()); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + )); + + builder.setResponseFormat(Struct.newBuilder().putAllFields(responseParams).build()); + } + + if (request.getPromptCacheRetention() != null) { + Duration javaDuration = request.getPromptCacheRetention(); + builder.setPromptCacheRetention( + com.google.protobuf.Duration.newBuilder() + .setSeconds(javaDuration.getSeconds()) + .setNanos(javaDuration.getNano()) + .build() + ); + } return builder.build(); } @@ -1974,14 +2005,38 @@ private List buildConversationResults( for (DaprAiProtos.ConversationResultChoices protoChoice : protoResult.getChoicesList()) { ConversationResultMessage message = buildConversationResultMessage(protoChoice); choices.add(new ConversationResultChoices(protoChoice.getFinishReason(), protoChoice.getIndex(), message)); - } + } - results.add(new ConversationResultAlpha2(choices)); + results.add(new ConversationResultAlpha2( + choices, + protoResult.getModel(), + getConversationResultCompletionUsage(protoResult)) + ); } return results; } + private static ConversationResultCompletionUsage getConversationResultCompletionUsage( + DaprAiProtos.ConversationResultAlpha2 protoResult) { + var usage = new ConversationResultCompletionUsage( + protoResult.getUsage().getCompletionTokens(), + protoResult.getUsage().getPromptTokens(), + protoResult.getUsage().getTotalTokens()); + + usage.setCompletionTokenDetails(new ConversationResultCompletionUsageDetails( + protoResult.getUsage().getCompletionTokensDetails().getAcceptedPredictionTokens(), + protoResult.getUsage().getCompletionTokensDetails().getAudioTokens(), + protoResult.getUsage().getCompletionTokensDetails().getReasoningTokens(), + protoResult.getUsage().getCompletionTokensDetails().getRejectedPredictionTokens())); + + usage.setPromptTokenDetails(new ConversationResultPromptUsageDetails( + protoResult.getUsage().getPromptTokensDetails().getAudioTokens(), + protoResult.getUsage().getPromptTokensDetails().getCachedTokens() + )); + return usage; + } + private ConversationResultMessage buildConversationResultMessage(DaprAiProtos.ConversationResultChoices protoChoice) { if (!protoChoice.hasMessage()) { return null; diff --git a/sdk/src/main/java/io/dapr/client/DaprPreviewClient.java b/sdk/src/main/java/io/dapr/client/DaprPreviewClient.java index fa4a1eb9b7..dd92d936a2 100644 --- a/sdk/src/main/java/io/dapr/client/DaprPreviewClient.java +++ b/sdk/src/main/java/io/dapr/client/DaprPreviewClient.java @@ -17,20 +17,15 @@ import io.dapr.client.domain.BulkPublishRequest; import io.dapr.client.domain.BulkPublishResponse; import io.dapr.client.domain.BulkPublishResponseFailedEntry; -import io.dapr.client.domain.CloudEvent; import io.dapr.client.domain.ConversationRequest; import io.dapr.client.domain.ConversationRequestAlpha2; import io.dapr.client.domain.ConversationResponse; import io.dapr.client.domain.ConversationResponseAlpha2; import io.dapr.client.domain.DecryptRequestAlpha1; -import io.dapr.client.domain.DeleteJobRequest; import io.dapr.client.domain.EncryptRequestAlpha1; -import io.dapr.client.domain.GetJobRequest; -import io.dapr.client.domain.GetJobResponse; import io.dapr.client.domain.LockRequest; import io.dapr.client.domain.QueryStateRequest; import io.dapr.client.domain.QueryStateResponse; -import io.dapr.client.domain.ScheduleJobRequest; import io.dapr.client.domain.UnlockRequest; import io.dapr.client.domain.UnlockResponseStatus; import io.dapr.client.domain.query.Query; diff --git a/sdk/src/main/java/io/dapr/client/domain/ConversationOutput.java b/sdk/src/main/java/io/dapr/client/domain/ConversationOutput.java index efe82e2eb3..fc9a8e7c3b 100644 --- a/sdk/src/main/java/io/dapr/client/domain/ConversationOutput.java +++ b/sdk/src/main/java/io/dapr/client/domain/ConversationOutput.java @@ -13,7 +13,6 @@ package io.dapr.client.domain; -import java.util.Collections; import java.util.Map; /** diff --git a/sdk/src/main/java/io/dapr/client/domain/ConversationRequestAlpha2.java b/sdk/src/main/java/io/dapr/client/domain/ConversationRequestAlpha2.java index 2f85fbd7d6..7d472ca37f 100644 --- a/sdk/src/main/java/io/dapr/client/domain/ConversationRequestAlpha2.java +++ b/sdk/src/main/java/io/dapr/client/domain/ConversationRequestAlpha2.java @@ -13,6 +13,7 @@ package io.dapr.client.domain; +import java.time.Duration; import java.util.List; import java.util.Map; @@ -31,6 +32,8 @@ public class ConversationRequestAlpha2 { private String toolChoice; private Map parameters; private Map metadata; + private Map responseFormat; + private Duration promptCacheRetention; /** * Constructs a ConversationRequestAlpha2 with a component name and conversation inputs. @@ -206,4 +209,22 @@ public ConversationRequestAlpha2 setMetadata(Map metadata) { this.metadata = metadata; return this; } + + public Map getResponseFormat() { + return responseFormat; + } + + public ConversationRequestAlpha2 setResponseFormat(Map responseFormat) { + this.responseFormat = responseFormat; + return this; + } + + public Duration getPromptCacheRetention() { + return promptCacheRetention; + } + + public ConversationRequestAlpha2 setPromptCacheRetention(Duration promptCacheRetention) { + this.promptCacheRetention = promptCacheRetention; + return this; + } } diff --git a/sdk/src/main/java/io/dapr/client/domain/ConversationResponse.java b/sdk/src/main/java/io/dapr/client/domain/ConversationResponse.java index 8059365544..6066df55f3 100644 --- a/sdk/src/main/java/io/dapr/client/domain/ConversationResponse.java +++ b/sdk/src/main/java/io/dapr/client/domain/ConversationResponse.java @@ -13,7 +13,6 @@ package io.dapr.client.domain; -import java.util.Collections; import java.util.List; /** diff --git a/sdk/src/main/java/io/dapr/client/domain/ConversationResultAlpha2.java b/sdk/src/main/java/io/dapr/client/domain/ConversationResultAlpha2.java index 369caeb654..e7842ea921 100644 --- a/sdk/src/main/java/io/dapr/client/domain/ConversationResultAlpha2.java +++ b/sdk/src/main/java/io/dapr/client/domain/ConversationResultAlpha2.java @@ -21,14 +21,22 @@ public class ConversationResultAlpha2 { private final List choices; + private final String model; + private final ConversationResultCompletionUsage usage; /** * Constructor. * * @param choices the list of conversation result choices. + * @param model the model used for the conversation. + * @param usage the usage of the model. */ - public ConversationResultAlpha2(List choices) { + public ConversationResultAlpha2(List choices, + String model, + ConversationResultCompletionUsage usage) { this.choices = List.copyOf(choices); + this.model = model; + this.usage = usage; } /** @@ -39,4 +47,22 @@ public ConversationResultAlpha2(List choices) { public List getChoices() { return choices; } + + /** + * Gets the model used for the conversation. + * + * @return the model used for the conversation. + */ + public String getModel() { + return model; + } + + /** + * Gets the usage of the model. + * + * @return the usage of the model. + */ + public ConversationResultCompletionUsage getUsage() { + return usage; + } } diff --git a/sdk/src/main/java/io/dapr/client/domain/ConversationResultCompletionUsage.java b/sdk/src/main/java/io/dapr/client/domain/ConversationResultCompletionUsage.java new file mode 100644 index 0000000000..e980b6bbb8 --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/domain/ConversationResultCompletionUsage.java @@ -0,0 +1,99 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client.domain; + +public class ConversationResultCompletionUsage { + private final long completionTokens; + private final long promptTokens; + private final long totalTokens; + + private ConversationResultCompletionUsageDetails completionTokenDetails; + private ConversationResultPromptUsageDetails promptTokenDetails; + + /** + * Constructor. + * + * @param completionTokens completion tokens used. + * @param promptTokens prompt tokens used. + * @param totalTokens total tokens used. + */ + public ConversationResultCompletionUsage(long completionTokens, long promptTokens, long totalTokens) { + this.completionTokens = completionTokens; + this.promptTokens = promptTokens; + this.totalTokens = totalTokens; + } + + /** + * Completion tokens used. + * + * @return completion tokens used. + */ + public long getCompletionTokens() { + return completionTokens; + } + + /** + * Prompt tokens used. + * + * @return prompt tokens used. + */ + public long getPromptTokens() { + return promptTokens; + } + + /** + * Total tokens used. + * + * @return total tokens used. + */ + public long getTotalTokens() { + return totalTokens; + } + + /** + * Completion token details. + * + * @return the completionTokenDetails + */ + public ConversationResultCompletionUsageDetails getCompletionTokenDetails() { + return completionTokenDetails; + } + + /** + * Completion token details. + * + * @param completionTokenDetails the completionTokenDetails to set + */ + public void setCompletionTokenDetails(ConversationResultCompletionUsageDetails completionTokenDetails) { + this.completionTokenDetails = completionTokenDetails; + } + + /** + * Prompt token details. + * + * @return the promptTokenDetails + */ + public ConversationResultPromptUsageDetails getPromptTokenDetails() { + return promptTokenDetails; + } + + /** + * Prompt token details. + * + * @param promptTokenDetails the promptTokenDetails to set + */ + public void setPromptTokenDetails(ConversationResultPromptUsageDetails promptTokenDetails) { + this.promptTokenDetails = promptTokenDetails; + } +} diff --git a/sdk/src/main/java/io/dapr/client/domain/ConversationResultCompletionUsageDetails.java b/sdk/src/main/java/io/dapr/client/domain/ConversationResultCompletionUsageDetails.java new file mode 100644 index 0000000000..8edd1aa277 --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/domain/ConversationResultCompletionUsageDetails.java @@ -0,0 +1,73 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client.domain; + +public class ConversationResultCompletionUsageDetails { + private final long acceptedPredictionTokens; + private final long audioTokens; + private final long reasoningTokens; + private final long rejectedPredictionTokens; + + /** + * Constructor. + * + * @param acceptedPredictionTokens accepted prediction tokens used. + * @param audioTokens audio tokens used. + * @param reasoningTokens reasoning tokens used. + * @param rejectedPredictionTokens rejected prediction tokens used. + */ + public ConversationResultCompletionUsageDetails(long acceptedPredictionTokens, long audioTokens, + long reasoningTokens, long rejectedPredictionTokens) { + this.acceptedPredictionTokens = acceptedPredictionTokens; + this.audioTokens = audioTokens; + this.reasoningTokens = reasoningTokens; + this.rejectedPredictionTokens = rejectedPredictionTokens; + } + + /** + * Accepted prediction tokens used. + * + * @return accepted prediction tokens used. + */ + public long getAcceptedPredictionTokens() { + return acceptedPredictionTokens; + } + + /** + * Audio tokens used. + * + * @return audio tokens used. + */ + public long getAudioTokens() { + return audioTokens; + } + + /** + * Reasoning tokens used. + * + * @return reasoning tokens used. + */ + public long getReasoningTokens() { + return reasoningTokens; + } + + /** + * Rejected prediction tokens used. + * + * @return rejected prediction tokens used. + */ + public long getRejectedPredictionTokens() { + return rejectedPredictionTokens; + } +} diff --git a/sdk/src/main/java/io/dapr/client/domain/ConversationResultPromptUsageDetails.java b/sdk/src/main/java/io/dapr/client/domain/ConversationResultPromptUsageDetails.java new file mode 100644 index 0000000000..b49483cede --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/domain/ConversationResultPromptUsageDetails.java @@ -0,0 +1,48 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client.domain; + +public class ConversationResultPromptUsageDetails { + private final long audioTokens; + private final long cachedTokens; + + /** + * Constructor. + * + * @param audioTokens audio input tokens present in the prompt. + * @param cachedTokens cached tokens present in the prompt. + */ + public ConversationResultPromptUsageDetails(long audioTokens, long cachedTokens) { + this.audioTokens = audioTokens; + this.cachedTokens = cachedTokens; + } + + /** + * Audio input tokens present in the prompt. + * + * @return audio input tokens present in the prompt. + */ + public long getAudioTokens() { + return audioTokens; + } + + /** + * Cached tokens present in the prompt. + * + * @return cached tokens present in the prompt. + */ + public long getCachedTokens() { + return cachedTokens; + } +} diff --git a/sdk/src/test/java/io/dapr/client/DaprPreviewClientGrpcTest.java b/sdk/src/test/java/io/dapr/client/DaprPreviewClientGrpcTest.java index 7c22a4326b..ef51126fe5 100644 --- a/sdk/src/test/java/io/dapr/client/DaprPreviewClientGrpcTest.java +++ b/sdk/src/test/java/io/dapr/client/DaprPreviewClientGrpcTest.java @@ -17,6 +17,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.protobuf.ByteString; +import com.google.protobuf.Struct; +import com.google.protobuf.Value; import io.dapr.client.domain.AssistantMessage; import io.dapr.client.domain.BulkPublishEntry; import io.dapr.client.domain.BulkPublishRequest; @@ -43,6 +45,7 @@ import io.dapr.client.domain.QueryStateRequest; import io.dapr.client.domain.QueryStateResponse; import io.dapr.client.domain.SystemMessage; +import io.dapr.client.domain.TestData; import io.dapr.client.domain.ToolMessage; import io.dapr.client.domain.UnlockResponseStatus; import io.dapr.client.domain.UserMessage; @@ -75,6 +78,7 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -1061,6 +1065,9 @@ public void converseAlpha2ComplexRequestTest() { Map parameters = new HashMap<>(); parameters.put("max_tokens", "1000"); + var responseFormat = new HashMap(); + responseFormat.put("temperature", 0.7); + responseFormat.put("data", new TestData("Peter", 40)); ConversationRequestAlpha2 request = new ConversationRequestAlpha2("openai", List.of(input)); request.setContextId("test-context"); request.setTemperature(0.7); @@ -1069,11 +1076,31 @@ public void converseAlpha2ComplexRequestTest() { request.setToolChoice("auto"); request.setMetadata(metadata); request.setParameters(parameters); + request.setPromptCacheRetention(Duration.ofDays(1)); + request.setResponseFormat(responseFormat); // Mock response with tool calls DaprAiProtos.ConversationResponseAlpha2 grpcResponse = DaprAiProtos.ConversationResponseAlpha2.newBuilder() .setContextId("test-context") .addOutputs(DaprAiProtos.ConversationResultAlpha2.newBuilder() + .setModel("gpt-3.5-turbo") + .setUsage(DaprAiProtos.ConversationResultAlpha2CompletionUsage.newBuilder() + .setPromptTokens(100) + .setCompletionTokens(100) + .setTotalTokens(200) + .setCompletionTokensDetails(DaprAiProtos.ConversationResultAlpha2CompletionUsageCompletionTokensDetails + .newBuilder() + .setAudioTokens(10) + .setReasoningTokens(11) + .setAcceptedPredictionTokens(222) + .setRejectedPredictionTokens(321) + .build()) + .setPromptTokensDetails(DaprAiProtos.ConversationResultAlpha2CompletionUsagePromptTokensDetails + .newBuilder() + .setAudioTokens(654) + .setCachedTokens(1112) + .build()) + .build()) .addChoices(DaprAiProtos.ConversationResultChoices.newBuilder() .setFinishReason("tool_calls") .setIndex(0) @@ -1108,6 +1135,17 @@ public void converseAlpha2ComplexRequestTest() { assertEquals("tool_calls", choice.getFinishReason()); assertEquals("I'll help you get the weather information.", choice.getMessage().getContent()); assertEquals(1, choice.getMessage().getToolCalls().size()); + assertEquals("gpt-3.5-turbo", response.getOutputs().get(0).getModel()); + assertEquals(100, response.getOutputs().get(0).getUsage().getCompletionTokens()); + assertEquals(100, response.getOutputs().get(0).getUsage().getPromptTokens()); + assertEquals(200, response.getOutputs().get(0).getUsage().getTotalTokens()); + assertEquals(10, response.getOutputs().get(0).getUsage().getCompletionTokenDetails().getAudioTokens()); + assertEquals(11, response.getOutputs().get(0).getUsage().getCompletionTokenDetails().getReasoningTokens()); + assertEquals(222, response.getOutputs().get(0).getUsage().getCompletionTokenDetails().getAcceptedPredictionTokens()); + assertEquals(321, response.getOutputs().get(0).getUsage().getCompletionTokenDetails().getRejectedPredictionTokens()); + assertEquals(654, response.getOutputs().get(0).getUsage().getPromptTokenDetails().getAudioTokens()); + assertEquals(1112, response.getOutputs().get(0).getUsage().getPromptTokenDetails().getCachedTokens()); + ConversationToolCalls toolCall = choice.getMessage().getToolCalls().get(0); assertEquals("call_123", toolCall.getId()); @@ -1128,6 +1166,13 @@ public void converseAlpha2ComplexRequestTest() { assertEquals("value1", capturedRequest.getMetadataMap().get("key1")); assertEquals(1, capturedRequest.getToolsCount()); assertEquals("get_weather", capturedRequest.getTools(0).getFunction().getName()); + assertEquals(Struct.newBuilder() + .putFields("temperature", Value.newBuilder().setNumberValue(0.7).build()) + .putFields("data", Value.newBuilder().setStringValue("TestData{name='Peter', age=40}").build()) + .build(), + capturedRequest.getResponseFormat()); + assertEquals(Duration.ofDays(1).getSeconds(), capturedRequest.getPromptCacheRetention().getSeconds()); + assertEquals(0, capturedRequest.getPromptCacheRetention().getNanos()); } @Test diff --git a/sdk/src/test/java/io/dapr/client/domain/TestData.java b/sdk/src/test/java/io/dapr/client/domain/TestData.java new file mode 100644 index 0000000000..a5dc86356c --- /dev/null +++ b/sdk/src/test/java/io/dapr/client/domain/TestData.java @@ -0,0 +1,27 @@ +package io.dapr.client.domain; + +public class TestData { + private final String name; + private final int age; + + public TestData(String name, int age) { + this.name = name; + this.age = age; + } + + public int getAge() { + return age; + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return "TestData{" + + "name='" + name + "'" + + ", age=" + age + + "}"; + } +} From 4517391e429d62ccf9e4ca4b0aa606869daa0dd7 Mon Sep 17 00:00:00 2001 From: Javier Aliaga Date: Thu, 5 Feb 2026 18:36:51 +0100 Subject: [PATCH 2/2] chore: Modify conversation examples Signed-off-by: Javier Aliaga --- .../conversation/AssistantMessageDemo.java | 3 +- .../examples/conversation/ToolsCallDemo.java | 2 + .../examples/conversation/UsageUtils.java | 83 +++++++++++++++++++ .../conversation/UserMessageDemo.java | 36 ++++++-- sdk/pom.xml | 4 + .../java/io/dapr/client/DaprClientImpl.java | 15 +--- .../domain/ConversationRequestAlpha2.java | 42 +++++++++- .../java/io/dapr/utils/ProtobufUtils.java | 42 ++++++++++ .../client/DaprPreviewClientGrpcTest.java | 13 ++- 9 files changed, 207 insertions(+), 33 deletions(-) create mode 100644 examples/src/main/java/io/dapr/examples/conversation/UsageUtils.java create mode 100644 sdk/src/main/java/io/dapr/utils/ProtobufUtils.java diff --git a/examples/src/main/java/io/dapr/examples/conversation/AssistantMessageDemo.java b/examples/src/main/java/io/dapr/examples/conversation/AssistantMessageDemo.java index 7ff2d43be6..4a9689f4e1 100644 --- a/examples/src/main/java/io/dapr/examples/conversation/AssistantMessageDemo.java +++ b/examples/src/main/java/io/dapr/examples/conversation/AssistantMessageDemo.java @@ -108,9 +108,10 @@ public static void main(String[] args) { // Process and display the response if (response != null && response.getOutputs() != null && !response.getOutputs().isEmpty()) { ConversationResultAlpha2 result = response.getOutputs().get(0); + UsageUtils.printUsage(result); + if (result.getChoices() != null && !result.getChoices().isEmpty()) { ConversationResultChoices choice = result.getChoices().get(0); - if (choice.getMessage() != null && choice.getMessage().getContent() != null) { System.out.printf("Assistant Response: %s%n", choice.getMessage().getContent()); } diff --git a/examples/src/main/java/io/dapr/examples/conversation/ToolsCallDemo.java b/examples/src/main/java/io/dapr/examples/conversation/ToolsCallDemo.java index 30335802c7..1fb58e6c5a 100644 --- a/examples/src/main/java/io/dapr/examples/conversation/ToolsCallDemo.java +++ b/examples/src/main/java/io/dapr/examples/conversation/ToolsCallDemo.java @@ -80,6 +80,8 @@ public static void main(String[] args) { // Process and display the response if (response != null && response.getOutputs() != null && !response.getOutputs().isEmpty()) { ConversationResultAlpha2 result = response.getOutputs().get(0); + UsageUtils.printUsage(result); + if (result.getChoices() != null && !result.getChoices().isEmpty()) { ConversationResultChoices choice = result.getChoices().get(0); diff --git a/examples/src/main/java/io/dapr/examples/conversation/UsageUtils.java b/examples/src/main/java/io/dapr/examples/conversation/UsageUtils.java new file mode 100644 index 0000000000..fdd2f30bf6 --- /dev/null +++ b/examples/src/main/java/io/dapr/examples/conversation/UsageUtils.java @@ -0,0 +1,83 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.examples.conversation; + +import io.dapr.client.domain.ConversationResultAlpha2; +import io.dapr.client.domain.ConversationResultCompletionUsage; +import org.springframework.util.StringUtils; + +public class UsageUtils { + static void printUsage(ConversationResultAlpha2 result) { + if (!StringUtils.hasText(result.getModel())){ + return; + } + + System.out.printf("Conversation model : %s\n", result.getModel()); + var usage = result.getUsage(); + printUsage(usage); + + printCompletionDetails(usage); + printPromptDetails(usage); + + } + + private static void printUsage(ConversationResultCompletionUsage usage) { + System.out.println("Token Usage Details:"); + System.out.printf(" Completion tokens: %d\n", usage.getCompletionTokens()); + System.out.printf(" Prompt tokens: %d\n", usage.getPromptTokens()); + System.out.printf(" Total tokens: %d\n", usage.getTotalTokens()); + System.out.println(); + } + + private static void printPromptDetails(ConversationResultCompletionUsage usage) { + var completionDetails = usage.getCompletionTokenDetails(); + + // Display completion token breakdown if available + System.out.println("Prompt Token Details:"); + if (completionDetails.getReasoningTokens() > 0) { + System.out.printf(" Reasoning tokens: %d\n", completionDetails.getReasoningTokens()); + } + if (completionDetails.getAudioTokens() > 0) { + System.out.printf(" Audio tokens: %d\n", completionDetails.getAudioTokens()); + } + System.out.println(); + } + + private static void printCompletionDetails(ConversationResultCompletionUsage usage) { + // Print detailed token usage information + var completionDetails = usage.getCompletionTokenDetails(); + + System.out.println("Completion Token Details:"); + // If audio tokens are available, display them + if ( completionDetails.getAudioTokens() > 0) { + System.out.printf(" Audio tokens: %d\n", completionDetails.getAudioTokens()); + } + + // Display completion token breakdown if available + if (completionDetails.getReasoningTokens() > 0) { + System.out.printf(" Reasoning tokens: %d\n", completionDetails.getReasoningTokens()); + } + + // Display completion token breakdown if available + if (completionDetails.getAcceptedPredictionTokens() > 0) { + System.out.printf(" Accepted prediction tokens: %d\n", completionDetails.getAcceptedPredictionTokens()); + } + + // Display completion token breakdown if available + if (completionDetails.getRejectedPredictionTokens() > 0) { + System.out.printf(" Rejected prediction tokens: %d\n", completionDetails.getRejectedPredictionTokens()); + } + System.out.println(); + } +} diff --git a/examples/src/main/java/io/dapr/examples/conversation/UserMessageDemo.java b/examples/src/main/java/io/dapr/examples/conversation/UserMessageDemo.java index e5afbc475d..eea8b5ea58 100644 --- a/examples/src/main/java/io/dapr/examples/conversation/UserMessageDemo.java +++ b/examples/src/main/java/io/dapr/examples/conversation/UserMessageDemo.java @@ -22,12 +22,9 @@ import io.dapr.client.domain.ConversationResultAlpha2; import io.dapr.client.domain.ConversationResultChoices; import io.dapr.client.domain.UserMessage; -import io.dapr.config.Properties; -import io.dapr.config.Property; import reactor.core.publisher.Mono; import java.util.List; -import java.util.Map; public class UserMessageDemo { /** @@ -46,18 +43,43 @@ public static void main(String[] args) { // Create conversation input with the user message ConversationInputAlpha2 daprConversationInput = new ConversationInputAlpha2(List.of(userMessage)); + // Define the JSON schema for the response format + String responseSchema = """ + { + "type": "object", + "properties": { + "greeting": { + "type": "string", + "description": "A friendly greeting response" + }, + "phone_number_detected": { + "type": "boolean", + "description": "Whether a phone number was detected in the input" + }, + "detected_number": { + "type": "string", + "description": "The phone number that was detected, if any" + } + }, + "required": ["greeting", "phone_number_detected"], + "additionalProperties": false + } + """; + // Component name is the name provided in the metadata block of the conversation.yaml file. Mono responseMono = client.converseAlpha2(new ConversationRequestAlpha2("echo", - List.of(daprConversationInput)) - .setContextId("contextId") - .setScrubPii(true) - .setTemperature(1.1d)); + List.of(daprConversationInput)) + .setContextId("contextId") + .setScrubPii(true) + .setTemperature(1.1d).setResponseFormat(responseSchema)); ConversationResponseAlpha2 response = responseMono.block(); // Extract and print the conversation result if (response != null && response.getOutputs() != null && !response.getOutputs().isEmpty()) { ConversationResultAlpha2 result = response.getOutputs().get(0); + UsageUtils.printUsage(result); + if (result.getChoices() != null && !result.getChoices().isEmpty()) { ConversationResultChoices choice = result.getChoices().get(0); if (choice.getMessage() != null && choice.getMessage().getContent() != null) { diff --git a/sdk/pom.xml b/sdk/pom.xml index 8a494fd6f5..43c0f625e3 100644 --- a/sdk/pom.xml +++ b/sdk/pom.xml @@ -37,6 +37,10 @@ com.fasterxml.jackson.core jackson-databind + + com.google.protobuf + protobuf-java-util + io.projectreactor reactor-core diff --git a/sdk/src/main/java/io/dapr/client/DaprClientImpl.java b/sdk/src/main/java/io/dapr/client/DaprClientImpl.java index cc7d4b890c..30deb57ded 100644 --- a/sdk/src/main/java/io/dapr/client/DaprClientImpl.java +++ b/sdk/src/main/java/io/dapr/client/DaprClientImpl.java @@ -1863,20 +1863,7 @@ private DaprAiProtos.ConversationRequestAlpha2 buildConversationRequestProto(Con } if (request.getResponseFormat() != null) { - Map responseParams = request.getResponseFormat() - .entrySet().stream() - .collect(Collectors.toMap( - Map.Entry::getKey, - e -> { - try { - return ProtobufValueHelper.toProtobufValue(e.getValue()); - } catch (IOException ex) { - throw new RuntimeException(ex); - } - } - )); - - builder.setResponseFormat(Struct.newBuilder().putAllFields(responseParams).build()); + builder.setResponseFormat(request.getResponseFormat()); } if (request.getPromptCacheRetention() != null) { diff --git a/sdk/src/main/java/io/dapr/client/domain/ConversationRequestAlpha2.java b/sdk/src/main/java/io/dapr/client/domain/ConversationRequestAlpha2.java index 7d472ca37f..0386746533 100644 --- a/sdk/src/main/java/io/dapr/client/domain/ConversationRequestAlpha2.java +++ b/sdk/src/main/java/io/dapr/client/domain/ConversationRequestAlpha2.java @@ -13,6 +13,9 @@ package io.dapr.client.domain; +import com.google.protobuf.Struct; +import io.dapr.utils.ProtobufUtils; + import java.time.Duration; import java.util.List; import java.util.Map; @@ -32,7 +35,7 @@ public class ConversationRequestAlpha2 { private String toolChoice; private Map parameters; private Map metadata; - private Map responseFormat; + private Struct responseFormat; private Duration promptCacheRetention; /** @@ -210,19 +213,52 @@ public ConversationRequestAlpha2 setMetadata(Map metadata) { return this; } - public Map getResponseFormat() { + /** + * Gets the response format in JSON-Schema format. + * + * @return the response format + */ + public Struct getResponseFormat() { return responseFormat; } - public ConversationRequestAlpha2 setResponseFormat(Map responseFormat) { + /** + * Sets the response format in JSON-Schema format. + * Structured output described using a JSON Schema object. + * Use this when you want typed structured output. + * Supported by Deepseek, Google AI, Hugging Face, OpenAI, and Anthropic components + * + * @param responseFormat the response format to set + * @return the current instance of {@link ConversationRequestAlpha2} + */ + public ConversationRequestAlpha2 setResponseFormat(Struct responseFormat) { this.responseFormat = responseFormat; return this; } + public ConversationRequestAlpha2 setResponseFormat(String responseFormat) { + this.responseFormat = ProtobufUtils.jsonToStruct(responseFormat); + return this; + } + + /** + * retention duration for the prompt cache. + * + * @return the prompt cache retention duration + */ public Duration getPromptCacheRetention() { return promptCacheRetention; } + /** + * Retention duration for the prompt cache. + * When set, enables extended prompt caching so cached prefixes stay active longer. + * With OpenAI, supports up to 24 hours. + * See [OpenAI prompt caching](https://platform.openai.com/docs/guides/prompt-caching#prompt-cache-retention). + * + * @param promptCacheRetention the prompt cache retention duration + * @return the current instance of {@link ConversationRequestAlpha2} + */ public ConversationRequestAlpha2 setPromptCacheRetention(Duration promptCacheRetention) { this.promptCacheRetention = promptCacheRetention; return this; diff --git a/sdk/src/main/java/io/dapr/utils/ProtobufUtils.java b/sdk/src/main/java/io/dapr/utils/ProtobufUtils.java new file mode 100644 index 0000000000..99ee1ef8fa --- /dev/null +++ b/sdk/src/main/java/io/dapr/utils/ProtobufUtils.java @@ -0,0 +1,42 @@ +/* + * Copyright 2026 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.utils; + +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.Struct; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ProtobufUtils { + private static final Logger log = LoggerFactory.getLogger(ProtobufUtils.class); + + /** + * Converts a JSON string to a protobuf Struct. + * + * @param json JSON string. + * @return Protobuf Struct. + */ + public static Struct jsonToStruct(String json) { + Struct.Builder builder = Struct.newBuilder(); + try { + com.google.protobuf.util.JsonFormat.parser() + .ignoringUnknownFields() // optional + .merge(json, builder); + } catch (InvalidProtocolBufferException e) { + log.error("Failed to parse json to protobuf struct", e); + return builder.build(); + } + return builder.build(); + } +} diff --git a/sdk/src/test/java/io/dapr/client/DaprPreviewClientGrpcTest.java b/sdk/src/test/java/io/dapr/client/DaprPreviewClientGrpcTest.java index ef51126fe5..bd734692da 100644 --- a/sdk/src/test/java/io/dapr/client/DaprPreviewClientGrpcTest.java +++ b/sdk/src/test/java/io/dapr/client/DaprPreviewClientGrpcTest.java @@ -45,7 +45,6 @@ import io.dapr.client.domain.QueryStateRequest; import io.dapr.client.domain.QueryStateResponse; import io.dapr.client.domain.SystemMessage; -import io.dapr.client.domain.TestData; import io.dapr.client.domain.ToolMessage; import io.dapr.client.domain.UnlockResponseStatus; import io.dapr.client.domain.UserMessage; @@ -1065,9 +1064,9 @@ public void converseAlpha2ComplexRequestTest() { Map parameters = new HashMap<>(); parameters.put("max_tokens", "1000"); - var responseFormat = new HashMap(); - responseFormat.put("temperature", 0.7); - responseFormat.put("data", new TestData("Peter", 40)); + Struct responseFormat = Struct.newBuilder().putFields("type", + Value.newBuilder().setStringValue("text").build()).build(); + ConversationRequestAlpha2 request = new ConversationRequestAlpha2("openai", List.of(input)); request.setContextId("test-context"); request.setTemperature(0.7); @@ -1166,10 +1165,8 @@ public void converseAlpha2ComplexRequestTest() { assertEquals("value1", capturedRequest.getMetadataMap().get("key1")); assertEquals(1, capturedRequest.getToolsCount()); assertEquals("get_weather", capturedRequest.getTools(0).getFunction().getName()); - assertEquals(Struct.newBuilder() - .putFields("temperature", Value.newBuilder().setNumberValue(0.7).build()) - .putFields("data", Value.newBuilder().setStringValue("TestData{name='Peter', age=40}").build()) - .build(), + assertEquals(Struct.newBuilder().putFields("type", + Value.newBuilder().setStringValue("text").build()).build(), capturedRequest.getResponseFormat()); assertEquals(Duration.ofDays(1).getSeconds(), capturedRequest.getPromptCacheRetention().getSeconds()); assertEquals(0, capturedRequest.getPromptCacheRetention().getNanos());