History

Fix Gemini function-calling INVALID_ARGUMENT by relaxing Gemini tool validation and cleaning schema

sowar1987 committed 9 months ago

Commit a2f8f591 parent c8884f58 files · +138 −29Browse files

Unified · Split

8 files
  1. internal/runtime/executor/antigravity_executor.go +18 −1
  2. internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go +5 −3
  3. internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response.go +5 −1
  4. internal/translator/gemini/gemini-cli/gemini_gemini-cli_request.go +0 −13
  5. internal/translator/gemini/openai/chat-completions/gemini_openai_request.go +5 −3
  6. internal/translator/gemini/openai/chat-completions/gemini_openai_response.go +5 −1
  7. internal/translator/gemini/openai/responses/gemini_openai-responses_request.go +5 −5
  8. internal/util/gemini_schema.go +95 −2
internal/runtime/executor/antigravity_executor.go+18−1
@@ -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 {
internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go+5−3
@@ -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 := 0265 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 }
internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response.go+5−1
@@ -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() {
internal/translator/gemini/gemini-cli/gemini_gemini-cli_request.go+0−13
@@ -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}
internal/translator/gemini/openai/chat-completions/gemini_openai_request.go+5−3
@@ -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 := 0271 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 }
internal/translator/gemini/openai/chat-completions/gemini_openai_response.go+5−1
@@ -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() {
internal/translator/gemini/openai/responses/gemini_openai-responses_request.go+5−5
@@ -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)
internal/util/gemini_schema.go+95−2
@@ -16,6 +16,93 @@ var gjsonPathKeyReplacer = strings.NewReplacer(".", "\\.", "*", "\\*", "?", "\\? · 15 unmodified lines
16// It handles unsupported keywords, type flattening, and schema simplification while preserving16// It handles unsupported keywords, type flattening, and schema simplification while preserving
17// semantic information as description hints.17// semantic information as description hints.
18func CleanJSONSchemaForAntigravity(jsonStr string) string {18func CleanJSONSchemaForAntigravity(jsonStr string) string {
19 return cleanJSONSchema(jsonStr, true)
20}
21
22func 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.
35func 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.
101func CleanJSONSchemaForGemini(jsonStr string) string {
102 return cleanJSONSchema(jsonStr, false)
103}
104
105func cleanJSONSchema(jsonStr string, addPlaceholder bool) string {
19 // Phase 1: Convert and add hints106 // 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: Cleanup119 // 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 jsonStr132 return jsonStr
40}133}