sanitize gemini tool schemas

This commit is contained in:
patriceckhart 2026-05-26 18:10:25 +02:00
parent 37ef90bbb3
commit 583b2a7db2
2 changed files with 77 additions and 4 deletions

View file

@ -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 {

View file

@ -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{