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 + }) + } +}