Unified · Split
| @@ -11,7 +11,6 @@ import ( · 10 unmodified lines | ||
| 11 | 11 | "strings" |
| 12 | 12 | |
| 13 | 13 | client "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" |
| 14 | − "github.com/router-for-me/CLIProxyAPI/v6/internal/util" | |
| 15 | 14 | "github.com/tidwall/gjson" |
| 16 | 15 | "github.com/tidwall/sjson" |
| 17 | 16 | ) |
| @@ -36,18 +35,6 @@ import ( · 18 unmodified lines | ||
| 36 | 35 | // - []byte: The transformed request data in Gemini CLI API format |
| 37 | 36 | func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) []byte { |
| 38 | 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 | 38 | rawJSON = bytes.Replace(rawJSON, []byte(`"url":{"type":"string","format":"uri",`), []byte(`"url":{"type":"string",`), -1) |
| 52 | 39 | |
| 53 | 40 | // system instruction |
| @@ -99,7 +86,7 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) [] · 45 unmodified lines | ||
| 99 | 86 | functionName := contentResult.Get("name").String() |
| 100 | 87 | functionArgs := contentResult.Get("input").String() |
| 101 | 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 | 90 | clientContent.Parts = append(clientContent.Parts, client.Part{FunctionCall: &client.FunctionCall{Name: functionName, Args: args}}) |
| 104 | 91 | } |
| 105 | 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 | 123 | inputSchemaResult := toolResult.Get("input_schema") |
| 137 | 124 | if inputSchemaResult.Exists() && inputSchemaResult.IsObject() { |
| 138 | 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 | 126 | tool, _ := sjson.Delete(toolResult.Raw, "input_schema") |
| 148 | − tool, _ = sjson.SetRaw(tool, "parameters", inputSchema) | |
| 127 | + tool, _ = sjson.SetRaw(tool, "parametersJsonSchema", inputSchema) | |
| 149 | 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 | 130 | tools[0].FunctionDeclarations = append(tools[0].FunctionDeclarations, toolDeclaration) |
| 152 | 131 | } |
| 153 | 132 | } |
| @@ -10,6 +10,7 @@ import ( · 9 unmodified lines | ||
| 10 | 10 | "encoding/json" |
| 11 | 11 | "fmt" |
| 12 | 12 | |
| 13 | + "github.com/router-for-me/CLIProxyAPI/v6/internal/util" | |
| 13 | 14 | log "github.com/sirupsen/logrus" |
| 14 | 15 | "github.com/tidwall/gjson" |
| 15 | 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 | 100 | return rawJSON |
| 82 | 101 | } |
| 83 | 102 | |
| @@ -26,21 +26,6 @@ import ( · 25 unmodified lines | ||
| 26 | 26 | // - []byte: The transformed request data in Gemini CLI API format |
| 27 | 27 | func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bool) []byte { |
| 28 | 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 | 29 | // Base envelope |
| 45 | 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 | 250 | if t.Get("type").String() == "function" { |
| 266 | 251 | fn := t.Get("function") |
| 267 | 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 | 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 | 11 | "strings" |
| 12 | 12 | |
| 13 | 13 | client "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" |
| 14 | − "github.com/router-for-me/CLIProxyAPI/v6/internal/util" | |
| 15 | 14 | "github.com/tidwall/gjson" |
| 16 | 15 | "github.com/tidwall/sjson" |
| 17 | 16 | ) |
| @@ -29,18 +28,6 @@ import ( · 11 unmodified lines | ||
| 29 | 28 | // - []byte: The transformed request in Gemini CLI format. |
| 30 | 29 | func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) []byte { |
| 31 | 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 | 31 | rawJSON = bytes.Replace(rawJSON, []byte(`"url":{"type":"string","format":"uri",`), []byte(`"url":{"type":"string",`), -1) |
| 45 | 32 | |
| 46 | 33 | // system instruction |
| @@ -92,7 +79,7 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) · 45 unmodified lines | ||
| 92 | 79 | functionName := contentResult.Get("name").String() |
| 93 | 80 | functionArgs := contentResult.Get("input").String() |
| 94 | 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 | 83 | clientContent.Parts = append(clientContent.Parts, client.Part{FunctionCall: &client.FunctionCall{Name: functionName, Args: args}}) |
| 97 | 84 | } |
| 98 | 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 | 116 | inputSchemaResult := toolResult.Get("input_schema") |
| 130 | 117 | if inputSchemaResult.Exists() && inputSchemaResult.IsObject() { |
| 131 | 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 | 119 | tool, _ := sjson.Delete(toolResult.Raw, "input_schema") |
| 141 | − tool, _ = sjson.SetRaw(tool, "parameters", inputSchema) | |
| 120 | + tool, _ = sjson.SetRaw(tool, "parametersJsonSchema", inputSchema) | |
| 142 | 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 | 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 | 8 | import ( |
| 9 | 9 | "bytes" |
| 10 | + "fmt" | |
| 10 | 11 | |
| 12 | + "github.com/router-for-me/CLIProxyAPI/v6/internal/util" | |
| 11 | 13 | "github.com/tidwall/gjson" |
| 12 | 14 | "github.com/tidwall/sjson" |
| 13 | 15 | ) |
| @@ -24,5 +26,24 @@ func ConvertGeminiCLIRequestToGemini(_ string, inputRawJSON []byte, _ bool) []by · 10 unmodified lines | ||
| 24 | 26 | rawJSON, _ = sjson.SetRawBytes(rawJSON, "system_instruction", []byte(gjson.GetBytes(rawJSON, "systemInstruction").Raw)) |
| 25 | 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 | 48 | return rawJSON |
| 28 | 49 | } |
| @@ -7,6 +7,7 @@ import ( · 6 unmodified lines | ||
| 7 | 7 | "bytes" |
| 8 | 8 | "fmt" |
| 9 | 9 | |
| 10 | + "github.com/router-for-me/CLIProxyAPI/v6/internal/util" | |
| 10 | 11 | "github.com/tidwall/gjson" |
| 11 | 12 | "github.com/tidwall/sjson" |
| 12 | 13 | ) |
| @@ -24,6 +25,24 @@ func ConvertGeminiRequestToGemini(_ string, inputRawJSON []byte, _ bool) []byte · 11 unmodified lines | ||
| 24 | 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 | 46 | // Walk contents and fix roles |
| 28 | 47 | out := rawJSON |
| 29 | 48 | prevRole := "" |
| @@ -26,21 +26,6 @@ import ( · 25 unmodified lines | ||
| 26 | 26 | // - []byte: The transformed request data in Gemini API format |
| 27 | 27 | func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool) []byte { |
| 28 | 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 | 29 | // Base envelope |
| 45 | 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 | 275 | if t.Get("type").String() == "function" { |
| 291 | 276 | fn := t.Get("function") |
| 292 | 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 | 285 | return out |
| 310 | 286 | } |
| 311 | 287 | |
| @@ -150,7 +150,7 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte · 149 unmodified lines | ||
| 150 | 150 | if outputResult.IsObject() { |
| 151 | 151 | functionResponse, _ = sjson.SetRaw(functionResponse, "functionResponse.response.content", outputResult.String()) |
| 152 | 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 | 169 | tools.ForEach(func(_, tool gjson.Result) bool { |
| 170 | 170 | if tool.Get("type").String() == "function" { |
| 171 | − funcDecl := `{"name":"","description":"","parameters":{}}` | |
| 171 | + funcDecl := `{"name":"","description":"","parametersJsonSchema":{}}` | |
| 172 | 172 | |
| 173 | 173 | if name := tool.Get("name"); name.Exists() { |
| 174 | 174 | funcDecl, _ = sjson.Set(funcDecl, "name", name.String()) |
| @@ -192,7 +192,7 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte · 17 unmodified lines | ||
| 192 | 192 | } |
| 193 | 193 | // Set the overall type to OBJECT |
| 194 | 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 | 198 | geminiTools, _ = sjson.SetRaw(geminiTools, "0.functionDeclarations.-1", funcDecl) |
| @@ -261,6 +261,5 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte · 62 unmodified lines | ||
| 261 | 261 | out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingBudget", -1) |
| 262 | 262 | } |
| 263 | 263 | } |
| 264 | − | |
| 265 | 264 | return []byte(out) |
| 266 | 265 | } |
| @@ -212,161 +212,3 @@ func FixJSON(input string) string { · 211 unmodified lines | ||
| 212 | 212 | |
| 213 | 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 | −} | |