fix(gemini): sanitize tool parameters to remove unsupported JSON Schema fields
Some checks failed
CI / Check (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Formatting (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Release Build (push) Has been cancelled

- 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.
This commit is contained in:
2026-03-05 15:48:29 +00:00
parent fb98f0ebb8
commit 3086a3b6d9

View File

@@ -367,10 +367,18 @@ impl GeminiProvider {
request.tools.as_ref().map(|tools| {
let declarations: Vec<GeminiFunctionDeclaration> = 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<GeminiToolConfig> {
request.tool_choice.as_ref().map(|tc| {