Unified · Split
| @@ -1214,6 +1214,17 @@ func (e *AntigravityExecutor) buildRequest(ctx context.Context, auth *cliproxyau · 1213 unmodified lines | |||
| 1214 | // const->enum conversion, and flattening of types/anyOf. | 1214 | // const->enum conversion, and flattening of types/anyOf. |
| 1215 | strJSON = util.CleanJSONSchemaForAntigravity(strJSON) | 1215 | strJSON = util.CleanJSONSchemaForAntigravity(strJSON) |
| 1216 | 1216 | ||
| 1217 | payload = []byte(strJSON) | ||
| 1218 | } else { | ||
| 1219 | strJSON := string(payload) | ||
| 1220 | paths := make([]string, 0) | ||
| 1221 | util.Walk(gjson.Parse(strJSON), "", "parametersJsonSchema", &paths) | ||
| 1222 | for _, p := range paths { | ||
| 1223 | strJSON, _ = util.RenameKey(strJSON, p, p[:len(p)-len("parametersJsonSchema")]+"parameters") | ||
| 1224 | } | ||
| 1225 | // Clean tool schemas for Gemini to remove unsupported JSON Schema keywords | ||
| 1226 | // without adding empty-schema placeholders. | ||
| 1227 | strJSON = util.CleanJSONSchemaForGemini(strJSON) | ||
| 1217 | payload = []byte(strJSON) | 1228 | payload = []byte(strJSON) |
| 1218 | } | 1229 | } |
| 1219 | 1230 | ||
| @@ -1405,7 +1416,13 @@ func geminiToAntigravity(modelName string, payload []byte, projectID string) []b · 185 unmodified lines | |||
| 1405 | template, _ = sjson.Set(template, "request.sessionId", generateStableSessionID(payload)) | 1416 | template, _ = sjson.Set(template, "request.sessionId", generateStableSessionID(payload)) |
| 1406 | 1417 | ||
| 1407 | template, _ = sjson.Delete(template, "request.safetySettings") | 1418 | template, _ = sjson.Delete(template, "request.safetySettings") |
| 1408 | // template, _ = sjson.Set(template, "request.toolConfig.functionCallingConfig.mode", "VALIDATED") | 1419 | if toolConfig := gjson.Get(template, "toolConfig"); toolConfig.Exists() && !gjson.Get(template, "request.toolConfig").Exists() { |
| 1420 | template, _ = sjson.SetRaw(template, "request.toolConfig", toolConfig.Raw) | ||
| 1421 | template, _ = sjson.Delete(template, "toolConfig") | ||
| 1422 | } | ||
| 1423 | if strings.Contains(modelName, "claude") { | ||
| 1424 | template, _ = sjson.Set(template, "request.toolConfig.functionCallingConfig.mode", "VALIDATED") | ||
| 1425 | } | ||
| 1409 | 1426 | ||
| 1410 | if strings.Contains(modelName, "claude") || strings.Contains(modelName, "gemini-3-pro-high") { | 1427 | if strings.Contains(modelName, "claude") || strings.Contains(modelName, "gemini-3-pro-high") { |
| 1411 | gjson.Get(template, "request.tools").ForEach(func(key, tool gjson.Result) bool { | 1428 | gjson.Get(template, "request.tools").ForEach(func(key, tool gjson.Result) bool { |
| @@ -249,7 +249,8 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo · 248 unmodified lines | |||
| 249 | fid := tc.Get("id").String() | 249 | fid := tc.Get("id").String() |
| 250 | fname := tc.Get("function.name").String() | 250 | fname := tc.Get("function.name").String() |
| 251 | fargs := tc.Get("function.arguments").String() | 251 | fargs := tc.Get("function.arguments").String() |
| 252 | node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname) | 252 | node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.id", fid) |
| 253 | node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname) | ||
| 253 | node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs)) | 254 | node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs)) |
| 254 | node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiCLIFunctionThoughtSignature) | 255 | node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiCLIFunctionThoughtSignature) |
| 255 | p++ | 256 | p++ |
| @@ -264,12 +265,13 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo · 8 unmodified lines | |||
| 264 | pp := 0 | 265 | pp := 0 |
| 265 | for _, fid := range fIDs { | 266 | for _, fid := range fIDs { |
| 266 | if name, ok := tcID2Name[fid]; ok { | 267 | if name, ok := tcID2Name[fid]; ok { |
| 267 | toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.name", name) | 268 | toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.id", fid) |
| 269 | toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.name", name) | ||
| 268 | resp := toolResponses[fid] | 270 | resp := toolResponses[fid] |
| 269 | if resp == "" { | 271 | if resp == "" { |
| 270 | resp = "{}" | 272 | resp = "{}" |
| 271 | } | 273 | } |
| 272 | toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.response.result", []byte(resp)) | 274 | toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.response.output", []byte(resp)) |
| 273 | pp++ | 275 | pp++ |
| 274 | } | 276 | } |
| 275 | } | 277 | } |
| @@ -149,7 +149,11 @@ func ConvertCliResponseToOpenAI(_ context.Context, _ string, originalRequestRawJ · 148 unmodified lines | |||
| 149 | 149 | ||
| 150 | functionCallTemplate := `{"id": "","index": 0,"type": "function","function": {"name": "","arguments": ""}}` | 150 | functionCallTemplate := `{"id": "","index": 0,"type": "function","function": {"name": "","arguments": ""}}` |
| 151 | fcName := functionCallResult.Get("name").String() | 151 | fcName := functionCallResult.Get("name").String() |
| 152 | functionCallTemplate, _ = sjson.Set(functionCallTemplate, "id", fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1))) | 152 | fcID := functionCallResult.Get("id").String() |
| 153 | if fcID == "" { | ||
| 154 | fcID = fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1)) | ||
| 155 | } | ||
| 156 | functionCallTemplate, _ = sjson.Set(functionCallTemplate, "id", fcID) | ||
| 153 | functionCallTemplate, _ = sjson.Set(functionCallTemplate, "index", functionCallIndex) | 157 | functionCallTemplate, _ = sjson.Set(functionCallTemplate, "index", functionCallIndex) |
| 154 | functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.name", fcName) | 158 | functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.name", fcName) |
| 155 | if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() { | 159 | if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() { |
| @@ -46,19 +46,6 @@ func ConvertGeminiCLIRequestToGemini(_ string, inputRawJSON []byte, _ bool) []by · 45 unmodified lines | |||
| 46 | } | 46 | } |
| 47 | } | 47 | } |
| 48 | 48 | ||
| 49 | gjson.GetBytes(rawJSON, "contents").ForEach(func(key, content gjson.Result) bool { | ||
| 50 | if content.Get("role").String() == "model" { | ||
| 51 | content.Get("parts").ForEach(func(partKey, part gjson.Result) bool { | ||
| 52 | if part.Get("functionCall").Exists() { | ||
| 53 | rawJSON, _ = sjson.SetBytes(rawJSON, fmt.Sprintf("contents.%d.parts.%d.thoughtSignature", key.Int(), partKey.Int()), "skip_thought_signature_validator") | ||
| 54 | } else if part.Get("thoughtSignature").Exists() { | ||
| 55 | rawJSON, _ = sjson.SetBytes(rawJSON, fmt.Sprintf("contents.%d.parts.%d.thoughtSignature", key.Int(), partKey.Int()), "skip_thought_signature_validator") | ||
| 56 | } | ||
| 57 | return true | ||
| 58 | }) | ||
| 59 | } | ||
| 60 | return true | ||
| 61 | }) | ||
| 62 | 49 | ||
| 63 | return common.AttachDefaultSafetySettings(rawJSON, "safetySettings") | 50 | return common.AttachDefaultSafetySettings(rawJSON, "safetySettings") |
| 64 | } | 51 | } |
| @@ -255,7 +255,8 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool) · 254 unmodified lines | |||
| 255 | fid := tc.Get("id").String() | 255 | fid := tc.Get("id").String() |
| 256 | fname := tc.Get("function.name").String() | 256 | fname := tc.Get("function.name").String() |
| 257 | fargs := tc.Get("function.arguments").String() | 257 | fargs := tc.Get("function.arguments").String() |
| 258 | node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname) | 258 | node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.id", fid) |
| 259 | node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname) | ||
| 259 | node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs)) | 260 | node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs)) |
| 260 | node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiFunctionThoughtSignature) | 261 | node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiFunctionThoughtSignature) |
| 261 | p++ | 262 | p++ |
| @@ -270,12 +271,13 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool) · 8 unmodified lines | |||
| 270 | pp := 0 | 271 | pp := 0 |
| 271 | for _, fid := range fIDs { | 272 | for _, fid := range fIDs { |
| 272 | if name, ok := tcID2Name[fid]; ok { | 273 | if name, ok := tcID2Name[fid]; ok { |
| 273 | toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.name", name) | 274 | toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.id", fid) |
| 275 | toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.name", name) | ||
| 274 | resp := toolResponses[fid] | 276 | resp := toolResponses[fid] |
| 275 | if resp == "" { | 277 | if resp == "" { |
| 276 | resp = "{}" | 278 | resp = "{}" |
| 277 | } | 279 | } |
| 278 | toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.response.result", []byte(resp)) | 280 | toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.response.output", []byte(resp)) |
| 279 | pp++ | 281 | pp++ |
| 280 | } | 282 | } |
| 281 | } | 283 | } |
| @@ -187,7 +187,11 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR · 186 unmodified lines | |||
| 187 | 187 | ||
| 188 | functionCallTemplate := `{"id": "","index": 0,"type": "function","function": {"name": "","arguments": ""}}` | 188 | functionCallTemplate := `{"id": "","index": 0,"type": "function","function": {"name": "","arguments": ""}}` |
| 189 | fcName := functionCallResult.Get("name").String() | 189 | fcName := functionCallResult.Get("name").String() |
| 190 | functionCallTemplate, _ = sjson.Set(functionCallTemplate, "id", fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1))) | 190 | fcID := functionCallResult.Get("id").String() |
| 191 | if fcID == "" { | ||
| 192 | fcID = fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1)) | ||
| 193 | } | ||
| 194 | functionCallTemplate, _ = sjson.Set(functionCallTemplate, "id", fcID) | ||
| 191 | functionCallTemplate, _ = sjson.Set(functionCallTemplate, "index", functionCallIndex) | 195 | functionCallTemplate, _ = sjson.Set(functionCallTemplate, "index", functionCallIndex) |
| 192 | functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.name", fcName) | 196 | functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.name", fcName) |
| 193 | if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() { | 197 | if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() { |
| @@ -290,11 +290,11 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte · 289 unmodified lines | |||
| 290 | // Set the raw JSON output directly (preserves string encoding) | 290 | // Set the raw JSON output directly (preserves string encoding) |
| 291 | if outputRaw != "" && outputRaw != "null" { | 291 | if outputRaw != "" && outputRaw != "null" { |
| 292 | output := gjson.Parse(outputRaw) | 292 | output := gjson.Parse(outputRaw) |
| 293 | if output.Type == gjson.JSON { | 293 | if output.Type == gjson.JSON { |
| 294 | functionResponse, _ = sjson.SetRaw(functionResponse, "functionResponse.response.result", output.Raw) | 294 | functionResponse, _ = sjson.SetRaw(functionResponse, "functionResponse.response.output", output.Raw) |
| 295 | } else { | 295 | } else { |
| 296 | functionResponse, _ = sjson.Set(functionResponse, "functionResponse.response.result", outputRaw) | 296 | functionResponse, _ = sjson.Set(functionResponse, "functionResponse.response.output", outputRaw) |
| 297 | } | 297 | } |
| 298 | } | 298 | } |
| 299 | functionContent, _ = sjson.SetRaw(functionContent, "parts.-1", functionResponse) | 299 | functionContent, _ = sjson.SetRaw(functionContent, "parts.-1", functionResponse) |
| 300 | out, _ = sjson.SetRaw(out, "contents.-1", functionContent) | 300 | out, _ = sjson.SetRaw(out, "contents.-1", functionContent) |
| @@ -16,6 +16,93 @@ var gjsonPathKeyReplacer = strings.NewReplacer(".", "\\.", "*", "\\*", "?", "\\? · 15 unmodified lines | |||
| 16 | // It handles unsupported keywords, type flattening, and schema simplification while preserving | 16 | // It handles unsupported keywords, type flattening, and schema simplification while preserving |
| 17 | // semantic information as description hints. | 17 | // semantic information as description hints. |
| 18 | func CleanJSONSchemaForAntigravity(jsonStr string) string { | 18 | func CleanJSONSchemaForAntigravity(jsonStr string) string { |
| 19 | return cleanJSONSchema(jsonStr, true) | ||
| 20 | } | ||
| 21 | |||
| 22 | func removeKeywords(jsonStr string, keywords []string) string { | ||
| 23 | for _, key := range keywords { | ||
| 24 | for _, p := range findPaths(jsonStr, key) { | ||
| 25 | if isPropertyDefinition(trimSuffix(p, "."+key)) { | ||
| 26 | continue | ||
| 27 | } | ||
| 28 | jsonStr, _ = sjson.Delete(jsonStr, p) | ||
| 29 | } | ||
| 30 | } | ||
| 31 | return jsonStr | ||
| 32 | } | ||
| 33 | |||
| 34 | // removePlaceholderFields removes placeholder-only properties ("_" and "reason") and their required entries. | ||
| 35 | func removePlaceholderFields(jsonStr string) string { | ||
| 36 | // Remove "_" placeholder properties. | ||
| 37 | paths := findPaths(jsonStr, "_") | ||
| 38 | sortByDepth(paths) | ||
| 39 | for _, p := range paths { | ||
| 40 | if !strings.HasSuffix(p, ".properties._") { | ||
| 41 | continue | ||
| 42 | } | ||
| 43 | jsonStr, _ = sjson.Delete(jsonStr, p) | ||
| 44 | parentPath := trimSuffix(p, ".properties._") | ||
| 45 | reqPath := joinPath(parentPath, "required") | ||
| 46 | req := gjson.Get(jsonStr, reqPath) | ||
| 47 | if req.IsArray() { | ||
| 48 | var filtered []string | ||
| 49 | for _, r := range req.Array() { | ||
| 50 | if r.String() != "_" { | ||
| 51 | filtered = append(filtered, r.String()) | ||
| 52 | } | ||
| 53 | } | ||
| 54 | if len(filtered) == 0 { | ||
| 55 | jsonStr, _ = sjson.Delete(jsonStr, reqPath) | ||
| 56 | } else { | ||
| 57 | jsonStr, _ = sjson.Set(jsonStr, reqPath, filtered) | ||
| 58 | } | ||
| 59 | } | ||
| 60 | } | ||
| 61 | |||
| 62 | // Remove placeholder-only "reason" objects. | ||
| 63 | reasonPaths := findPaths(jsonStr, "reason") | ||
| 64 | sortByDepth(reasonPaths) | ||
| 65 | for _, p := range reasonPaths { | ||
| 66 | if !strings.HasSuffix(p, ".properties.reason") { | ||
| 67 | continue | ||
| 68 | } | ||
| 69 | parentPath := trimSuffix(p, ".properties.reason") | ||
| 70 | props := gjson.Get(jsonStr, joinPath(parentPath, "properties")) | ||
| 71 | if !props.IsObject() || len(props.Map()) != 1 { | ||
| 72 | continue | ||
| 73 | } | ||
| 74 | desc := gjson.Get(jsonStr, p+".description").String() | ||
| 75 | if desc != "Brief explanation of why you are calling this tool" { | ||
| 76 | continue | ||
| 77 | } | ||
| 78 | jsonStr, _ = sjson.Delete(jsonStr, p) | ||
| 79 | reqPath := joinPath(parentPath, "required") | ||
| 80 | req := gjson.Get(jsonStr, reqPath) | ||
| 81 | if req.IsArray() { | ||
| 82 | var filtered []string | ||
| 83 | for _, r := range req.Array() { | ||
| 84 | if r.String() != "reason" { | ||
| 85 | filtered = append(filtered, r.String()) | ||
| 86 | } | ||
| 87 | } | ||
| 88 | if len(filtered) == 0 { | ||
| 89 | jsonStr, _ = sjson.Delete(jsonStr, reqPath) | ||
| 90 | } else { | ||
| 91 | jsonStr, _ = sjson.Set(jsonStr, reqPath, filtered) | ||
| 92 | } | ||
| 93 | } | ||
| 94 | } | ||
| 95 | |||
| 96 | return jsonStr | ||
| 97 | } | ||
| 98 | |||
| 99 | // CleanJSONSchemaForGemini transforms a JSON schema to be compatible with Gemini tool calling. | ||
| 100 | // It removes unsupported keywords and simplifies schemas, without adding empty-schema placeholders. | ||
| 101 | func CleanJSONSchemaForGemini(jsonStr string) string { | ||
| 102 | return cleanJSONSchema(jsonStr, false) | ||
| 103 | } | ||
| 104 | |||
| 105 | func cleanJSONSchema(jsonStr string, addPlaceholder bool) string { | ||
| 19 | // Phase 1: Convert and add hints | 106 | // Phase 1: Convert and add hints |
| 20 | jsonStr = convertRefsToHints(jsonStr) | 107 | jsonStr = convertRefsToHints(jsonStr) |
| 21 | jsonStr = convertConstToEnum(jsonStr) | 108 | jsonStr = convertConstToEnum(jsonStr) |
| @@ -31,10 +118,16 @@ func CleanJSONSchemaForAntigravity(jsonStr string) string { · 9 unmodified lines | |||
| 31 | 118 | ||
| 32 | // Phase 3: Cleanup | 119 | // Phase 3: Cleanup |
| 33 | jsonStr = removeUnsupportedKeywords(jsonStr) | 120 | jsonStr = removeUnsupportedKeywords(jsonStr) |
| 121 | if !addPlaceholder { | ||
| 122 | // Gemini schema cleanup: remove nullable/title and placeholder-only fields. | ||
| 123 | jsonStr = removeKeywords(jsonStr, []string{"nullable", "title"}) | ||
| 124 | jsonStr = removePlaceholderFields(jsonStr) | ||
| 125 | } | ||
| 34 | jsonStr = cleanupRequiredFields(jsonStr) | 126 | jsonStr = cleanupRequiredFields(jsonStr) |
| 35 | |||
| 36 | // Phase 4: Add placeholder for empty object schemas (Claude VALIDATED mode requirement) | 127 | // Phase 4: Add placeholder for empty object schemas (Claude VALIDATED mode requirement) |
| 37 | jsonStr = addEmptySchemaPlaceholder(jsonStr) | 128 | if addPlaceholder { |
| 129 | jsonStr = addEmptySchemaPlaceholder(jsonStr) | ||
| 130 | } | ||
| 38 | 131 | ||
| 39 | return jsonStr | 132 | return jsonStr |
| 40 | } | 133 | } |