fix(gemini): normalize exclusive minimum tool schemas

This commit is contained in:
wucm667
2026-08-11 12:34:51 +08:00
parent 0f73203e35
commit c8d9af6ce1
2 changed files with 116 additions and 1 deletions
@@ -3542,7 +3542,7 @@ func cleanToolSchema(schema any) any {
if key == "$schema" || key == "$id" || key == "$ref" ||
key == "$defs" || key == "definitions" ||
key == "additionalProperties" || key == "patternProperties" || key == "minLength" ||
key == "maxLength" || key == "minItems" || key == "maxItems" {
key == "maxLength" || key == "minItems" || key == "maxItems" || key == "exclusiveMinimum" {
continue
}
// 递归清理嵌套对象
@@ -3563,6 +3563,13 @@ func cleanToolSchema(schema any) any {
delete(cleaned, "type")
}
}
if cleaned["type"] == "INTEGER" {
if minimum, ok := incrementIntegralSchemaBound(v["exclusiveMinimum"]); ok {
if existing, exists := cleaned["minimum"]; !exists || schemaNumberLess(existing, minimum) {
cleaned["minimum"] = minimum
}
}
}
return cleaned
case []any:
cleaned := make([]any, len(v))
@@ -3575,6 +3582,56 @@ func cleanToolSchema(schema any) any {
}
}
func incrementIntegralSchemaBound(value any) (any, bool) {
switch v := value.(type) {
case float64:
if math.IsNaN(v) || math.IsInf(v, 0) || v != math.Trunc(v) || v+1 <= v {
return nil, false
}
return v + 1, true
case int:
if v == math.MaxInt {
return nil, false
}
return v + 1, true
case int64:
if v == math.MaxInt64 {
return nil, false
}
return v + 1, true
case json.Number:
i, err := v.Int64()
if err != nil || i == math.MaxInt64 {
return nil, false
}
return json.Number(fmt.Sprintf("%d", i+1)), true
default:
return nil, false
}
}
func schemaNumberLess(left, right any) bool {
leftNumber, leftOK := schemaNumberFloat64(left)
rightNumber, rightOK := schemaNumberFloat64(right)
return leftOK && rightOK && leftNumber < rightNumber
}
func schemaNumberFloat64(value any) (float64, bool) {
switch v := value.(type) {
case float64:
return v, !math.IsNaN(v) && !math.IsInf(v, 0)
case int:
return float64(v), true
case int64:
return float64(v), true
case json.Number:
n, err := v.Float64()
return n, err == nil && !math.IsInf(n, 0)
default:
return 0, false
}
}
func convertClaudeGenerationConfig(req map[string]any) map[string]any {
out := make(map[string]any)
if mt, ok := asInt(req["max_tokens"]); ok && mt > 0 {
@@ -405,6 +405,64 @@ func TestCleanToolSchema_NormalizesGeminiUnsupportedSchemaFields(t *testing.T) {
require.NotContains(t, emptySchema, "type")
}
func TestCleanToolSchema_ConvertsNestedIntegerExclusiveMinimum(t *testing.T) {
schema := map[string]any{
"type": "object",
"properties": map[string]any{
"counts": map[string]any{
"type": "array",
"items": map[string]any{
"type": "integer",
"exclusiveMinimum": float64(0),
},
},
"strict": map[string]any{
"type": "integer",
"exclusiveMinimum": 0,
"minimum": 5,
},
"weak": map[string]any{
"type": "integer",
"exclusiveMinimum": 2,
"minimum": 1,
},
},
}
cleaned := cleanToolSchema(schema).(map[string]any)
properties := cleaned["properties"].(map[string]any)
items := properties["counts"].(map[string]any)["items"].(map[string]any)
require.NotContains(t, items, "exclusiveMinimum")
require.Equal(t, float64(1), items["minimum"])
strict := properties["strict"].(map[string]any)
require.NotContains(t, strict, "exclusiveMinimum")
require.Equal(t, 5, strict["minimum"])
weak := properties["weak"].(map[string]any)
require.NotContains(t, weak, "exclusiveMinimum")
require.Equal(t, 3, weak["minimum"])
}
func TestCleanToolSchema_DropsAmbiguousExclusiveMinimumWithoutConversion(t *testing.T) {
for name, schema := range map[string]map[string]any{
"number schema": {
"type": "number",
"exclusiveMinimum": 0,
},
"fractional integer bound": {
"type": "integer",
"exclusiveMinimum": 0.5,
},
} {
t.Run(name, func(t *testing.T) {
cleaned := cleanToolSchema(schema).(map[string]any)
require.NotContains(t, cleaned, "exclusiveMinimum")
require.NotContains(t, cleaned, "minimum")
})
}
}
func TestConvertClaudeToolsToGeminiTools_PreservesWebSearchAlongsideFunctions(t *testing.T) {
tools := []any{
map[string]any{