diff --git a/agentscope-core/pom.xml b/agentscope-core/pom.xml
index b6bbaf4ef..2778d6e03 100644
--- a/agentscope-core/pom.xml
+++ b/agentscope-core/pom.xml
@@ -103,11 +103,6 @@
-
-
- com.google.genai
- google-genai
-
diff --git a/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiChatFormatter.java b/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiChatFormatter.java
index 9a3fd744f..5be4f13b4 100644
--- a/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiChatFormatter.java
+++ b/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiChatFormatter.java
@@ -15,20 +15,26 @@
*/
package io.agentscope.core.formatter.gemini;
-import com.google.genai.types.Content;
-import com.google.genai.types.GenerateContentConfig;
-import com.google.genai.types.GenerateContentResponse;
-import com.google.genai.types.ThinkingConfig;
-import com.google.genai.types.Tool;
-import com.google.genai.types.ToolConfig;
import io.agentscope.core.formatter.AbstractBaseFormatter;
+import io.agentscope.core.formatter.gemini.dto.GeminiContent;
+import io.agentscope.core.formatter.gemini.dto.GeminiGenerationConfig;
+import io.agentscope.core.formatter.gemini.dto.GeminiGenerationConfig.GeminiThinkingConfig;
+import io.agentscope.core.formatter.gemini.dto.GeminiRequest;
+import io.agentscope.core.formatter.gemini.dto.GeminiResponse;
+import io.agentscope.core.formatter.gemini.dto.GeminiTool;
+import io.agentscope.core.formatter.gemini.dto.GeminiToolConfig;
import io.agentscope.core.message.Msg;
+import io.agentscope.core.message.MsgRole;
+import io.agentscope.core.message.ToolUseBlock;
import io.agentscope.core.model.ChatResponse;
import io.agentscope.core.model.GenerateOptions;
import io.agentscope.core.model.ToolChoice;
import io.agentscope.core.model.ToolSchema;
import java.time.Instant;
+import java.util.ArrayList;
import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Function;
/**
* Formatter for Gemini Content Generation API.
@@ -48,8 +54,7 @@
*
*/
public class GeminiChatFormatter
- extends AbstractBaseFormatter<
- Content, GenerateContentResponse, GenerateContentConfig.Builder> {
+ extends AbstractBaseFormatter {
private final GeminiMessageConverter messageConverter;
private final GeminiResponseParser responseParser;
@@ -65,142 +70,219 @@ public GeminiChatFormatter() {
}
@Override
- protected List doFormat(List msgs) {
- return messageConverter.convertMessages(msgs);
+ protected List doFormat(List msgs) {
+ if (msgs == null) {
+ return new ArrayList<>();
+ }
+ int startIndex = computeStartIndex(msgs);
+
+ // Gemini API requires contents to start with "user" role
+ // If first remaining message is ASSISTANT (from another agent), convert it to USER
+ // Exception: Do not convert if it contains ToolUseBlock, as function calls must be MODEL
+ // role
+ if (startIndex < msgs.size()
+ && msgs.get(startIndex).getRole() == MsgRole.ASSISTANT
+ && msgs.get(startIndex).getContent().stream()
+ .noneMatch(block -> block instanceof ToolUseBlock)) {
+ List result = new ArrayList<>();
+
+ // Convert first ASSISTANT message to USER role for multi-agent compatibility
+ GeminiContent userContent = new GeminiContent();
+ userContent.setRole("user");
+ userContent.setParts(
+ messageConverter
+ .convertMessages(List.of(msgs.get(startIndex)))
+ .get(0)
+ .getParts());
+ result.add(userContent);
+
+ // Add remaining messages
+ if (startIndex + 1 < msgs.size()) {
+ result.addAll(
+ messageConverter.convertMessages(
+ msgs.subList(startIndex + 1, msgs.size())));
+ }
+
+ return result;
+ }
+
+ // Return remaining messages (excluding SYSTEM)
+ if (startIndex > 0 && startIndex < msgs.size()) {
+ return messageConverter.convertMessages(msgs.subList(startIndex, msgs.size()));
+ } else if (startIndex == 0) {
+ return messageConverter.convertMessages(msgs);
+ }
+
+ return new ArrayList<>();
+ }
+
+ /**
+ * Apply system instruction to the request if present.
+ *
+ * @param request The Gemini request to configure
+ * @param originalMessages The original message list (used to extract system prompt)
+ */
+ public void applySystemInstruction(GeminiRequest request, List originalMessages) {
+ GeminiContent systemInstruction = buildSystemInstruction(originalMessages);
+ if (systemInstruction != null) {
+ request.setSystemInstruction(systemInstruction);
+ } else {
+ request.setSystemInstruction(null);
+ }
}
@Override
- public ChatResponse parseResponse(GenerateContentResponse response, Instant startTime) {
+ public ChatResponse parseResponse(GeminiResponse response, Instant startTime) {
return responseParser.parseResponse(response, startTime);
}
@Override
public void applyOptions(
- GenerateContentConfig.Builder configBuilder,
- GenerateOptions options,
- GenerateOptions defaultOptions) {
+ GeminiRequest request, GenerateOptions options, GenerateOptions defaultOptions) {
+
+ // Ensure generation config exists
+ if (request.getGenerationConfig() == null) {
+ request.setGenerationConfig(new GeminiGenerationConfig());
+ }
+ GeminiGenerationConfig config = request.getGenerationConfig();
// Apply each option with fallback to defaultOptions
- applyFloatOption(
- GenerateOptions::getTemperature,
- options,
- defaultOptions,
- configBuilder::temperature);
+ applyDoubleOption(
+ GenerateOptions::getTemperature, options, defaultOptions, config::setTemperature);
- applyFloatOption(GenerateOptions::getTopP, options, defaultOptions, configBuilder::topP);
+ applyDoubleOption(GenerateOptions::getTopP, options, defaultOptions, config::setTopP);
- // Apply topK (Gemini uses Float for topK)
- applyIntegerAsFloatOption(
- GenerateOptions::getTopK, options, defaultOptions, configBuilder::topK);
+ // topK: Integer in GenerateOptions -> Double in GeminiGenerationConfig
+ applyIntegerAsDoubleOption(
+ GenerateOptions::getTopK, options, defaultOptions, config::setTopK);
- // Apply seed
- applyLongAsIntOption(
- GenerateOptions::getSeed, options, defaultOptions, configBuilder::seed);
+ // seed: Long in GenerateOptions -> Integer in GeminiGenerationConfig
+ applyLongAsIntegerOption(
+ GenerateOptions::getSeed, options, defaultOptions, config::setSeed);
applyIntegerOption(
- GenerateOptions::getMaxTokens,
- options,
- defaultOptions,
- configBuilder::maxOutputTokens);
+ GenerateOptions::getMaxTokens, options, defaultOptions, config::setMaxOutputTokens);
- applyFloatOption(
+ applyDoubleOption(
GenerateOptions::getFrequencyPenalty,
options,
defaultOptions,
- configBuilder::frequencyPenalty);
+ config::setFrequencyPenalty);
- applyFloatOption(
+ applyDoubleOption(
GenerateOptions::getPresencePenalty,
options,
defaultOptions,
- configBuilder::presencePenalty);
+ config::setPresencePenalty);
// Apply ThinkingConfig if either includeThoughts or thinkingBudget is set
Integer thinkingBudget =
getOptionOrDefault(options, defaultOptions, GenerateOptions::getThinkingBudget);
if (thinkingBudget != null) {
- ThinkingConfig.Builder thinkingConfigBuilder = ThinkingConfig.builder();
- thinkingConfigBuilder.includeThoughts(true);
- thinkingConfigBuilder.thinkingBudget(thinkingBudget);
- configBuilder.thinkingConfig(thinkingConfigBuilder.build());
+ GeminiThinkingConfig thinkingConfig = new GeminiThinkingConfig();
+ thinkingConfig.setIncludeThoughts(true);
+ thinkingConfig.setThinkingBudget(thinkingBudget);
+ config.setThinkingConfig(thinkingConfig);
}
}
/**
- * Apply Float option with fallback logic.
+ * Apply Double option with fallback logic.
*/
- private void applyFloatOption(
- java.util.function.Function accessor,
+ private void applyDoubleOption(
+ Function accessor,
GenerateOptions options,
GenerateOptions defaultOptions,
- java.util.function.Consumer setter) {
+ Consumer setter) {
Double value = getOptionOrDefault(options, defaultOptions, accessor);
if (value != null) {
- setter.accept(value.floatValue());
+ setter.accept(value);
}
}
/**
- * Apply Integer option with fallback logic.
+ * Apply Integer option as Double with fallback logic.
*/
- private void applyIntegerOption(
- java.util.function.Function accessor,
+ private void applyIntegerAsDoubleOption(
+ Function accessor,
GenerateOptions options,
GenerateOptions defaultOptions,
- java.util.function.Consumer setter) {
+ Consumer setter) {
Integer value = getOptionOrDefault(options, defaultOptions, accessor);
if (value != null) {
- setter.accept(value);
+ setter.accept(value.doubleValue());
}
}
/**
- * Apply Integer option as Float with fallback logic (for Gemini topK which requires Float).
+ * Apply Long option as Integer with fallback logic.
*/
- private void applyIntegerAsFloatOption(
- java.util.function.Function accessor,
+ private void applyLongAsIntegerOption(
+ Function accessor,
GenerateOptions options,
GenerateOptions defaultOptions,
- java.util.function.Consumer setter) {
+ Consumer setter) {
- Integer value = getOptionOrDefault(options, defaultOptions, accessor);
+ Long value = getOptionOrDefault(options, defaultOptions, accessor);
if (value != null) {
- setter.accept(value.floatValue());
+ setter.accept(value.intValue());
}
}
/**
- * Apply Long option as Integer with fallback logic (for Gemini seed which requires Integer).
+ * Apply Integer option with fallback logic.
*/
- private void applyLongAsIntOption(
- java.util.function.Function accessor,
+ private void applyIntegerOption(
+ Function accessor,
GenerateOptions options,
GenerateOptions defaultOptions,
- java.util.function.Consumer setter) {
+ Consumer setter) {
- Long value = getOptionOrDefault(options, defaultOptions, accessor);
+ Integer value = getOptionOrDefault(options, defaultOptions, accessor);
if (value != null) {
- setter.accept(value.intValue());
+ setter.accept(value);
}
}
@Override
- public void applyTools(GenerateContentConfig.Builder configBuilder, List tools) {
- Tool tool = toolsHelper.convertToGeminiTool(tools);
+ public void applyTools(GeminiRequest request, List tools) {
+ GeminiTool tool = toolsHelper.convertToGeminiTool(tools);
if (tool != null) {
- configBuilder.tools(List.of(tool));
+ // Gemini API expects a list of tools, typically one tool object containing
+ // function declarations
+ request.setTools(List.of(tool));
}
}
@Override
- public void applyToolChoice(
- GenerateContentConfig.Builder configBuilder, ToolChoice toolChoice) {
- ToolConfig toolConfig = toolsHelper.convertToolChoice(toolChoice);
+ public void applyToolChoice(GeminiRequest request, ToolChoice toolChoice) {
+ GeminiToolConfig toolConfig = toolsHelper.convertToolChoice(toolChoice);
if (toolConfig != null) {
- configBuilder.toolConfig(toolConfig);
+ request.setToolConfig(toolConfig);
+ }
+ }
+
+ private int computeStartIndex(List msgs) {
+ if (msgs == null || msgs.isEmpty()) {
+ return 0;
+ }
+ return msgs.get(0).getRole() == MsgRole.SYSTEM ? 1 : 0;
+ }
+
+ private GeminiContent buildSystemInstruction(List msgs) {
+ if (msgs == null || msgs.isEmpty()) {
+ return null;
}
+
+ Msg first = msgs.get(0);
+ if (first.getRole() != MsgRole.SYSTEM) {
+ return null;
+ }
+
+ List converted = messageConverter.convertMessages(List.of(first));
+ return converted.isEmpty() ? null : converted.get(0);
}
}
diff --git a/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiConversationMerger.java b/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiConversationMerger.java
index a34d681be..1d6e90637 100644
--- a/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiConversationMerger.java
+++ b/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiConversationMerger.java
@@ -15,8 +15,8 @@
*/
package io.agentscope.core.formatter.gemini;
-import com.google.genai.types.Content;
-import com.google.genai.types.Part;
+import io.agentscope.core.formatter.gemini.dto.GeminiContent;
+import io.agentscope.core.formatter.gemini.dto.GeminiPart;
import io.agentscope.core.message.AudioBlock;
import io.agentscope.core.message.ContentBlock;
import io.agentscope.core.message.ImageBlock;
@@ -79,13 +79,13 @@ public GeminiConversationMerger(String conversationHistoryPrompt) {
* @param historyPrompt The prompt to prepend (empty if not first group)
* @return Single merged Content for Gemini API
*/
- public Content mergeToContent(
+ public GeminiContent mergeToContent(
List msgs,
Function nameExtractor,
Function, String> toolResultConverter,
String historyPrompt) {
- List parts = new ArrayList<>();
+ List parts = new ArrayList<>();
List accumulatedText = new ArrayList<>();
// Process each message and its content blocks
@@ -110,7 +110,9 @@ public Content mergeToContent(
} else if (block instanceof ImageBlock ib) {
// Flush accumulated text as a Part
if (!accumulatedText.isEmpty()) {
- parts.add(Part.builder().text(String.join("\n", accumulatedText)).build());
+ GeminiPart part = new GeminiPart();
+ part.setText(String.join("\n", accumulatedText));
+ parts.add(part);
accumulatedText.clear();
}
// Add image as separate Part
@@ -119,7 +121,9 @@ public Content mergeToContent(
} else if (block instanceof AudioBlock ab) {
// Flush accumulated text as a Part
if (!accumulatedText.isEmpty()) {
- parts.add(Part.builder().text(String.join("\n", accumulatedText)).build());
+ GeminiPart part = new GeminiPart();
+ part.setText(String.join("\n", accumulatedText));
+ parts.add(part);
accumulatedText.clear();
}
// Add audio as separate Part
@@ -128,7 +132,9 @@ public Content mergeToContent(
} else if (block instanceof VideoBlock vb) {
// Flush accumulated text as a Part
if (!accumulatedText.isEmpty()) {
- parts.add(Part.builder().text(String.join("\n", accumulatedText)).build());
+ GeminiPart part = new GeminiPart();
+ part.setText(String.join("\n", accumulatedText));
+ parts.add(part);
accumulatedText.clear();
}
// Add video as separate Part
@@ -139,32 +145,38 @@ public Content mergeToContent(
// Flush any remaining accumulated text
if (!accumulatedText.isEmpty()) {
- parts.add(Part.builder().text(String.join("\n", accumulatedText)).build());
+ GeminiPart part = new GeminiPart();
+ part.setText(String.join("\n", accumulatedText));
+ parts.add(part);
}
// Add conversation history prompt and tags
if (!parts.isEmpty()) {
- Part firstPart = parts.get(0);
- if (firstPart.text().isPresent()) {
- String modifiedText = historyPrompt + HISTORY_START_TAG + firstPart.text().get();
- parts.set(0, Part.builder().text(modifiedText).build());
+ GeminiPart firstPart = parts.get(0);
+ if (firstPart.getText() != null) {
+ String modifiedText = historyPrompt + HISTORY_START_TAG + firstPart.getText();
+ firstPart.setText(modifiedText);
} else {
// First part is media, insert text part at beginning
- parts.add(0, Part.builder().text(historyPrompt + HISTORY_START_TAG).build());
+ GeminiPart part = new GeminiPart();
+ part.setText(historyPrompt + HISTORY_START_TAG);
+ parts.add(0, part);
}
// Add closing tag to last text part
- Part lastPart = parts.get(parts.size() - 1);
- if (lastPart.text().isPresent()) {
- String modifiedText = lastPart.text().get() + "\n" + HISTORY_END_TAG;
- parts.set(parts.size() - 1, Part.builder().text(modifiedText).build());
+ GeminiPart lastPart = parts.get(parts.size() - 1);
+ if (lastPart.getText() != null) {
+ String modifiedText = lastPart.getText() + "\n" + HISTORY_END_TAG;
+ lastPart.setText(modifiedText);
} else {
// Last part is media, append text part at end
- parts.add(Part.builder().text(HISTORY_END_TAG).build());
+ GeminiPart part = new GeminiPart();
+ part.setText(HISTORY_END_TAG);
+ parts.add(part);
}
}
// Return Content with "user" role
- return Content.builder().role("user").parts(parts).build();
+ return new GeminiContent("user", parts);
}
}
diff --git a/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiMediaConverter.java b/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiMediaConverter.java
index cdaca8425..4f7d4ede6 100644
--- a/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiMediaConverter.java
+++ b/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiMediaConverter.java
@@ -15,8 +15,8 @@
*/
package io.agentscope.core.formatter.gemini;
-import com.google.genai.types.Blob;
-import com.google.genai.types.Part;
+import io.agentscope.core.formatter.gemini.dto.GeminiPart;
+import io.agentscope.core.formatter.gemini.dto.GeminiPart.GeminiBlob;
import io.agentscope.core.message.AudioBlock;
import io.agentscope.core.message.Base64Source;
import io.agentscope.core.message.ImageBlock;
@@ -64,7 +64,7 @@ public class GeminiMediaConverter {
* @param block ImageBlock to convert
* @return Part object containing inline data
*/
- public Part convertToInlineDataPart(ImageBlock block) {
+ public GeminiPart convertToInlineDataPart(ImageBlock block) {
return convertMediaBlockToInlineDataPart(block.getSource(), "image");
}
@@ -74,7 +74,7 @@ public Part convertToInlineDataPart(ImageBlock block) {
* @param block AudioBlock to convert
* @return Part object containing inline data
*/
- public Part convertToInlineDataPart(AudioBlock block) {
+ public GeminiPart convertToInlineDataPart(AudioBlock block) {
return convertMediaBlockToInlineDataPart(block.getSource(), "audio");
}
@@ -84,31 +84,39 @@ public Part convertToInlineDataPart(AudioBlock block) {
* @param block VideoBlock to convert
* @return Part object containing inline data
*/
- public Part convertToInlineDataPart(VideoBlock block) {
+ public GeminiPart convertToInlineDataPart(VideoBlock block) {
return convertMediaBlockToInlineDataPart(block.getSource(), "video");
}
/**
* Convert a media source to Gemini Part with inline data.
*
- * @param source Source object (Base64Source or URLSource)
+ * @param source Source object (Base64Source or URLSource)
* @param mediaType Media type string ("image", "audio", or "video")
* @return Part object with inline data
*/
- private Part convertMediaBlockToInlineDataPart(Source source, String mediaType) {
- byte[] data;
+ private GeminiPart convertMediaBlockToInlineDataPart(Source source, String mediaType) {
+ String base64Data;
String mimeType;
if (source instanceof Base64Source base64Source) {
- // Base64: decode and use directly
- data = Base64.getDecoder().decode(base64Source.getData());
+ // Base64: validate and use directly
+ String data = base64Source.getData();
+ try {
+ // Validate that the data is valid base64
+ Base64.getDecoder().decode(data);
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Base64Source data is not valid base64", e);
+ }
+ base64Data = data;
mimeType = base64Source.getMediaType();
} else if (source instanceof URLSource urlSource) {
// URL: read file and get mime type
String url = urlSource.getUrl();
try {
- data = readFileAsBytes(url);
+ byte[] data = readFileAsBytes(url);
+ base64Data = Base64.getEncoder().encodeToString(data);
mimeType = getMimeType(url, mediaType);
} catch (IOException e) {
throw new RuntimeException("Failed to read file: " + url, e);
@@ -120,9 +128,11 @@ private Part convertMediaBlockToInlineDataPart(Source source, String mediaType)
}
// Create Blob and Part
- Blob blob = Blob.builder().data(data).mimeType(mimeType).build();
+ GeminiBlob blob = new GeminiBlob(mimeType, base64Data);
+ GeminiPart part = new GeminiPart();
+ part.setInlineData(blob);
- return Part.builder().inlineData(blob).build();
+ return part;
}
/**
@@ -158,7 +168,7 @@ private byte[] readFileAsBytes(String url) throws IOException {
/**
* Determine MIME type from file extension.
*
- * @param url File URL or path
+ * @param url File URL or path
* @param mediaType Media type category ("image", "audio", "video")
* @return MIME type string (e.g., "image/png")
*/
diff --git a/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiMessageConverter.java b/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiMessageConverter.java
index 34cd30dce..6bc8d1353 100644
--- a/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiMessageConverter.java
+++ b/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiMessageConverter.java
@@ -15,10 +15,10 @@
*/
package io.agentscope.core.formatter.gemini;
-import com.google.genai.types.Content;
-import com.google.genai.types.FunctionCall;
-import com.google.genai.types.FunctionResponse;
-import com.google.genai.types.Part;
+import io.agentscope.core.formatter.gemini.dto.GeminiContent;
+import io.agentscope.core.formatter.gemini.dto.GeminiPart;
+import io.agentscope.core.formatter.gemini.dto.GeminiPart.GeminiFunctionCall;
+import io.agentscope.core.formatter.gemini.dto.GeminiPart.GeminiFunctionResponse;
import io.agentscope.core.message.AudioBlock;
import io.agentscope.core.message.Base64Source;
import io.agentscope.core.message.ContentBlock;
@@ -81,15 +81,17 @@ public GeminiMessageConverter() {
* @param msgs List of AgentScope messages
* @return List of Gemini Content objects
*/
- public List convertMessages(List msgs) {
- List result = new ArrayList<>();
+ public List convertMessages(List msgs) {
+ List result = new ArrayList<>();
for (Msg msg : msgs) {
- List parts = new ArrayList<>();
+ List parts = new ArrayList<>();
for (ContentBlock block : msg.getContent()) {
if (block instanceof TextBlock tb) {
- parts.add(Part.builder().text(tb.getText()).build());
+ GeminiPart part = new GeminiPart();
+ part.setText(tb.getText());
+ parts.add(part);
} else if (block instanceof ToolUseBlock tub) {
// Prioritize using content field (raw arguments string), fallback to input map
@@ -112,51 +114,42 @@ public List convertMessages(List msgs) {
}
// Create FunctionCall
- FunctionCall functionCall =
- FunctionCall.builder()
- .id(tub.getId())
- .name(tub.getName())
- .args(args)
- .build();
-
- // Build Part with FunctionCall and optional thought signature
- Part.Builder partBuilder = Part.builder().functionCall(functionCall);
-
- // Check for thought signature in metadata
- Map metadata = tub.getMetadata();
- if (metadata != null
- && metadata.containsKey(ToolUseBlock.METADATA_THOUGHT_SIGNATURE)) {
- Object signature = metadata.get(ToolUseBlock.METADATA_THOUGHT_SIGNATURE);
- if (signature instanceof byte[]) {
- partBuilder.thoughtSignature((byte[]) signature);
+ GeminiFunctionCall functionCall =
+ new GeminiFunctionCall(tub.getId(), tub.getName(), args);
+
+ // Build Part
+ GeminiPart part = new GeminiPart();
+ part.setFunctionCall(functionCall);
+
+ // Restore thoughtSignature from metadata if present (required for Gemini 2.5+)
+ if (tub.getMetadata() != null
+ && tub.getMetadata()
+ .containsKey(ToolUseBlock.METADATA_THOUGHT_SIGNATURE)) {
+ Object thoughtSig =
+ tub.getMetadata().get(ToolUseBlock.METADATA_THOUGHT_SIGNATURE);
+ if (thoughtSig instanceof String) {
+ part.setThoughtSignature((String) thoughtSig);
}
}
- parts.add(partBuilder.build());
+ parts.add(part);
} else if (block instanceof ToolResultBlock trb) {
// IMPORTANT: Tool result as independent Content with "user" role
String textOutput = convertToolResultToString(trb.getOutput());
- // Create response map with "output" key
+ // Create response map with "output" key (or whatever standard Gemini expects)
Map responseMap = new HashMap<>();
responseMap.put("output", textOutput);
- FunctionResponse functionResponse =
- FunctionResponse.builder()
- .id(trb.getId())
- .name(trb.getName())
- .response(responseMap)
- .build();
+ GeminiFunctionResponse functionResponse =
+ new GeminiFunctionResponse(trb.getId(), trb.getName(), responseMap);
- Part functionResponsePart =
- Part.builder().functionResponse(functionResponse).build();
+ GeminiPart functionResponsePart = new GeminiPart();
+ functionResponsePart.setFunctionResponse(functionResponse);
- Content toolResultContent =
- Content.builder()
- .role("user")
- .parts(List.of(functionResponsePart))
- .build();
+ GeminiContent toolResultContent =
+ new GeminiContent("user", List.of(functionResponsePart));
result.add(toolResultContent);
// Skip adding to current message parts
@@ -186,7 +179,7 @@ public List convertMessages(List msgs) {
// Add message if there are parts
if (!parts.isEmpty()) {
String role = convertRole(msg.getRole());
- Content content = Content.builder().role(role).parts(parts).build();
+ GeminiContent content = new GeminiContent(role, parts);
result.add(content);
}
}
@@ -253,10 +246,13 @@ private String convertToolResultToString(List output) {
/**
* Convert a media block to textual reference for tool results.
- * Returns a formatted string: "The returned {mediaType} can be found at: {path}"
+ * Returns a formatted string: "The returned {mediaType} can be found at:
+ * {path}"
*
- *
For URL sources, returns the URL directly.
- * For Base64 sources, saves the data to a temporary file and returns the file path.
+ *
+ * For URL sources, returns the URL directly.
+ * For Base64 sources, saves the data to a temporary file and returns the file
+ * path.
*
* @param block The media block (ImageBlock, AudioBlock, or VideoBlock)
* @param mediaType Media type string ("image", "audio", or "video")
@@ -307,8 +303,11 @@ private Source extractSourceFromBlock(ContentBlock block) {
/**
* Save base64 data to a temporary file.
*
- *
The file extension is extracted from the MIME type (e.g., "audio/wav" → ".wav").
- * The file is created with prefix "agentscope_" and will not be automatically deleted.
+ *
+ * The file extension is extracted from the MIME type (e.g., "audio/wav" →
+ * ".wav").
+ * The file is created with prefix "agentscope_" and will not be automatically
+ * deleted.
*
* @param mediaType The MIME type (e.g., "image/png", "audio/wav")
* @param base64Data The base64-encoded data (without prefix)
diff --git a/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiMultiAgentFormatter.java b/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiMultiAgentFormatter.java
index 4eec05b9c..2bb163e77 100644
--- a/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiMultiAgentFormatter.java
+++ b/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiMultiAgentFormatter.java
@@ -15,11 +15,10 @@
*/
package io.agentscope.core.formatter.gemini;
-import com.google.genai.types.Content;
-import com.google.genai.types.GenerateContentConfig;
-import com.google.genai.types.GenerateContentResponse;
-import com.google.genai.types.Part;
import io.agentscope.core.formatter.AbstractBaseFormatter;
+import io.agentscope.core.formatter.gemini.dto.GeminiContent;
+import io.agentscope.core.formatter.gemini.dto.GeminiRequest;
+import io.agentscope.core.formatter.gemini.dto.GeminiResponse;
import io.agentscope.core.message.Msg;
import io.agentscope.core.message.MsgRole;
import io.agentscope.core.message.ToolResultBlock;
@@ -46,8 +45,7 @@
*
*/
public class GeminiMultiAgentFormatter
- extends AbstractBaseFormatter<
- Content, GenerateContentResponse, GenerateContentConfig.Builder> {
+ extends AbstractBaseFormatter {
private static final String DEFAULT_CONVERSATION_HISTORY_PROMPT =
"# Conversation History\n"
@@ -81,25 +79,49 @@ public GeminiMultiAgentFormatter(String conversationHistoryPrompt) {
}
@Override
- protected List doFormat(List msgs) {
- List result = new ArrayList<>();
- int startIndex = 0;
-
- // Process system message first (if any) - convert to user role
- if (!msgs.isEmpty() && msgs.get(0).getRole() == MsgRole.SYSTEM) {
- Msg systemMsg = msgs.get(0);
- // Gemini doesn't support system role in contents, convert to user
- Content systemContent =
- Content.builder()
- .role("user")
- .parts(
- List.of(
- Part.builder()
- .text(extractTextContent(systemMsg))
- .build()))
- .build();
- result.add(systemContent);
- startIndex = 1;
+ protected List doFormat(List msgs) {
+ if (msgs == null) {
+ return new ArrayList<>();
+ }
+ List result = new ArrayList<>();
+ int startIndex = computeStartIndex(msgs);
+
+ // Gemini API requires contents to start with "user" role
+ // If first remaining message is ASSISTANT (from another agent), convert it to USER
+ // EXCEPTION: If the message is a tool call (which uses ASSISTANT role), we must preserve it
+ // as is (it will be converted to MODEL role by converter later), because tool calls must
+ // come from MODEL.
+ if (startIndex < msgs.size() && msgs.get(startIndex).getRole() == MsgRole.ASSISTANT) {
+ Msg firstMsg = msgs.get(startIndex);
+
+ boolean isToolRelated = firstMsg.hasContentBlocks(ToolUseBlock.class);
+
+ if (!isToolRelated) {
+ // Convert ASSISTANT message to USER role for multi-agent compatibility
+ GeminiContent userContent = new GeminiContent();
+ userContent.setRole("user");
+ userContent.setParts(
+ messageConverter.convertMessages(List.of(firstMsg)).get(0).getParts());
+ result.add(userContent);
+ startIndex++;
+ }
+ }
+
+ // Optimization: If only one message remains and it's not a tool result/use,
+ // format it directly to avoid unnecessary wrapping.
+ // This fixes structured output issues where simple prompts were being wrapped
+ // in history tags.
+ if (msgs.size() - startIndex == 1) {
+ Msg singleMsg = msgs.get(startIndex);
+ boolean isToolRelated =
+ singleMsg.getRole() == MsgRole.TOOL
+ || singleMsg.hasContentBlocks(ToolUseBlock.class)
+ || singleMsg.hasContentBlocks(ToolResultBlock.class);
+
+ if (!isToolRelated) {
+ result.addAll(messageConverter.convertMessages(List.of(singleMsg)));
+ return result;
+ }
}
// Group remaining messages and process each group
@@ -130,32 +152,65 @@ protected List doFormat(List msgs) {
}
@Override
- public ChatResponse parseResponse(GenerateContentResponse response, Instant startTime) {
+ public ChatResponse parseResponse(GeminiResponse response, Instant startTime) {
return responseParser.parseResponse(response, startTime);
}
@Override
public void applyOptions(
- GenerateContentConfig.Builder configBuilder,
- GenerateOptions options,
- GenerateOptions defaultOptions) {
+ GeminiRequest request, GenerateOptions options, GenerateOptions defaultOptions) {
// Delegate to chat formatter
- chatFormatter.applyOptions(configBuilder, options, defaultOptions);
+ chatFormatter.applyOptions(request, options, defaultOptions);
}
@Override
- public void applyTools(GenerateContentConfig.Builder configBuilder, List tools) {
- chatFormatter.applyTools(configBuilder, tools);
+ public void applyTools(GeminiRequest request, List tools) {
+ chatFormatter.applyTools(request, tools);
}
@Override
- public void applyToolChoice(
- GenerateContentConfig.Builder configBuilder, ToolChoice toolChoice) {
- chatFormatter.applyToolChoice(configBuilder, toolChoice);
+ public void applyToolChoice(GeminiRequest request, ToolChoice toolChoice) {
+ chatFormatter.applyToolChoice(request, toolChoice);
+ }
+
+ /**
+ * Apply system instruction to the request if present.
+ *
+ * @param request The Gemini request to configure
+ * @param originalMessages The original message list (used to extract system prompt)
+ */
+ public void applySystemInstruction(GeminiRequest request, List originalMessages) {
+ GeminiContent systemInstruction = buildSystemInstruction(originalMessages);
+ if (systemInstruction != null) {
+ request.setSystemInstruction(systemInstruction);
+ } else {
+ request.setSystemInstruction(null);
+ }
}
// ========== Private Helper Methods ==========
+ private int computeStartIndex(List msgs) {
+ if (msgs == null || msgs.isEmpty()) {
+ return 0;
+ }
+ return msgs.get(0).getRole() == MsgRole.SYSTEM ? 1 : 0;
+ }
+
+ private GeminiContent buildSystemInstruction(List msgs) {
+ if (msgs == null || msgs.isEmpty()) {
+ return null;
+ }
+
+ Msg first = msgs.get(0);
+ if (first.getRole() != MsgRole.SYSTEM) {
+ return null;
+ }
+
+ List converted = messageConverter.convertMessages(List.of(first));
+ return converted.isEmpty() ? null : converted.get(0);
+ }
+
/**
* Group messages sequentially into agent_message and tool_sequence groups.
*
diff --git a/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiResponseParser.java b/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiResponseParser.java
index de879e408..82adbe93b 100644
--- a/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiResponseParser.java
+++ b/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiResponseParser.java
@@ -15,13 +15,13 @@
*/
package io.agentscope.core.formatter.gemini;
-import com.google.genai.types.Candidate;
-import com.google.genai.types.Content;
-import com.google.genai.types.FunctionCall;
-import com.google.genai.types.GenerateContentResponse;
-import com.google.genai.types.GenerateContentResponseUsageMetadata;
-import com.google.genai.types.Part;
import io.agentscope.core.formatter.FormatterException;
+import io.agentscope.core.formatter.gemini.dto.GeminiContent;
+import io.agentscope.core.formatter.gemini.dto.GeminiPart;
+import io.agentscope.core.formatter.gemini.dto.GeminiPart.GeminiFunctionCall;
+import io.agentscope.core.formatter.gemini.dto.GeminiResponse;
+import io.agentscope.core.formatter.gemini.dto.GeminiResponse.GeminiCandidate;
+import io.agentscope.core.formatter.gemini.dto.GeminiResponse.GeminiUsageMetadata;
import io.agentscope.core.message.ContentBlock;
import io.agentscope.core.message.TextBlock;
import io.agentscope.core.message.ThinkingBlock;
@@ -69,43 +69,96 @@ public GeminiResponseParser() {}
* @param startTime Request start time for calculating duration
* @return AgentScope ChatResponse
*/
- public ChatResponse parseResponse(GenerateContentResponse response, Instant startTime) {
+ public ChatResponse parseResponse(GeminiResponse response, Instant startTime) {
try {
+ // Log raw response for debugging
+ try {
+ String responseJson = JsonUtils.getJsonCodec().toJson(response);
+ } catch (Exception e) {
+ log.error("Failed to serialize response for logging: {}", e.getMessage(), e);
+ }
+
List blocks = new ArrayList<>();
String finishReason = null;
// Parse content from first candidate
- if (response.candidates().isPresent() && !response.candidates().get().isEmpty()) {
- Candidate candidate = response.candidates().get().get(0);
+ if (response.getCandidates() != null && !response.getCandidates().isEmpty()) {
+ GeminiCandidate candidate = response.getCandidates().get(0);
- if (candidate.content().isPresent()) {
- Content content = candidate.content().get();
+ if (candidate.getContent() != null) {
+ GeminiContent content = candidate.getContent();
- if (content.parts().isPresent()) {
- List parts = content.parts().get();
+ if (content.getParts() != null) {
+ List parts = content.getParts();
parsePartsToBlocks(parts, blocks);
}
}
- finishReason = candidate.finishMessage().orElse(null);
+ finishReason = candidate.getFinishReason();
+
+ // Log warning if content is empty
+ if (blocks.isEmpty()) {
+ log.warn(
+ "Gemini returned empty content. finishReason={}, "
+ + "candidateContent={}, promptFeedback={}",
+ finishReason,
+ candidate.getContent(),
+ response.getPromptFeedback());
+
+ // Add a text block explaining the empty response
+ String emptyReason = "Gemini returned empty content";
+ if (finishReason != null && !finishReason.isEmpty()) {
+ emptyReason += " (finishReason: " + finishReason + ")";
+ }
+ blocks.add(TextBlock.builder().text(emptyReason).build());
+ }
+ } else {
+ // No candidates at all
+ log.warn(
+ "Gemini returned no candidates. promptFeedback={}",
+ response.getPromptFeedback());
+ blocks.add(
+ TextBlock.builder()
+ .text("Gemini returned no candidates in response")
+ .build());
}
// Parse usage metadata
ChatUsage usage = null;
- if (response.usageMetadata().isPresent()) {
- GenerateContentResponseUsageMetadata metadata = response.usageMetadata().get();
+ if (response.getUsageMetadata() != null) {
+ GeminiUsageMetadata metadata = response.getUsageMetadata();
- int inputTokens = metadata.promptTokenCount().orElse(0);
- int totalOutputTokens = metadata.candidatesTokenCount().orElse(0);
- int thinkingTokens = metadata.thoughtsTokenCount().orElse(0);
+ int inputTokens =
+ metadata.getPromptTokenCount() != null ? metadata.getPromptTokenCount() : 0;
+ int totalOutputTokens =
+ metadata.getCandidatesTokenCount() != null
+ ? metadata.getCandidatesTokenCount()
+ : 0;
- // Output tokens exclude thinking tokens (following DashScope behavior)
- // In Gemini, candidatesTokenCount includes thinking, so we subtract it
- int outputTokens = totalOutputTokens - thinkingTokens;
+ int outputTokens = totalOutputTokens;
+ int reasoningTokens = 0;
+
+ // Extract thinking/reasoning tokens if available
+ if (metadata.getCandidatesTokensDetails() != null) {
+ Map details = metadata.getCandidatesTokensDetails();
+ if (details.containsKey("modalityTokenCount")
+ && details.get("modalityTokenCount") instanceof Map) {
+ Map, ?> modalityCount = (Map, ?>) details.get("modalityTokenCount");
+ // Check for common keys for thinking tokens
+ if (modalityCount.containsKey("thought")
+ && modalityCount.get("thought") instanceof Number) {
+ reasoningTokens = ((Number) modalityCount.get("thought")).intValue();
+ } else if (modalityCount.containsKey("reasoning")
+ && modalityCount.get("reasoning") instanceof Number) {
+ reasoningTokens = ((Number) modalityCount.get("reasoning")).intValue();
+ }
+ }
+ }
usage =
ChatUsage.builder()
.inputTokens(inputTokens)
.outputTokens(outputTokens)
+ .reasoningTokens(reasoningTokens)
.time(
Duration.between(startTime, Instant.now()).toMillis()
/ 1000.0)
@@ -113,7 +166,11 @@ public ChatResponse parseResponse(GenerateContentResponse response, Instant star
}
return ChatResponse.builder()
- .id(response.responseId().orElse(null))
+ // Use actual response ID if available, otherwise generate one
+ .id(
+ response.getResponseId() != null
+ ? response.getResponseId()
+ : java.util.UUID.randomUUID().toString())
.content(blocks)
.usage(usage)
.finishReason(finishReason)
@@ -129,33 +186,68 @@ public ChatResponse parseResponse(GenerateContentResponse response, Instant star
* Parse Gemini Part objects to AgentScope ContentBlocks.
* Order of block types: ThinkingBlock, TextBlock, ToolUseBlock
*
- * @param parts List of Gemini Part objects
+ * @param parts List of Gemini Part objects
* @param blocks List to add parsed ContentBlocks to
*/
- protected void parsePartsToBlocks(List parts, List blocks) {
- for (Part part : parts) {
- // Check for thinking content first (parts with thought=true flag)
- if (part.thought().isPresent() && part.thought().get() && part.text().isPresent()) {
- String thinkingText = part.text().get();
- if (thinkingText != null && !thinkingText.isEmpty()) {
- blocks.add(ThinkingBlock.builder().thinking(thinkingText).build());
+ protected void parsePartsToBlocks(List parts, List blocks) {
+ // Debug: Log the parts received from Gemini
+ if (log.isDebugEnabled()) {
+ try {
+ log.debug("=== Parsing {} parts from Gemini response", parts.size());
+ for (int i = 0; i < parts.size(); i++) {
+ GeminiPart part = parts.get(i);
+ log.debug(
+ "=== Part {}: text={}, functionCall={}, thought={}",
+ i,
+ part.getText() != null ? "present" : "null",
+ part.getFunctionCall() != null ? "present" : "null",
+ part.getThought());
}
- continue;
+ } catch (Exception e) {
+ // Ignore logging errors
}
+ }
- // Check for text content
- if (part.text().isPresent()) {
- String text = part.text().get();
- if (text != null && !text.isEmpty()) {
+ for (GeminiPart part : parts) {
+ boolean processedAsThought = false;
+
+ // Check for thinking content (parts with thought=true flag)
+ if (Boolean.TRUE.equals(part.getThought()) && part.getText() != null) {
+ String thinkingText = part.getText();
+ if (!thinkingText.isEmpty()) {
+ // Build metadata if signature is present
+ Map metadata = null;
+ if (part.getSignature() != null && !part.getSignature().isEmpty()) {
+ metadata = new HashMap<>();
+ metadata.put(ThinkingBlock.METADATA_THOUGHT_SIGNATURE, part.getSignature());
+ }
+
+ blocks.add(
+ ThinkingBlock.builder()
+ .thinking(thinkingText)
+ .metadata(metadata)
+ .build());
+ processedAsThought = true;
+ }
+ }
+
+ // Check for standard text content (only if not processed as thought)
+ if (!processedAsThought && part.getText() != null) {
+ String text = part.getText();
+ if (!text.isEmpty()) {
blocks.add(TextBlock.builder().text(text).build());
}
}
- // Check for function call (tool use)
- if (part.functionCall().isPresent()) {
- FunctionCall functionCall = part.functionCall().get();
- byte[] thoughtSignature = part.thoughtSignature().orElse(null);
- parseToolCall(functionCall, thoughtSignature, blocks);
+ // Check for function call (tool use) - check this INDEPENDENTLY
+ if (part.getFunctionCall() != null) {
+ GeminiFunctionCall functionCall = part.getFunctionCall();
+ // Try thoughtSignature first (Gemini 2.5+), fall back to signature
+ String thoughtSig = part.getThoughtSignature();
+ if (thoughtSig == null || thoughtSig.isEmpty()) {
+ thoughtSig = part.getSignature();
+ }
+ parseToolCall(functionCall, thoughtSig, blocks);
}
}
}
@@ -163,15 +255,18 @@ protected void parsePartsToBlocks(List parts, List blocks) {
/**
* Parse Gemini FunctionCall to ToolUseBlock.
*
- * @param functionCall Gemini FunctionCall object
+ * @param functionCall Gemini FunctionCall object
* @param thoughtSignature Thought signature from the Part (may be null)
- * @param blocks List to add parsed ToolUseBlock to
+ * @param blocks List to add parsed ToolUseBlock to
*/
protected void parseToolCall(
- FunctionCall functionCall, byte[] thoughtSignature, List blocks) {
+ GeminiFunctionCall functionCall, String thoughtSignature, List blocks) {
try {
- String id = functionCall.id().orElse("tool_call_" + System.currentTimeMillis());
- String name = functionCall.name().orElse("");
+ String id = functionCall.getId();
+ if (id == null || id.isEmpty()) {
+ id = "tool_call_" + System.currentTimeMillis(); // Fallback if ID is missing
+ }
+ String name = functionCall.getName() != null ? functionCall.getName() : "";
if (name.isEmpty()) {
log.warn("FunctionCall with empty name, skipping");
@@ -182,22 +277,19 @@ protected void parseToolCall(
Map argsMap = new HashMap<>();
String rawContent = null;
- if (functionCall.args().isPresent()) {
- Map args = functionCall.args().get();
- if (args != null && !args.isEmpty()) {
- argsMap.putAll(args);
- // Convert to JSON string for raw content
- try {
- rawContent = JsonUtils.getJsonCodec().toJson(args);
- } catch (Exception e) {
- log.warn("Failed to serialize function call arguments: {}", e.getMessage());
- }
+ if (functionCall.getArgs() != null && !functionCall.getArgs().isEmpty()) {
+ argsMap.putAll(functionCall.getArgs());
+ // Convert to JSON string for raw content
+ try {
+ rawContent = JsonUtils.getJsonCodec().toJson(functionCall.getArgs());
+ } catch (Exception e) {
+ log.warn("Failed to serialize function call arguments: {}", e.getMessage());
}
}
// Build metadata with thought signature if present
Map metadata = null;
- if (thoughtSignature != null) {
+ if (thoughtSignature != null && !thoughtSignature.isEmpty()) {
metadata = new HashMap<>();
metadata.put(ToolUseBlock.METADATA_THOUGHT_SIGNATURE, thoughtSignature);
}
diff --git a/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiToolsHelper.java b/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiToolsHelper.java
index 1c308db43..f48b78e2d 100644
--- a/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiToolsHelper.java
+++ b/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiToolsHelper.java
@@ -15,17 +15,13 @@
*/
package io.agentscope.core.formatter.gemini;
-import com.google.genai.types.FunctionCallingConfig;
-import com.google.genai.types.FunctionCallingConfigMode;
-import com.google.genai.types.FunctionDeclaration;
-import com.google.genai.types.Schema;
-import com.google.genai.types.Tool;
-import com.google.genai.types.ToolConfig;
-import com.google.genai.types.Type;
+import io.agentscope.core.formatter.gemini.dto.GeminiTool;
+import io.agentscope.core.formatter.gemini.dto.GeminiTool.GeminiFunctionDeclaration;
+import io.agentscope.core.formatter.gemini.dto.GeminiToolConfig;
+import io.agentscope.core.formatter.gemini.dto.GeminiToolConfig.GeminiFunctionCallingConfig;
import io.agentscope.core.model.ToolChoice;
import io.agentscope.core.model.ToolSchema;
import java.util.ArrayList;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
@@ -63,34 +59,51 @@ public GeminiToolsHelper() {}
* @param tools List of tool schemas (may be null or empty)
* @return Gemini Tool object with function declarations, or null if no tools
*/
- public Tool convertToGeminiTool(List tools) {
+ public GeminiTool convertToGeminiTool(List tools) {
if (tools == null || tools.isEmpty()) {
return null;
}
- List functionDeclarations = new ArrayList<>();
+ List functionDeclarations = new ArrayList<>();
for (ToolSchema toolSchema : tools) {
try {
- FunctionDeclaration.Builder builder = FunctionDeclaration.builder();
+ GeminiFunctionDeclaration declaration = new GeminiFunctionDeclaration();
// Set name (required)
if (toolSchema.getName() != null) {
- builder.name(toolSchema.getName());
+ declaration.setName(toolSchema.getName());
}
// Set description (optional)
if (toolSchema.getDescription() != null) {
- builder.description(toolSchema.getDescription());
+ declaration.setDescription(toolSchema.getDescription());
}
- // Convert parameters to Gemini Schema
+ // Convert parameters (directly modify toolSchema Map structure if needed,
+ // but usually it is already in JSON Schema format compatible with Gemini)
if (toolSchema.getParameters() != null && !toolSchema.getParameters().isEmpty()) {
- Schema schema = convertParametersToSchema(toolSchema.getParameters());
- builder.parameters(schema);
+ // Clean schema to remove Gemini-incompatible fields
+ Map cleanedParams =
+ cleanSchemaForGemini(toolSchema.getParameters());
+ declaration.setParameters(cleanedParams);
+
+ // Debug: Log the cleaned schema
+ try {
+ String schemaJson =
+ new com.fasterxml.jackson.databind.ObjectMapper()
+ .writerWithDefaultPrettyPrinter()
+ .writeValueAsString(cleanedParams);
+ log.debug(
+ "Cleaned schema for tool '{}': {}",
+ toolSchema.getName(),
+ schemaJson);
+ } catch (Exception e) {
+ log.debug("Could not serialize schema for logging: {}", e.getMessage());
+ }
}
- functionDeclarations.add(builder.build());
+ functionDeclarations.add(declaration);
log.debug("Converted tool schema: {}", toolSchema.getName());
} catch (Exception e) {
@@ -106,93 +119,9 @@ public Tool convertToGeminiTool(List tools) {
return null;
}
- return Tool.builder().functionDeclarations(functionDeclarations).build();
- }
-
- /**
- * Convert parameters map to Gemini Schema object.
- *
- * @param parameters Parameter schema map (JSON Schema format)
- * @return Gemini Schema object
- */
- protected Schema convertParametersToSchema(Map parameters) {
- Schema.Builder schemaBuilder = Schema.builder();
-
- // Set type (default to OBJECT)
- if (parameters.containsKey("type")) {
- String typeStr = (String) parameters.get("type");
- Type type = convertJsonTypeToGeminiType(typeStr);
- schemaBuilder.type(type);
- } else {
- schemaBuilder.type(new Type(Type.Known.OBJECT));
- }
-
- // Set description
- if (parameters.containsKey("description")) {
- schemaBuilder.description((String) parameters.get("description"));
- }
-
- // Set properties (for OBJECT type)
- if (parameters.containsKey("properties")) {
- @SuppressWarnings("unchecked")
- Map propertiesMap = (Map) parameters.get("properties");
-
- Map propertiesSchemas = new HashMap<>();
- for (Map.Entry entry : propertiesMap.entrySet()) {
- @SuppressWarnings("unchecked")
- Map propertySchema = (Map) entry.getValue();
- propertiesSchemas.put(entry.getKey(), convertParametersToSchema(propertySchema));
- }
- schemaBuilder.properties(propertiesSchemas);
- }
-
- // Set required fields
- if (parameters.containsKey("required")) {
- @SuppressWarnings("unchecked")
- List required = (List) parameters.get("required");
- schemaBuilder.required(required);
- }
-
- // Set items (for ARRAY type)
- if (parameters.containsKey("items")) {
- @SuppressWarnings("unchecked")
- Map itemsSchema = (Map) parameters.get("items");
- schemaBuilder.items(convertParametersToSchema(itemsSchema));
- }
-
- // Set enum values
- if (parameters.containsKey("enum")) {
- @SuppressWarnings("unchecked")
- List enumValues = (List) parameters.get("enum");
- schemaBuilder.enum_(enumValues);
- }
-
- return schemaBuilder.build();
- }
-
- /**
- * Convert JSON Schema type string to Gemini Type.
- *
- * @param jsonType JSON Schema type string (e.g., "object", "string", "number")
- * @return Gemini Type object
- */
- protected Type convertJsonTypeToGeminiType(String jsonType) {
- if (jsonType == null) {
- return new Type(Type.Known.TYPE_UNSPECIFIED);
- }
-
- return switch (jsonType.toLowerCase()) {
- case "object" -> new Type(Type.Known.OBJECT);
- case "array" -> new Type(Type.Known.ARRAY);
- case "string" -> new Type(Type.Known.STRING);
- case "number" -> new Type(Type.Known.NUMBER);
- case "integer" -> new Type(Type.Known.INTEGER);
- case "boolean" -> new Type(Type.Known.BOOLEAN);
- default -> {
- log.warn("Unknown JSON type '{}', using TYPE_UNSPECIFIED", jsonType);
- yield new Type(Type.Known.TYPE_UNSPECIFIED);
- }
- };
+ GeminiTool tool = new GeminiTool();
+ tool.setFunctionDeclarations(functionDeclarations);
+ return tool;
}
/**
@@ -209,29 +138,29 @@ protected Type convertJsonTypeToGeminiType(String jsonType) {
* @param toolChoice The tool choice configuration (null means auto)
* @return Gemini ToolConfig object, or null if auto (default behavior)
*/
- public ToolConfig convertToolChoice(ToolChoice toolChoice) {
+ public GeminiToolConfig convertToolChoice(ToolChoice toolChoice) {
if (toolChoice == null || toolChoice instanceof ToolChoice.Auto) {
// Auto is the default behavior, no need to set explicit config
log.debug("ToolChoice.Auto: using default AUTO mode");
return null;
}
- FunctionCallingConfig.Builder configBuilder = FunctionCallingConfig.builder();
+ GeminiFunctionCallingConfig config = new GeminiFunctionCallingConfig();
if (toolChoice instanceof ToolChoice.None) {
// NONE: disable tool calling
- configBuilder.mode(FunctionCallingConfigMode.Known.NONE);
+ config.setMode("NONE");
log.debug("ToolChoice.None: set mode to NONE");
} else if (toolChoice instanceof ToolChoice.Required) {
// ANY: force tool call from all provided tools
- configBuilder.mode(FunctionCallingConfigMode.Known.ANY);
+ config.setMode("ANY");
log.debug("ToolChoice.Required: set mode to ANY");
} else if (toolChoice instanceof ToolChoice.Specific specific) {
// ANY with allowedFunctionNames: force specific tool call
- configBuilder.mode(FunctionCallingConfigMode.Known.ANY);
- configBuilder.allowedFunctionNames(List.of(specific.toolName()));
+ config.setMode("ANY");
+ config.setAllowedFunctionNames(List.of(specific.toolName()));
log.debug("ToolChoice.Specific: set mode to ANY with tool '{}'", specific.toolName());
} else {
@@ -241,7 +170,81 @@ public ToolConfig convertToolChoice(ToolChoice toolChoice) {
return null;
}
- FunctionCallingConfig functionCallingConfig = configBuilder.build();
- return ToolConfig.builder().functionCallingConfig(functionCallingConfig).build();
+ GeminiToolConfig toolConfig = new GeminiToolConfig();
+ toolConfig.setFunctionCallingConfig(config);
+ return toolConfig;
+ }
+
+ /**
+ * Clean JSON Schema by removing Gemini-incompatible fields.
+ * Recursively removes 'id' fields from the schema and its nested properties.
+ *
+ * @param schema The schema map to clean
+ * @return Cleaned schema map (creates a new map to avoid modifying the
+ * original)
+ */
+ @SuppressWarnings("unchecked")
+ private Map cleanSchemaForGemini(Map schema) {
+ if (schema == null) {
+ return null;
+ }
+
+ // Create a new map to avoid modifying the original
+ Map cleaned = new java.util.HashMap<>(schema);
+
+ // Remove unsupported/unnecessary fields
+ cleaned.remove("id");
+ cleaned.remove("$schema");
+ cleaned.remove("title");
+ cleaned.remove("default");
+ cleaned.remove("nullable");
+
+ // Recursively clean nested properties
+ if (cleaned.containsKey("properties") && cleaned.get("properties") instanceof Map) {
+ Map properties = (Map) cleaned.get("properties");
+ Map cleanedProperties = new java.util.HashMap<>();
+ for (Map.Entry entry : properties.entrySet()) {
+ if (entry.getValue() instanceof Map) {
+ cleanedProperties.put(
+ entry.getKey(),
+ cleanSchemaForGemini((Map) entry.getValue()));
+ } else {
+ cleanedProperties.put(entry.getKey(), entry.getValue());
+ }
+ }
+ cleaned.put("properties", cleanedProperties);
+ }
+
+ // Clean items in arrays
+ if (cleaned.containsKey("items") && cleaned.get("items") instanceof Map) {
+ cleaned.put("items", cleanSchemaForGemini((Map) cleaned.get("items")));
+ }
+
+ // Clean additionalProperties
+ if (cleaned.containsKey("additionalProperties")
+ && cleaned.get("additionalProperties") instanceof Map) {
+ cleaned.put(
+ "additionalProperties",
+ cleanSchemaForGemini(
+ (Map) cleaned.get("additionalProperties")));
+ }
+
+ // Gemini-specific: Ensure all properties are marked as required if not
+ // specified
+ // This prevents Gemini from treating fields as optional and returning partial
+ // data
+ if (cleaned.containsKey("properties") && !cleaned.containsKey("required")) {
+ Object propertiesObj = cleaned.get("properties");
+ if (propertiesObj instanceof Map) {
+ Map properties = (Map) propertiesObj;
+ if (!properties.isEmpty()) {
+ List allProperties = new java.util.ArrayList<>(properties.keySet());
+ cleaned.put("required", allProperties);
+ log.debug("Gemini: Added all properties as required fields: {}", allProperties);
+ }
+ }
+ }
+
+ return cleaned;
}
}
diff --git a/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/dto/GeminiContent.java b/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/dto/GeminiContent.java
new file mode 100644
index 000000000..77df35104
--- /dev/null
+++ b/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/dto/GeminiContent.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2024-2026 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.agentscope.core.formatter.gemini.dto;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.List;
+
+/**
+ * Gemini Content DTO.
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class GeminiContent {
+ @JsonProperty("role")
+ private String role;
+
+ @JsonProperty("parts")
+ private List parts;
+
+ public GeminiContent() {}
+
+ public GeminiContent(String role, List parts) {
+ this.role = role;
+ this.parts = parts;
+ }
+
+ public String getRole() {
+ return role;
+ }
+
+ public void setRole(String role) {
+ this.role = role;
+ }
+
+ public List getParts() {
+ return parts;
+ }
+
+ public void setParts(List parts) {
+ this.parts = parts;
+ }
+}
diff --git a/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/dto/GeminiGenerationConfig.java b/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/dto/GeminiGenerationConfig.java
new file mode 100644
index 000000000..8bed3e8b5
--- /dev/null
+++ b/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/dto/GeminiGenerationConfig.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright 2024-2026 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.agentscope.core.formatter.gemini.dto;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.List;
+
+/**
+ * Gemini Generation Config DTO.
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class GeminiGenerationConfig {
+
+ @JsonProperty("stopSequences")
+ private List stopSequences;
+
+ @JsonProperty("responseMimeType")
+ private String responseMimeType;
+
+ @JsonProperty("responseSchema")
+ private Object responseSchema;
+
+ @JsonProperty("candidateCount")
+ private Integer candidateCount;
+
+ @JsonProperty("maxOutputTokens")
+ private Integer maxOutputTokens;
+
+ @JsonProperty("temperature")
+ private Double temperature;
+
+ @JsonProperty("topP")
+ private Double topP;
+
+ @JsonProperty("topK")
+ private Double topK; // Gemini uses number (double) or integer for topK, float in SDK
+
+ @JsonProperty("presencePenalty")
+ private Double presencePenalty;
+
+ @JsonProperty("frequencyPenalty")
+ private Double frequencyPenalty;
+
+ @JsonProperty("seed")
+ private Integer seed;
+
+ @JsonProperty("thinkingConfig")
+ private GeminiThinkingConfig thinkingConfig;
+
+ // Getters and Builders
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public List getStopSequences() {
+ return stopSequences;
+ }
+
+ public void setStopSequences(List stopSequences) {
+ this.stopSequences = stopSequences;
+ }
+
+ public String getResponseMimeType() {
+ return responseMimeType;
+ }
+
+ public void setResponseMimeType(String responseMimeType) {
+ this.responseMimeType = responseMimeType;
+ }
+
+ public Object getResponseSchema() {
+ return responseSchema;
+ }
+
+ public void setResponseSchema(Object responseSchema) {
+ this.responseSchema = responseSchema;
+ }
+
+ public Integer getCandidateCount() {
+ return candidateCount;
+ }
+
+ public void setCandidateCount(Integer candidateCount) {
+ this.candidateCount = candidateCount;
+ }
+
+ public Integer getMaxOutputTokens() {
+ return maxOutputTokens;
+ }
+
+ public void setMaxOutputTokens(Integer maxOutputTokens) {
+ this.maxOutputTokens = maxOutputTokens;
+ }
+
+ public Double getTemperature() {
+ return temperature;
+ }
+
+ public void setTemperature(Double temperature) {
+ this.temperature = temperature;
+ }
+
+ public Double getTopP() {
+ return topP;
+ }
+
+ public void setTopP(Double topP) {
+ this.topP = topP;
+ }
+
+ public Double getTopK() {
+ return topK;
+ }
+
+ public void setTopK(Double topK) {
+ this.topK = topK;
+ }
+
+ public Double getPresencePenalty() {
+ return presencePenalty;
+ }
+
+ public void setPresencePenalty(Double presencePenalty) {
+ this.presencePenalty = presencePenalty;
+ }
+
+ public Double getFrequencyPenalty() {
+ return frequencyPenalty;
+ }
+
+ public void setFrequencyPenalty(Double frequencyPenalty) {
+ this.frequencyPenalty = frequencyPenalty;
+ }
+
+ public Integer getSeed() {
+ return seed;
+ }
+
+ public void setSeed(Integer seed) {
+ this.seed = seed;
+ }
+
+ public GeminiThinkingConfig getThinkingConfig() {
+ return thinkingConfig;
+ }
+
+ public void setThinkingConfig(GeminiThinkingConfig thinkingConfig) {
+ this.thinkingConfig = thinkingConfig;
+ }
+
+ public static class Builder {
+ private final GeminiGenerationConfig config = new GeminiGenerationConfig();
+
+ public Builder stopSequences(List stopSequences) {
+ config.stopSequences = stopSequences;
+ return this;
+ }
+
+ public Builder responseMimeType(String responseMimeType) {
+ config.responseMimeType = responseMimeType;
+ return this;
+ }
+
+ public Builder responseSchema(Object responseSchema) {
+ config.responseSchema = responseSchema;
+ return this;
+ }
+
+ public Builder candidateCount(Integer candidateCount) {
+ config.candidateCount = candidateCount;
+ return this;
+ }
+
+ public Builder maxOutputTokens(Integer maxOutputTokens) {
+ config.maxOutputTokens = maxOutputTokens;
+ return this;
+ }
+
+ public Builder temperature(Double temperature) {
+ config.temperature = temperature;
+ return this;
+ }
+
+ public Builder topP(Double topP) {
+ config.topP = topP;
+ return this;
+ }
+
+ public Builder topK(Double topK) {
+ config.topK = topK;
+ return this;
+ }
+
+ public Builder presencePenalty(Double presencePenalty) {
+ config.presencePenalty = presencePenalty;
+ return this;
+ }
+
+ public Builder frequencyPenalty(Double frequencyPenalty) {
+ config.frequencyPenalty = frequencyPenalty;
+ return this;
+ }
+
+ public Builder seed(Integer seed) {
+ config.seed = seed;
+ return this;
+ }
+
+ public Builder thinkingConfig(GeminiThinkingConfig thinkingConfig) {
+ config.thinkingConfig = thinkingConfig;
+ return this;
+ }
+
+ public GeminiGenerationConfig build() {
+ return config;
+ }
+ }
+
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public static class GeminiThinkingConfig {
+ @JsonProperty("includeThoughts")
+ private Boolean includeThoughts;
+
+ @JsonProperty("thinkingBudget")
+ private Integer thinkingBudget;
+
+ @JsonProperty("thinkingLevel")
+ private String thinkingLevel;
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public Boolean getIncludeThoughts() {
+ return includeThoughts;
+ }
+
+ public void setIncludeThoughts(Boolean includeThoughts) {
+ this.includeThoughts = includeThoughts;
+ }
+
+ public Integer getThinkingBudget() {
+ return thinkingBudget;
+ }
+
+ public void setThinkingBudget(Integer thinkingBudget) {
+ this.thinkingBudget = thinkingBudget;
+ }
+
+ public String getThinkingLevel() {
+ return thinkingLevel;
+ }
+
+ public void setThinkingLevel(String thinkingLevel) {
+ this.thinkingLevel = thinkingLevel;
+ }
+
+ public static class Builder {
+ private GeminiThinkingConfig config = new GeminiThinkingConfig();
+
+ public Builder includeThoughts(Boolean includeThoughts) {
+ config.includeThoughts = includeThoughts;
+ return this;
+ }
+
+ public Builder thinkingBudget(Integer thinkingBudget) {
+ config.thinkingBudget = thinkingBudget;
+ return this;
+ }
+
+ public Builder thinkingLevel(String thinkingLevel) {
+ config.thinkingLevel = thinkingLevel;
+ return this;
+ }
+
+ public GeminiThinkingConfig build() {
+ return config;
+ }
+ }
+ }
+}
diff --git a/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/dto/GeminiPart.java b/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/dto/GeminiPart.java
new file mode 100644
index 000000000..1b01f1a49
--- /dev/null
+++ b/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/dto/GeminiPart.java
@@ -0,0 +1,278 @@
+/*
+ * Copyright 2024-2026 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.agentscope.core.formatter.gemini.dto;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.Map;
+
+/**
+ * Gemini Part DTO.
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class GeminiPart {
+ @JsonProperty("text")
+ private String text;
+
+ @JsonProperty("functionCall")
+ private GeminiFunctionCall functionCall;
+
+ @JsonProperty("functionResponse")
+ private GeminiFunctionResponse functionResponse;
+
+ @JsonProperty("inlineData")
+ private GeminiBlob inlineData;
+
+ @JsonProperty("fileData")
+ private GeminiFileData fileData;
+
+ @JsonProperty("thought")
+ private Boolean thought;
+
+ @JsonProperty("signature")
+ private String signature;
+
+ @JsonProperty("thoughtSignature")
+ private String thoughtSignature;
+
+ public String getText() {
+ return text;
+ }
+
+ public void setText(String text) {
+ this.text = text;
+ }
+
+ public GeminiFunctionCall getFunctionCall() {
+ return functionCall;
+ }
+
+ public void setFunctionCall(GeminiFunctionCall functionCall) {
+ this.functionCall = functionCall;
+ }
+
+ public GeminiFunctionResponse getFunctionResponse() {
+ return functionResponse;
+ }
+
+ public void setFunctionResponse(GeminiFunctionResponse functionResponse) {
+ this.functionResponse = functionResponse;
+ }
+
+ public GeminiBlob getInlineData() {
+ return inlineData;
+ }
+
+ public void setInlineData(GeminiBlob inlineData) {
+ this.inlineData = inlineData;
+ }
+
+ public GeminiFileData getFileData() {
+ return fileData;
+ }
+
+ public void setFileData(GeminiFileData fileData) {
+ this.fileData = fileData;
+ }
+
+ public Boolean getThought() {
+ return thought;
+ }
+
+ public void setThought(Boolean thought) {
+ this.thought = thought;
+ }
+
+ public String getSignature() {
+ return signature;
+ }
+
+ public void setSignature(String signature) {
+ this.signature = signature;
+ }
+
+ public String getThoughtSignature() {
+ return thoughtSignature;
+ }
+
+ public void setThoughtSignature(String thoughtSignature) {
+ this.thoughtSignature = thoughtSignature;
+ }
+
+ // Inner classes for Part content types
+
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public static class GeminiFunctionCall {
+ @JsonProperty("id")
+ private String id; // Added ID field
+
+ @JsonProperty("name")
+ private String name;
+
+ @JsonProperty("args")
+ private Map args;
+
+ public GeminiFunctionCall() {}
+
+ public GeminiFunctionCall(String name, Map args) {
+ this.name = name;
+ this.args = args;
+ }
+
+ public GeminiFunctionCall(String id, String name, Map args) {
+ this.id = id;
+ this.name = name;
+ this.args = args;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public Map getArgs() {
+ return args;
+ }
+
+ public void setArgs(Map args) {
+ this.args = args;
+ }
+ }
+
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public static class GeminiFunctionResponse {
+ @JsonProperty("id")
+ private String id; // Added ID field
+
+ @JsonProperty("name")
+ private String name;
+
+ @JsonProperty("response")
+ private Map response;
+
+ public GeminiFunctionResponse() {}
+
+ public GeminiFunctionResponse(String name, Map response) {
+ this.name = name;
+ this.response = response;
+ }
+
+ public GeminiFunctionResponse(String id, String name, Map response) {
+ this.id = id;
+ this.name = name;
+ this.response = response;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public Map getResponse() {
+ return response;
+ }
+
+ public void setResponse(Map response) {
+ this.response = response;
+ }
+ }
+
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public static class GeminiBlob {
+ @JsonProperty("mimeType")
+ private String mimeType;
+
+ @JsonProperty("data")
+ private String data; // Base64 string
+
+ public GeminiBlob() {}
+
+ public GeminiBlob(String mimeType, String data) {
+ this.mimeType = mimeType;
+ this.data = data;
+ }
+
+ public String getMimeType() {
+ return mimeType;
+ }
+
+ public void setMimeType(String mimeType) {
+ this.mimeType = mimeType;
+ }
+
+ public String getData() {
+ return data;
+ }
+
+ public void setData(String data) {
+ this.data = data;
+ }
+ }
+
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public static class GeminiFileData {
+ @JsonProperty("mimeType")
+ private String mimeType;
+
+ @JsonProperty("fileUri")
+ private String fileUri;
+
+ public GeminiFileData() {}
+
+ public GeminiFileData(String mimeType, String fileUri) {
+ this.mimeType = mimeType;
+ this.fileUri = fileUri;
+ }
+
+ public String getMimeType() {
+ return mimeType;
+ }
+
+ public void setMimeType(String mimeType) {
+ this.mimeType = mimeType;
+ }
+
+ public String getFileUri() {
+ return fileUri;
+ }
+
+ public void setFileUri(String fileUri) {
+ this.fileUri = fileUri;
+ }
+ }
+}
diff --git a/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/dto/GeminiRequest.java b/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/dto/GeminiRequest.java
new file mode 100644
index 000000000..ff6736ba9
--- /dev/null
+++ b/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/dto/GeminiRequest.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2024-2026 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.agentscope.core.formatter.gemini.dto;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.List;
+
+/**
+ * Gemini API Request DTO.
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class GeminiRequest {
+
+ @JsonProperty("contents")
+ private List contents;
+
+ @JsonProperty("tools")
+ private List tools;
+
+ @JsonProperty("toolConfig")
+ private GeminiToolConfig toolConfig;
+
+ @JsonProperty("safetySettings")
+ private List safetySettings;
+
+ @JsonProperty("systemInstruction")
+ private GeminiContent systemInstruction;
+
+ @JsonProperty("generationConfig")
+ private GeminiGenerationConfig generationConfig;
+
+ public List getContents() {
+ return contents;
+ }
+
+ public void setContents(List contents) {
+ this.contents = contents;
+ }
+
+ public List getTools() {
+ return tools;
+ }
+
+ public void setTools(List tools) {
+ this.tools = tools;
+ }
+
+ public GeminiToolConfig getToolConfig() {
+ return toolConfig;
+ }
+
+ public void setToolConfig(GeminiToolConfig toolConfig) {
+ this.toolConfig = toolConfig;
+ }
+
+ public List getSafetySettings() {
+ return safetySettings;
+ }
+
+ public void setSafetySettings(List safetySettings) {
+ this.safetySettings = safetySettings;
+ }
+
+ public GeminiContent getSystemInstruction() {
+ return systemInstruction;
+ }
+
+ public void setSystemInstruction(GeminiContent systemInstruction) {
+ this.systemInstruction = systemInstruction;
+ }
+
+ public GeminiGenerationConfig getGenerationConfig() {
+ return generationConfig;
+ }
+
+ public void setGenerationConfig(GeminiGenerationConfig generationConfig) {
+ this.generationConfig = generationConfig;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+ private List contents;
+ private List tools;
+ private GeminiToolConfig toolConfig;
+ private List safetySettings;
+ private GeminiContent systemInstruction;
+ private GeminiGenerationConfig generationConfig;
+
+ public Builder contents(List contents) {
+ this.contents = contents;
+ return this;
+ }
+
+ public Builder tools(List tools) {
+ this.tools = tools;
+ return this;
+ }
+
+ public Builder toolConfig(GeminiToolConfig toolConfig) {
+ this.toolConfig = toolConfig;
+ return this;
+ }
+
+ public Builder safetySettings(List safetySettings) {
+ this.safetySettings = safetySettings;
+ return this;
+ }
+
+ public Builder systemInstruction(GeminiContent systemInstruction) {
+ this.systemInstruction = systemInstruction;
+ return this;
+ }
+
+ public Builder generationConfig(GeminiGenerationConfig generationConfig) {
+ this.generationConfig = generationConfig;
+ return this;
+ }
+
+ public GeminiRequest build() {
+ GeminiRequest request = new GeminiRequest();
+ request.setContents(contents);
+ request.setTools(tools);
+ request.setToolConfig(toolConfig);
+ request.setSafetySettings(safetySettings);
+ request.setSystemInstruction(systemInstruction);
+ request.setGenerationConfig(generationConfig);
+ return request;
+ }
+ }
+}
diff --git a/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/dto/GeminiResponse.java b/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/dto/GeminiResponse.java
new file mode 100644
index 000000000..96b8b9812
--- /dev/null
+++ b/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/dto/GeminiResponse.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2024-2026 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.agentscope.core.formatter.gemini.dto;
+
+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;
+
+/**
+ * Gemini API Response DTO.
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class GeminiResponse {
+
+ @JsonProperty("candidates")
+ private List candidates;
+
+ @JsonProperty("usageMetadata")
+ private GeminiUsageMetadata usageMetadata;
+
+ @JsonProperty("promptFeedback")
+ private Object promptFeedback; // Simplification
+
+ @JsonProperty("requestId")
+ private String responseId;
+
+ public String getResponseId() {
+ return responseId;
+ }
+
+ public void setResponseId(String responseId) {
+ this.responseId = responseId;
+ }
+
+ public List getCandidates() {
+ return candidates;
+ }
+
+ public void setCandidates(List candidates) {
+ this.candidates = candidates;
+ }
+
+ public GeminiUsageMetadata getUsageMetadata() {
+ return usageMetadata;
+ }
+
+ public void setUsageMetadata(GeminiUsageMetadata usageMetadata) {
+ this.usageMetadata = usageMetadata;
+ }
+
+ public Object getPromptFeedback() {
+ return promptFeedback;
+ }
+
+ public void setPromptFeedback(Object promptFeedback) {
+ this.promptFeedback = promptFeedback;
+ }
+
+ // Inner classes
+
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public static class GeminiCandidate {
+ @JsonProperty("content")
+ private GeminiContent content;
+
+ @JsonProperty("finishReason")
+ private String finishReason;
+
+ @JsonProperty("safetyRatings")
+ private List