diff --git a/core/src/main/java/com/google/adk/models/ChatCompletionsResponse.java b/core/src/main/java/com/google/adk/models/ChatCompletionsResponse.java deleted file mode 100644 index fe5cdd116..000000000 --- a/core/src/main/java/com/google/adk/models/ChatCompletionsResponse.java +++ /dev/null @@ -1,224 +0,0 @@ -/* - * Copyright 2026 Google LLC - * - * 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 - * - * https://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 com.google.adk.models; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.List; -import java.util.Map; - -/** - * Data Transfer Objects for Chat Completion and Chat Completion Chunk API responses. - * - *

These classes are used for deserializing JSON responses from the `/chat/completions` endpoint. - */ -@JsonIgnoreProperties(ignoreUnknown = true) -final class ChatCompletionsResponse { - - private ChatCompletionsResponse() {} - - @JsonIgnoreProperties(ignoreUnknown = true) - static class ChatCompletion { - public String id; - public List choices; - public Long created; - public String model; - public String object; - - @JsonProperty("service_tier") - public String serviceTier; - - @JsonProperty("system_fingerprint") - public String systemFingerprint; - - public Usage usage; - } - - @JsonIgnoreProperties(ignoreUnknown = true) - static class Choice { - @JsonProperty("finish_reason") - public String finishReason; - - public Integer index; - public Logprobs logprobs; - public Message message; - } - - @JsonIgnoreProperties(ignoreUnknown = true) - static class ChatCompletionChunk { - public String id; - public List choices; - public Long created; - public String model; - public String object; - - @JsonProperty("service_tier") - public String serviceTier; - - @JsonProperty("system_fingerprint") - public String systemFingerprint; - - public Usage usage; - } - - @JsonIgnoreProperties(ignoreUnknown = true) - static class ChunkChoice { - @JsonProperty("finish_reason") - public String finishReason; - - public Integer index; - public Logprobs logprobs; - public Message delta; - } - - @JsonIgnoreProperties(ignoreUnknown = true) - static class Message { - public String content; - public String refusal; - public String role; - - @JsonProperty("tool_calls") - public List toolCalls; - - // function_call is not supported in ChatCompletionChunk and ChatCompletion support is - // deprecated. - @JsonProperty("function_call") - public Function functionCall; // Fallback for deprecated top-level function calls - - public List annotations; - public Audio audio; - } - - @JsonIgnoreProperties(ignoreUnknown = true) - static class ToolCall { - // Index is only used in ChatCompletionChunk. - public Integer index; - public String id; - public String type; - public Function function; - public Custom custom; - - @JsonProperty("extra_content") - public Map extraContent; - } - - @JsonIgnoreProperties(ignoreUnknown = true) - static class Function { - public String name; - public String arguments; // JSON string - } - - @JsonIgnoreProperties(ignoreUnknown = true) - static class Custom { - public String input; - public String name; - } - - @JsonIgnoreProperties(ignoreUnknown = true) - static class Logprobs { - public List content; - public List refusal; - } - - @JsonIgnoreProperties(ignoreUnknown = true) - @JsonInclude(JsonInclude.Include.NON_NULL) - static class TokenLogprob { - public String token; - public List bytes; - public Double logprob; - - @JsonProperty("top_logprobs") - public List topLogprobs; - } - - @JsonIgnoreProperties(ignoreUnknown = true) - static class Usage { - @JsonProperty("completion_tokens") - public Integer completionTokens; - - @JsonProperty("prompt_tokens") - public Integer promptTokens; - - @JsonProperty("total_tokens") - public Integer totalTokens; - - @JsonProperty("thoughts_token_count") - public Integer thoughtsTokenCount; // Gemini-specific extension - - @JsonProperty("completion_tokens_details") - public CompletionTokensDetails completionTokensDetails; - - @JsonProperty("prompt_tokens_details") - public PromptTokensDetails promptTokensDetails; - } - - @JsonIgnoreProperties(ignoreUnknown = true) - static class CompletionTokensDetails { - @JsonProperty("accepted_prediction_tokens") - public Integer acceptedPredictionTokens; - - @JsonProperty("audio_tokens") - public Integer audioTokens; - - @JsonProperty("reasoning_tokens") - public Integer reasoningTokens; - - @JsonProperty("rejected_prediction_tokens") - public Integer rejectedPredictionTokens; - } - - @JsonIgnoreProperties(ignoreUnknown = true) - static class PromptTokensDetails { - @JsonProperty("audio_tokens") - public Integer audioTokens; - - @JsonProperty("cached_tokens") - public Integer cachedTokens; - } - - @JsonIgnoreProperties(ignoreUnknown = true) - static class Annotation { - public String type; - - @JsonProperty("url_citation") - public UrlCitation urlCitation; - } - - @JsonIgnoreProperties(ignoreUnknown = true) - static class UrlCitation { - @JsonProperty("end_index") - public Integer endIndex; - - @JsonProperty("start_index") - public Integer startIndex; - - public String title; - public String url; - } - - @JsonIgnoreProperties(ignoreUnknown = true) - static class Audio { - public String id; - public String data; - - @JsonProperty("expires_at") - public Long expiresAt; - - public String transcript; - } -} diff --git a/core/src/main/java/com/google/adk/models/chat/ChatCompletionsCommon.java b/core/src/main/java/com/google/adk/models/chat/ChatCompletionsCommon.java new file mode 100644 index 000000000..cd5b4d7bf --- /dev/null +++ b/core/src/main/java/com/google/adk/models/chat/ChatCompletionsCommon.java @@ -0,0 +1,169 @@ +/* + * Copyright 2026 Google LLC + * + * 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 + * + * https://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 com.google.adk.models.chat; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.genai.types.FunctionCall; +import com.google.genai.types.Part; +import java.util.Base64; +import java.util.Map; +import org.jspecify.annotations.Nullable; + +/** Shared models for Chat Completions Request and Response. */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +final class ChatCompletionsCommon { + + private ChatCompletionsCommon() {} + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + public static final String ROLE_ASSISTANT = "assistant"; + public static final String ROLE_MODEL = "model"; + + public static final String METADATA_KEY_ID = "id"; + public static final String METADATA_KEY_CREATED = "created"; + public static final String METADATA_KEY_OBJECT = "object"; + public static final String METADATA_KEY_SYSTEM_FINGERPRINT = "system_fingerprint"; + public static final String METADATA_KEY_SERVICE_TIER = "service_tier"; + + /** + * See + * https://developers.openai.com/api/reference/resources/chat#(resource)%20chat.completions%20%3E%20(model)%20chat_completion_message_tool_call%20%3E%20(schema) + */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + static class ToolCall { + /** See class definition for more details. */ + public Integer index; + + /** See class definition for more details. */ + public String id; + + /** See class definition for more details. */ + public String type; + + /** See class definition for more details. */ + public Function function; + + /** See class definition for more details. */ + public Custom custom; + + /** + * Used to supply additional parameters for specific models, for example: + * https://ai.google.dev/gemini-api/docs/openai#thinking + */ + @JsonProperty("extra_content") + public Map extraContent; + + /** + * Converts the tool call to a {@link Part}. + * + * @return a {@link Part} containing the function call, or {@code null} if this tool call does + * not contain a function call. + */ + public @Nullable Part toPart() { + if (function != null) { + FunctionCall fc = function.toFunctionCall(id); + Part part = Part.builder().functionCall(fc).build(); + return applyThoughtSignature(part); + } + return null; + } + + /** + * Applies the thought signature from {@code extraContent} to the given {@link Part} if present. + * This is used to support the Google Gemini/Vertex AI implementation of the chat/completions + * API. + * + * @param part the {@link Part} to modify. + * @return a new {@link Part} with the thought signature applied, or the original {@link Part} + * if no thought signature is found. + */ + public Part applyThoughtSignature(Part part) { + if (extraContent != null && extraContent.containsKey("google")) { + Object googleObj = extraContent.get("google"); + if (googleObj instanceof Map googleMap) { + Object sigObj = googleMap.get("thought_signature"); + if (sigObj instanceof String sig) { + return part.toBuilder().thoughtSignature(Base64.getDecoder().decode(sig)).build(); + } + } + } + return part; + } + } + + /** + * See + * https://developers.openai.com/api/reference/resources/chat#(resource)%20chat.completions%20%3E%20(model)%20chat_completion_message_function_tool_call%20%3E%20(schema) + */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + static class Function { + /** See class definition for more details. */ + public String name; + + /** See class definition for more details. */ + public String arguments; // JSON string + + /** + * Converts this function to a {@link FunctionCall}. + * + * @param toolCallId the ID of the tool call, or {@code null} if not applicable. + * @return the {@link FunctionCall} object. + */ + public FunctionCall toFunctionCall(@Nullable String toolCallId) { + FunctionCall.Builder fcBuilder = FunctionCall.builder(); + if (name != null) { + fcBuilder.name(name); + } + if (arguments != null) { + try { + Map args = + objectMapper.readValue(arguments, new TypeReference>() {}); + fcBuilder.args(args); + } catch (Exception e) { + throw new IllegalArgumentException( + "Failed to parse function arguments JSON: " + arguments, e); + } + } + if (toolCallId != null) { + fcBuilder.id(toolCallId); + } + return fcBuilder.build(); + } + } + + /** + * See + * https://developers.openai.com/api/reference/resources/chat#(resource)%20chat.completions%20%3E%20(model)%20chat_completion_custom_tool%20%3E%20(schema) + */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + static class Custom { + /** See class definition for more details. */ + public String input; + + /** See class definition for more details. */ + public String name; + } +} diff --git a/core/src/main/java/com/google/adk/models/chat/ChatCompletionsRequest.java b/core/src/main/java/com/google/adk/models/chat/ChatCompletionsRequest.java new file mode 100644 index 000000000..4b6747fb1 --- /dev/null +++ b/core/src/main/java/com/google/adk/models/chat/ChatCompletionsRequest.java @@ -0,0 +1,728 @@ +/* + * Copyright 2026 Google LLC + * + * 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 + * + * https://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 com.google.adk.models.chat; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonValue; +import java.util.List; +import java.util.Map; + +/** + * Data Transfer Objects for Chat Completion API requests. + * + *

See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +final class ChatCompletionsRequest { + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20messages%20%3E%20(schema) + */ + public List messages; + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20model%20%3E%20(schema) + */ + public String model; + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20audio%20%3E%20(schema) + */ + public AudioParam audio; + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20frequency_penalty%20%3E%20(schema) + */ + @JsonProperty("frequency_penalty") + public Double frequencyPenalty; + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20logit_bias%20%3E%20(schema) + */ + @JsonProperty("logit_bias") + public Map logitBias; + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20logprobs%20%3E%20(schema) + */ + public Boolean logprobs; + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20max_completion_tokens%20%3E%20(schema) + */ + @JsonProperty("max_completion_tokens") + public Integer maxCompletionTokens; + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20metadata%20%3E%20(schema) + */ + public Map metadata; + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20modalities%20%3E%20(schema) + */ + public List modalities; + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20n%20%3E%20(schema) + */ + public Integer n; + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20parallel_tool_calls%20%3E%20(schema) + */ + @JsonProperty("parallel_tool_calls") + public Boolean parallelToolCalls; + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20prediction%20%3E%20(schema) + */ + public Prediction prediction; + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20presence_penalty%20%3E%20(schema) + */ + @JsonProperty("presence_penalty") + public Double presencePenalty; + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20prompt_cache_key%20%3E%20(schema) + */ + @JsonProperty("prompt_cache_key") + public String promptCacheKey; + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20prompt_cache_retention%20%3E%20(schema) + */ + @JsonProperty("prompt_cache_retention") + public String promptCacheRetention; + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20reasoning_effort%20%3E%20(schema) + */ + @JsonProperty("reasoning_effort") + public String reasoningEffort; + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20response_format%20%3E%20(schema) + */ + @JsonProperty("response_format") + public ResponseFormat responseFormat; + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20safety_identifier%20%3E%20(schema) + */ + @JsonProperty("safety_identifier") + public String safetyIdentifier; + + /** + * Deprecated. Use temperature instead. See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20seed%20%3E%20(schema) + */ + public Long seed; + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20service_tier%20%3E%20(schema) + */ + @JsonProperty("service_tier") + public String serviceTier; + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20stop%20%3E%20(schema) + */ + public StopCondition stop; + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20store%20%3E%20(schema) + */ + public Boolean store; + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20stream%20%3E%20(schema) + */ + public Boolean stream; + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20stream_options%20%3E%20(schema) + */ + @JsonProperty("stream_options") + public StreamOptions streamOptions; + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20temperature%20%3E%20(schema) + */ + public Double temperature; + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20tool_choice%20%3E%20(schema) + */ + @JsonProperty("tool_choice") + public ToolChoice toolChoice; + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20tools%20%3E%20(schema) + */ + public List tools; + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20top_logprobs%20%3E%20(schema) + */ + @JsonProperty("top_logprobs") + public Integer topLogprobs; + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20top_p%20%3E%20(schema) + */ + @JsonProperty("top_p") + public Double topP; + + /** + * Deprecated, use safety_identifier and prompt_cache_key instead. See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20user%20%3E%20(schema) + */ + public String user; + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20verbosity%20%3E%20(schema) + */ + public String verbosity; + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20web_search_options%20%3E%20(schema) + */ + @JsonProperty("web_search_options") + public WebSearchOptions webSearchOptions; + + /** + * Additional body parameters used for specific models, for example: + * https://ai.google.dev/gemini-api/docs/openai#extra-body + */ + @JsonProperty("extra_body") + public Map extraBody; + + /** + * A catch-all class for message parameters. See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20messages%20%3E%20(schema) + */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + static class Message { + /** See class definition for more details. */ + public String role; + + /** See class definition for more details. */ + public MessageContent content; + + /** See class definition for more details. */ + public String name; + + /** See class definition for more details. */ + @JsonProperty("tool_calls") + public List toolCalls; + + /** Deprecated. Use tool_calls instead.See class definition for more details. */ + @JsonProperty("function_call") + public FunctionCall functionCall; + + /** See class definition for more details. */ + @JsonProperty("tool_call_id") + public String toolCallId; + + /** See class definition for more details. */ + public Audio audio; + + /** See class definition for more details. */ + public String refusal; + } + + /** + * See + * https://developers.openai.com/api/reference/resources/chat#(resource)%20chat.completions%20%3E%20(model)%20chat_completion_content_part_text%20%3E%20(schema) + */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + static class ContentPart { + /** See class definition for more details. */ + public String type; + + /** See class definition for more details. */ + public String text; + + /** See class definition for more details. */ + public String refusal; + + /** See class definition for more details. */ + @JsonProperty("image_url") + public ImageUrl imageUrl; + + /** See class definition for more details. */ + @JsonProperty("input_audio") + public InputAudio inputAudio; + + /** See class definition for more details. */ + public File file; + } + + /** + * See + * https://developers.openai.com/api/reference/resources/chat#(resource)%20chat.completions%20%3E%20(model)%20chat_completion_content_part_text%20%3E%20(schema) + */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + static class ImageUrl { + /** See class definition for more details. */ + public String url; + + /** See class definition for more details. */ + public String detail; + } + + /** + * See + * https://developers.openai.com/api/reference/resources/chat#(resource)%20chat.completions%20%3E%20(model)%20chat_completion_content_part_text%20%3E%20(schema) + */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + static class InputAudio { + /** See class definition for more details. */ + public String data; + + /** See class definition for more details. */ + public String format; + } + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20messages%20%3E%20(schema) + */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + static class File { + /** See class definition for more details. */ + @JsonProperty("file_data") + public String fileData; + + /** See class definition for more details. */ + @JsonProperty("file_id") + public String fileId; + + /** See class definition for more details. */ + public String filename; + } + + /** + * Deprecated. Function call details replaced by tool_calls. See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20messages%20%3E%20(schema) + */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + static class FunctionCall { + /** See class definition for more details. */ + public String name; + + /** See class definition for more details. */ + public String arguments; + } + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20audio%20%3E%20(schema) + */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + static class AudioParam { + /** See class definition for more details. */ + public String format; + + /** See class definition for more details. */ + public VoiceConfig voice; + } + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20audio%20%3E%20(schema) + */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + static class Audio { + /** See class definition for more details. */ + public String id; + } + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20prediction%20%3E%20(schema) + */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + static class Prediction { + /** See class definition for more details. */ + public String type; + + /** See class definition for more details. */ + public Object content; + } + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20stream_options%20%3E%20(schema) + */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + static class StreamOptions { + /** See class definition for more details. */ + @JsonProperty("include_obfuscation") + public Boolean includeObfuscation; + + /** See class definition for more details. */ + @JsonProperty("include_usage") + public Boolean includeUsage; + } + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20tools%20%3E%20(schema) + */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + static class Tool { + /** See class definition for more details. */ + public String type; + + /** See class definition for more details. */ + public FunctionDefinition function; + + /** See class definition for more details. */ + public CustomTool custom; + } + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20tools%20%3E%20(schema) + */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + static class FunctionDefinition { + /** See class definition for more details. */ + public String name; + + /** See class definition for more details. */ + public String description; + + /** See class definition for more details. */ + public Map parameters; + + /** See class definition for more details. */ + public Boolean strict; + } + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(model)%20chat_completion_custom_tool%20%3E%20(schema) + */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + static class CustomTool { + /** See class definition for more details. */ + public String name; + + /** See class definition for more details. */ + public String description; + + /** See class definition for more details. */ + public Object format; + } + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20web_search_options%20%3E%20(schema) + */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + static class WebSearchOptions { + /** See class definition for more details. */ + @JsonProperty("search_context_size") + public String searchContextSize; + + /** See class definition for more details. */ + @JsonProperty("user_location") + public UserLocation userLocation; + } + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20web_search_options%20%3E%20(schema) + */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + static class UserLocation { + /** See class definition for more details. */ + public String type; + + /** See class definition for more details. */ + public ApproximateLocation approximate; + } + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20web_search_options%20%3E%20(schema) + */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + static class ApproximateLocation { + /** See class definition for more details. */ + public String city; + + /** See class definition for more details. */ + public String country; + + /** See class definition for more details. */ + public String region; + + /** See class definition for more details. */ + public String timezone; + } + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20response_format%20%3E%20(schema) + */ + interface ResponseFormat {} + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20response_format%20%3E%20(schema) + */ + static class ResponseFormatText implements ResponseFormat { + public String type = "text"; + } + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20response_format%20%3E%20(schema) + */ + static class ResponseFormatJsonObject implements ResponseFormat { + public String type = "json_object"; + } + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20response_format%20%3E%20(schema) + */ + static class ResponseFormatJsonSchema implements ResponseFormat { + public String type = "json_schema"; + + @JsonProperty("json_schema") + public JsonSchema jsonSchema; + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20response_format%20%3E%20(schema) + */ + static class JsonSchema { + /** See class definition for more details. */ + public String name; + + /** See class definition for more details. */ + public String description; + + /** See class definition for more details. */ + public Map schema; + + /** See class definition for more details. */ + public Boolean strict; + } + } + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20tool_choice%20%3E%20(schema) + */ + interface ToolChoice {} + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20tool_choice%20%3E%20(schema) + */ + static class ToolChoiceMode implements ToolChoice { + private final String mode; + + public ToolChoiceMode(String mode) { + this.mode = mode; + } + + @JsonValue + public String getMode() { + return mode; + } + } + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20tool_choice%20%3E%20(schema) + */ + static class NamedToolChoice implements ToolChoice { + /** See class definition for more details. */ + public String type = "function"; + + /** See class definition for more details. */ + public FunctionName function; + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20tool_choice%20%3E%20(schema) + */ + static class FunctionName { + /** See class definition for more details. */ + public String name; + } + } + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20tool_choice%20%3E%20(schema) + */ + static class NamedToolChoiceCustom implements ToolChoice { + /** See class definition for more details. */ + public String type = "custom"; + + /** See class definition for more details. */ + public CustomName custom; + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20tool_choice%20%3E%20(schema) + */ + static class CustomName { + /** See class definition for more details. */ + public String name; + } + } + + /** + * Wrapper class for stop. See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20stop%20%3E%20(schema) + */ + static class StopCondition { + private final String stringValue; + private final List listValue; + + @JsonCreator + public StopCondition(String stringValue) { + this.stringValue = stringValue; + this.listValue = null; + } + + @JsonCreator + public StopCondition(List listValue) { + this.stringValue = null; + this.listValue = listValue; + } + + @JsonValue + public Object getValue() { + return stringValue != null ? stringValue : listValue; + } + } + + /** + * Wrapper class for messages. See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20messages%20%3E%20(schema) + */ + static class MessageContent { + private final String stringValue; + private final List listValue; + + @JsonCreator + public MessageContent(String stringValue) { + this.stringValue = stringValue; + this.listValue = null; + } + + @JsonCreator + public MessageContent(List listValue) { + this.stringValue = null; + this.listValue = listValue; + } + + @JsonValue + public Object getValue() { + return stringValue != null ? stringValue : listValue; + } + } + + /** + * See + * https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create#(resource)%20chat.completions%20%3E%20(method)%20create%20%3E%20(params)%200.non_streaming%20%3E%20(param)%20audio%20%3E%20(schema) + */ + static class VoiceConfig { + private final String stringValue; + private final Map mapValue; + + @JsonCreator + public VoiceConfig(String stringValue) { + this.stringValue = stringValue; + this.mapValue = null; + } + + @JsonCreator + public VoiceConfig(Map mapValue) { + this.stringValue = null; + this.mapValue = mapValue; + } + + @JsonValue + public Object getValue() { + return stringValue != null ? stringValue : mapValue; + } + } +} diff --git a/core/src/main/java/com/google/adk/models/chat/ChatCompletionsResponse.java b/core/src/main/java/com/google/adk/models/chat/ChatCompletionsResponse.java new file mode 100644 index 000000000..c52389aa3 --- /dev/null +++ b/core/src/main/java/com/google/adk/models/chat/ChatCompletionsResponse.java @@ -0,0 +1,492 @@ +/* + * Copyright 2026 Google LLC + * + * 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 + * + * https://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 com.google.adk.models.chat; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.adk.models.LlmResponse; +import com.google.genai.types.Content; +import com.google.genai.types.CustomMetadata; +import com.google.genai.types.FinishReason; +import com.google.genai.types.FinishReason.Known; +import com.google.genai.types.GenerateContentResponseUsageMetadata; +import com.google.genai.types.Part; +import java.util.ArrayList; +import java.util.List; +import org.jspecify.annotations.Nullable; + +/** + * Data Transfer Objects for Chat Completion and Chat Completion Chunk API responses. + * + *

See https://developers.openai.com/api/reference/resources/chat + */ +@JsonIgnoreProperties(ignoreUnknown = true) +final class ChatCompletionsResponse { + + private ChatCompletionsResponse() {} + + /** + * See + * https://developers.openai.com/api/reference/resources/chat#(resource)%20chat.completions%20%3E%20(model)%20chat_completion%20%3E%20(schema) + */ + @JsonIgnoreProperties(ignoreUnknown = true) + static class ChatCompletion { + /** See class definition for more details. */ + public String id; + + /** See class definition for more details. */ + public List choices; + + /** See class definition for more details. */ + public Long created; + + /** See class definition for more details. */ + public String model; + + /** See class definition for more details. */ + public String object; + + /** See class definition for more details. */ + @JsonProperty("service_tier") + public String serviceTier; + + /** Deprecated. See class definition for more details. */ + @JsonProperty("system_fingerprint") + public String systemFingerprint; + + /** See class definition for more details. */ + public Usage usage; + + /** + * Converts this chat completion to a {@link LlmResponse}. + * + * @return the {@link LlmResponse} object. + */ + public LlmResponse toLlmResponse() { + Choice choice = (choices != null && !choices.isEmpty()) ? choices.get(0) : null; + Content content = mapChoiceToContent(choice); + + LlmResponse.Builder builder = LlmResponse.builder().content(content); + + if (choice != null) { + builder.finishReason(mapFinishReason(choice.finishReason)); + } + + if (model != null) { + builder.modelVersion(model); + } + + if (usage != null) { + builder.usageMetadata(mapUsage(usage)); + } + + List customMetadataList = buildCustomMetadata(); + return builder.customMetadata(customMetadataList).build(); + } + + /** + * Maps the finish reason string to a {@link FinishReason}. + * + * @param reason the finish reason string. + * @return the {@link FinishReason}, or {@code null} if the input reason is null. + */ + private @Nullable FinishReason mapFinishReason(String reason) { + if (reason == null) { + return null; + } + return switch (reason) { + case "stop", "tool_calls" -> new FinishReason(Known.STOP.toString()); + case "length" -> new FinishReason(Known.MAX_TOKENS.toString()); + case "content_filter" -> new FinishReason(Known.SAFETY.toString()); + default -> new FinishReason(Known.OTHER.toString()); + }; + } + + private GenerateContentResponseUsageMetadata mapUsage(Usage usage) { + GenerateContentResponseUsageMetadata.Builder builder = + GenerateContentResponseUsageMetadata.builder(); + if (usage.promptTokens != null) { + builder.promptTokenCount(usage.promptTokens); + } + if (usage.completionTokens != null) { + builder.candidatesTokenCount(usage.completionTokens); + } + if (usage.totalTokens != null) { + builder.totalTokenCount(usage.totalTokens); + } + if (usage.thoughtsTokenCount != null) { + builder.thoughtsTokenCount(usage.thoughtsTokenCount); + } else if (usage.completionTokensDetails != null + && usage.completionTokensDetails.reasoningTokens != null) { + builder.thoughtsTokenCount(usage.completionTokensDetails.reasoningTokens); + } + return builder.build(); + } + + /** + * Maps the chosen completion to a {@link Content} object. + * + * @param choice the completion choice to map, or {@code null}. + * @return the {@link Content} object, which will be empty if the choice or its message is null. + */ + private Content mapChoiceToContent(@Nullable Choice choice) { + Content.Builder contentBuilder = Content.builder(); + if (choice != null && choice.message != null) { + contentBuilder.role(mapRole(choice.message.role)).parts(mapMessageToParts(choice.message)); + } + return contentBuilder.build(); + } + + private String mapRole(@Nullable String role) { + return (role != null && role.equals(ChatCompletionsCommon.ROLE_ASSISTANT)) + ? ChatCompletionsCommon.ROLE_MODEL + : role; + } + + private List mapMessageToParts(Message message) { + List parts = new ArrayList<>(); + if (message.content != null) { + parts.add(Part.fromText(message.content)); + } + if (message.refusal != null) { + parts.add(Part.fromText(message.refusal)); + } + if (message.toolCalls != null) { + parts.addAll(mapToolCallsToParts(message.toolCalls)); + } + return parts; + } + + private List mapToolCallsToParts(List toolCalls) { + List parts = new ArrayList<>(); + for (ChatCompletionsCommon.ToolCall toolCall : toolCalls) { + Part part = toolCall.toPart(); + if (part != null) { + parts.add(part); + } + } + return parts; + } + + /** + * Builds the list of custom metadata from the chat completion fields. + * + * @return a list of {@link CustomMetadata}, which will be empty if no relevant fields are set. + */ + private List buildCustomMetadata() { + List customMetadataList = new ArrayList<>(); + if (id != null) { + customMetadataList.add( + CustomMetadata.builder() + .key(ChatCompletionsCommon.METADATA_KEY_ID) + .stringValue(id) + .build()); + } + if (created != null) { + customMetadataList.add( + CustomMetadata.builder() + .key(ChatCompletionsCommon.METADATA_KEY_CREATED) + .stringValue(created.toString()) + .build()); + } + if (object != null) { + customMetadataList.add( + CustomMetadata.builder() + .key(ChatCompletionsCommon.METADATA_KEY_OBJECT) + .stringValue(object) + .build()); + } + if (systemFingerprint != null) { + customMetadataList.add( + CustomMetadata.builder() + .key(ChatCompletionsCommon.METADATA_KEY_SYSTEM_FINGERPRINT) + .stringValue(systemFingerprint) + .build()); + } + if (serviceTier != null) { + customMetadataList.add( + CustomMetadata.builder() + .key(ChatCompletionsCommon.METADATA_KEY_SERVICE_TIER) + .stringValue(serviceTier) + .build()); + } + return customMetadataList; + } + } + + /** + * See + * https://developers.openai.com/api/reference/resources/chat#(resource)%20chat.completions%20%3E%20(model)%20chat_completion%20%3E%20(schema)%20%3E%20(property)%20choices + */ + @JsonIgnoreProperties(ignoreUnknown = true) + static class Choice { + /** See class definition for more details. */ + @JsonProperty("finish_reason") + public String finishReason; + + /** See class definition for more details. */ + public Integer index; + + /** See class definition for more details. */ + public Logprobs logprobs; + + /** See class definition for more details. */ + public Message message; + } + + /** + * See + * https://developers.openai.com/api/reference/resources/chat#(resource)%20chat.completions%20%3E%20(model)%20chat_completion_chunk%20%3E%20(schema) + */ + @JsonIgnoreProperties(ignoreUnknown = true) + static class ChatCompletionChunk { + /** See class definition for more details. */ + public String id; + + /** See class definition for more details. */ + public List choices; + + /** See class definition for more details. */ + public Long created; + + /** See class definition for more details. */ + public String model; + + /** See class definition for more details. */ + public String object; + + /** See class definition for more details. */ + @JsonProperty("service_tier") + public String serviceTier; + + /** Deprecated. See class definition for more details. */ + @JsonProperty("system_fingerprint") + public String systemFingerprint; + + /** See class definition for more details. */ + public Usage usage; + } + + /** + * Used for streaming responses. See + * https://developers.openai.com/api/reference/resources/chat#(resource)%20chat.completions%20%3E%20(model)%20chat_completion_chunk%20%3E%20(schema) + */ + @JsonIgnoreProperties(ignoreUnknown = true) + static class ChunkChoice { + /** See class definition for more details. */ + @JsonProperty("finish_reason") + public String finishReason; + + /** See class definition for more details. */ + public Integer index; + + /** See class definition for more details. */ + public Logprobs logprobs; + + /** See class definition for more details. */ + public Message delta; + } + + /** + * See + * https://developers.openai.com/api/reference/resources/chat#(resource)%20chat.completions%20%3E%20(model)%20chat_completion_message%20%3E%20(schema) + */ + @JsonIgnoreProperties(ignoreUnknown = true) + static class Message { + /** See class definition for more details. */ + public String content; + + /** See class definition for more details. */ + public String refusal; + + /** See class definition for more details. */ + public String role; + + /** See class definition for more details. */ + @JsonProperty("tool_calls") + public List toolCalls; + + /** Deprecated. Use tool_calls instead. See class definition for more details. */ + @JsonProperty("function_call") + public ChatCompletionsCommon.Function functionCall; + + /** See class definition for more details. */ + public List annotations; + + /** See class definition for more details. */ + public Audio audio; + } + + /** + * See + * https://developers.openai.com/api/reference/resources/chat#(resource)%20chat.completions%20%3E%20(model)%20chat_completion_logprobs%20%3E%20(schema) + */ + @JsonIgnoreProperties(ignoreUnknown = true) + static class Logprobs { + /** See class definition for more details. */ + public List content; + + /** See class definition for more details. */ + public List refusal; + } + + /** + * See + * https://developers.openai.com/api/reference/resources/chat#(resource)%20chat.completions%20%3E%20(model)%20chat_completion_token_logprob%20%3E%20(schema) + */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + static class TokenLogprob { + /** See class definition for more details. */ + public String token; + + /** See class definition for more details. */ + public List bytes; + + /** See class definition for more details. */ + public Double logprob; + + /** See class definition for more details. */ + @JsonProperty("top_logprobs") + public List topLogprobs; + } + + /** + * See + * https://developers.openai.com/api/reference/resources/completions#(resource)%20completions%20%3E%20(model)%20completion_usage%20%3E%20(schema) + */ + @JsonIgnoreProperties(ignoreUnknown = true) + static class Usage { + /** See class definition for more details. */ + @JsonProperty("completion_tokens") + public Integer completionTokens; + + /** See class definition for more details. */ + @JsonProperty("prompt_tokens") + public Integer promptTokens; + + /** See class definition for more details. */ + @JsonProperty("total_tokens") + public Integer totalTokens; + + /** See class definition for more details. */ + @JsonProperty("thoughts_token_count") + public Integer thoughtsTokenCount; + + /** See class definition for more details. */ + @JsonProperty("completion_tokens_details") + public CompletionTokensDetails completionTokensDetails; + + /** See class definition for more details. */ + @JsonProperty("prompt_tokens_details") + public PromptTokensDetails promptTokensDetails; + } + + /** + * See + * https://developers.openai.com/api/reference/resources/completions#(resource)%20completions%20%3E%20(model)%20completion_usage%20%3E%20(schema) + */ + @JsonIgnoreProperties(ignoreUnknown = true) + static class CompletionTokensDetails { + /** See class definition for more details. */ + @JsonProperty("accepted_prediction_tokens") + public Integer acceptedPredictionTokens; + + /** See class definition for more details. */ + @JsonProperty("audio_tokens") + public Integer audioTokens; + + /** See class definition for more details. */ + @JsonProperty("reasoning_tokens") + public Integer reasoningTokens; + + /** See class definition for more details. */ + @JsonProperty("rejected_prediction_tokens") + public Integer rejectedPredictionTokens; + } + + /** + * See + * https://developers.openai.com/api/reference/resources/completions#(resource)%20completions%20%3E%20(model)%20completion_usage%20%3E%20(schema) + */ + @JsonIgnoreProperties(ignoreUnknown = true) + static class PromptTokensDetails { + /** See class definition for more details. */ + @JsonProperty("audio_tokens") + public Integer audioTokens; + + /** See class definition for more details. */ + @JsonProperty("cached_tokens") + public Integer cachedTokens; + } + + /** + * See + * https://developers.openai.com/api/reference/resources/chat#(resource)%20chat.completions%20%3E%20(model)%20chat_completion_message%20%3E%20(schema)%20%3E%20(property)%20annotations + */ + @JsonIgnoreProperties(ignoreUnknown = true) + static class Annotation { + /** See class definition for more details. */ + public String type; + + /** See class definition for more details. */ + @JsonProperty("url_citation") + public UrlCitation urlCitation; + } + + /** + * See + * https://developers.openai.com/api/reference/resources/chat#(resource)%20chat.completions%20%3E%20(model)%20chat_completion_message%20%3E%20(schema)%20%3E%20(property)%20annotations%20%3E%20(items)%20%3E%20(property)%20url_citation + */ + @JsonIgnoreProperties(ignoreUnknown = true) + static class UrlCitation { + /** See class definition for more details. */ + @JsonProperty("end_index") + public Integer endIndex; + + /** See class definition for more details. */ + @JsonProperty("start_index") + public Integer startIndex; + + /** See class definition for more details. */ + public String title; + + /** See class definition for more details. */ + public String url; + } + + /** + * See + * https://developers.openai.com/api/reference/resources/chat#(resource)%20chat.completions%20%3E%20(model)%20chat_completion_audio%20%3E%20(schema) + */ + @JsonIgnoreProperties(ignoreUnknown = true) + static class Audio { + /** See class definition for more details. */ + public String id; + + /** See class definition for more details. */ + public String data; + + /** See class definition for more details. */ + @JsonProperty("expires_at") + public Long expiresAt; + + /** See class definition for more details. */ + public String transcript; + } +} diff --git a/core/src/test/java/com/google/adk/models/ChatCompletionsResponseTest.java b/core/src/test/java/com/google/adk/models/ChatCompletionsResponseTest.java deleted file mode 100644 index 53fcdfbdf..000000000 --- a/core/src/test/java/com/google/adk/models/ChatCompletionsResponseTest.java +++ /dev/null @@ -1,328 +0,0 @@ -/* - * Copyright 2026 Google LLC - * - * 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 - * - * https://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 com.google.adk.models; - -import static com.google.common.truth.Truth.assertThat; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.adk.models.ChatCompletionsResponse.ChatCompletion; -import com.google.adk.models.ChatCompletionsResponse.ChatCompletionChunk; -import java.util.Map; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public final class ChatCompletionsResponseTest { - - private ObjectMapper objectMapper; - - @Before - public void setUp() { - objectMapper = new ObjectMapper(); - } - - @Test - public void testDeserializeChatCompletion_standardResponse() throws Exception { - String json = - """ - { - "id": "chatcmpl-123", - "object": "chat.completion", - "created": 1677652288, - "model": "gpt-4o-mini", - "choices": [{ - "index": 0, - "message": { - "role": "assistant", - "content": "Hello!" - }, - "finish_reason": "stop" - }], - "usage": { - "prompt_tokens": 9, - "completion_tokens": 12, - "total_tokens": 21 - } - } - """; - - ChatCompletion completion = objectMapper.readValue(json, ChatCompletion.class); - - assertThat(completion.id).isEqualTo("chatcmpl-123"); - assertThat(completion.object).isEqualTo("chat.completion"); - assertThat(completion.created).isEqualTo(1677652288L); - assertThat(completion.model).isEqualTo("gpt-4o-mini"); - assertThat(completion.choices).hasSize(1); - assertThat(completion.choices.get(0).index).isEqualTo(0); - assertThat(completion.choices.get(0).message.role).isEqualTo("assistant"); - assertThat(completion.choices.get(0).message.content).isEqualTo("Hello!"); - assertThat(completion.choices.get(0).finishReason).isEqualTo("stop"); - assertThat(completion.usage.promptTokens).isEqualTo(9); - assertThat(completion.usage.completionTokens).isEqualTo(12); - assertThat(completion.usage.totalTokens).isEqualTo(21); - } - - @Test - public void testDeserializeChatCompletion_withFunctionCallFallback() throws Exception { - String json = - """ - { - "id": "chatcmpl-123", - "choices": [{ - "message": { - "role": "assistant", - "function_call": { - "name": "get_current_weather", - "arguments": "{\\"location\\": \\"Boston\\"}" - } - } - }] - } - """; - - ChatCompletion completion = objectMapper.readValue(json, ChatCompletion.class); - - assertThat(completion.choices.get(0).message.functionCall).isNotNull(); - assertThat(completion.choices.get(0).message.functionCall.name) - .isEqualTo("get_current_weather"); - assertThat(completion.choices.get(0).message.functionCall.arguments) - .isEqualTo("{\"location\": \"Boston\"}"); - } - - @Test - public void testDeserializeChatCompletion_withThoughtSignatureAndGeminiTokens() throws Exception { - String json = - """ - { - "choices": [{ - "message": { - "role": "assistant", - "tool_calls": [{ - "id": "call_abc", - "type": "function", - "extra_content": { - "google": { - "thought_signature": "c2lnbmF0dXJl" - } - } - }] - } - }], - "usage": { - "thoughts_token_count": 50 - } - } - """; - - ChatCompletion completion = objectMapper.readValue(json, ChatCompletion.class); - - assertThat(completion.choices.get(0).message.toolCalls).hasSize(1); - assertThat(completion.choices.get(0).message.toolCalls.get(0).extraContent).isNotNull(); - Map extraContentMap = - (Map) completion.choices.get(0).message.toolCalls.get(0).extraContent; - @SuppressWarnings("unchecked") // This code won't run in production and it's is a JSON object. - Map googleMap = (Map) extraContentMap.get("google"); - assertThat(googleMap.get("thought_signature")).isEqualTo("c2lnbmF0dXJl"); - assertThat(completion.usage.thoughtsTokenCount).isEqualTo(50); - } - - @Test - public void testDeserializeChatCompletion_withArbitraryExtraContent() throws Exception { - String json = - """ - { - "choices": [{ - "message": { - "role": "assistant", - "tool_calls": [{ - "id": "call_abc", - "type": "function", - "extra_content": { - "custom_key": "custom_value", - "nested": { - "key": 123 - } - } - }] - } - }] - } - """; - - ChatCompletion got = objectMapper.readValue(json, ChatCompletion.class); - - assertThat(got.choices.get(0).message.toolCalls).hasSize(1); - Map extraContent = - (Map) got.choices.get(0).message.toolCalls.get(0).extraContent; - assertThat(extraContent.get("custom_key")).isEqualTo("custom_value"); - @SuppressWarnings("unchecked") // This code won't run in production and it's is a JSON object. - Map nested = (Map) extraContent.get("nested"); - assertThat(nested.get("key")).isEqualTo(123); - } - - @Test - public void testDeserializeChatCompletion_withAudio() throws Exception { - String json = - """ - { - "choices": [{ - "message": { - "role": "assistant", - "content": "Hello", - "annotations": [{ - "type": "url_citation", - "url_citation": { - "end_index": 5, - "start_index": 0, - "title": "Example Title", - "url": "https://example.com" - } - }], - "audio": { - "id": "audio_123", - "data": "base64data", - "expires_at": 1234567890, - "transcript": "Hello" - } - } - }] - } - """; - - ChatCompletionsResponse.ChatCompletion completion = - objectMapper.readValue(json, ChatCompletionsResponse.ChatCompletion.class); - - assertThat(completion.choices.get(0).message.annotations).hasSize(1); - ChatCompletionsResponse.Annotation annotation = - completion.choices.get(0).message.annotations.get(0); - assertThat(annotation.type).isEqualTo("url_citation"); - assertThat(annotation.urlCitation.title).isEqualTo("Example Title"); - assertThat(annotation.urlCitation.url).isEqualTo("https://example.com"); - - assertThat(completion.choices.get(0).message.audio).isNotNull(); - assertThat(completion.choices.get(0).message.audio.id).isEqualTo("audio_123"); - assertThat(completion.choices.get(0).message.audio.data).isEqualTo("base64data"); - } - - @Test - public void testDeserializeChatCompletion_withCustomToolCall() throws Exception { - String json = - """ - { - "choices": [{ - "message": { - "role": "assistant", - "tool_calls": [{ - "id": "call_custom", - "type": "custom", - "custom": { - "input": "{\\\"arg\\\":\\\"val\\\"}", - "name": "custom_tool" - } - }] - } - }] - } - """; - - ChatCompletionsResponse.ChatCompletion completion = - objectMapper.readValue(json, ChatCompletionsResponse.ChatCompletion.class); - - assertThat(completion.choices.get(0).message.toolCalls).hasSize(1); - ChatCompletionsResponse.ToolCall toolCall = completion.choices.get(0).message.toolCalls.get(0); - assertThat(toolCall.type).isEqualTo("custom"); - assertThat(toolCall.custom.name).isEqualTo("custom_tool"); - assertThat(toolCall.custom.input).isEqualTo("{\"arg\":\"val\"}"); - } - - @Test - public void testDeserializeChatCompletionChunk_streamingResponse() throws Exception { - String json = - """ - { - "id": "chatcmpl-123", - "object": "chat.completion.chunk", - "created": 1694268190, - "choices": [{ - "index": 0, - "delta": { - "content": "Hello" - } - }] - } - """; - - ChatCompletionChunk chunk = objectMapper.readValue(json, ChatCompletionChunk.class); - - assertThat(chunk.id).isEqualTo("chatcmpl-123"); - assertThat(chunk.object).isEqualTo("chat.completion.chunk"); - assertThat(chunk.choices).hasSize(1); - assertThat(chunk.choices.get(0).delta.content).isEqualTo("Hello"); - } - - @Test - public void testDeserializeChatCompletionChunk_withToolCallDelta() throws Exception { - String json = - """ - { - "choices": [{ - "delta": { - "tool_calls": [{ - "index": 1, - "id": "call_abc", - "type": "function", - "function": { - "name": "get_weather", - "arguments": "{\\\"location\\\":\\\"Boston\\\"}" - }, - "extra_content": { - "google": { - "thought_signature": "sig" - } - } - }] - } - }], - "usage": { - "completion_tokens": 10, - "prompt_tokens": 5, - "total_tokens": 15 - } - } - """; - - ChatCompletionChunk chunk = objectMapper.readValue(json, ChatCompletionChunk.class); - - assertThat(chunk.choices.get(0).delta.toolCalls).hasSize(1); - ChatCompletionsResponse.ToolCall toolCall = chunk.choices.get(0).delta.toolCalls.get(0); - assertThat(toolCall.index).isEqualTo(1); - assertThat(toolCall.id).isEqualTo("call_abc"); - assertThat(toolCall.type).isEqualTo("function"); - assertThat(toolCall.function.name).isEqualTo("get_weather"); - assertThat(toolCall.function.arguments).isEqualTo("{\"location\":\"Boston\"}"); - @SuppressWarnings("unchecked") // This code won't run in production and it's is a JSON object. - Map google = (Map) toolCall.extraContent.get("google"); - assertThat(google).containsEntry("thought_signature", "sig"); - - assertThat(chunk.usage).isNotNull(); - assertThat(chunk.usage.completionTokens).isEqualTo(10); - assertThat(chunk.usage.promptTokens).isEqualTo(5); - assertThat(chunk.usage.totalTokens).isEqualTo(15); - } -} diff --git a/core/src/test/java/com/google/adk/models/chat/ChatCompletionsRequestTest.java b/core/src/test/java/com/google/adk/models/chat/ChatCompletionsRequestTest.java new file mode 100644 index 000000000..9dc63c5d6 --- /dev/null +++ b/core/src/test/java/com/google/adk/models/chat/ChatCompletionsRequestTest.java @@ -0,0 +1,221 @@ +/* + * Copyright 2026 Google LLC + * + * 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 + * + * https://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 com.google.adk.models.chat; + +import static com.google.common.truth.Truth.assertThat; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableList; +import java.util.HashMap; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class ChatCompletionsRequestTest { + + private ObjectMapper objectMapper; + + @Before + public void setUp() { + objectMapper = new ObjectMapper(); + } + + @Test + public void testSerializeChatCompletionRequest_standard() throws Exception { + ChatCompletionsRequest request = new ChatCompletionsRequest(); + request.model = "gemini-3-flash-preview"; + + ChatCompletionsRequest.Message message = new ChatCompletionsRequest.Message(); + message.role = "user"; + message.content = new ChatCompletionsRequest.MessageContent("Hello"); + request.messages = ImmutableList.of(message); + + String json = objectMapper.writeValueAsString(request); + + assertThat(json).contains("\"model\":\"gemini-3-flash-preview\""); + assertThat(json).contains("\"role\":\"user\""); + assertThat(json).contains("\"content\":\"Hello\""); + } + + @Test + public void testSerializeChatCompletionRequest_withExtraBody() throws Exception { + ChatCompletionsRequest request = new ChatCompletionsRequest(); + request.model = "gemini-3-flash-preview"; + + ChatCompletionsRequest.Message message = new ChatCompletionsRequest.Message(); + message.role = "user"; + message.content = new ChatCompletionsRequest.MessageContent("Explain to me how AI works"); + request.messages = ImmutableList.of(message); + + Map thinkingConfig = new HashMap<>(); + thinkingConfig.put("thinking_level", "low"); + thinkingConfig.put("include_thoughts", true); + + Map google = new HashMap<>(); + google.put("thinking_config", thinkingConfig); + + Map extraBody = new HashMap<>(); + extraBody.put("google", google); + + request.extraBody = extraBody; + + String json = objectMapper.writeValueAsString(request); + + assertThat(json).contains("\"extra_body\":{"); + assertThat(json).contains("\"thinking_level\":\"low\""); + assertThat(json).contains("\"include_thoughts\":true"); + } + + @Test + public void testSerializeChatCompletionRequest_withToolCallsAndExtraContent() throws Exception { + ChatCompletionsRequest request = new ChatCompletionsRequest(); + request.model = "gemini-3-flash-preview"; + + ChatCompletionsRequest.Message userMessage = new ChatCompletionsRequest.Message(); + userMessage.role = "user"; + userMessage.content = new ChatCompletionsRequest.MessageContent("Check flight status"); + + ChatCompletionsRequest.Message modelMessage = new ChatCompletionsRequest.Message(); + modelMessage.role = "model"; + + ChatCompletionsCommon.ToolCall toolCall = new ChatCompletionsCommon.ToolCall(); + toolCall.id = "function-call-1"; + toolCall.type = "function"; + + ChatCompletionsCommon.Function function = new ChatCompletionsCommon.Function(); + function.name = "check_flight"; + function.arguments = "{\"flight\":\"AA100\"}"; + toolCall.function = function; + + Map google = new HashMap<>(); + google.put("thought_signature", ""); + + Map extraContent = new HashMap<>(); + extraContent.put("google", google); + + toolCall.extraContent = extraContent; + + modelMessage.toolCalls = ImmutableList.of(toolCall); + + ChatCompletionsRequest.Message toolMessage = new ChatCompletionsRequest.Message(); + toolMessage.role = "tool"; + toolMessage.name = "check_flight"; + toolMessage.toolCallId = "function-call-1"; + toolMessage.content = new ChatCompletionsRequest.MessageContent("{\"status\":\"delayed\"}"); + + request.messages = ImmutableList.of(userMessage, modelMessage, toolMessage); + + String json = objectMapper.writeValueAsString(request); + + assertThat(json).contains("\"role\":\"user\""); + assertThat(json).contains("\"role\":\"model\""); + assertThat(json).contains("\"role\":\"tool\""); + assertThat(json).contains("\"extra_content\":{"); + assertThat(json).contains("\"thought_signature\":\"\""); + assertThat(json).contains("\"tool_call_id\":\"function-call-1\""); + } + + @Test + public void testSerializeChatCompletionRequest_comprehensive() throws Exception { + ChatCompletionsRequest request = new ChatCompletionsRequest(); + request.model = "gemini-3-flash-preview"; + + // Developer message with name + ChatCompletionsRequest.Message devMsg = new ChatCompletionsRequest.Message(); + devMsg.role = "developer"; + devMsg.content = new ChatCompletionsRequest.MessageContent("System instruction"); + devMsg.name = "system-bot"; + + request.messages = ImmutableList.of(devMsg); + + // Response Format JSON Schema + ChatCompletionsRequest.ResponseFormatJsonSchema format = + new ChatCompletionsRequest.ResponseFormatJsonSchema(); + format.jsonSchema = new ChatCompletionsRequest.ResponseFormatJsonSchema.JsonSchema(); + format.jsonSchema.name = "MySchema"; + format.jsonSchema.strict = true; + request.responseFormat = format; + + // Tool Choice Named + ChatCompletionsRequest.NamedToolChoice choice = new ChatCompletionsRequest.NamedToolChoice(); + choice.function = new ChatCompletionsRequest.NamedToolChoice.FunctionName(); + choice.function.name = "my_function"; + request.toolChoice = choice; + + String json = objectMapper.writeValueAsString(request); + + // Assert Developer Message + assertThat(json).contains("\"role\":\"developer\""); + assertThat(json).contains("\"name\":\"system-bot\""); + assertThat(json).contains("\"content\":\"System instruction\""); + + // Assert Response Format + assertThat(json).contains("\"response_format\":{"); + assertThat(json).contains("\"type\":\"json_schema\""); + assertThat(json).contains("\"name\":\"MySchema\""); + assertThat(json).contains("\"strict\":true"); + + // Assert Tool Choice + assertThat(json).contains("\"tool_choice\":{"); + assertThat(json).contains("\"type\":\"function\""); + assertThat(json).contains("\"name\":\"my_function\""); + } + + @Test + public void testSerializeChatCompletionRequest_withToolChoiceMode() throws Exception { + ChatCompletionsRequest request = new ChatCompletionsRequest(); + request.model = "gemini-3-flash-preview"; + + request.toolChoice = new ChatCompletionsRequest.ToolChoiceMode("none"); + + String json = objectMapper.writeValueAsString(request); + + assertThat(json).contains("\"tool_choice\":\"none\""); + } + + @Test + public void testSerializeChatCompletionRequest_withStopAndVoice() throws Exception { + ChatCompletionsRequest request = new ChatCompletionsRequest(); + request.model = "gemini-3-flash-preview"; + + request.stop = new ChatCompletionsRequest.StopCondition("STOP"); + + ChatCompletionsRequest.AudioParam audio = new ChatCompletionsRequest.AudioParam(); + audio.voice = new ChatCompletionsRequest.VoiceConfig("alloy"); + request.audio = audio; + + String json = objectMapper.writeValueAsString(request); + + assertThat(json).contains("\"stop\":\"STOP\""); + assertThat(json).contains("\"voice\":\"alloy\""); + } + + @Test + public void testSerializeChatCompletionRequest_withStopList() throws Exception { + ChatCompletionsRequest request = new ChatCompletionsRequest(); + request.model = "gemini-3-flash-preview"; + + request.stop = new ChatCompletionsRequest.StopCondition(ImmutableList.of("STOP1", "STOP2")); + + String json = objectMapper.writeValueAsString(request); + + assertThat(json).contains("\"stop\":[\"STOP1\",\"STOP2\"]"); + } +} diff --git a/core/src/test/java/com/google/adk/models/chat/ChatCompletionsResponseTest.java b/core/src/test/java/com/google/adk/models/chat/ChatCompletionsResponseTest.java new file mode 100644 index 000000000..ff3482284 --- /dev/null +++ b/core/src/test/java/com/google/adk/models/chat/ChatCompletionsResponseTest.java @@ -0,0 +1,666 @@ +/* + * Copyright 2026 Google LLC + * + * 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 + * + * https://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 com.google.adk.models.chat; + +import static com.google.common.truth.Truth.assertThat; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.adk.models.LlmResponse; +import com.google.adk.models.chat.ChatCompletionsResponse.ChatCompletion; +import com.google.adk.models.chat.ChatCompletionsResponse.ChatCompletionChunk; +import com.google.genai.types.CustomMetadata; +import com.google.genai.types.FinishReason.Known; +import com.google.genai.types.FunctionCall; +import com.google.genai.types.Part; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class ChatCompletionsResponseTest { + + private ObjectMapper objectMapper; + + @Before + public void setUp() { + objectMapper = new ObjectMapper(); + } + + @Test + public void testDeserializeChatCompletion_standardResponse() throws Exception { + String json = + """ + { + "id": "chatcmpl-123", + "object": "chat.completion", + "created": 1677652288, + "model": "gpt-4o-mini", + "choices": [{ + "index": 0, + "message": { + "role": "assistant", + "content": "Hello!" + }, + "finish_reason": "stop" + }], + "usage": { + "prompt_tokens": 9, + "completion_tokens": 12, + "total_tokens": 21 + } + } + """; + + ChatCompletion completion = objectMapper.readValue(json, ChatCompletion.class); + + assertThat(completion.id).isEqualTo("chatcmpl-123"); + assertThat(completion.object).isEqualTo("chat.completion"); + assertThat(completion.created).isEqualTo(1677652288L); + assertThat(completion.model).isEqualTo("gpt-4o-mini"); + assertThat(completion.choices).hasSize(1); + assertThat(completion.choices.get(0).index).isEqualTo(0); + assertThat(completion.choices.get(0).message.role).isEqualTo("assistant"); + assertThat(completion.choices.get(0).message.content).isEqualTo("Hello!"); + assertThat(completion.choices.get(0).finishReason).isEqualTo("stop"); + assertThat(completion.usage.promptTokens).isEqualTo(9); + assertThat(completion.usage.completionTokens).isEqualTo(12); + assertThat(completion.usage.totalTokens).isEqualTo(21); + } + + @Test + public void testDeserializeChatCompletion_withFunctionCallFallback() throws Exception { + String json = + """ + { + "id": "chatcmpl-123", + "choices": [{ + "message": { + "role": "assistant", + "function_call": { + "name": "get_current_weather", + "arguments": "{\\"location\\": \\"Boston\\"}" + } + } + }] + } + """; + + ChatCompletion completion = objectMapper.readValue(json, ChatCompletion.class); + + assertThat(completion.choices.get(0).message.functionCall).isNotNull(); + assertThat(completion.choices.get(0).message.functionCall.name) + .isEqualTo("get_current_weather"); + assertThat(completion.choices.get(0).message.functionCall.arguments) + .isEqualTo("{\"location\": \"Boston\"}"); + } + + @Test + public void testDeserializeChatCompletion_withThoughtSignatureAndGeminiTokens() throws Exception { + String json = + """ + { + "choices": [{ + "message": { + "role": "assistant", + "tool_calls": [{ + "id": "call_abc", + "type": "function", + "extra_content": { + "google": { + "thought_signature": "c2lnbmF0dXJl" + } + } + }] + } + }], + "usage": { + "thoughts_token_count": 50 + } + } + """; + + ChatCompletion completion = objectMapper.readValue(json, ChatCompletion.class); + + assertThat(completion.choices.get(0).message.toolCalls).hasSize(1); + assertThat(completion.choices.get(0).message.toolCalls.get(0).extraContent).isNotNull(); + Map extraContentMap = + completion.choices.get(0).message.toolCalls.get(0).extraContent; + @SuppressWarnings("unchecked") // This code won't run in production and it's is a JSON object. + Map googleMap = (Map) extraContentMap.get("google"); + assertThat(googleMap.get("thought_signature")).isEqualTo("c2lnbmF0dXJl"); + assertThat(completion.usage.thoughtsTokenCount).isEqualTo(50); + } + + @Test + public void testDeserializeChatCompletion_withArbitraryExtraContent() throws Exception { + String json = + """ + { + "choices": [{ + "message": { + "role": "assistant", + "tool_calls": [{ + "id": "call_abc", + "type": "function", + "extra_content": { + "custom_key": "custom_value", + "nested": { + "key": 123 + } + } + }] + } + }] + } + """; + + ChatCompletion got = objectMapper.readValue(json, ChatCompletion.class); + + assertThat(got.choices.get(0).message.toolCalls).hasSize(1); + Map extraContent = got.choices.get(0).message.toolCalls.get(0).extraContent; + assertThat(extraContent.get("custom_key")).isEqualTo("custom_value"); + @SuppressWarnings("unchecked") // This code won't run in production and it's is a JSON object. + Map nested = (Map) extraContent.get("nested"); + assertThat(nested.get("key")).isEqualTo(123); + } + + @Test + public void testDeserializeChatCompletion_withAudio() throws Exception { + String json = + """ + { + "choices": [{ + "message": { + "role": "assistant", + "content": "Hello", + "annotations": [{ + "type": "url_citation", + "url_citation": { + "end_index": 5, + "start_index": 0, + "title": "Example Title", + "url": "https://example.com" + } + }], + "audio": { + "id": "audio_123", + "data": "base64data", + "expires_at": 1234567890, + "transcript": "Hello" + } + } + }] + } + """; + + ChatCompletionsResponse.ChatCompletion completion = + objectMapper.readValue(json, ChatCompletionsResponse.ChatCompletion.class); + + assertThat(completion.choices.get(0).message.annotations).hasSize(1); + ChatCompletionsResponse.Annotation annotation = + completion.choices.get(0).message.annotations.get(0); + assertThat(annotation.type).isEqualTo("url_citation"); + assertThat(annotation.urlCitation.title).isEqualTo("Example Title"); + assertThat(annotation.urlCitation.url).isEqualTo("https://example.com"); + + assertThat(completion.choices.get(0).message.audio).isNotNull(); + assertThat(completion.choices.get(0).message.audio.id).isEqualTo("audio_123"); + assertThat(completion.choices.get(0).message.audio.data).isEqualTo("base64data"); + } + + @Test + public void testDeserializeChatCompletion_withCustomToolCall() throws Exception { + String json = + """ + { + "choices": [{ + "message": { + "role": "assistant", + "tool_calls": [{ + "id": "call_custom", + "type": "custom", + "custom": { + "input": "{\\\"arg\\\":\\\"val\\\"}", + "name": "custom_tool" + } + }] + } + }] + } + """; + + ChatCompletionsResponse.ChatCompletion completion = + objectMapper.readValue(json, ChatCompletionsResponse.ChatCompletion.class); + + assertThat(completion.choices.get(0).message.toolCalls).hasSize(1); + ChatCompletionsCommon.ToolCall toolCall = completion.choices.get(0).message.toolCalls.get(0); + assertThat(toolCall.type).isEqualTo("custom"); + assertThat(toolCall.custom.name).isEqualTo("custom_tool"); + assertThat(toolCall.custom.input).isEqualTo("{\"arg\":\"val\"}"); + } + + @Test + public void testDeserializeChatCompletionChunk_streamingResponse() throws Exception { + String json = + """ + { + "id": "chatcmpl-123", + "object": "chat.completion.chunk", + "created": 1694268190, + "choices": [{ + "index": 0, + "delta": { + "content": "Hello" + } + }] + } + """; + + ChatCompletionChunk chunk = objectMapper.readValue(json, ChatCompletionChunk.class); + + assertThat(chunk.id).isEqualTo("chatcmpl-123"); + assertThat(chunk.object).isEqualTo("chat.completion.chunk"); + assertThat(chunk.choices).hasSize(1); + assertThat(chunk.choices.get(0).delta.content).isEqualTo("Hello"); + } + + @Test + public void testDeserializeChatCompletionChunk_withToolCallDelta() throws Exception { + String json = + """ + { + "choices": [{ + "delta": { + "tool_calls": [{ + "index": 1, + "id": "call_abc", + "type": "function", + "function": { + "name": "get_weather", + "arguments": "{\\\"location\\\":\\\"Boston\\\"}" + }, + "extra_content": { + "google": { + "thought_signature": "sig" + } + } + }] + } + }], + "usage": { + "completion_tokens": 10, + "prompt_tokens": 5, + "total_tokens": 15 + } + } + """; + + ChatCompletionChunk chunk = objectMapper.readValue(json, ChatCompletionChunk.class); + + assertThat(chunk.choices.get(0).delta.toolCalls).hasSize(1); + ChatCompletionsCommon.ToolCall toolCall = chunk.choices.get(0).delta.toolCalls.get(0); + assertThat(toolCall.index).isEqualTo(1); + assertThat(toolCall.id).isEqualTo("call_abc"); + assertThat(toolCall.type).isEqualTo("function"); + assertThat(toolCall.function.name).isEqualTo("get_weather"); + assertThat(toolCall.function.arguments).isEqualTo("{\"location\":\"Boston\"}"); + @SuppressWarnings("unchecked") // This code won't run in production and it's is a JSON object. + Map google = (Map) toolCall.extraContent.get("google"); + assertThat(google).containsEntry("thought_signature", "sig"); + + assertThat(chunk.usage).isNotNull(); + assertThat(chunk.usage.completionTokens).isEqualTo(10); + assertThat(chunk.usage.promptTokens).isEqualTo(5); + assertThat(chunk.usage.totalTokens).isEqualTo(15); + } + + @Test + public void testToLlmResponse_simpleText() throws Exception { + String json = + """ + { + "id": "chatcmpl-123", + "object": "chat.completion", + "created": 1694268190, + "model": "gpt-4", + "system_fingerprint": "fp_123", + "service_tier": "scale", + "choices": [{ + "index": 0, + "message": { + "role": "assistant", + "content": "Hello world" + }, + "finish_reason": "stop" + }], + "usage": { + "completion_tokens": 10, + "prompt_tokens": 5, + "total_tokens": 15, + "thoughts_token_count": 42 + } + } + """; + + ChatCompletionsResponse.ChatCompletion completion = + objectMapper.readValue(json, ChatCompletionsResponse.ChatCompletion.class); + + LlmResponse response = completion.toLlmResponse(); + + assertThat(response.modelVersion()).hasValue("gpt-4"); + assertThat(response.finishReason().get().knownEnum()).isEqualTo(Known.STOP); + + // Usage Metadata + assertThat(response.usageMetadata().get().promptTokenCount()).hasValue(5); + assertThat(response.usageMetadata().get().candidatesTokenCount()).hasValue(10); + assertThat(response.usageMetadata().get().totalTokenCount()).hasValue(15); + assertThat(response.usageMetadata().get().thoughtsTokenCount()).hasValue(42); + + // Content + assertThat(response.content().get().role()).hasValue("model"); + assertThat(response.content().get().parts().get().get(0).text()).hasValue("Hello world"); + + // Custom Metadata + List metadata = response.customMetadata().get(); + assertThat(metadata).hasSize(5); + assertThat(metadata.get(0).key()).hasValue("id"); + assertThat(metadata.get(0).stringValue()).hasValue("chatcmpl-123"); + assertThat(metadata.get(1).key()).hasValue("created"); + assertThat(metadata.get(1).stringValue()).hasValue("1694268190"); + assertThat(metadata.get(2).key()).hasValue("object"); + assertThat(metadata.get(2).stringValue()).hasValue("chat.completion"); + assertThat(metadata.get(3).key()).hasValue("system_fingerprint"); + assertThat(metadata.get(3).stringValue()).hasValue("fp_123"); + assertThat(metadata.get(4).key()).hasValue("service_tier"); + assertThat(metadata.get(4).stringValue()).hasValue("scale"); + } + + @Test + public void testToLlmResponse_withToolCall_simple() throws Exception { + String json = + """ + { + "choices": [{ + "message": { + "role": "assistant", + "tool_calls": [{ + "id": "call_123", + "type": "function", + "function": { + "name": "get_weather", + "arguments": "{\\\"location\\\":\\\"Seattle\\\"}" + } + }] + } + }] + } + """; + + ChatCompletionsResponse.ChatCompletion completion = + objectMapper.readValue(json, ChatCompletion.class); + + LlmResponse response = completion.toLlmResponse(); + + Part part = response.content().get().parts().get().get(0); + FunctionCall fc = part.functionCall().get(); + assertThat(fc.id()).hasValue("call_123"); + assertThat(fc.name()).hasValue("get_weather"); + assertThat(fc.args().get().get("location")).isEqualTo("Seattle"); + + assertThat(response.customMetadata().get()).isEmpty(); + } + + @Test + public void testToLlmResponse_thoughtSignature() throws Exception { + String json = + """ + { + "choices": [{ + "message": { + "role": "assistant", + "tool_calls": [{ + "id": "call_123", + "type": "function", + "function": { + "name": "get_weather", + "arguments": "{\\\"location\\\":\\\"Seattle\\\"}" + }, + "extra_content": { + "google": { + "thought_signature": "c2ln" + } + } + }] + } + }] + } + """; + + ChatCompletionsResponse.ChatCompletion completion = + objectMapper.readValue(json, ChatCompletion.class); + + LlmResponse response = completion.toLlmResponse(); + + assertThat(response.content().get().parts().get().get(0).thoughtSignature().get()) + .isEqualTo(Base64.getDecoder().decode("c2ln")); + } + + @Test + public void testToLlmResponse_withRefusal() throws Exception { + String json = + """ + { + "id": "chatcmpl-123", + "object": "chat.completion", + "created": 1677652288, + "model": "gpt-3.5-turbo-0125", + "choices": [{ + "index": 0, + "message": { + "role": "assistant", + "refusal": "System error or refusal" + }, + "finish_reason": "stop" + }] + } + """; + + ChatCompletionsResponse.ChatCompletion completion = + objectMapper.readValue(json, ChatCompletionsResponse.ChatCompletion.class); + + LlmResponse response = completion.toLlmResponse(); + + assertThat(response.modelVersion()).hasValue("gpt-3.5-turbo-0125"); + assertThat(response.finishReason().get().knownEnum()).isEqualTo(Known.STOP); + + // Content + assertThat(response.content().get().role()).hasValue("model"); + assertThat(response.content().get().parts().get().get(0).text()) + .hasValue("System error or refusal"); + + // Custom Metadata + List metadata = response.customMetadata().get(); + assertThat(metadata).hasSize(3); + assertThat(metadata.get(0).key()).hasValue("id"); + assertThat(metadata.get(0).stringValue()).hasValue("chatcmpl-123"); + assertThat(metadata.get(1).key()).hasValue("created"); + assertThat(metadata.get(1).stringValue()).hasValue("1677652288"); + assertThat(metadata.get(2).key()).hasValue("object"); + assertThat(metadata.get(2).stringValue()).hasValue("chat.completion"); + } + + @Test + public void testToLlmResponse_reasoningTokens() throws Exception { + String json = + """ + { + "choices": [{ + "message": { + "role": "assistant", + "content": "hello" + }, + "finish_reason": "stop" + }], + "usage": { + "prompt_tokens": 10, + "completion_tokens": 5, + "total_tokens": 15, + "completion_tokens_details": { + "reasoning_tokens": 4 + } + } + } + """; + + ChatCompletionsResponse.ChatCompletion completion = + objectMapper.readValue(json, ChatCompletionsResponse.ChatCompletion.class); + + LlmResponse response = completion.toLlmResponse(); + + assertThat(response.finishReason().get().knownEnum()).isEqualTo(Known.STOP); + + // Content + assertThat(response.content().get().role()).hasValue("model"); + assertThat(response.content().get().parts().get().get(0).text()).hasValue("hello"); + + // Usage Metadata + assertThat(response.usageMetadata().get().promptTokenCount()).hasValue(10); + assertThat(response.usageMetadata().get().candidatesTokenCount()).hasValue(5); + assertThat(response.usageMetadata().get().totalTokenCount()).hasValue(15); + assertThat(response.usageMetadata().get().thoughtsTokenCount()).hasValue(4); + + assertThat(response.customMetadata().get()).isEmpty(); + } + + @Test + public void testToolCallToPart_withFunction() throws Exception { + String json = + """ + { + "id": "call_123", + "type": "function", + "function": { + "name": "get_weather", + "arguments": "{\\\"location\\\":\\\"Seattle\\\"}" + } + } + """; + ChatCompletionsCommon.ToolCall toolCall = + objectMapper.readValue(json, ChatCompletionsCommon.ToolCall.class); + + Part part = toolCall.toPart(); + + assertThat(part).isNotNull(); + assertThat(part.functionCall()).isPresent(); + FunctionCall fc = part.functionCall().get(); + assertThat(fc.id()).hasValue("call_123"); + assertThat(fc.name()).hasValue("get_weather"); + } + + @Test + public void testToolCallToPart_withFunction_nullId() throws Exception { + String json = + """ + { + "type": "function", + "function": { + "name": "get_weather", + "arguments": "{\\\"location\\\":\\\"Seattle\\\"}" + } + } + """; + ChatCompletionsCommon.ToolCall toolCall = + objectMapper.readValue(json, ChatCompletionsCommon.ToolCall.class); + + Part part = toolCall.toPart(); + + assertThat(part).isNotNull(); + assertThat(part.functionCall()).isPresent(); + FunctionCall fc = part.functionCall().get(); + assertThat(fc.id().isPresent()).isFalse(); + } + + @Test + public void testToolCallToPart_withThoughtSignature() throws Exception { + String json = + """ + { + "id": "call_123", + "type": "function", + "function": { + "name": "get_weather", + "arguments": "{\\\"location\\\":\\\"Seattle\\\"}" + }, + "extra_content": { + "google": { + "thought_signature": "c2ln" + } + } + } + """; + ChatCompletionsCommon.ToolCall toolCall = + objectMapper.readValue(json, ChatCompletionsCommon.ToolCall.class); + + Part part = toolCall.toPart(); + + assertThat(part).isNotNull(); + assertThat(part.thoughtSignature().get()).isEqualTo(Base64.getDecoder().decode("c2ln")); + } + + @Test + public void testToolCallToPart_nullFunction() throws Exception { + String json = + """ + { + "id": "call_123", + "type": "function" + } + """; + ChatCompletionsCommon.ToolCall toolCall = + objectMapper.readValue(json, ChatCompletionsCommon.ToolCall.class); + + Part part = toolCall.toPart(); + + assertThat(part).isNull(); + } + + @Test + public void testToLlmResponse_noChoices() throws Exception { + String json = + """ + { + "id": "chatcmpl-123", + "object": "chat.completion", + "created": 1677652288, + "model": "gpt-4" + } + """; + + ChatCompletionsResponse.ChatCompletion completion = + objectMapper.readValue(json, ChatCompletionsResponse.ChatCompletion.class); + + LlmResponse response = completion.toLlmResponse(); + + assertThat(response.modelVersion()).hasValue("gpt-4"); + assertThat(response.content()).isPresent(); + assertThat(response.content().get().parts().isPresent()).isFalse(); + } +}