Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@
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)
Expand All @@ -28,6 +34,17 @@ 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)
Expand Down Expand Up @@ -56,6 +73,43 @@ static class ToolCall {
*/
@JsonProperty("extra_content")
public Map<String, Object> 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;
}
}

/**
Expand All @@ -70,6 +124,33 @@ static class Function {

/** 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<String, Object> args =
objectMapper.readValue(arguments, new TypeReference<Map<String, Object>>() {});
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();
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,16 @@
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.
Expand Down Expand Up @@ -62,6 +71,162 @@ static class ChatCompletion {

/** 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<CustomMetadata> 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<Part> mapMessageToParts(Message message) {
List<Part> 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<Part> mapToolCallsToParts(List<ChatCompletionsCommon.ToolCall> toolCalls) {
List<Part> 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<CustomMetadata> buildCustomMetadata() {
List<CustomMetadata> 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;
}
}

/**
Expand Down
Loading