History

Merge pull request #3554 from sususu98/fix/gemini-cli-request-schema-cleanup

sususu98 committed 5 months ago

Commit b5959c32 parents 167edfe 4a85b6b2 files · +137 −0Browse files

Unified · Split

2 files
  1. internal/runtime/executor/gemini_cli_executor.go +62 −0
  2. internal/runtime/executor/gemini_cli_executor_test.go +75 −0
internal/runtime/executor/gemini_cli_executor.go+62−0
@@ -141,6 +141,7 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth · 140 unmodified lines
141141 requestedModel := helps.PayloadRequestedModel(opts, req.Model)
142142 requestPath := helps.PayloadRequestPath(opts)
143143 basePayload = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, "gemini", from.String(), "request", basePayload, originalTranslated, requestedModel, requestPath, opts.Headers)
144+ basePayload = cleanGeminiCLIRequestSchemas(basePayload)
144145
145146 action := "generateContent"
146147 if req.Metadata != nil {
@@ -297,6 +298,7 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut · 150 unmodified lines
297298 requestedModel := helps.PayloadRequestedModel(opts, req.Model)
298299 requestPath := helps.PayloadRequestPath(opts)
299300 basePayload = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, "gemini", from.String(), "request", basePayload, originalTranslated, requestedModel, requestPath, opts.Headers)
301+ basePayload = cleanGeminiCLIRequestSchemas(basePayload)
300302
301303 projectID := resolveGeminiProjectID(auth)
302304
@@ -530,6 +532,7 @@ func (e *GeminiCLIExecutor) CountTokens(ctx context.Context, auth *cliproxyauth. · 227 unmodified lines
530532 payload = deleteJSONField(payload, "model")
531533 payload = deleteJSONField(payload, "request.safetySettings")
532534 payload = fixGeminiCLIImageAspectRatio(baseModel, payload)
535+ payload = cleanGeminiCLIRequestSchemas(payload)
533536
534537 tok, errTok := tokenSource.Token()
535538 if errTok != nil {
@@ -859,6 +862,65 @@ func deleteJSONField(body []byte, key string) []byte { · 323 unmodified lines
859862 return updated
860863 }
861864
865+func cleanGeminiCLIRequestSchemas(body []byte) []byte {
866+ if len(body) == 0 {
867+ return body
868+ }
869+ hasTools := gjson.GetBytes(body, "request.tools.0").Exists()
870+ hasResponseSchema := gjson.GetBytes(body, "request.generationConfig.responseSchema").Exists()
871+ hasResponseJSONSchema := gjson.GetBytes(body, "request.generationConfig.responseJsonSchema").Exists()
872+ if !hasTools && !hasResponseSchema && !hasResponseJSONSchema {
873+ return body
874+ }
875+
876+ tools := gjson.GetBytes(body, "request.tools")
877+ if tools.IsArray() {
878+ for i, tool := range tools.Array() {
879+ for _, declarationsKey := range []string{"function_declarations", "functionDeclarations"} {
880+ funcDecls := tool.Get(declarationsKey)
881+ if !funcDecls.IsArray() {
882+ continue
883+ }
884+ for j, decl := range funcDecls.Array() {
885+ for _, schemaKey := range []string{"parameters", "parametersJsonSchema"} {
886+ params := decl.Get(schemaKey)
887+ if !params.Exists() || !params.IsObject() {
888+ continue
889+ }
890+ cleaned := util.CleanJSONSchemaForGemini(params.Raw)
891+ path := fmt.Sprintf("request.tools.%d.%s.%d.%s", i, declarationsKey, j, schemaKey)
892+ updated, errSet := sjson.SetRawBytes(body, path, []byte(cleaned))
893+ if errSet != nil {
894+ log.Errorf("gemini cli executor: failed to set cleaned schema at %s: %v", path, errSet)
895+ continue
896+ }
897+ body = updated
898+ }
899+ }
900+ }
901+ }
902+ }
903+
904+ for _, schemaPath := range []string{
905+ "request.generationConfig.responseSchema",
906+ "request.generationConfig.responseJsonSchema",
907+ } {
908+ responseSchema := gjson.GetBytes(body, schemaPath)
909+ if !responseSchema.IsObject() {
910+ continue
911+ }
912+ cleaned := util.CleanJSONSchemaForGemini(responseSchema.Raw)
913+ updated, errSet := sjson.SetRawBytes(body, schemaPath, []byte(cleaned))
914+ if errSet != nil {
915+ log.Errorf("gemini cli executor: failed to set cleaned response schema at %s: %v", schemaPath, errSet)
916+ continue
917+ }
918+ body = updated
919+ }
920+
921+ return body
922+}
923+
862924 func fixGeminiCLIImageAspectRatio(modelName string, rawJSON []byte) []byte {
863925 if modelName == "gemini-2.5-flash-image-preview" {
864926 aspectRatioResult := gjson.GetBytes(rawJSON, "request.generationConfig.imageConfig.aspectRatio")
internal/runtime/executor/gemini_cli_executor_test.go+75−0
@@ -0,0 +1,75 @@
1+package executor
2+
3+import (
4+ "strings"
5+ "testing"
6+
7+ "github.com/tidwall/gjson"
8+)
9+
10+func TestCleanGeminiCLIRequestSchemasFlattensFunctionDeclarationTypeArray(t *testing.T) {
11+ input := []byte(`{
12+ "request": {
13+ "tools": [
14+ {
15+ "function_declarations": [
16+ {
17+ "name": "wecom_mcp",
18+ "parameters": {
19+ "type": "object",
20+ "properties": {
21+ "args": {
22+ "description": "call args",
23+ "type": ["string", "object"]
24+ }
25+ }
26+ }
27+ }
28+ ]
29+ },
30+ {
31+ "functionDeclarations": [
32+ {
33+ "name": "camel_tool",
34+ "parametersJsonSchema": {
35+ "type": "object",
36+ "properties": {
37+ "value": {
38+ "type": ["integer", "string"]
39+ }
40+ }
41+ }
42+ }
43+ ]
44+ }
45+ ],
46+ "nonSchema": {
47+ "type": ["string", "object"]
48+ }
49+ }
50+ }`)
51+
52+ out := cleanGeminiCLIRequestSchemas(input)
53+
54+ argsType := gjson.GetBytes(out, "request.tools.0.function_declarations.0.parameters.properties.args.type")
55+ if argsType.String() != "string" {
56+ t.Fatalf("args.type = %s, want string; body=%s", argsType.Raw, string(out))
57+ }
58+ argsDesc := gjson.GetBytes(out, "request.tools.0.function_declarations.0.parameters.properties.args.description").String()
59+ if !strings.Contains(argsDesc, "Accepts: string | object") {
60+ t.Fatalf("args.description = %q, want accepted type hint", argsDesc)
61+ }
62+
63+ valueType := gjson.GetBytes(out, "request.tools.1.functionDeclarations.0.parametersJsonSchema.properties.value.type")
64+ if valueType.String() != "integer" {
65+ t.Fatalf("value.type = %s, want integer; body=%s", valueType.Raw, string(out))
66+ }
67+ valueDesc := gjson.GetBytes(out, "request.tools.1.functionDeclarations.0.parametersJsonSchema.properties.value.description").String()
68+ if !strings.Contains(valueDesc, "Accepts: integer | string") {
69+ t.Fatalf("value.description = %q, want accepted type hint", valueDesc)
70+ }
71+
72+ if nonSchema := gjson.GetBytes(out, "request.nonSchema.type"); !nonSchema.IsArray() {
73+ t.Fatalf("request.nonSchema.type should be preserved outside schema paths, got %s", nonSchema.Raw)
74+ }
75+}