Unified · Split
| @@ -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 format | 35 | // - []byte: The transformed request data in Gemini CLI API format |
| 37 | func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) []byte { | 36 | func 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 instruction | 40 | // 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]any | 88 | 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.Raw | 125 | 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 any | 128 | 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 | } |
| @@ -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 rawJSON | 100 | return rawJSON |
| 82 | } | 101 | } |
| 83 | 102 | ||
| @@ -26,21 +26,6 @@ import ( · 25 unmodified lines | |||
| 26 | // - []byte: The transformed request data in Gemini CLI API format | 26 | // - []byte: The transformed request data in Gemini CLI API format |
| 27 | func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bool) []byte { | 27 | func 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 envelope | 29 | // 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 out | 260 | return out |
| 285 | } | 261 | } |
| 286 | 262 | ||
File renamed without changes.
File renamed without changes.
File renamed without changes.
| @@ -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. |
| 30 | func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) []byte { | 29 | func 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 instruction | 33 | // 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]any | 81 | 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.Raw | 118 | 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 any | 121 | 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 | } |
| @@ -7,7 +7,9 @@ package geminiCLI · 6 unmodified lines | |||
| 7 | 7 | ||
| 8 | import ( | 8 | import ( |
| 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 rawJSON | 48 | return rawJSON |
| 28 | } | 49 | } |
| @@ -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 rawJSON | 25 | 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 roles | 46 | // Walk contents and fix roles |
| 28 | out := rawJSON | 47 | out := rawJSON |
| 29 | prevRole := "" | 48 | prevRole := "" |
| @@ -26,21 +26,6 @@ import ( · 25 unmodified lines | |||
| 26 | // - []byte: The transformed request data in Gemini API format | 26 | // - []byte: The transformed request data in Gemini API format |
| 27 | func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool) []byte { | 27 | func 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 envelope | 29 | // 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 out | 285 | return out |
| 310 | } | 286 | } |
| 311 | 287 | ||
| @@ -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 OBJECT | 193 | // 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 | } |
| @@ -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" | ||
| 234 | func 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 | ||
| 268 | func 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 | ||
| 307 | func 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 | ||
| 329 | func 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 | ||
| 347 | func 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 | } | ||