mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 04:50:52 +08:00
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.
This commit is contained in:
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user