diff --git a/internal/provider/gemini.go b/internal/provider/gemini.go index 54a235f..f20ed6a 100644 --- a/internal/provider/gemini.go +++ b/internal/provider/gemini.go @@ -157,10 +157,7 @@ func (c *geminiClient) buildRequest(req Request) (*gemRequest, string, error) { if functionsEnabled && len(req.Tools) > 0 { decls := make([]gemFunctionDecl, 0, len(req.Tools)) for _, t := range req.Tools { - schema := t.Schema - if len(schema) == 0 || !json.Valid(schema) { - schema = json.RawMessage(`{"type":"object","properties":{}}`) - } + schema := sanitizeGeminiToolSchema(t.Schema) decls = append(decls, gemFunctionDecl{ Name: t.Name, Description: t.Description, @@ -226,6 +223,46 @@ func (c *geminiClient) buildRequest(req Request) (*gemRequest, string, error) { return out, m.ID, nil } +func sanitizeGeminiToolSchema(schema json.RawMessage) json.RawMessage { + if len(schema) == 0 || !json.Valid(schema) { + return json.RawMessage(`{"type":"object","properties":{}}`) + } + var v any + if err := json.Unmarshal(schema, &v); err != nil { + return json.RawMessage(`{"type":"object","properties":{}}`) + } + v = stripGeminiUnsupportedSchemaFields(v) + out, err := json.Marshal(v) + if err != nil || len(out) == 0 || !json.Valid(out) { + return json.RawMessage(`{"type":"object","properties":{}}`) + } + return out +} + +func stripGeminiUnsupportedSchemaFields(v any) any { + switch x := v.(type) { + case map[string]any: + out := make(map[string]any, len(x)) + for k, val := range x { + switch k { + case "additionalProperties", "$schema": + continue + default: + out[k] = stripGeminiUnsupportedSchemaFields(val) + } + } + return out + case []any: + out := make([]any, len(x)) + for i, val := range x { + out[i] = stripGeminiUnsupportedSchemaFields(val) + } + return out + default: + return v + } +} + func convertGemUserParts(blocks []Content) []gemPart { var parts []gemPart for _, b := range blocks { diff --git a/internal/provider/gemini_test.go b/internal/provider/gemini_test.go index a46ba2c..a379fee 100644 --- a/internal/provider/gemini_test.go +++ b/internal/provider/gemini_test.go @@ -206,6 +206,42 @@ func TestGeminiBuildRequestSystemAndTools(t *testing.T) { // TestGeminiBuildRequestImageModelOmitsTools confirms image-generation // models receive direct multimodal prompts without function declarations. +func TestGeminiBuildRequestStripsUnsupportedSchemaFields(t *testing.T) { + c := NewGemini("k", "https://example.invalid").(*geminiClient) + wire, _, err := c.buildRequest(Request{ + Model: "gemini-2.5-pro", + Tools: []Tool{{ + Name: "edit", + Description: "edit a file", + Schema: json.RawMessage(`{ + "$schema":"http://json-schema.org/draft-07/schema#", + "type":"object", + "additionalProperties":false, + "properties":{ + "edits":{ + "type":"array", + "items":{ + "type":"object", + "additionalProperties":false, + "properties":{"oldText":{"type":"string"},"newText":{"type":"string"}} + } + } + } + }`), + }}, + }) + if err != nil { + t.Fatal(err) + } + got := string(wire.Tools[0].FunctionDeclarations[0].Parameters) + if strings.Contains(got, "additionalProperties") || strings.Contains(got, "$schema") { + t.Fatalf("Gemini schema should strip unsupported fields, got %s", got) + } + if !strings.Contains(got, `"oldText"`) || !strings.Contains(got, `"newText"`) { + t.Fatalf("Gemini schema lost nested properties, got %s", got) + } +} + func TestGeminiBuildRequestImageModelOmitsTools(t *testing.T) { c := NewGemini("k", "https://example.invalid").(*geminiClient) wire, _, err := c.buildRequest(Request{