From 70a8cf026f0047c79424a190661a66f5ddc058ae Mon Sep 17 00:00:00 2001 From: sususu98 Date: Tue, 26 May 2026 10:36:59 +0800 Subject: [PATCH 1/2] fix: clean gemini cli request schemas --- .../runtime/executor/gemini_cli_executor.go | 52 +++++++++++++ .../executor/gemini_cli_executor_test.go | 75 +++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 internal/runtime/executor/gemini_cli_executor_test.go diff --git a/internal/runtime/executor/gemini_cli_executor.go b/internal/runtime/executor/gemini_cli_executor.go index d9cf84567..af93a3f34 100644 --- a/internal/runtime/executor/gemini_cli_executor.go +++ b/internal/runtime/executor/gemini_cli_executor.go @@ -141,6 +141,7 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth requestedModel := helps.PayloadRequestedModel(opts, req.Model) requestPath := helps.PayloadRequestPath(opts) basePayload = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, "gemini", from.String(), "request", basePayload, originalTranslated, requestedModel, requestPath, opts.Headers) + basePayload = cleanGeminiCLIRequestSchemas(basePayload) action := "generateContent" if req.Metadata != nil { @@ -297,6 +298,7 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut requestedModel := helps.PayloadRequestedModel(opts, req.Model) requestPath := helps.PayloadRequestPath(opts) basePayload = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, "gemini", from.String(), "request", basePayload, originalTranslated, requestedModel, requestPath, opts.Headers) + basePayload = cleanGeminiCLIRequestSchemas(basePayload) projectID := resolveGeminiProjectID(auth) @@ -530,6 +532,7 @@ func (e *GeminiCLIExecutor) CountTokens(ctx context.Context, auth *cliproxyauth. payload = deleteJSONField(payload, "model") payload = deleteJSONField(payload, "request.safetySettings") payload = fixGeminiCLIImageAspectRatio(baseModel, payload) + payload = cleanGeminiCLIRequestSchemas(payload) tok, errTok := tokenSource.Token() if errTok != nil { @@ -859,6 +862,55 @@ func deleteJSONField(body []byte, key string) []byte { 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) + body, _ = sjson.SetRawBytes(body, path, []byte(cleaned)) + } + } + } + } + } + + for _, schemaPath := range []string{ + "request.generationConfig.responseSchema", + "request.generationConfig.responseJsonSchema", + } { + responseSchema := gjson.GetBytes(body, schemaPath) + if !responseSchema.IsObject() { + continue + } + cleaned := util.CleanJSONSchemaForGemini(responseSchema.Raw) + body, _ = sjson.SetRawBytes(body, schemaPath, []byte(cleaned)) + } + + return body +} + func fixGeminiCLIImageAspectRatio(modelName string, rawJSON []byte) []byte { if modelName == "gemini-2.5-flash-image-preview" { aspectRatioResult := gjson.GetBytes(rawJSON, "request.generationConfig.imageConfig.aspectRatio") diff --git a/internal/runtime/executor/gemini_cli_executor_test.go b/internal/runtime/executor/gemini_cli_executor_test.go new file mode 100644 index 000000000..b77134ed8 --- /dev/null +++ b/internal/runtime/executor/gemini_cli_executor_test.go @@ -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) + } +} From 4a85b6b97e19de77b0ffd57baa6af8dc8d20304d Mon Sep 17 00:00:00 2001 From: sususu98 Date: Tue, 26 May 2026 10:52:53 +0800 Subject: [PATCH 2/2] fix: log gemini cli schema cleanup errors --- internal/runtime/executor/gemini_cli_executor.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/internal/runtime/executor/gemini_cli_executor.go b/internal/runtime/executor/gemini_cli_executor.go index af93a3f34..95fcd9e0c 100644 --- a/internal/runtime/executor/gemini_cli_executor.go +++ b/internal/runtime/executor/gemini_cli_executor.go @@ -889,7 +889,12 @@ func cleanGeminiCLIRequestSchemas(body []byte) []byte { } cleaned := util.CleanJSONSchemaForGemini(params.Raw) path := fmt.Sprintf("request.tools.%d.%s.%d.%s", i, declarationsKey, j, schemaKey) - body, _ = sjson.SetRawBytes(body, path, []byte(cleaned)) + 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 } } } @@ -905,7 +910,12 @@ func cleanGeminiCLIRequestSchemas(body []byte) []byte { continue } cleaned := util.CleanJSONSchemaForGemini(responseSchema.Raw) - body, _ = sjson.SetRawBytes(body, schemaPath, []byte(cleaned)) + 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