mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 13:00:52 +08:00
Fix Gemini function-calling INVALID_ARGUMENT by relaxing Gemini tool validation and cleaning schema
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user