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 <noreply@anthropic.com>
This commit is contained in:
@@ -231,29 +231,47 @@ function convertMessages(
|
|||||||
* which causes 400 errors on OpenAI/Codex endpoints. This normalizes the
|
* which causes 400 errors on OpenAI/Codex endpoints. This normalizes the
|
||||||
* schema by ensuring `required` is a superset of `properties` keys.
|
* schema by ensuring `required` is a superset of `properties` keys.
|
||||||
*/
|
*/
|
||||||
function normalizeSchemaForOpenAI(schema: Record<string, unknown>): Record<string, unknown> {
|
function normalizeSchemaForOpenAI(
|
||||||
|
schema: Record<string, unknown>,
|
||||||
|
strict = true,
|
||||||
|
): Record<string, unknown> {
|
||||||
if (schema.type !== 'object' || !schema.properties) return schema
|
if (schema.type !== 'object' || !schema.properties) return schema
|
||||||
const properties = schema.properties as Record<string, unknown>
|
const properties = schema.properties as Record<string, unknown>
|
||||||
const existingRequired = Array.isArray(schema.required) ? schema.required as string[] : []
|
const existingRequired = Array.isArray(schema.required) ? schema.required as string[] : []
|
||||||
|
// 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 allKeys = Object.keys(properties)
|
||||||
const required = Array.from(new Set([...existingRequired, ...allKeys]))
|
const required = Array.from(new Set([...existingRequired, ...allKeys]))
|
||||||
return { ...schema, required }
|
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(
|
function convertTools(
|
||||||
tools: Array<{ name: string; description?: string; input_schema?: Record<string, unknown> }>,
|
tools: Array<{ name: string; description?: string; input_schema?: Record<string, unknown> }>,
|
||||||
): OpenAITool[] {
|
): OpenAITool[] {
|
||||||
|
const isGemini =
|
||||||
|
process.env.CLAUDE_CODE_USE_GEMINI === '1' ||
|
||||||
|
process.env.CLAUDE_CODE_USE_GEMINI === 'true'
|
||||||
|
|
||||||
return tools
|
return tools
|
||||||
.filter(t => t.name !== 'ToolSearchTool') // Not relevant for OpenAI
|
.filter(t => t.name !== 'ToolSearchTool') // Not relevant for OpenAI
|
||||||
.map(t => {
|
.map(t => {
|
||||||
// Estraiamo lo schema
|
const schema = { ...(t.input_schema ?? { type: 'object', properties: {} }) } as Record<string, unknown>
|
||||||
const schema = (t.input_schema ?? { type: 'object', properties: {} }) as any;
|
|
||||||
|
|
||||||
// PATCH PER CODEX: Se è lo strumento Agent, forziamo i campi obbligatori
|
// 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) {
|
if (t.name === 'Agent' && schema.properties) {
|
||||||
if (!schema.required) schema.required = [];
|
const props = schema.properties as Record<string, unknown>
|
||||||
if (!schema.required.includes('message')) schema.required.push('message');
|
if (!Array.isArray(schema.required)) schema.required = []
|
||||||
if (!schema.required.includes('subagent_type')) schema.required.push('subagent_type');
|
const req = schema.required as string[]
|
||||||
|
for (const key of ['message', 'subagent_type']) {
|
||||||
|
if (key in props && !req.includes(key)) req.push(key)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -261,7 +279,7 @@ function convertTools(
|
|||||||
function: {
|
function: {
|
||||||
name: t.name,
|
name: t.name,
|
||||||
description: t.description ?? '',
|
description: t.description ?? '',
|
||||||
parameters: normalizeSchemaForOpenAI(schema),
|
parameters: normalizeSchemaForOpenAI(schema, !isGemini),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user