Unified · Split
| @@ -1214,6 +1214,17 @@ func (e *AntigravityExecutor) buildRequest(ctx context.Context, auth *cliproxyau · 1213 unmodified lines | ||
| 1214 | 1214 | // const->enum conversion, and flattening of types/anyOf. |
| 1215 | 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 | 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 | 1416 | template, _ = sjson.Set(template, "request.sessionId", generateStableSessionID(payload)) |
| 1406 | 1417 | |
| 1407 | 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 | 1427 | if strings.Contains(modelName, "claude") || strings.Contains(modelName, "gemini-3-pro-high") { |
| 1411 | 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 | 249 | fid := tc.Get("id").String() |
| 250 | 250 | fname := tc.Get("function.name").String() |
| 251 | 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 | 254 | node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs)) |
| 254 | 255 | node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiCLIFunctionThoughtSignature) |
| 255 | 256 | p++ |
| @@ -264,12 +265,13 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo · 8 unmodified lines | ||
| 264 | 265 | pp := 0 |
| 265 | 266 | for _, fid := range fIDs { |
| 266 | 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 | 270 | resp := toolResponses[fid] |
| 269 | 271 | if resp == "" { |
| 270 | 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 | 275 | pp++ |
| 274 | 276 | } |
| 275 | 277 | } |
| @@ -149,7 +149,11 @@ func ConvertCliResponseToOpenAI(_ context.Context, _ string, originalRequestRawJ · 148 unmodified lines | ||
| 149 | 149 | |
| 150 | 150 | functionCallTemplate := `{"id": "","index": 0,"type": "function","function": {"name": "","arguments": ""}}` |
| 151 | 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 | 157 | functionCallTemplate, _ = sjson.Set(functionCallTemplate, "index", functionCallIndex) |
| 154 | 158 | functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.name", fcName) |
| 155 | 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 | 50 | return common.AttachDefaultSafetySettings(rawJSON, "safetySettings") |
| 64 | 51 | } |
| @@ -255,7 +255,8 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool) · 254 unmodified lines | ||
| 255 | 255 | fid := tc.Get("id").String() |
| 256 | 256 | fname := tc.Get("function.name").String() |
| 257 | 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 | 260 | node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs)) |
| 260 | 261 | node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiFunctionThoughtSignature) |
| 261 | 262 | p++ |
| @@ -270,12 +271,13 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool) · 8 unmodified lines | ||
| 270 | 271 | pp := 0 |
| 271 | 272 | for _, fid := range fIDs { |
| 272 | 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 | 276 | resp := toolResponses[fid] |
| 275 | 277 | if resp == "" { |
| 276 | 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 | 281 | pp++ |
| 280 | 282 | } |
| 281 | 283 | } |
| @@ -187,7 +187,11 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR · 186 unmodified lines | ||
| 187 | 187 | |
| 188 | 188 | functionCallTemplate := `{"id": "","index": 0,"type": "function","function": {"name": "","arguments": ""}}` |
| 189 | 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 | 195 | functionCallTemplate, _ = sjson.Set(functionCallTemplate, "index", functionCallIndex) |
| 192 | 196 | functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.name", fcName) |
| 193 | 197 | if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() { |
| @@ -290,11 +290,11 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte · 289 unmodified lines | ||
| 290 | 290 | // Set the raw JSON output directly (preserves string encoding) |
| 291 | 291 | if outputRaw != "" && outputRaw != "null" { |
| 292 | 292 | output := gjson.Parse(outputRaw) |
| 293 | − if output.Type == gjson.JSON { | |
| 294 | − functionResponse, _ = sjson.SetRaw(functionResponse, "functionResponse.response.result", output.Raw) | |
| 295 | − } else { | |
| 296 | − functionResponse, _ = sjson.Set(functionResponse, "functionResponse.response.result", outputRaw) | |
| 297 | − } | |
| 293 | + if output.Type == gjson.JSON { | |
| 294 | + functionResponse, _ = sjson.SetRaw(functionResponse, "functionResponse.response.output", output.Raw) | |
| 295 | + } else { | |
| 296 | + functionResponse, _ = sjson.Set(functionResponse, "functionResponse.response.output", outputRaw) | |
| 297 | + } | |
| 298 | 298 | } |
| 299 | 299 | functionContent, _ = sjson.SetRaw(functionContent, "parts.-1", functionResponse) |
| 300 | 300 | out, _ = sjson.SetRaw(out, "contents.-1", functionContent) |
| @@ -16,6 +16,93 @@ var gjsonPathKeyReplacer = strings.NewReplacer(".", "\\.", "*", "\\*", "?", "\\? · 15 unmodified lines | ||
| 16 | 16 | // It handles unsupported keywords, type flattening, and schema simplification while preserving |
| 17 | 17 | // semantic information as description hints. |
| 18 | 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 | 106 | // Phase 1: Convert and add hints |
| 20 | 107 | jsonStr = convertRefsToHints(jsonStr) |
| 21 | 108 | jsonStr = convertConstToEnum(jsonStr) |
| @@ -31,10 +118,16 @@ func CleanJSONSchemaForAntigravity(jsonStr string) string { · 9 unmodified lines | ||
| 31 | 118 | |
| 32 | 119 | // Phase 3: Cleanup |
| 33 | 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 | 126 | jsonStr = cleanupRequiredFields(jsonStr) |
| 35 | − | |
| 36 | 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 | 132 | return jsonStr |
| 40 | 133 | } |