From f122b8a66239c0421033a9fa19827c2fb0749d53 Mon Sep 17 00:00:00 2001 From: iBenzene Date: Fri, 16 Jan 2026 23:05:54 +0800 Subject: [PATCH 1/4] feat: add support for OpenAI structured output (response_format) - Add response_format parameter handling in FormatConverter - Convert OpenAI json_schema format to Gemini responseSchema - Support both json_schema and json_object modes - Extract convertParameterTypes helper for reuse - Add proper error handling and logging Fixes #11 --- src/core/FormatConverter.js | 134 ++++++++++++++++++++++++------------ 1 file changed, 91 insertions(+), 43 deletions(-) diff --git a/src/core/FormatConverter.js b/src/core/FormatConverter.js index bd8e3fe..332d6fc 100644 --- a/src/core/FormatConverter.js +++ b/src/core/FormatConverter.js @@ -498,63 +498,64 @@ 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 - const convertParameterTypes = obj => { - 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 + // This function is used for both tools and response_format schema conversion + 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)) { - // Skip fields not supported by Gemini API - // Gemini only supports: type, description, enum, items, properties, required, nullable - if (key === "$schema" || key === "additionalProperties") { - continue; - } + 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" || key === "strict") { + continue; + } - 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 (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 (hasNull) { - result.nullable = true; - } + 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: keep as array (uppercase) - result[key] = nonNullTypes.map(t => t.toUpperCase()); - } else { - // 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]); + 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()); } 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]); } else { result[key] = obj[key]; } + } else if (typeof obj[key] === "object" && obj[key] !== null) { + result[key] = convertParameterTypes(obj[key]); + } 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: {...} } @@ -619,6 +620,53 @@ 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 { + // Convert schema to Gemini format (reuse convertParameterTypes helper defined above) + const convertedSchema = convertParameterTypes(schema); + + // 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) { From 9178a38ca36b006c1de663fd214a3d1176d187e8 Mon Sep 17 00:00:00 2001 From: bbbugg Date: Sat, 17 Jan 2026 03:43:04 +0800 Subject: [PATCH 2/4] feat: enhance parameter type conversion for OpenAI structured outputs --- src/core/FormatConverter.js | 54 ++++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/src/core/FormatConverter.js b/src/core/FormatConverter.js index 332d6fc..eb166e6 100644 --- a/src/core/FormatConverter.js +++ b/src/core/FormatConverter.js @@ -501,15 +501,23 @@ class FormatConverter { // 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 - const convertParameterTypes = obj => { + // param {boolean} isResponseSchema - If true, applies stricter rules (e.g. anyOf for unions) for Structured Outputs + const convertParameterTypes = (obj, isResponseSchema = false) => { if (!obj || typeof obj !== "object") return obj; const result = Array.isArray(obj) ? [] : {}; 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" || key === "strict") { + // 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"]; + + if (isResponseSchema) { + // For Structured Outputs: stricter filtering of metadata that causes 400 errors + unsupportedKeys.push("title", "default", "examples", "$defs", "id", "patternProperties"); + } + + if (unsupportedKeys.includes(key)) { continue; } @@ -528,8 +536,15 @@ class FormatConverter { // 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()); + // 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 { + result[key] = nonNullTypes.map(t => t.toUpperCase()); + } } else { // Only null type, default to STRING result[key] = "STRING"; @@ -538,12 +553,23 @@ class FormatConverter { // 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); + } + 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]); + result[key] = convertParameterTypes(obj[key], isResponseSchema); } else { result[key] = obj[key]; } @@ -573,7 +599,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); @@ -631,8 +658,15 @@ class FormatConverter { 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) - const convertedSchema = convertParameterTypes(schema); + // 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"; From 65ad0ba1c27238f03d70f2ad3143c6b99e087e4b Mon Sep 17 00:00:00 2001 From: bbbugg Date: Sat, 17 Jan 2026 03:52:36 +0800 Subject: [PATCH 3/4] chore: update error report templates for tool calls and structured outputs --- .github/ISSUE_TEMPLATE/tool_call_error_cn.yml | 14 +++++++------- .github/ISSUE_TEMPLATE/tool_call_error_en.yml | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/tool_call_error_cn.yml b/.github/ISSUE_TEMPLATE/tool_call_error_cn.yml index e89b258..33bb0d3 100644 --- a/.github/ISSUE_TEMPLATE/tool_call_error_cn.yml +++ b/.github/ISSUE_TEMPLATE/tool_call_error_cn.yml @@ -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 @@ -49,7 +49,7 @@ body: id: problem_description attributes: label: 问题描述 - description: "请简要描述您遇到的错误现象,例如:请求成功但参数解析错误、返回 400 错误等" + description: "请简要描述您遇到的错误现象,例如:请求成功但参数解析错误、Schema 校验失败、返回 400 错误等" placeholder: 描述错误的具体表现... validations: required: true diff --git a/.github/ISSUE_TEMPLATE/tool_call_error_en.yml b/.github/ISSUE_TEMPLATE/tool_call_error_en.yml index 4be1da7..1cb5bf3 100644 --- a/.github/ISSUE_TEMPLATE/tool_call_error_en.yml +++ b/.github/ISSUE_TEMPLATE/tool_call_error_en.yml @@ -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 @@ -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 From b8174e11c35bcb2152b09d425a917f552235764b Mon Sep 17 00:00:00 2001 From: bbbugg Date: Sat, 17 Jan 2026 04:27:44 +0800 Subject: [PATCH 4/4] feat: enhance parameter type conversion for structured outputs --- src/core/FormatConverter.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/core/FormatConverter.js b/src/core/FormatConverter.js index eb166e6..164baae 100644 --- a/src/core/FormatConverter.js +++ b/src/core/FormatConverter.js @@ -498,10 +498,15 @@ class FormatConverter { googleRequest.generationConfig = generationConfig; - // 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 {boolean} isResponseSchema - If true, applies stricter rules (e.g. anyOf for unions) for Structured Outputs + /** + * 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; @@ -562,6 +567,8 @@ class FormatConverter { 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 {