mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-18 04:10:51 +08:00
Merge pull request #43 from kyle-the-dev/fix/gemini-schema-sanitization
fix: comprehensive JSON Schema sanitization for Claude to Gemini
This commit is contained in:
@@ -136,8 +136,14 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) []
|
|||||||
inputSchemaResult := toolResult.Get("input_schema")
|
inputSchemaResult := toolResult.Get("input_schema")
|
||||||
if inputSchemaResult.Exists() && inputSchemaResult.IsObject() {
|
if inputSchemaResult.Exists() && inputSchemaResult.IsObject() {
|
||||||
inputSchema := inputSchemaResult.Raw
|
inputSchema := inputSchemaResult.Raw
|
||||||
inputSchema, _ = sjson.Delete(inputSchema, "additionalProperties")
|
// Use comprehensive schema sanitization for Gemini API compatibility
|
||||||
inputSchema, _ = sjson.Delete(inputSchema, "$schema")
|
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.Delete(toolResult.Raw, "input_schema")
|
||||||
tool, _ = sjson.SetRaw(tool, "parameters", inputSchema)
|
tool, _ = sjson.SetRaw(tool, "parameters", inputSchema)
|
||||||
var toolDeclaration any
|
var toolDeclaration any
|
||||||
|
|||||||
@@ -129,8 +129,14 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
inputSchemaResult := toolResult.Get("input_schema")
|
inputSchemaResult := toolResult.Get("input_schema")
|
||||||
if inputSchemaResult.Exists() && inputSchemaResult.IsObject() {
|
if inputSchemaResult.Exists() && inputSchemaResult.IsObject() {
|
||||||
inputSchema := inputSchemaResult.Raw
|
inputSchema := inputSchemaResult.Raw
|
||||||
inputSchema, _ = sjson.Delete(inputSchema, "additionalProperties")
|
// Use comprehensive schema sanitization for Gemini API compatibility
|
||||||
inputSchema, _ = sjson.Delete(inputSchema, "$schema")
|
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.Delete(toolResult.Raw, "input_schema")
|
||||||
tool, _ = sjson.SetRaw(tool, "parameters", inputSchema)
|
tool, _ = sjson.SetRaw(tool, "parameters", inputSchema)
|
||||||
var toolDeclaration any
|
var toolDeclaration any
|
||||||
|
|||||||
@@ -212,3 +212,160 @@ func FixJSON(input string) string {
|
|||||||
|
|
||||||
return out.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