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 ba689c45..229debee 100644 --- a/internal/translator/gemini-cli/claude/gemini-cli_claude_request.go +++ b/internal/translator/gemini-cli/claude/gemini-cli_claude_request.go @@ -11,7 +11,6 @@ import ( "strings" client "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) @@ -36,18 +35,6 @@ import ( // - []byte: The transformed request data in Gemini CLI API format func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) []byte { rawJSON := bytes.Clone(inputRawJSON) - var pathsToDelete []string - root := gjson.ParseBytes(rawJSON) - util.Walk(root, "", "additionalProperties", &pathsToDelete) - util.Walk(root, "", "$schema", &pathsToDelete) - - var err error - for _, p := range pathsToDelete { - rawJSON, err = sjson.DeleteBytes(rawJSON, p) - if err != nil { - continue - } - } rawJSON = bytes.Replace(rawJSON, []byte(`"url":{"type":"string","format":"uri",`), []byte(`"url":{"type":"string",`), -1) // system instruction @@ -99,7 +86,7 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) [] functionName := contentResult.Get("name").String() functionArgs := contentResult.Get("input").String() var args map[string]any - if err = json.Unmarshal([]byte(functionArgs), &args); err == nil { + if err := json.Unmarshal([]byte(functionArgs), &args); err == nil { clientContent.Parts = append(clientContent.Parts, client.Part{FunctionCall: &client.FunctionCall{Name: functionName, Args: args}}) } } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_result" { @@ -136,18 +123,10 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) [] inputSchemaResult := toolResult.Get("input_schema") if inputSchemaResult.Exists() && inputSchemaResult.IsObject() { inputSchema := inputSchemaResult.Raw - // 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) + tool, _ = sjson.SetRaw(tool, "parametersJsonSchema", inputSchema) var toolDeclaration any - if err = json.Unmarshal([]byte(tool), &toolDeclaration); err == nil { + if err := json.Unmarshal([]byte(tool), &toolDeclaration); err == nil { tools[0].FunctionDeclarations = append(tools[0].FunctionDeclarations, toolDeclaration) } } diff --git a/internal/translator/gemini-cli/gemini/gemini-cli_gemini_request.go b/internal/translator/gemini-cli/gemini/gemini-cli_gemini_request.go index a933649b..a406a8aa 100644 --- a/internal/translator/gemini-cli/gemini/gemini-cli_gemini_request.go +++ b/internal/translator/gemini-cli/gemini/gemini-cli_gemini_request.go @@ -10,6 +10,7 @@ import ( "encoding/json" "fmt" + "github.com/router-for-me/CLIProxyAPI/v6/internal/util" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson" @@ -78,6 +79,24 @@ func ConvertGeminiRequestToGeminiCLI(_ string, inputRawJSON []byte, _ bool) []by }) } + toolsResult := gjson.GetBytes(rawJSON, "request.tools") + if toolsResult.Exists() && toolsResult.IsArray() { + toolResults := toolsResult.Array() + for i := 0; i < len(toolResults); i++ { + functionDeclarationsResult := gjson.GetBytes(rawJSON, fmt.Sprintf("request.tools.%d.function_declarations", i)) + if functionDeclarationsResult.Exists() && functionDeclarationsResult.IsArray() { + functionDeclarationsResults := functionDeclarationsResult.Array() + for j := 0; j < len(functionDeclarationsResults); j++ { + parametersResult := gjson.GetBytes(rawJSON, fmt.Sprintf("request.tools.%d.function_declarations.%d.parameters", i, j)) + if parametersResult.Exists() { + strJson, _ := util.RenameKey(string(rawJSON), fmt.Sprintf("request.tools.%d.function_declarations.%d.parameters", i, j), fmt.Sprintf("request.tools.%d.function_declarations.%d.parametersJsonSchema", i, j)) + rawJSON = []byte(strJson) + } + } + } + } + } + return rawJSON } diff --git a/internal/translator/gemini-cli/openai/chat-completions/cli_openai_request.go b/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go similarity index 93% rename from internal/translator/gemini-cli/openai/chat-completions/cli_openai_request.go rename to internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go index 453aca7b..a7d7002d 100644 --- a/internal/translator/gemini-cli/openai/chat-completions/cli_openai_request.go +++ b/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go @@ -26,21 +26,6 @@ import ( // - []byte: The transformed request data in Gemini CLI API format func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bool) []byte { rawJSON := bytes.Clone(inputRawJSON) - var pathsToDelete []string - root := gjson.ParseBytes(rawJSON) - util.Walk(root, "", "additionalProperties", &pathsToDelete) - util.Walk(root, "", "$schema", &pathsToDelete) - util.Walk(root, "", "ref", &pathsToDelete) - util.Walk(root, "", "strict", &pathsToDelete) - - var err error - for _, p := range pathsToDelete { - rawJSON, err = sjson.DeleteBytes(rawJSON, p) - if err != nil { - continue - } - } - // Base envelope out := []byte(`{"project":"","request":{"contents":[],"generationConfig":{"thinkingConfig":{"include_thoughts":true}}},"model":"gemini-2.5-pro"}`) @@ -265,22 +250,13 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo if t.Get("type").String() == "function" { fn := t.Get("function") if fn.Exists() && fn.IsObject() { - out, _ = sjson.SetRawBytes(out, fdPath+".-1", []byte(fn.Raw)) + parametersJsonSchema, _ := util.RenameKey(fn.Raw, "parameters", "parametersJsonSchema") + out, _ = sjson.SetRawBytes(out, fdPath+".-1", []byte(parametersJsonSchema)) } } } } - var pathsToType []string - root = gjson.ParseBytes(out) - util.Walk(root, "", "type", &pathsToType) - for _, p := range pathsToType { - typeResult := gjson.GetBytes(out, p) - if strings.ToLower(typeResult.String()) == "select" { - out, _ = sjson.SetBytes(out, p, "STRING") - } - } - return out } diff --git a/internal/translator/gemini-cli/openai/chat-completions/cli_openai_response.go b/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response.go similarity index 100% rename from internal/translator/gemini-cli/openai/chat-completions/cli_openai_response.go rename to internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response.go diff --git a/internal/translator/gemini-cli/openai/responses/cli_openai-responses_request.go b/internal/translator/gemini-cli/openai/responses/gemini-cli_openai-responses_request.go similarity index 100% rename from internal/translator/gemini-cli/openai/responses/cli_openai-responses_request.go rename to internal/translator/gemini-cli/openai/responses/gemini-cli_openai-responses_request.go diff --git a/internal/translator/gemini-cli/openai/responses/cli_openai-responses_response.go b/internal/translator/gemini-cli/openai/responses/gemini-cli_openai-responses_response.go similarity index 100% rename from internal/translator/gemini-cli/openai/responses/cli_openai-responses_response.go rename to internal/translator/gemini-cli/openai/responses/gemini-cli_openai-responses_response.go diff --git a/internal/translator/gemini/claude/gemini_claude_request.go b/internal/translator/gemini/claude/gemini_claude_request.go index 70b82ee1..6a477dbd 100644 --- a/internal/translator/gemini/claude/gemini_claude_request.go +++ b/internal/translator/gemini/claude/gemini_claude_request.go @@ -11,7 +11,6 @@ import ( "strings" client "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) @@ -29,18 +28,6 @@ import ( // - []byte: The transformed request in Gemini CLI format. func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) []byte { rawJSON := bytes.Clone(inputRawJSON) - var pathsToDelete []string - root := gjson.ParseBytes(rawJSON) - util.Walk(root, "", "additionalProperties", &pathsToDelete) - util.Walk(root, "", "$schema", &pathsToDelete) - - var err error - for _, p := range pathsToDelete { - rawJSON, err = sjson.DeleteBytes(rawJSON, p) - if err != nil { - continue - } - } rawJSON = bytes.Replace(rawJSON, []byte(`"url":{"type":"string","format":"uri",`), []byte(`"url":{"type":"string",`), -1) // system instruction @@ -92,7 +79,7 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) functionName := contentResult.Get("name").String() functionArgs := contentResult.Get("input").String() var args map[string]any - if err = json.Unmarshal([]byte(functionArgs), &args); err == nil { + if err := json.Unmarshal([]byte(functionArgs), &args); err == nil { clientContent.Parts = append(clientContent.Parts, client.Part{FunctionCall: &client.FunctionCall{Name: functionName, Args: args}}) } } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_result" { @@ -129,18 +116,10 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) inputSchemaResult := toolResult.Get("input_schema") if inputSchemaResult.Exists() && inputSchemaResult.IsObject() { inputSchema := inputSchemaResult.Raw - // 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) + tool, _ = sjson.SetRaw(tool, "parametersJsonSchema", inputSchema) var toolDeclaration any - if err = json.Unmarshal([]byte(tool), &toolDeclaration); err == nil { + if err := json.Unmarshal([]byte(tool), &toolDeclaration); err == nil { tools[0].FunctionDeclarations = append(tools[0].FunctionDeclarations, toolDeclaration) } } diff --git a/internal/translator/gemini/gemini-cli/gemini_gemini-cli_request.go b/internal/translator/gemini/gemini-cli/gemini_gemini-cli_request.go index bc660929..9dc4d39a 100644 --- a/internal/translator/gemini/gemini-cli/gemini_gemini-cli_request.go +++ b/internal/translator/gemini/gemini-cli/gemini_gemini-cli_request.go @@ -7,7 +7,9 @@ package geminiCLI import ( "bytes" + "fmt" + "github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) @@ -24,5 +26,24 @@ func ConvertGeminiCLIRequestToGemini(_ string, inputRawJSON []byte, _ bool) []by rawJSON, _ = sjson.SetRawBytes(rawJSON, "system_instruction", []byte(gjson.GetBytes(rawJSON, "systemInstruction").Raw)) rawJSON, _ = sjson.DeleteBytes(rawJSON, "systemInstruction") } + + toolsResult := gjson.GetBytes(rawJSON, "tools") + if toolsResult.Exists() && toolsResult.IsArray() { + toolResults := toolsResult.Array() + for i := 0; i < len(toolResults); i++ { + functionDeclarationsResult := gjson.GetBytes(rawJSON, fmt.Sprintf("tools.%d.function_declarations", i)) + if functionDeclarationsResult.Exists() && functionDeclarationsResult.IsArray() { + functionDeclarationsResults := functionDeclarationsResult.Array() + for j := 0; j < len(functionDeclarationsResults); j++ { + parametersResult := gjson.GetBytes(rawJSON, fmt.Sprintf("tools.%d.function_declarations.%d.parameters", i, j)) + if parametersResult.Exists() { + strJson, _ := util.RenameKey(string(rawJSON), fmt.Sprintf("tools.%d.function_declarations.%d.parameters", i, j), fmt.Sprintf("tools.%d.function_declarations.%d.parametersJsonSchema", i, j)) + rawJSON = []byte(strJson) + } + } + } + } + } + return rawJSON } diff --git a/internal/translator/gemini/gemini/gemini_gemini_request.go b/internal/translator/gemini/gemini/gemini_gemini_request.go index 779bd175..cc23edb3 100644 --- a/internal/translator/gemini/gemini/gemini_gemini_request.go +++ b/internal/translator/gemini/gemini/gemini_gemini_request.go @@ -7,6 +7,7 @@ import ( "bytes" "fmt" + "github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) @@ -24,6 +25,24 @@ func ConvertGeminiRequestToGemini(_ string, inputRawJSON []byte, _ bool) []byte return rawJSON } + toolsResult := gjson.GetBytes(rawJSON, "tools") + if toolsResult.Exists() && toolsResult.IsArray() { + toolResults := toolsResult.Array() + for i := 0; i < len(toolResults); i++ { + functionDeclarationsResult := gjson.GetBytes(rawJSON, fmt.Sprintf("tools.%d.function_declarations", i)) + if functionDeclarationsResult.Exists() && functionDeclarationsResult.IsArray() { + functionDeclarationsResults := functionDeclarationsResult.Array() + for j := 0; j < len(functionDeclarationsResults); j++ { + parametersResult := gjson.GetBytes(rawJSON, fmt.Sprintf("tools.%d.function_declarations.%d.parameters", i, j)) + if parametersResult.Exists() { + strJson, _ := util.RenameKey(string(rawJSON), fmt.Sprintf("tools.%d.function_declarations.%d.parameters", i, j), fmt.Sprintf("tools.%d.function_declarations.%d.parametersJsonSchema", i, j)) + rawJSON = []byte(strJson) + } + } + } + } + } + // Walk contents and fix roles out := rawJSON prevRole := "" diff --git a/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go b/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go index 0fc1cf10..98de3195 100644 --- a/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go +++ b/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go @@ -26,21 +26,6 @@ import ( // - []byte: The transformed request data in Gemini API format func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool) []byte { rawJSON := bytes.Clone(inputRawJSON) - var pathsToDelete []string - root := gjson.ParseBytes(rawJSON) - util.Walk(root, "", "additionalProperties", &pathsToDelete) - util.Walk(root, "", "$schema", &pathsToDelete) - util.Walk(root, "", "ref", &pathsToDelete) - util.Walk(root, "", "strict", &pathsToDelete) - - var err error - for _, p := range pathsToDelete { - rawJSON, err = sjson.DeleteBytes(rawJSON, p) - if err != nil { - continue - } - } - // Base envelope out := []byte(`{"contents":[],"generationConfig":{"thinkingConfig":{"include_thoughts":true}}}`) @@ -290,22 +275,13 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool) if t.Get("type").String() == "function" { fn := t.Get("function") if fn.Exists() && fn.IsObject() { - out, _ = sjson.SetRawBytes(out, fdPath+".-1", []byte(fn.Raw)) + parametersJsonSchema, _ := util.RenameKey(fn.Raw, "parameters", "parametersJsonSchema") + out, _ = sjson.SetRawBytes(out, fdPath+".-1", []byte(parametersJsonSchema)) } } } } - var pathsToType []string - root = gjson.ParseBytes(out) - util.Walk(root, "", "type", &pathsToType) - for _, p := range pathsToType { - typeResult := gjson.GetBytes(out, p) - if strings.ToLower(typeResult.String()) == "select" { - out, _ = sjson.SetBytes(out, p, "STRING") - } - } - return out } diff --git a/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go b/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go index af7923ab..beffb317 100644 --- a/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go +++ b/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go @@ -150,7 +150,7 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte if outputResult.IsObject() { functionResponse, _ = sjson.SetRaw(functionResponse, "functionResponse.response.content", outputResult.String()) } else { - functionResponse, _ = sjson.Set(functionResponse, "functionResponse.response.content", outputResult.String()) + functionResponse, _ = sjson.Set(functionResponse, "functionResponse.response.content", output) } } @@ -168,7 +168,7 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte tools.ForEach(func(_, tool gjson.Result) bool { if tool.Get("type").String() == "function" { - funcDecl := `{"name":"","description":"","parameters":{}}` + funcDecl := `{"name":"","description":"","parametersJsonSchema":{}}` if name := tool.Get("name"); name.Exists() { funcDecl, _ = sjson.Set(funcDecl, "name", name.String()) @@ -192,7 +192,7 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte } // Set the overall type to OBJECT cleaned, _ = sjson.Set(cleaned, "type", "OBJECT") - funcDecl, _ = sjson.SetRaw(funcDecl, "parameters", cleaned) + funcDecl, _ = sjson.SetRaw(funcDecl, "parametersJsonSchema", cleaned) } geminiTools, _ = sjson.SetRaw(geminiTools, "0.functionDeclarations.-1", funcDecl) @@ -261,6 +261,5 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingBudget", -1) } } - return []byte(out) } diff --git a/internal/util/translator.go b/internal/util/translator.go index 329f9e94..40274aca 100644 --- a/internal/util/translator.go +++ b/internal/util/translator.go @@ -212,161 +212,3 @@ 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 - } 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 - }) - default: - - } -} - -// 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 - }) - default: - - } -}