From 3086a3b6d9576d5091c60df22fc4588f2579052c Mon Sep 17 00:00:00 2001 From: hobokenchicken Date: Thu, 5 Mar 2026 15:48:29 +0000 Subject: [PATCH] fix(gemini): sanitize tool parameters to remove unsupported JSON Schema fields - Recursively remove '$schema', 'additionalProperties', 'exclusiveMaximum', and 'exclusiveMinimum' from tool definitions. - These fields are frequently included by clients like opencode but are rejected by the Gemini API with 400 errors. --- src/providers/gemini.rs | 65 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/src/providers/gemini.rs b/src/providers/gemini.rs index 54e48c94..1b123b8d 100644 --- a/src/providers/gemini.rs +++ b/src/providers/gemini.rs @@ -367,10 +367,18 @@ impl GeminiProvider { request.tools.as_ref().map(|tools| { let declarations: Vec = tools .iter() - .map(|t| GeminiFunctionDeclaration { - name: t.function.name.clone(), - description: t.function.description.clone(), - parameters: t.function.parameters.clone(), + .map(|t| { + let mut parameters = t.function.parameters.clone().unwrap_or(serde_json::json!({ + "type": "object", + "properties": {} + })); + Self::sanitize_schema(&mut parameters); + + GeminiFunctionDeclaration { + name: t.function.name.clone(), + description: t.function.description.clone(), + parameters: Some(parameters), + } }) .collect(); vec![GeminiTool { @@ -379,6 +387,55 @@ impl GeminiProvider { }) } + /// Recursively remove unsupported JSON Schema fields that Gemini's API rejects. + fn sanitize_schema(value: &mut Value) { + if let Value::Object(map) = value { + // Remove unsupported fields at this level + map.remove("$schema"); + map.remove("additionalProperties"); + map.remove("exclusiveMaximum"); + map.remove("exclusiveMinimum"); + + // Recursively sanitize all object properties + if let Some(properties) = map.get_mut("properties") { + if let Value::Object(props_map) = properties { + for prop_value in props_map.values_mut() { + Self::sanitize_schema(prop_value); + } + } + } + + // Recursively sanitize array items + if let Some(items) = map.get_mut("items") { + Self::sanitize_schema(items); + } + + // Gemini 1.5/2.0+ supports anyOf in some contexts, but it's often + // the source of additionalProperties errors when nested. + if let Some(any_of) = map.get_mut("anyOf") { + if let Value::Array(arr) = any_of { + for item in arr { + Self::sanitize_schema(item); + } + } + } + if let Some(one_of) = map.get_mut("oneOf") { + if let Value::Array(arr) = one_of { + for item in arr { + Self::sanitize_schema(item); + } + } + } + if let Some(all_of) = map.get_mut("allOf") { + if let Value::Array(arr) = all_of { + for item in arr { + Self::sanitize_schema(item); + } + } + } + } + } + /// Convert OpenAI tool_choice to Gemini tool_config. fn convert_tool_config(request: &UnifiedRequest) -> Option { request.tool_choice.as_ref().map(|tc| {