From a0c389a854ef310efa18e9e6a4204e259c71badc Mon Sep 17 00:00:00 2001 From: Kyle Ryan Date: Tue, 16 Sep 2025 17:45:33 +0800 Subject: [PATCH] fix: comprehensive JSON Schema sanitization for Claude to Gemini - Add SanitizeSchemaForGemini utility handling union types, allOf, exclusiveMinimum - Fix both gemini-cli and gemini API translators - Resolve "Proto field is not repeating, cannot start list" errors - Maintain backward compatibility with fallback logic This fixes Claude Code CLI compatibility issues when using tools with either Gemini CLI credentials or direct Gemini API keys by properly sanitizing JSON Schema fields that are incompatible with Gemini's Protocol Buffer validation. --- .../claude/gemini-cli_claude_request.go | 10 +- .../gemini/claude/gemini_claude_request.go | 10 +- internal/util/translator.go | 157 ++++++++++++++++++ 3 files changed, 173 insertions(+), 4 deletions(-) diff --git a/internal/translator/gemini-cli/claude/gemini-cli_claude_request.go b/internal/translator/gemini-cli/claude/gemini-cli_claude_request.go index 93dbf1e8..29814cf0 100644 --- a/internal/translator/gemini-cli/claude/gemini-cli_claude_request.go +++ b/internal/translator/gemini-cli/claude/gemini-cli_claude_request.go @@ -136,8 +136,14 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) [] inputSchemaResult := toolResult.Get("input_schema") if inputSchemaResult.Exists() && inputSchemaResult.IsObject() { inputSchema := inputSchemaResult.Raw - inputSchema, _ = sjson.Delete(inputSchema, "additionalProperties") - inputSchema, _ = sjson.Delete(inputSchema, "$schema") + // Use comprehensive schema sanitization for Gemini API compatibility + if sanitizedSchema, sanitizeErr := util.SanitizeSchemaForGemini(inputSchema); sanitizeErr == nil { + inputSchema = sanitizedSchema + } else { + // Fallback to basic cleanup if sanitization fails + inputSchema, _ = sjson.Delete(inputSchema, "additionalProperties") + inputSchema, _ = sjson.Delete(inputSchema, "$schema") + } tool, _ := sjson.Delete(toolResult.Raw, "input_schema") tool, _ = sjson.SetRaw(tool, "parameters", inputSchema) var toolDeclaration any diff --git a/internal/translator/gemini/claude/gemini_claude_request.go b/internal/translator/gemini/claude/gemini_claude_request.go index 45ab0bef..43e01b1f 100644 --- a/internal/translator/gemini/claude/gemini_claude_request.go +++ b/internal/translator/gemini/claude/gemini_claude_request.go @@ -129,8 +129,14 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) inputSchemaResult := toolResult.Get("input_schema") if inputSchemaResult.Exists() && inputSchemaResult.IsObject() { inputSchema := inputSchemaResult.Raw - inputSchema, _ = sjson.Delete(inputSchema, "additionalProperties") - inputSchema, _ = sjson.Delete(inputSchema, "$schema") + // Use comprehensive schema sanitization for Gemini API compatibility + if sanitizedSchema, sanitizeErr := util.SanitizeSchemaForGemini(inputSchema); sanitizeErr == nil { + inputSchema = sanitizedSchema + } else { + // Fallback to basic cleanup if sanitization fails + inputSchema, _ = sjson.Delete(inputSchema, "additionalProperties") + inputSchema, _ = sjson.Delete(inputSchema, "$schema") + } tool, _ := sjson.Delete(toolResult.Raw, "input_schema") tool, _ = sjson.SetRaw(tool, "parameters", inputSchema) var toolDeclaration any diff --git a/internal/util/translator.go b/internal/util/translator.go index 40274aca..6744eab0 100644 --- a/internal/util/translator.go +++ b/internal/util/translator.go @@ -212,3 +212,160 @@ func FixJSON(input string) string { return out.String() } + +// SanitizeSchemaForGemini removes JSON Schema fields that are incompatible with Gemini API +// to prevent "Proto field is not repeating, cannot start list" errors. +// +// Parameters: +// - schemaJSON: The JSON schema string to sanitize +// +// Returns: +// - string: The sanitized schema string +// - error: An error if the operation fails +// +// This function removes the following incompatible fields: +// - additionalProperties: Not supported in Gemini function declarations +// - $schema: JSON Schema meta-schema identifier, not needed for API +// - allOf/anyOf/oneOf: Union type constructs not supported +// - exclusiveMinimum/exclusiveMaximum: Advanced validation constraints +// - patternProperties: Advanced property pattern matching +// - dependencies: Property dependencies not supported +// - type arrays: Converts ["string", "null"] to just "string" +func SanitizeSchemaForGemini(schemaJSON string) (string, error) { + // Remove top-level incompatible fields + fieldsToRemove := []string{ + "additionalProperties", + "$schema", + "allOf", + "anyOf", + "oneOf", + "exclusiveMinimum", + "exclusiveMaximum", + "patternProperties", + "dependencies", + } + + result := schemaJSON + var err error + + for _, field := range fieldsToRemove { + result, err = sjson.Delete(result, field) + if err != nil { + continue // Continue even if deletion fails + } + } + + // Handle type arrays by converting them to single types + result = sanitizeTypeFields(result) + + // Recursively clean nested objects + result = cleanNestedSchemas(result) + + return result, nil +} + +// sanitizeTypeFields converts type arrays to single types for Gemini compatibility +func sanitizeTypeFields(jsonStr string) string { + // Parse the JSON to find all "type" fields + parsed := gjson.Parse(jsonStr) + result := jsonStr + + // Walk through all paths to find type fields + var typeFields []string + walkForTypeFields(parsed, "", &typeFields) + + // Process each type field + for _, path := range typeFields { + typeValue := gjson.Get(result, path) + if typeValue.IsArray() { + // Convert array to single type (prioritize string, then others) + arr := typeValue.Array() + if len(arr) > 0 { + var preferredType string + for _, t := range arr { + typeStr := t.String() + if typeStr == "string" { + preferredType = "string" + break + } else if typeStr == "number" || typeStr == "integer" { + preferredType = typeStr + if preferredType == "" { + preferredType = typeStr + } + } else if preferredType == "" { + preferredType = typeStr + } + } + if preferredType != "" { + result, _ = sjson.Set(result, path, preferredType) + } + } + } + } + + return result +} + +// walkForTypeFields recursively finds all "type" field paths in the JSON +func walkForTypeFields(value gjson.Result, path string, paths *[]string) { + switch value.Type { + case gjson.JSON: + value.ForEach(func(key, val gjson.Result) bool { + var childPath string + if path == "" { + childPath = key.String() + } else { + childPath = path + "." + key.String() + } + if key.String() == "type" { + *paths = append(*paths, childPath) + } + walkForTypeFields(val, childPath, paths) + return true + }) + } +} + +// cleanNestedSchemas recursively removes incompatible fields from nested schema objects +func cleanNestedSchemas(jsonStr string) string { + fieldsToRemove := []string{"allOf", "anyOf", "oneOf", "exclusiveMinimum", "exclusiveMaximum"} + + // Find all nested paths that might contain these fields + var pathsToClean []string + parsed := gjson.Parse(jsonStr) + findNestedSchemaPaths(parsed, "", fieldsToRemove, &pathsToClean) + + result := jsonStr + // Remove fields from all found paths + for _, path := range pathsToClean { + result, _ = sjson.Delete(result, path) + } + + return result +} + +// findNestedSchemaPaths recursively finds paths containing incompatible schema fields +func findNestedSchemaPaths(value gjson.Result, path string, fieldsToFind []string, paths *[]string) { + switch value.Type { + case gjson.JSON: + value.ForEach(func(key, val gjson.Result) bool { + var childPath string + if path == "" { + childPath = key.String() + } else { + childPath = path + "." + key.String() + } + + // Check if this key is one we want to remove + for _, field := range fieldsToFind { + if key.String() == field { + *paths = append(*paths, childPath) + break + } + } + + findNestedSchemaPaths(val, childPath, fieldsToFind, paths) + return true + }) + } +}