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
12141214 // const->enum conversion, and flattening of types/anyOf.
12151215 strJSON = util.CleanJSONSchemaForAntigravity(strJSON)
12161216
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)
12171228 payload = []byte(strJSON)
12181229 }
12191230
@@ -1405,7 +1416,13 @@ func geminiToAntigravity(modelName string, payload []byte, projectID string) []b · 185 unmodified lines
14051416 template, _ = sjson.Set(template, "request.sessionId", generateStableSessionID(payload))
14061417
14071418 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+ }
14091426
14101427 if strings.Contains(modelName, "claude") || strings.Contains(modelName, "gemini-3-pro-high") {
14111428 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
249249 fid := tc.Get("id").String()
250250 fname := tc.Get("function.name").String()
251251 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)
253254 node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs))
254255 node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiCLIFunctionThoughtSignature)
255256 p++
@@ -264,12 +265,13 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo · 8 unmodified lines
264265 pp := 0
265266 for _, fid := range fIDs {
266267 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)
268270 resp := toolResponses[fid]
269271 if resp == "" {
270272 resp = "{}"
271273 }
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))
273275 pp++
274276 }
275277 }
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
149149
150150 functionCallTemplate := `{"id": "","index": 0,"type": "function","function": {"name": "","arguments": ""}}`
151151 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)
153157 functionCallTemplate, _ = sjson.Set(functionCallTemplate, "index", functionCallIndex)
154158 functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.name", fcName)
155159 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
4646 }
4747 }
4848
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− })
6249
6350 return common.AttachDefaultSafetySettings(rawJSON, "safetySettings")
6451 }
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
255255 fid := tc.Get("id").String()
256256 fname := tc.Get("function.name").String()
257257 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)
259260 node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs))
260261 node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiFunctionThoughtSignature)
261262 p++
@@ -270,12 +271,13 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool) · 8 unmodified lines
270271 pp := 0
271272 for _, fid := range fIDs {
272273 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)
274276 resp := toolResponses[fid]
275277 if resp == "" {
276278 resp = "{}"
277279 }
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))
279281 pp++
280282 }
281283 }
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
187187
188188 functionCallTemplate := `{"id": "","index": 0,"type": "function","function": {"name": "","arguments": ""}}`
189189 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)
191195 functionCallTemplate, _ = sjson.Set(functionCallTemplate, "index", functionCallIndex)
192196 functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.name", fcName)
193197 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
290290 // Set the raw JSON output directly (preserves string encoding)
291291 if outputRaw != "" && outputRaw != "null" {
292292 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+ }
298298 }
299299 functionContent, _ = sjson.SetRaw(functionContent, "parts.-1", functionResponse)
300300 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
1616 // It handles unsupported keywords, type flattening, and schema simplification while preserving
1717 // semantic information as description hints.
1818 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 {
19106 // Phase 1: Convert and add hints
20107 jsonStr = convertRefsToHints(jsonStr)
21108 jsonStr = convertConstToEnum(jsonStr)
@@ -31,10 +118,16 @@ func CleanJSONSchemaForAntigravity(jsonStr string) string { · 9 unmodified lines
31118
32119 // Phase 3: Cleanup
33120 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+ }
34126 jsonStr = cleanupRequiredFields(jsonStr)
35−
36127 // Phase 4: Add placeholder for empty object schemas (Claude VALIDATED mode requirement)
37− jsonStr = addEmptySchemaPlaceholder(jsonStr)
128+ if addPlaceholder {
129+ jsonStr = addEmptySchemaPlaceholder(jsonStr)
130+ }
38131
39132 return jsonStr
40133 }