History

Fixed: #129 #123 #102 #97

Luis Pater committed 1 year agov6.2.17

Commit 4477c721 parent 0d89a2212 files · +72 −262Browse files

Unified · Split

12 files
  1. internal/translator/gemini-cli/claude/gemini-cli_claude_request.go +3 −24
  2. internal/translator/gemini-cli/gemini/gemini-cli_gemini_request.go +19 −0
  3. internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go +2 −26
  4. internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response.go +0 −0
  5. internal/translator/gemini-cli/openai/responses/gemini-cli_openai-responses_request.go +0 −0
  6. internal/translator/gemini-cli/openai/responses/gemini-cli_openai-responses_response.go +0 −0
  7. internal/translator/gemini/claude/gemini_claude_request.go +3 −24
  8. internal/translator/gemini/gemini-cli/gemini_gemini-cli_request.go +21 −0
  9. internal/translator/gemini/gemini/gemini_gemini_request.go +19 −0
  10. internal/translator/gemini/openai/chat-completions/gemini_openai_request.go +2 −26
  11. internal/translator/gemini/openai/responses/gemini_openai-responses_request.go +3 −4
  12. internal/util/translator.go +0 −158
internal/translator/gemini-cli/claude/gemini-cli_claude_request.go+3−24
@@ -11,7 +11,6 @@ import ( · 10 unmodified lines
11 "strings"11 "strings"
12 12
13 client "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"13 client "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
14 "github.com/router-for-me/CLIProxyAPI/v6/internal/util"
15 "github.com/tidwall/gjson"14 "github.com/tidwall/gjson"
16 "github.com/tidwall/sjson"15 "github.com/tidwall/sjson"
17)16)
@@ -36,18 +35,6 @@ import ( · 18 unmodified lines
36// - []byte: The transformed request data in Gemini CLI API format35// - []byte: The transformed request data in Gemini CLI API format
37func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) []byte {36func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) []byte {
38 rawJSON := bytes.Clone(inputRawJSON)37 rawJSON := bytes.Clone(inputRawJSON)
39 var pathsToDelete []string
40 root := gjson.ParseBytes(rawJSON)
41 util.Walk(root, "", "additionalProperties", &pathsToDelete)
42 util.Walk(root, "", "$schema", &pathsToDelete)
43
44 var err error
45 for _, p := range pathsToDelete {
46 rawJSON, err = sjson.DeleteBytes(rawJSON, p)
47 if err != nil {
48 continue
49 }
50 }
51 rawJSON = bytes.Replace(rawJSON, []byte(`"url":{"type":"string","format":"uri",`), []byte(`"url":{"type":"string",`), -1)38 rawJSON = bytes.Replace(rawJSON, []byte(`"url":{"type":"string","format":"uri",`), []byte(`"url":{"type":"string",`), -1)
52 39
53 // system instruction40 // system instruction
@@ -99,7 +86,7 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) [] · 45 unmodified lines
99 functionName := contentResult.Get("name").String()86 functionName := contentResult.Get("name").String()
100 functionArgs := contentResult.Get("input").String()87 functionArgs := contentResult.Get("input").String()
101 var args map[string]any88 var args map[string]any
102 if err = json.Unmarshal([]byte(functionArgs), &args); err == nil {89 if err := json.Unmarshal([]byte(functionArgs), &args); err == nil {
103 clientContent.Parts = append(clientContent.Parts, client.Part{FunctionCall: &client.FunctionCall{Name: functionName, Args: args}})90 clientContent.Parts = append(clientContent.Parts, client.Part{FunctionCall: &client.FunctionCall{Name: functionName, Args: args}})
104 }91 }
105 } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_result" {92 } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_result" {
@@ -136,18 +123,10 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) [] · 30 unmodified lines
136 inputSchemaResult := toolResult.Get("input_schema")123 inputSchemaResult := toolResult.Get("input_schema")
137 if inputSchemaResult.Exists() && inputSchemaResult.IsObject() {124 if inputSchemaResult.Exists() && inputSchemaResult.IsObject() {
138 inputSchema := inputSchemaResult.Raw125 inputSchema := inputSchemaResult.Raw
139 // Use comprehensive schema sanitization for Gemini API compatibility
140 if sanitizedSchema, sanitizeErr := util.SanitizeSchemaForGemini(inputSchema); sanitizeErr == nil {
141 inputSchema = sanitizedSchema
142 } else {
143 // Fallback to basic cleanup if sanitization fails
144 inputSchema, _ = sjson.Delete(inputSchema, "additionalProperties")
145 inputSchema, _ = sjson.Delete(inputSchema, "$schema")
146 }
147 tool, _ := sjson.Delete(toolResult.Raw, "input_schema")126 tool, _ := sjson.Delete(toolResult.Raw, "input_schema")
148 tool, _ = sjson.SetRaw(tool, "parameters", inputSchema)127 tool, _ = sjson.SetRaw(tool, "parametersJsonSchema", inputSchema)
149 var toolDeclaration any128 var toolDeclaration any
150 if err = json.Unmarshal([]byte(tool), &toolDeclaration); err == nil {129 if err := json.Unmarshal([]byte(tool), &toolDeclaration); err == nil {
151 tools[0].FunctionDeclarations = append(tools[0].FunctionDeclarations, toolDeclaration)130 tools[0].FunctionDeclarations = append(tools[0].FunctionDeclarations, toolDeclaration)
152 }131 }
153 }132 }
internal/translator/gemini-cli/gemini/gemini-cli_gemini_request.go+19−0
@@ -10,6 +10,7 @@ import ( · 9 unmodified lines
10 "encoding/json"10 "encoding/json"
11 "fmt"11 "fmt"
12 12
13 "github.com/router-for-me/CLIProxyAPI/v6/internal/util"
13 log "github.com/sirupsen/logrus"14 log "github.com/sirupsen/logrus"
14 "github.com/tidwall/gjson"15 "github.com/tidwall/gjson"
15 "github.com/tidwall/sjson"16 "github.com/tidwall/sjson"
@@ -78,6 +79,24 @@ func ConvertGeminiRequestToGeminiCLI(_ string, inputRawJSON []byte, _ bool) []by · 62 unmodified lines
78 })79 })
79 }80 }
80 81
82 toolsResult := gjson.GetBytes(rawJSON, "request.tools")
83 if toolsResult.Exists() && toolsResult.IsArray() {
84 toolResults := toolsResult.Array()
85 for i := 0; i < len(toolResults); i++ {
86 functionDeclarationsResult := gjson.GetBytes(rawJSON, fmt.Sprintf("request.tools.%d.function_declarations", i))
87 if functionDeclarationsResult.Exists() && functionDeclarationsResult.IsArray() {
88 functionDeclarationsResults := functionDeclarationsResult.Array()
89 for j := 0; j < len(functionDeclarationsResults); j++ {
90 parametersResult := gjson.GetBytes(rawJSON, fmt.Sprintf("request.tools.%d.function_declarations.%d.parameters", i, j))
91 if parametersResult.Exists() {
92 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))
93 rawJSON = []byte(strJson)
94 }
95 }
96 }
97 }
98 }
99
81 return rawJSON100 return rawJSON
82}101}
83 102
internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.gorenamed from internal/translator/gemini-cli/openai/chat-completions/cli_openai_request.go+2−26
@@ -26,21 +26,6 @@ import ( · 25 unmodified lines
26// - []byte: The transformed request data in Gemini CLI API format26// - []byte: The transformed request data in Gemini CLI API format
27func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bool) []byte {27func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bool) []byte {
28 rawJSON := bytes.Clone(inputRawJSON)28 rawJSON := bytes.Clone(inputRawJSON)
29 var pathsToDelete []string
30 root := gjson.ParseBytes(rawJSON)
31 util.Walk(root, "", "additionalProperties", &pathsToDelete)
32 util.Walk(root, "", "$schema", &pathsToDelete)
33 util.Walk(root, "", "ref", &pathsToDelete)
34 util.Walk(root, "", "strict", &pathsToDelete)
35
36 var err error
37 for _, p := range pathsToDelete {
38 rawJSON, err = sjson.DeleteBytes(rawJSON, p)
39 if err != nil {
40 continue
41 }
42 }
43
44 // Base envelope29 // Base envelope
45 out := []byte(`{"project":"","request":{"contents":[],"generationConfig":{"thinkingConfig":{"include_thoughts":true}}},"model":"gemini-2.5-pro"}`)30 out := []byte(`{"project":"","request":{"contents":[],"generationConfig":{"thinkingConfig":{"include_thoughts":true}}},"model":"gemini-2.5-pro"}`)
46 31
@@ -265,22 +250,13 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo · 218 unmodified lines
265 if t.Get("type").String() == "function" {250 if t.Get("type").String() == "function" {
266 fn := t.Get("function")251 fn := t.Get("function")
267 if fn.Exists() && fn.IsObject() {252 if fn.Exists() && fn.IsObject() {
268 out, _ = sjson.SetRawBytes(out, fdPath+".-1", []byte(fn.Raw))253 parametersJsonSchema, _ := util.RenameKey(fn.Raw, "parameters", "parametersJsonSchema")
254 out, _ = sjson.SetRawBytes(out, fdPath+".-1", []byte(parametersJsonSchema))
269 }255 }
270 }256 }
271 }257 }
272 }258 }
273 259
274 var pathsToType []string
275 root = gjson.ParseBytes(out)
276 util.Walk(root, "", "type", &pathsToType)
277 for _, p := range pathsToType {
278 typeResult := gjson.GetBytes(out, p)
279 if strings.ToLower(typeResult.String()) == "select" {
280 out, _ = sjson.SetBytes(out, p, "STRING")
281 }
282 }
283
284 return out260 return out
285}261}
286 262
internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response.gorenamed from internal/translator/gemini-cli/openai/chat-completions/cli_openai_response.go+0−0

File renamed without changes.

internal/translator/gemini-cli/openai/responses/gemini-cli_openai-responses_request.gorenamed from internal/translator/gemini-cli/openai/responses/cli_openai-responses_request.go+0−0

File renamed without changes.

internal/translator/gemini-cli/openai/responses/gemini-cli_openai-responses_response.gorenamed from internal/translator/gemini-cli/openai/responses/cli_openai-responses_response.go+0−0

File renamed without changes.

internal/translator/gemini/claude/gemini_claude_request.go+3−24
@@ -11,7 +11,6 @@ import ( · 10 unmodified lines
11 "strings"11 "strings"
12 12
13 client "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"13 client "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
14 "github.com/router-for-me/CLIProxyAPI/v6/internal/util"
15 "github.com/tidwall/gjson"14 "github.com/tidwall/gjson"
16 "github.com/tidwall/sjson"15 "github.com/tidwall/sjson"
17)16)
@@ -29,18 +28,6 @@ import ( · 11 unmodified lines
29// - []byte: The transformed request in Gemini CLI format.28// - []byte: The transformed request in Gemini CLI format.
30func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) []byte {29func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) []byte {
31 rawJSON := bytes.Clone(inputRawJSON)30 rawJSON := bytes.Clone(inputRawJSON)
32 var pathsToDelete []string
33 root := gjson.ParseBytes(rawJSON)
34 util.Walk(root, "", "additionalProperties", &pathsToDelete)
35 util.Walk(root, "", "$schema", &pathsToDelete)
36
37 var err error
38 for _, p := range pathsToDelete {
39 rawJSON, err = sjson.DeleteBytes(rawJSON, p)
40 if err != nil {
41 continue
42 }
43 }
44 rawJSON = bytes.Replace(rawJSON, []byte(`"url":{"type":"string","format":"uri",`), []byte(`"url":{"type":"string",`), -1)31 rawJSON = bytes.Replace(rawJSON, []byte(`"url":{"type":"string","format":"uri",`), []byte(`"url":{"type":"string",`), -1)
45 32
46 // system instruction33 // system instruction
@@ -92,7 +79,7 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) · 45 unmodified lines
92 functionName := contentResult.Get("name").String()79 functionName := contentResult.Get("name").String()
93 functionArgs := contentResult.Get("input").String()80 functionArgs := contentResult.Get("input").String()
94 var args map[string]any81 var args map[string]any
95 if err = json.Unmarshal([]byte(functionArgs), &args); err == nil {82 if err := json.Unmarshal([]byte(functionArgs), &args); err == nil {
96 clientContent.Parts = append(clientContent.Parts, client.Part{FunctionCall: &client.FunctionCall{Name: functionName, Args: args}})83 clientContent.Parts = append(clientContent.Parts, client.Part{FunctionCall: &client.FunctionCall{Name: functionName, Args: args}})
97 }84 }
98 } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_result" {85 } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_result" {
@@ -129,18 +116,10 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) · 30 unmodified lines
129 inputSchemaResult := toolResult.Get("input_schema")116 inputSchemaResult := toolResult.Get("input_schema")
130 if inputSchemaResult.Exists() && inputSchemaResult.IsObject() {117 if inputSchemaResult.Exists() && inputSchemaResult.IsObject() {
131 inputSchema := inputSchemaResult.Raw118 inputSchema := inputSchemaResult.Raw
132 // Use comprehensive schema sanitization for Gemini API compatibility
133 if sanitizedSchema, sanitizeErr := util.SanitizeSchemaForGemini(inputSchema); sanitizeErr == nil {
134 inputSchema = sanitizedSchema
135 } else {
136 // Fallback to basic cleanup if sanitization fails
137 inputSchema, _ = sjson.Delete(inputSchema, "additionalProperties")
138 inputSchema, _ = sjson.Delete(inputSchema, "$schema")
139 }
140 tool, _ := sjson.Delete(toolResult.Raw, "input_schema")119 tool, _ := sjson.Delete(toolResult.Raw, "input_schema")
141 tool, _ = sjson.SetRaw(tool, "parameters", inputSchema)120 tool, _ = sjson.SetRaw(tool, "parametersJsonSchema", inputSchema)
142 var toolDeclaration any121 var toolDeclaration any
143 if err = json.Unmarshal([]byte(tool), &toolDeclaration); err == nil {122 if err := json.Unmarshal([]byte(tool), &toolDeclaration); err == nil {
144 tools[0].FunctionDeclarations = append(tools[0].FunctionDeclarations, toolDeclaration)123 tools[0].FunctionDeclarations = append(tools[0].FunctionDeclarations, toolDeclaration)
145 }124 }
146 }125 }
internal/translator/gemini/gemini-cli/gemini_gemini-cli_request.go+21−0
@@ -7,7 +7,9 @@ package geminiCLI · 6 unmodified lines
7 7
8import (8import (
9 "bytes"9 "bytes"
10 "fmt"
10 11
12 "github.com/router-for-me/CLIProxyAPI/v6/internal/util"
11 "github.com/tidwall/gjson"13 "github.com/tidwall/gjson"
12 "github.com/tidwall/sjson"14 "github.com/tidwall/sjson"
13)15)
@@ -24,5 +26,24 @@ func ConvertGeminiCLIRequestToGemini(_ string, inputRawJSON []byte, _ bool) []by · 10 unmodified lines
24 rawJSON, _ = sjson.SetRawBytes(rawJSON, "system_instruction", []byte(gjson.GetBytes(rawJSON, "systemInstruction").Raw))26 rawJSON, _ = sjson.SetRawBytes(rawJSON, "system_instruction", []byte(gjson.GetBytes(rawJSON, "systemInstruction").Raw))
25 rawJSON, _ = sjson.DeleteBytes(rawJSON, "systemInstruction")27 rawJSON, _ = sjson.DeleteBytes(rawJSON, "systemInstruction")
26 }28 }
29
30 toolsResult := gjson.GetBytes(rawJSON, "tools")
31 if toolsResult.Exists() && toolsResult.IsArray() {
32 toolResults := toolsResult.Array()
33 for i := 0; i < len(toolResults); i++ {
34 functionDeclarationsResult := gjson.GetBytes(rawJSON, fmt.Sprintf("tools.%d.function_declarations", i))
35 if functionDeclarationsResult.Exists() && functionDeclarationsResult.IsArray() {
36 functionDeclarationsResults := functionDeclarationsResult.Array()
37 for j := 0; j < len(functionDeclarationsResults); j++ {
38 parametersResult := gjson.GetBytes(rawJSON, fmt.Sprintf("tools.%d.function_declarations.%d.parameters", i, j))
39 if parametersResult.Exists() {
40 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))
41 rawJSON = []byte(strJson)
42 }
43 }
44 }
45 }
46 }
47
27 return rawJSON48 return rawJSON
28}49}
internal/translator/gemini/gemini/gemini_gemini_request.go+19−0
@@ -7,6 +7,7 @@ import ( · 6 unmodified lines
7 "bytes"7 "bytes"
8 "fmt"8 "fmt"
9 9
10 "github.com/router-for-me/CLIProxyAPI/v6/internal/util"
10 "github.com/tidwall/gjson"11 "github.com/tidwall/gjson"
11 "github.com/tidwall/sjson"12 "github.com/tidwall/sjson"
12)13)
@@ -24,6 +25,24 @@ func ConvertGeminiRequestToGemini(_ string, inputRawJSON []byte, _ bool) []byte · 11 unmodified lines
24 return rawJSON25 return rawJSON
25 }26 }
26 27
28 toolsResult := gjson.GetBytes(rawJSON, "tools")
29 if toolsResult.Exists() && toolsResult.IsArray() {
30 toolResults := toolsResult.Array()
31 for i := 0; i < len(toolResults); i++ {
32 functionDeclarationsResult := gjson.GetBytes(rawJSON, fmt.Sprintf("tools.%d.function_declarations", i))
33 if functionDeclarationsResult.Exists() && functionDeclarationsResult.IsArray() {
34 functionDeclarationsResults := functionDeclarationsResult.Array()
35 for j := 0; j < len(functionDeclarationsResults); j++ {
36 parametersResult := gjson.GetBytes(rawJSON, fmt.Sprintf("tools.%d.function_declarations.%d.parameters", i, j))
37 if parametersResult.Exists() {
38 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))
39 rawJSON = []byte(strJson)
40 }
41 }
42 }
43 }
44 }
45
27 // Walk contents and fix roles46 // Walk contents and fix roles
28 out := rawJSON47 out := rawJSON
29 prevRole := ""48 prevRole := ""
internal/translator/gemini/openai/chat-completions/gemini_openai_request.go+2−26
@@ -26,21 +26,6 @@ import ( · 25 unmodified lines
26// - []byte: The transformed request data in Gemini API format26// - []byte: The transformed request data in Gemini API format
27func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool) []byte {27func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool) []byte {
28 rawJSON := bytes.Clone(inputRawJSON)28 rawJSON := bytes.Clone(inputRawJSON)
29 var pathsToDelete []string
30 root := gjson.ParseBytes(rawJSON)
31 util.Walk(root, "", "additionalProperties", &pathsToDelete)
32 util.Walk(root, "", "$schema", &pathsToDelete)
33 util.Walk(root, "", "ref", &pathsToDelete)
34 util.Walk(root, "", "strict", &pathsToDelete)
35
36 var err error
37 for _, p := range pathsToDelete {
38 rawJSON, err = sjson.DeleteBytes(rawJSON, p)
39 if err != nil {
40 continue
41 }
42 }
43
44 // Base envelope29 // Base envelope
45 out := []byte(`{"contents":[],"generationConfig":{"thinkingConfig":{"include_thoughts":true}}}`)30 out := []byte(`{"contents":[],"generationConfig":{"thinkingConfig":{"include_thoughts":true}}}`)
46 31
@@ -290,22 +275,13 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool) · 243 unmodified lines
290 if t.Get("type").String() == "function" {275 if t.Get("type").String() == "function" {
291 fn := t.Get("function")276 fn := t.Get("function")
292 if fn.Exists() && fn.IsObject() {277 if fn.Exists() && fn.IsObject() {
293 out, _ = sjson.SetRawBytes(out, fdPath+".-1", []byte(fn.Raw))278 parametersJsonSchema, _ := util.RenameKey(fn.Raw, "parameters", "parametersJsonSchema")
279 out, _ = sjson.SetRawBytes(out, fdPath+".-1", []byte(parametersJsonSchema))
294 }280 }
295 }281 }
296 }282 }
297 }283 }
298 284
299 var pathsToType []string
300 root = gjson.ParseBytes(out)
301 util.Walk(root, "", "type", &pathsToType)
302 for _, p := range pathsToType {
303 typeResult := gjson.GetBytes(out, p)
304 if strings.ToLower(typeResult.String()) == "select" {
305 out, _ = sjson.SetBytes(out, p, "STRING")
306 }
307 }
308
309 return out285 return out
310}286}
311 287
internal/translator/gemini/openai/responses/gemini_openai-responses_request.go+3−4
@@ -150,7 +150,7 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte · 149 unmodified lines
150 if outputResult.IsObject() {150 if outputResult.IsObject() {
151 functionResponse, _ = sjson.SetRaw(functionResponse, "functionResponse.response.content", outputResult.String())151 functionResponse, _ = sjson.SetRaw(functionResponse, "functionResponse.response.content", outputResult.String())
152 } else {152 } else {
153 functionResponse, _ = sjson.Set(functionResponse, "functionResponse.response.content", outputResult.String())153 functionResponse, _ = sjson.Set(functionResponse, "functionResponse.response.content", output)
154 }154 }
155 }155 }
156 156
@@ -168,7 +168,7 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte · 11 unmodified lines
168 168
169 tools.ForEach(func(_, tool gjson.Result) bool {169 tools.ForEach(func(_, tool gjson.Result) bool {
170 if tool.Get("type").String() == "function" {170 if tool.Get("type").String() == "function" {
171 funcDecl := `{"name":"","description":"","parameters":{}}`171 funcDecl := `{"name":"","description":"","parametersJsonSchema":{}}`
172 172
173 if name := tool.Get("name"); name.Exists() {173 if name := tool.Get("name"); name.Exists() {
174 funcDecl, _ = sjson.Set(funcDecl, "name", name.String())174 funcDecl, _ = sjson.Set(funcDecl, "name", name.String())
@@ -192,7 +192,7 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte · 17 unmodified lines
192 }192 }
193 // Set the overall type to OBJECT193 // Set the overall type to OBJECT
194 cleaned, _ = sjson.Set(cleaned, "type", "OBJECT")194 cleaned, _ = sjson.Set(cleaned, "type", "OBJECT")
195 funcDecl, _ = sjson.SetRaw(funcDecl, "parameters", cleaned)195 funcDecl, _ = sjson.SetRaw(funcDecl, "parametersJsonSchema", cleaned)
196 }196 }
197 197
198 geminiTools, _ = sjson.SetRaw(geminiTools, "0.functionDeclarations.-1", funcDecl)198 geminiTools, _ = sjson.SetRaw(geminiTools, "0.functionDeclarations.-1", funcDecl)
@@ -261,6 +261,5 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte · 62 unmodified lines
261 out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingBudget", -1)261 out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingBudget", -1)
262 }262 }
263 }263 }
264
265 return []byte(out)264 return []byte(out)
266}265}
internal/util/translator.go+0−158
@@ -212,161 +212,3 @@ func FixJSON(input string) string { · 211 unmodified lines
212 212
213 return out.String()213 return out.String()
214}214}
215
216// SanitizeSchemaForGemini removes JSON Schema fields that are incompatible with Gemini API
217// to prevent "Proto field is not repeating, cannot start list" errors.
218//
219// Parameters:
220// - schemaJSON: The JSON schema string to sanitize
221//
222// Returns:
223// - string: The sanitized schema string
224// - error: An error if the operation fails
225//
226// This function removes the following incompatible fields:
227// - additionalProperties: Not supported in Gemini function declarations
228// - $schema: JSON Schema meta-schema identifier, not needed for API
229// - allOf/anyOf/oneOf: Union type constructs not supported
230// - exclusiveMinimum/exclusiveMaximum: Advanced validation constraints
231// - patternProperties: Advanced property pattern matching
232// - dependencies: Property dependencies not supported
233// - type arrays: Converts ["string", "null"] to just "string"
234func SanitizeSchemaForGemini(schemaJSON string) (string, error) {
235 // Remove top-level incompatible fields
236 fieldsToRemove := []string{
237 "additionalProperties",
238 "$schema",
239 "allOf",
240 "anyOf",
241 "oneOf",
242 "exclusiveMinimum",
243 "exclusiveMaximum",
244 "patternProperties",
245 "dependencies",
246 }
247
248 result := schemaJSON
249 var err error
250
251 for _, field := range fieldsToRemove {
252 result, err = sjson.Delete(result, field)
253 if err != nil {
254 continue // Continue even if deletion fails
255 }
256 }
257
258 // Handle type arrays by converting them to single types
259 result = sanitizeTypeFields(result)
260
261 // Recursively clean nested objects
262 result = cleanNestedSchemas(result)
263
264 return result, nil
265}
266
267// sanitizeTypeFields converts type arrays to single types for Gemini compatibility
268func sanitizeTypeFields(jsonStr string) string {
269 // Parse the JSON to find all "type" fields
270 parsed := gjson.Parse(jsonStr)
271 result := jsonStr
272
273 // Walk through all paths to find type fields
274 var typeFields []string
275 walkForTypeFields(parsed, "", &typeFields)
276
277 // Process each type field
278 for _, path := range typeFields {
279 typeValue := gjson.Get(result, path)
280 if typeValue.IsArray() {
281 // Convert array to single type (prioritize string, then others)
282 arr := typeValue.Array()
283 if len(arr) > 0 {
284 var preferredType string
285 for _, t := range arr {
286 typeStr := t.String()
287 if typeStr == "string" {
288 preferredType = "string"
289 break
290 } else if typeStr == "number" || typeStr == "integer" {
291 preferredType = typeStr
292 } else if preferredType == "" {
293 preferredType = typeStr
294 }
295 }
296 if preferredType != "" {
297 result, _ = sjson.Set(result, path, preferredType)
298 }
299 }
300 }
301 }
302
303 return result
304}
305
306// walkForTypeFields recursively finds all "type" field paths in the JSON
307func walkForTypeFields(value gjson.Result, path string, paths *[]string) {
308 switch value.Type {
309 case gjson.JSON:
310 value.ForEach(func(key, val gjson.Result) bool {
311 var childPath string
312 if path == "" {
313 childPath = key.String()
314 } else {
315 childPath = path + "." + key.String()
316 }
317 if key.String() == "type" {
318 *paths = append(*paths, childPath)
319 }
320 walkForTypeFields(val, childPath, paths)
321 return true
322 })
323 default:
324
325 }
326}
327
328// cleanNestedSchemas recursively removes incompatible fields from nested schema objects
329func cleanNestedSchemas(jsonStr string) string {
330 fieldsToRemove := []string{"allOf", "anyOf", "oneOf", "exclusiveMinimum", "exclusiveMaximum"}
331
332 // Find all nested paths that might contain these fields
333 var pathsToClean []string
334 parsed := gjson.Parse(jsonStr)
335 findNestedSchemaPaths(parsed, "", fieldsToRemove, &pathsToClean)
336
337 result := jsonStr
338 // Remove fields from all found paths
339 for _, path := range pathsToClean {
340 result, _ = sjson.Delete(result, path)
341 }
342
343 return result
344}
345
346// findNestedSchemaPaths recursively finds paths containing incompatible schema fields
347func findNestedSchemaPaths(value gjson.Result, path string, fieldsToFind []string, paths *[]string) {
348 switch value.Type {
349 case gjson.JSON:
350 value.ForEach(func(key, val gjson.Result) bool {
351 var childPath string
352 if path == "" {
353 childPath = key.String()
354 } else {
355 childPath = path + "." + key.String()
356 }
357
358 // Check if this key is one we want to remove
359 for _, field := range fieldsToFind {
360 if key.String() == field {
361 *paths = append(*paths, childPath)
362 break
363 }
364 }
365
366 findNestedSchemaPaths(val, childPath, fieldsToFind, paths)
367 return true
368 })
369 default:
370
371 }
372}