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

This commit is contained in:
sowar1987
2026-01-21 09:09:40 +08:00
parent c8884f5e25
commit a2f8f59192
8 changed files with 138 additions and 29 deletions

View File

@@ -1214,6 +1214,17 @@ func (e *AntigravityExecutor) buildRequest(ctx context.Context, auth *cliproxyau
// const->enum conversion, and flattening of types/anyOf. // const->enum conversion, and flattening of types/anyOf.
strJSON = util.CleanJSONSchemaForAntigravity(strJSON) strJSON = util.CleanJSONSchemaForAntigravity(strJSON)
payload = []byte(strJSON)
} else {
strJSON := string(payload)
paths := make([]string, 0)
util.Walk(gjson.Parse(strJSON), "", "parametersJsonSchema", &paths)
for _, p := range paths {
strJSON, _ = util.RenameKey(strJSON, p, p[:len(p)-len("parametersJsonSchema")]+"parameters")
}
// Clean tool schemas for Gemini to remove unsupported JSON Schema keywords
// without adding empty-schema placeholders.
strJSON = util.CleanJSONSchemaForGemini(strJSON)
payload = []byte(strJSON) payload = []byte(strJSON)
} }
@@ -1405,7 +1416,13 @@ func geminiToAntigravity(modelName string, payload []byte, projectID string) []b
template, _ = sjson.Set(template, "request.sessionId", generateStableSessionID(payload)) template, _ = sjson.Set(template, "request.sessionId", generateStableSessionID(payload))
template, _ = sjson.Delete(template, "request.safetySettings") template, _ = sjson.Delete(template, "request.safetySettings")
// template, _ = sjson.Set(template, "request.toolConfig.functionCallingConfig.mode", "VALIDATED") if toolConfig := gjson.Get(template, "toolConfig"); toolConfig.Exists() && !gjson.Get(template, "request.toolConfig").Exists() {
template, _ = sjson.SetRaw(template, "request.toolConfig", toolConfig.Raw)
template, _ = sjson.Delete(template, "toolConfig")
}
if strings.Contains(modelName, "claude") {
template, _ = sjson.Set(template, "request.toolConfig.functionCallingConfig.mode", "VALIDATED")
}
if strings.Contains(modelName, "claude") || strings.Contains(modelName, "gemini-3-pro-high") { if strings.Contains(modelName, "claude") || strings.Contains(modelName, "gemini-3-pro-high") {
gjson.Get(template, "request.tools").ForEach(func(key, tool gjson.Result) bool { gjson.Get(template, "request.tools").ForEach(func(key, tool gjson.Result) bool {

View File

@@ -249,7 +249,8 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo
fid := tc.Get("id").String() fid := tc.Get("id").String()
fname := tc.Get("function.name").String() fname := tc.Get("function.name").String()
fargs := tc.Get("function.arguments").String() fargs := tc.Get("function.arguments").String()
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname) node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.id", fid)
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname)
node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs)) node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs))
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiCLIFunctionThoughtSignature) node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiCLIFunctionThoughtSignature)
p++ p++
@@ -264,12 +265,13 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo
pp := 0 pp := 0
for _, fid := range fIDs { for _, fid := range fIDs {
if name, ok := tcID2Name[fid]; ok { if name, ok := tcID2Name[fid]; ok {
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.name", name) toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.id", fid)
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.name", name)
resp := toolResponses[fid] resp := toolResponses[fid]
if resp == "" { if resp == "" {
resp = "{}" resp = "{}"
} }
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.response.result", []byte(resp)) toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.response.output", []byte(resp))
pp++ pp++
} }
} }

View File

@@ -149,7 +149,11 @@ func ConvertCliResponseToOpenAI(_ context.Context, _ string, originalRequestRawJ
functionCallTemplate := `{"id": "","index": 0,"type": "function","function": {"name": "","arguments": ""}}` functionCallTemplate := `{"id": "","index": 0,"type": "function","function": {"name": "","arguments": ""}}`
fcName := functionCallResult.Get("name").String() fcName := functionCallResult.Get("name").String()
functionCallTemplate, _ = sjson.Set(functionCallTemplate, "id", fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1))) fcID := functionCallResult.Get("id").String()
if fcID == "" {
fcID = fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1))
}
functionCallTemplate, _ = sjson.Set(functionCallTemplate, "id", fcID)
functionCallTemplate, _ = sjson.Set(functionCallTemplate, "index", functionCallIndex) functionCallTemplate, _ = sjson.Set(functionCallTemplate, "index", functionCallIndex)
functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.name", fcName) functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.name", fcName)
if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() { if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() {

View File

@@ -46,19 +46,6 @@ func ConvertGeminiCLIRequestToGemini(_ string, inputRawJSON []byte, _ bool) []by
} }
} }
gjson.GetBytes(rawJSON, "contents").ForEach(func(key, content gjson.Result) bool {
if content.Get("role").String() == "model" {
content.Get("parts").ForEach(func(partKey, part gjson.Result) bool {
if part.Get("functionCall").Exists() {
rawJSON, _ = sjson.SetBytes(rawJSON, fmt.Sprintf("contents.%d.parts.%d.thoughtSignature", key.Int(), partKey.Int()), "skip_thought_signature_validator")
} else if part.Get("thoughtSignature").Exists() {
rawJSON, _ = sjson.SetBytes(rawJSON, fmt.Sprintf("contents.%d.parts.%d.thoughtSignature", key.Int(), partKey.Int()), "skip_thought_signature_validator")
}
return true
})
}
return true
})
return common.AttachDefaultSafetySettings(rawJSON, "safetySettings") return common.AttachDefaultSafetySettings(rawJSON, "safetySettings")
} }

View File

@@ -255,7 +255,8 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
fid := tc.Get("id").String() fid := tc.Get("id").String()
fname := tc.Get("function.name").String() fname := tc.Get("function.name").String()
fargs := tc.Get("function.arguments").String() fargs := tc.Get("function.arguments").String()
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname) node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.id", fid)
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname)
node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs)) node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs))
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiFunctionThoughtSignature) node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiFunctionThoughtSignature)
p++ p++
@@ -270,12 +271,13 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
pp := 0 pp := 0
for _, fid := range fIDs { for _, fid := range fIDs {
if name, ok := tcID2Name[fid]; ok { if name, ok := tcID2Name[fid]; ok {
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.name", name) toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.id", fid)
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.name", name)
resp := toolResponses[fid] resp := toolResponses[fid]
if resp == "" { if resp == "" {
resp = "{}" resp = "{}"
} }
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.response.result", []byte(resp)) toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.response.output", []byte(resp))
pp++ pp++
} }
} }

View File

@@ -187,7 +187,11 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR
functionCallTemplate := `{"id": "","index": 0,"type": "function","function": {"name": "","arguments": ""}}` functionCallTemplate := `{"id": "","index": 0,"type": "function","function": {"name": "","arguments": ""}}`
fcName := functionCallResult.Get("name").String() fcName := functionCallResult.Get("name").String()
functionCallTemplate, _ = sjson.Set(functionCallTemplate, "id", fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1))) fcID := functionCallResult.Get("id").String()
if fcID == "" {
fcID = fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1))
}
functionCallTemplate, _ = sjson.Set(functionCallTemplate, "id", fcID)
functionCallTemplate, _ = sjson.Set(functionCallTemplate, "index", functionCallIndex) functionCallTemplate, _ = sjson.Set(functionCallTemplate, "index", functionCallIndex)
functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.name", fcName) functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.name", fcName)
if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() { if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() {

View File

@@ -290,11 +290,11 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte
// Set the raw JSON output directly (preserves string encoding) // Set the raw JSON output directly (preserves string encoding)
if outputRaw != "" && outputRaw != "null" { if outputRaw != "" && outputRaw != "null" {
output := gjson.Parse(outputRaw) output := gjson.Parse(outputRaw)
if output.Type == gjson.JSON { if output.Type == gjson.JSON {
functionResponse, _ = sjson.SetRaw(functionResponse, "functionResponse.response.result", output.Raw) functionResponse, _ = sjson.SetRaw(functionResponse, "functionResponse.response.output", output.Raw)
} else { } else {
functionResponse, _ = sjson.Set(functionResponse, "functionResponse.response.result", outputRaw) functionResponse, _ = sjson.Set(functionResponse, "functionResponse.response.output", outputRaw)
} }
} }
functionContent, _ = sjson.SetRaw(functionContent, "parts.-1", functionResponse) functionContent, _ = sjson.SetRaw(functionContent, "parts.-1", functionResponse)
out, _ = sjson.SetRaw(out, "contents.-1", functionContent) out, _ = sjson.SetRaw(out, "contents.-1", functionContent)

View File

@@ -16,6 +16,93 @@ var gjsonPathKeyReplacer = strings.NewReplacer(".", "\\.", "*", "\\*", "?", "\\?
// It handles unsupported keywords, type flattening, and schema simplification while preserving // It handles unsupported keywords, type flattening, and schema simplification while preserving
// semantic information as description hints. // semantic information as description hints.
func CleanJSONSchemaForAntigravity(jsonStr string) string { func CleanJSONSchemaForAntigravity(jsonStr string) string {
return cleanJSONSchema(jsonStr, true)
}
func removeKeywords(jsonStr string, keywords []string) string {
for _, key := range keywords {
for _, p := range findPaths(jsonStr, key) {
if isPropertyDefinition(trimSuffix(p, "."+key)) {
continue
}
jsonStr, _ = sjson.Delete(jsonStr, p)
}
}
return jsonStr
}
// removePlaceholderFields removes placeholder-only properties ("_" and "reason") and their required entries.
func removePlaceholderFields(jsonStr string) string {
// Remove "_" placeholder properties.
paths := findPaths(jsonStr, "_")
sortByDepth(paths)
for _, p := range paths {
if !strings.HasSuffix(p, ".properties._") {
continue
}
jsonStr, _ = sjson.Delete(jsonStr, p)
parentPath := trimSuffix(p, ".properties._")
reqPath := joinPath(parentPath, "required")
req := gjson.Get(jsonStr, reqPath)
if req.IsArray() {
var filtered []string
for _, r := range req.Array() {
if r.String() != "_" {
filtered = append(filtered, r.String())
}
}
if len(filtered) == 0 {
jsonStr, _ = sjson.Delete(jsonStr, reqPath)
} else {
jsonStr, _ = sjson.Set(jsonStr, reqPath, filtered)
}
}
}
// Remove placeholder-only "reason" objects.
reasonPaths := findPaths(jsonStr, "reason")
sortByDepth(reasonPaths)
for _, p := range reasonPaths {
if !strings.HasSuffix(p, ".properties.reason") {
continue
}
parentPath := trimSuffix(p, ".properties.reason")
props := gjson.Get(jsonStr, joinPath(parentPath, "properties"))
if !props.IsObject() || len(props.Map()) != 1 {
continue
}
desc := gjson.Get(jsonStr, p+".description").String()
if desc != "Brief explanation of why you are calling this tool" {
continue
}
jsonStr, _ = sjson.Delete(jsonStr, p)
reqPath := joinPath(parentPath, "required")
req := gjson.Get(jsonStr, reqPath)
if req.IsArray() {
var filtered []string
for _, r := range req.Array() {
if r.String() != "reason" {
filtered = append(filtered, r.String())
}
}
if len(filtered) == 0 {
jsonStr, _ = sjson.Delete(jsonStr, reqPath)
} else {
jsonStr, _ = sjson.Set(jsonStr, reqPath, filtered)
}
}
}
return jsonStr
}
// CleanJSONSchemaForGemini transforms a JSON schema to be compatible with Gemini tool calling.
// It removes unsupported keywords and simplifies schemas, without adding empty-schema placeholders.
func CleanJSONSchemaForGemini(jsonStr string) string {
return cleanJSONSchema(jsonStr, false)
}
func cleanJSONSchema(jsonStr string, addPlaceholder bool) string {
// Phase 1: Convert and add hints // Phase 1: Convert and add hints
jsonStr = convertRefsToHints(jsonStr) jsonStr = convertRefsToHints(jsonStr)
jsonStr = convertConstToEnum(jsonStr) jsonStr = convertConstToEnum(jsonStr)
@@ -31,10 +118,16 @@ func CleanJSONSchemaForAntigravity(jsonStr string) string {
// Phase 3: Cleanup // Phase 3: Cleanup
jsonStr = removeUnsupportedKeywords(jsonStr) jsonStr = removeUnsupportedKeywords(jsonStr)
if !addPlaceholder {
// Gemini schema cleanup: remove nullable/title and placeholder-only fields.
jsonStr = removeKeywords(jsonStr, []string{"nullable", "title"})
jsonStr = removePlaceholderFields(jsonStr)
}
jsonStr = cleanupRequiredFields(jsonStr) jsonStr = cleanupRequiredFields(jsonStr)
// Phase 4: Add placeholder for empty object schemas (Claude VALIDATED mode requirement) // Phase 4: Add placeholder for empty object schemas (Claude VALIDATED mode requirement)
jsonStr = addEmptySchemaPlaceholder(jsonStr) if addPlaceholder {
jsonStr = addEmptySchemaPlaceholder(jsonStr)
}
return jsonStr return jsonStr
} }