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
141 requestedModel := helps.PayloadRequestedModel(opts, req.Model)141 requestedModel := helps.PayloadRequestedModel(opts, req.Model)
142 requestPath := helps.PayloadRequestPath(opts)142 requestPath := helps.PayloadRequestPath(opts)
143 basePayload = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, "gemini", from.String(), "request", basePayload, originalTranslated, requestedModel, requestPath, opts.Headers)143 basePayload = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, "gemini", from.String(), "request", basePayload, originalTranslated, requestedModel, requestPath, opts.Headers)
144 basePayload = cleanGeminiCLIRequestSchemas(basePayload)
144 145
145 action := "generateContent"146 action := "generateContent"
146 if req.Metadata != nil {147 if req.Metadata != nil {
@@ -297,6 +298,7 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut · 150 unmodified lines
297 requestedModel := helps.PayloadRequestedModel(opts, req.Model)298 requestedModel := helps.PayloadRequestedModel(opts, req.Model)
298 requestPath := helps.PayloadRequestPath(opts)299 requestPath := helps.PayloadRequestPath(opts)
299 basePayload = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, "gemini", from.String(), "request", basePayload, originalTranslated, requestedModel, requestPath, opts.Headers)300 basePayload = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, "gemini", from.String(), "request", basePayload, originalTranslated, requestedModel, requestPath, opts.Headers)
301 basePayload = cleanGeminiCLIRequestSchemas(basePayload)
300 302
301 projectID := resolveGeminiProjectID(auth)303 projectID := resolveGeminiProjectID(auth)
302 304
@@ -530,6 +532,7 @@ func (e *GeminiCLIExecutor) CountTokens(ctx context.Context, auth *cliproxyauth. · 227 unmodified lines
530 payload = deleteJSONField(payload, "model")532 payload = deleteJSONField(payload, "model")
531 payload = deleteJSONField(payload, "request.safetySettings")533 payload = deleteJSONField(payload, "request.safetySettings")
532 payload = fixGeminiCLIImageAspectRatio(baseModel, payload)534 payload = fixGeminiCLIImageAspectRatio(baseModel, payload)
535 payload = cleanGeminiCLIRequestSchemas(payload)
533 536
534 tok, errTok := tokenSource.Token()537 tok, errTok := tokenSource.Token()
535 if errTok != nil {538 if errTok != nil {
@@ -859,6 +862,65 @@ func deleteJSONField(body []byte, key string) []byte { · 323 unmodified lines
859 return updated862 return updated
860}863}
861 864
865func 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
862func fixGeminiCLIImageAspectRatio(modelName string, rawJSON []byte) []byte {924func fixGeminiCLIImageAspectRatio(modelName string, rawJSON []byte) []byte {
863 if modelName == "gemini-2.5-flash-image-preview" {925 if modelName == "gemini-2.5-flash-image-preview" {
864 aspectRatioResult := gjson.GetBytes(rawJSON, "request.generationConfig.imageConfig.aspectRatio")926 aspectRatioResult := gjson.GetBytes(rawJSON, "request.generationConfig.imageConfig.aspectRatio")
internal/runtime/executor/gemini_cli_executor_test.go+75−0
@@ -0,0 +1,75 @@
1package executor
2
3import (
4 "strings"
5 "testing"
6
7 "github.com/tidwall/gjson"
8)
9
10func 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}