From ab911d1ed1832c09584f76f2df3617f5c74df491 Mon Sep 17 00:00:00 2001 From: gnanam1990 Date: Thu, 2 Apr 2026 08:28:07 +0530 Subject: [PATCH] fix: make schema normalization provider-aware for Gemini compatibility Two bugs in convertTools() caused Gemini's OpenAI-compatible endpoint to reject tool schemas with 400 "schema requires unspecified property": 1. The Agent tool patch unconditionally pushed 'message' into required[] even though 'message' is not a property of the Agent schema. Gemini strictly validates that every key in required[] exists in properties. 2. normalizeSchemaForOpenAI() added all property keys to required[] for OpenAI strict mode, but this conflicts with Gemini's stricter schema validation which rejects required keys absent from properties. Fix: - Agent tool patch now only adds a key to required[] if it exists in schema.properties (fixes the 'message' 400 error on Gemini) - normalizeSchemaForOpenAI() accepts a strict flag: true for OpenAI (promotes all property keys into required[]), false for Gemini (filters required[] to only keys present in properties) - convertTools() detects CLAUDE_CODE_USE_GEMINI and passes strict=false Fixes #82 Co-Authored-By: Claude Sonnet 4.6 --- src/services/api/openaiShim.ts | 62 ++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/src/services/api/openaiShim.ts b/src/services/api/openaiShim.ts index fcbf4938..0b99f80c 100644 --- a/src/services/api/openaiShim.ts +++ b/src/services/api/openaiShim.ts @@ -231,40 +231,58 @@ function convertMessages( * which causes 400 errors on OpenAI/Codex endpoints. This normalizes the * schema by ensuring `required` is a superset of `properties` keys. */ -function normalizeSchemaForOpenAI(schema: Record): Record { +function normalizeSchemaForOpenAI( + schema: Record, + strict = true, +): Record { if (schema.type !== 'object' || !schema.properties) return schema const properties = schema.properties as Record const existingRequired = Array.isArray(schema.required) ? schema.required as string[] : [] - const allKeys = Object.keys(properties) - const required = Array.from(new Set([...existingRequired, ...allKeys])) + // OpenAI strict mode requires every property to be listed in required[]. + // Gemini rejects schemas where required[] contains keys absent from properties, + // so only promote keys that actually exist in properties. + if (strict) { + const allKeys = Object.keys(properties) + const required = Array.from(new Set([...existingRequired, ...allKeys])) + return { ...schema, required } + } + // For Gemini: keep only existing required keys that are present in properties + const required = existingRequired.filter(k => k in properties) return { ...schema, required } } function convertTools( tools: Array<{ name: string; description?: string; input_schema?: Record }>, ): OpenAITool[] { + const isGemini = + process.env.CLAUDE_CODE_USE_GEMINI === '1' || + process.env.CLAUDE_CODE_USE_GEMINI === 'true' + return tools - .filter(t => t.name !== 'ToolSearchTool') // Not relevant for OpenAI - .map(t => { - // Estraiamo lo schema - const schema = (t.input_schema ?? { type: 'object', properties: {} }) as any; + .filter(t => t.name !== 'ToolSearchTool') // Not relevant for OpenAI + .map(t => { + const schema = { ...(t.input_schema ?? { type: 'object', properties: {} }) } as Record - // PATCH PER CODEX: Se รจ lo strumento Agent, forziamo i campi obbligatori - if (t.name === 'Agent' && schema.properties) { - if (!schema.required) schema.required = []; - if (!schema.required.includes('message')) schema.required.push('message'); - if (!schema.required.includes('subagent_type')) schema.required.push('subagent_type'); - } + // For Codex/OpenAI: promote known Agent sub-fields into required[] only if + // they actually exist in properties (Gemini rejects required keys absent from properties). + if (t.name === 'Agent' && schema.properties) { + const props = schema.properties as Record + if (!Array.isArray(schema.required)) schema.required = [] + const req = schema.required as string[] + for (const key of ['message', 'subagent_type']) { + if (key in props && !req.includes(key)) req.push(key) + } + } - return { - type: 'function' as const, - function: { - name: t.name, - description: t.description ?? '', - parameters: normalizeSchemaForOpenAI(schema), - }, - } - }) + return { + type: 'function' as const, + function: { + name: t.name, + description: t.description ?? '', + parameters: normalizeSchemaForOpenAI(schema, !isGemini), + }, + } + }) } // ---------------------------------------------------------------------------