mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-07-01 02:04:35 +08:00
Merge pull request #3554 from sususu98/fix/gemini-cli-request-schema-cleanup
fix: clean Gemini CLI request schemas
This commit is contained in:
@@ -141,6 +141,7 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth
|
|||||||
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
requestPath := helps.PayloadRequestPath(opts)
|
requestPath := helps.PayloadRequestPath(opts)
|
||||||
basePayload = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, "gemini", from.String(), "request", basePayload, originalTranslated, requestedModel, requestPath, opts.Headers)
|
basePayload = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, "gemini", from.String(), "request", basePayload, originalTranslated, requestedModel, requestPath, opts.Headers)
|
||||||
|
basePayload = cleanGeminiCLIRequestSchemas(basePayload)
|
||||||
|
|
||||||
action := "generateContent"
|
action := "generateContent"
|
||||||
if req.Metadata != nil {
|
if req.Metadata != nil {
|
||||||
@@ -297,6 +298,7 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut
|
|||||||
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
requestPath := helps.PayloadRequestPath(opts)
|
requestPath := helps.PayloadRequestPath(opts)
|
||||||
basePayload = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, "gemini", from.String(), "request", basePayload, originalTranslated, requestedModel, requestPath, opts.Headers)
|
basePayload = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, "gemini", from.String(), "request", basePayload, originalTranslated, requestedModel, requestPath, opts.Headers)
|
||||||
|
basePayload = cleanGeminiCLIRequestSchemas(basePayload)
|
||||||
|
|
||||||
projectID := resolveGeminiProjectID(auth)
|
projectID := resolveGeminiProjectID(auth)
|
||||||
|
|
||||||
@@ -530,6 +532,7 @@ func (e *GeminiCLIExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.
|
|||||||
payload = deleteJSONField(payload, "model")
|
payload = deleteJSONField(payload, "model")
|
||||||
payload = deleteJSONField(payload, "request.safetySettings")
|
payload = deleteJSONField(payload, "request.safetySettings")
|
||||||
payload = fixGeminiCLIImageAspectRatio(baseModel, payload)
|
payload = fixGeminiCLIImageAspectRatio(baseModel, payload)
|
||||||
|
payload = cleanGeminiCLIRequestSchemas(payload)
|
||||||
|
|
||||||
tok, errTok := tokenSource.Token()
|
tok, errTok := tokenSource.Token()
|
||||||
if errTok != nil {
|
if errTok != nil {
|
||||||
@@ -859,6 +862,65 @@ func deleteJSONField(body []byte, key string) []byte {
|
|||||||
return updated
|
return updated
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cleanGeminiCLIRequestSchemas(body []byte) []byte {
|
||||||
|
if len(body) == 0 {
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
hasTools := gjson.GetBytes(body, "request.tools.0").Exists()
|
||||||
|
hasResponseSchema := gjson.GetBytes(body, "request.generationConfig.responseSchema").Exists()
|
||||||
|
hasResponseJSONSchema := gjson.GetBytes(body, "request.generationConfig.responseJsonSchema").Exists()
|
||||||
|
if !hasTools && !hasResponseSchema && !hasResponseJSONSchema {
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
tools := gjson.GetBytes(body, "request.tools")
|
||||||
|
if tools.IsArray() {
|
||||||
|
for i, tool := range tools.Array() {
|
||||||
|
for _, declarationsKey := range []string{"function_declarations", "functionDeclarations"} {
|
||||||
|
funcDecls := tool.Get(declarationsKey)
|
||||||
|
if !funcDecls.IsArray() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for j, decl := range funcDecls.Array() {
|
||||||
|
for _, schemaKey := range []string{"parameters", "parametersJsonSchema"} {
|
||||||
|
params := decl.Get(schemaKey)
|
||||||
|
if !params.Exists() || !params.IsObject() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cleaned := util.CleanJSONSchemaForGemini(params.Raw)
|
||||||
|
path := fmt.Sprintf("request.tools.%d.%s.%d.%s", i, declarationsKey, j, schemaKey)
|
||||||
|
updated, errSet := sjson.SetRawBytes(body, path, []byte(cleaned))
|
||||||
|
if errSet != nil {
|
||||||
|
log.Errorf("gemini cli executor: failed to set cleaned schema at %s: %v", path, errSet)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
body = updated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, schemaPath := range []string{
|
||||||
|
"request.generationConfig.responseSchema",
|
||||||
|
"request.generationConfig.responseJsonSchema",
|
||||||
|
} {
|
||||||
|
responseSchema := gjson.GetBytes(body, schemaPath)
|
||||||
|
if !responseSchema.IsObject() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cleaned := util.CleanJSONSchemaForGemini(responseSchema.Raw)
|
||||||
|
updated, errSet := sjson.SetRawBytes(body, schemaPath, []byte(cleaned))
|
||||||
|
if errSet != nil {
|
||||||
|
log.Errorf("gemini cli executor: failed to set cleaned response schema at %s: %v", schemaPath, errSet)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
body = updated
|
||||||
|
}
|
||||||
|
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
func fixGeminiCLIImageAspectRatio(modelName string, rawJSON []byte) []byte {
|
func fixGeminiCLIImageAspectRatio(modelName string, rawJSON []byte) []byte {
|
||||||
if modelName == "gemini-2.5-flash-image-preview" {
|
if modelName == "gemini-2.5-flash-image-preview" {
|
||||||
aspectRatioResult := gjson.GetBytes(rawJSON, "request.generationConfig.imageConfig.aspectRatio")
|
aspectRatioResult := gjson.GetBytes(rawJSON, "request.generationConfig.imageConfig.aspectRatio")
|
||||||
|
|||||||
75
internal/runtime/executor/gemini_cli_executor_test.go
Normal file
75
internal/runtime/executor/gemini_cli_executor_test.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package executor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCleanGeminiCLIRequestSchemasFlattensFunctionDeclarationTypeArray(t *testing.T) {
|
||||||
|
input := []byte(`{
|
||||||
|
"request": {
|
||||||
|
"tools": [
|
||||||
|
{
|
||||||
|
"function_declarations": [
|
||||||
|
{
|
||||||
|
"name": "wecom_mcp",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"args": {
|
||||||
|
"description": "call args",
|
||||||
|
"type": ["string", "object"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"functionDeclarations": [
|
||||||
|
{
|
||||||
|
"name": "camel_tool",
|
||||||
|
"parametersJsonSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"value": {
|
||||||
|
"type": ["integer", "string"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nonSchema": {
|
||||||
|
"type": ["string", "object"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
|
||||||
|
out := cleanGeminiCLIRequestSchemas(input)
|
||||||
|
|
||||||
|
argsType := gjson.GetBytes(out, "request.tools.0.function_declarations.0.parameters.properties.args.type")
|
||||||
|
if argsType.String() != "string" {
|
||||||
|
t.Fatalf("args.type = %s, want string; body=%s", argsType.Raw, string(out))
|
||||||
|
}
|
||||||
|
argsDesc := gjson.GetBytes(out, "request.tools.0.function_declarations.0.parameters.properties.args.description").String()
|
||||||
|
if !strings.Contains(argsDesc, "Accepts: string | object") {
|
||||||
|
t.Fatalf("args.description = %q, want accepted type hint", argsDesc)
|
||||||
|
}
|
||||||
|
|
||||||
|
valueType := gjson.GetBytes(out, "request.tools.1.functionDeclarations.0.parametersJsonSchema.properties.value.type")
|
||||||
|
if valueType.String() != "integer" {
|
||||||
|
t.Fatalf("value.type = %s, want integer; body=%s", valueType.Raw, string(out))
|
||||||
|
}
|
||||||
|
valueDesc := gjson.GetBytes(out, "request.tools.1.functionDeclarations.0.parametersJsonSchema.properties.value.description").String()
|
||||||
|
if !strings.Contains(valueDesc, "Accepts: integer | string") {
|
||||||
|
t.Fatalf("value.description = %q, want accepted type hint", valueDesc)
|
||||||
|
}
|
||||||
|
|
||||||
|
if nonSchema := gjson.GetBytes(out, "request.nonSchema.type"); !nonSchema.IsArray() {
|
||||||
|
t.Fatalf("request.nonSchema.type should be preserved outside schema paths, got %s", nonSchema.Raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user