Skip to content
Merged
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
14 changes: 7 additions & 7 deletions .github/ISSUE_TEMPLATE/tool_call_error_cn.yml
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
name: 工具调用错误报告
description: 报告工具调用相关的错误 (中文)
title: "[工具错误]: "
name: 工具调用/结构化输出报告
description: 报告工具调用或结构化输出相关的错误 (中文)
title: "[特性错误]: "
labels: ["🐛 Bug"]
body:
- type: markdown
attributes:
value: |
如果您遇到了工具调用(Tool Calls)相关的错误,请务必按照以下格式提交一个新的 Issue,以便我们能够快速定位和解决问题。
如果您遇到了**工具调用(Tool Calls)**或**结构化输出(Structured Output/JSON Schema)**相关的错误,请务必按照以下格式提交一个新的 Issue,以便我们能够快速定位和解决问题。

**请注意:** 为了排查工具调用问题,我们需要您提供详细的上下文日志。请在复现问题时开启调试模式,并提供完整的请求和响应日志。
**请注意:** 为了排查此类问题,我们需要您提供详细的上下文日志。请在复现问题时开启调试模式,并提供完整的请求和响应日志。

- type: input
id: version
attributes:
label: 版本号
description: 您当前使用的版本
placeholder: "例如: v0.2.11"
placeholder: "例如: v0.3.0"
validations:
required: true

Expand Down Expand Up @@ -49,7 +49,7 @@ body:
id: problem_description
attributes:
label: 问题描述
description: "请简要描述您遇到的错误现象,例如:请求成功但参数解析错误、返回 400 错误等"
description: "请简要描述您遇到的错误现象,例如:请求成功但参数解析错误、Schema 校验失败、返回 400 错误等"
placeholder: 描述错误的具体表现...
validations:
required: true
Expand Down
14 changes: 7 additions & 7 deletions .github/ISSUE_TEMPLATE/tool_call_error_en.yml
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
name: Tool Call Error Report
description: Report an error related to tool calls (English)
title: "[Tool Error]: "
name: Tool Call / Structured Output Report
description: Report errors related to tool calls or structured outputs (English)
title: "[Feature Error]: "
labels: ["🐛 Bug"]
body:
- type: markdown
attributes:
value: |
If you encounter errors related to Tool Calls, please submit a new issue using the format below so that we can quickly identify and resolve the problem.
If you encounter errors related to **Tool Calls** or **Structured Output (JSON Schema)**, please submit a new issue using the format below so that we can quickly identify and resolve the problem.

**Note:** To troubleshoot tool call issues, we need detailed context logs. Please enable debug mode when reproducing the issue and provide full request and response logs.
**Note:** To troubleshoot such issues, we need detailed context logs. Please enable debug mode when reproducing the issue and provide full request and response logs.

- type: input
id: version
attributes:
label: Version
description: The version you are currently using
placeholder: "e.g., v0.2.11"
placeholder: "e.g., v0.3.0"
validations:
required: true

Expand Down Expand Up @@ -49,7 +49,7 @@ body:
id: problem_description
attributes:
label: Problem Description
description: "Briefly describe the error you encountered (e.g., request succeeded but parameter parsing failed, returned 400 error, etc.)"
description: "Briefly describe the error you encountered (e.g., request succeeded but parameter parsing failed, Schema validation error, returned 400 error, etc.)"
placeholder: Describe the error details...
validations:
required: true
Expand Down
173 changes: 131 additions & 42 deletions src/core/FormatConverter.js
Original file line number Diff line number Diff line change
Expand Up @@ -498,63 +498,97 @@ class FormatConverter {

googleRequest.generationConfig = generationConfig;

// Convert OpenAI tools to Gemini functionDeclarations
const openaiTools = openaiBody.tools || openaiBody.functions;
if (openaiTools && Array.isArray(openaiTools) && openaiTools.length > 0) {
const functionDeclarations = [];
/**
* Helper function to convert OpenAI parameter types to Gemini format (uppercase).
* Also handles nullable types like ["string", "null"] -> type: "STRING", nullable: true.
* This function is used for both tools and response_format schema conversion.
*
* @param {Object} obj - The schema object to convert
* @param {boolean} [isResponseSchema=false] - If true, applies stricter rules (e.g. anyOf for unions) for Structured Outputs
* @returns {Object} The converted schema
*/
const convertParameterTypes = (obj, isResponseSchema = false) => {
if (!obj || typeof obj !== "object") return obj;

// Helper function to convert OpenAI parameter types to Gemini format (uppercase)
// Also handles nullable types like ["string", "null"] -> type: "STRING", nullable: true
const convertParameterTypes = obj => {
if (!obj || typeof obj !== "object") return obj;
const result = Array.isArray(obj) ? [] : {};

const result = Array.isArray(obj) ? [] : {};
for (const key of Object.keys(obj)) {
// 1. Filter out unsupported fields using a blacklist approach
// This allows potentially valid fields to pass through while blocking known problematic ones
const unsupportedKeys = ["$schema", "additionalProperties"];

for (const key of Object.keys(obj)) {
// Skip fields not supported by Gemini API
// Gemini only supports: type, description, enum, items, properties, required, nullable
if (key === "$schema" || key === "additionalProperties") {
continue;
}
if (isResponseSchema) {
// For Structured Outputs: stricter filtering of metadata that causes 400 errors
unsupportedKeys.push("title", "default", "examples", "$defs", "id", "patternProperties");
}

if (key === "type") {
if (Array.isArray(obj[key])) {
// Handle nullable types like ["string", "null"]
const types = obj[key];
const nonNullTypes = types.filter(t => t !== "null");
const hasNull = types.includes("null");
if (unsupportedKeys.includes(key)) {
continue;
}

if (hasNull) {
result.nullable = true;
}
if (key === "type") {
if (Array.isArray(obj[key])) {
// Handle nullable types like ["string", "null"]
const types = obj[key];
const nonNullTypes = types.filter(t => t !== "null");
const hasNull = types.includes("null");

if (nonNullTypes.length === 1) {
// Single non-null type: use it directly
result[key] = nonNullTypes[0].toUpperCase();
} else if (nonNullTypes.length > 1) {
// Multiple non-null types: keep as array (uppercase)
result[key] = nonNullTypes.map(t => t.toUpperCase());
if (hasNull) {
result.nullable = true;
}

if (nonNullTypes.length === 1) {
// Single non-null type: use it directly
result[key] = nonNullTypes[0].toUpperCase();
} else if (nonNullTypes.length > 1) {
// Multiple non-null types: e.g. ["string", "integer"]
if (isResponseSchema) {
// For Response Schema: Gemini doesn't support array types, use anyOf
result.anyOf = nonNullTypes.map(t => ({
type: t.toUpperCase(),
}));
} else {
// Only null type, default to STRING
result[key] = "STRING";
result[key] = nonNullTypes.map(t => t.toUpperCase());
}
} else if (typeof obj[key] === "string") {
// Convert lowercase type to uppercase for Gemini
result[key] = obj[key].toUpperCase();
} else if (typeof obj[key] === "object" && obj[key] !== null) {
result[key] = convertParameterTypes(obj[key]);
} else {
result[key] = obj[key];
// Only null type, default to STRING
result[key] = "STRING";
}
} else if (typeof obj[key] === "string") {
// Convert lowercase type to uppercase for Gemini
result[key] = obj[key].toUpperCase();
} else if (typeof obj[key] === "object" && obj[key] !== null) {
result[key] = convertParameterTypes(obj[key]);
result[key] = convertParameterTypes(obj[key], isResponseSchema);
} else {
result[key] = obj[key];
}
} else if (key === "enum") {
// 2. Ensure all enum values are strings (Only for Response Schema)
if (isResponseSchema) {
if (Array.isArray(obj[key])) {
result[key] = obj[key].map(String);
} else if (obj[key] !== undefined && obj[key] !== null) {
result[key] = [String(obj[key])];
}
result["type"] = "STRING";
} else {
// For Tools: Allow original enum values
result[key] = obj[key];
}
} else if (typeof obj[key] === "object" && obj[key] !== null) {
result[key] = convertParameterTypes(obj[key], isResponseSchema);
} else {
result[key] = obj[key];
}
}

return result;
};
return result;
};

// Convert OpenAI tools to Gemini functionDeclarations
const openaiTools = openaiBody.tools || openaiBody.functions;
if (openaiTools && Array.isArray(openaiTools) && openaiTools.length > 0) {
const functionDeclarations = [];

for (const tool of openaiTools) {
// Handle OpenAI tools format: { type: "function", function: {...} }
Expand All @@ -572,7 +606,8 @@ class FormatConverter {

if (funcDef.parameters) {
// Convert parameter types from lowercase to uppercase
declaration.parameters = convertParameterTypes(funcDef.parameters);
// isResponseSchema = false for Tools
declaration.parameters = convertParameterTypes(funcDef.parameters, false);
}

functionDeclarations.push(declaration);
Expand Down Expand Up @@ -619,6 +654,60 @@ class FormatConverter {
}
}

// Handle response_format for structured output
// Convert OpenAI response_format to Gemini responseSchema
const responseFormat = openaiBody.response_format;
if (responseFormat) {
if (responseFormat.type === "json_schema" && responseFormat.json_schema) {
// Extract schema from OpenAI format
const jsonSchema = responseFormat.json_schema;
const schema = jsonSchema.schema;

if (schema) {
try {
this.logger.debug(`[Adapter] Debug: Converting OpenAI JSON Schema: ${JSON.stringify(schema)}`);

// Convert schema to Gemini format (reuse convertParameterTypes helper defined above)
// isResponseSchema = true for Structured Output
const convertedSchema = convertParameterTypes(schema, true);

this.logger.debug(
`[Adapter] Debug: Converted Gemini JSON Schema: ${JSON.stringify(convertedSchema)}`
);

// Set Gemini config for structured output
generationConfig.responseMimeType = "application/json";
generationConfig.responseSchema = convertedSchema;

this.logger.info(
`[Adapter] Converted OpenAI response_format to Gemini responseSchema: ${jsonSchema.name || "unnamed"}`
);

// Log warning if tools are also present (may cause conflicts)
if (googleRequest.tools && googleRequest.tools.length > 0) {
this.logger.warn(
"[Adapter] ⚠️ Both response_format and tools are present. This may cause unexpected behavior."
);
}
} catch (error) {
this.logger.error(
`[Adapter] Failed to convert response_format schema: ${error.message}`,
error
);
}
}
} else if (responseFormat.type === "json_object") {
// Simple JSON mode without schema validation
generationConfig.responseMimeType = "application/json";
this.logger.info("[Adapter] Enabled JSON mode (no schema validation)");
} else if (responseFormat.type === "text") {
// Explicit text mode (default behavior, no action needed)
this.logger.debug("[Adapter] Response format set to text (default)");
} else {
this.logger.warn(`[Adapter] Unsupported response_format type: ${responseFormat.type}. Ignoring.`);
}
}

// Force web search and URL context
if (this.serverSystem.forceWebSearch || this.serverSystem.forceUrlContext) {
if (!googleRequest.tools) {
Expand Down
Loading