History

fix(aistudio): normalize thinking level to uppercase

- Normalize `generationConfig.thinkingConfig.thinkingLevel` to canonical uppercase enum values (`MINIMAL`, `LOW`, `MEDIUM`, `HIGH`). - Prevent upstream HTTP 400 invalid argument errors caused by case-sensitive validation.

Luis Pater committed 2 months ago

Commit 9dfddd61 parent 31ec4362 files · +106 −0Browse files

Unified · Split

2 files
  1. internal/runtime/executor/aistudio_executor.go +28 −0
  2. internal/runtime/executor/aistudio_executor_test.go +78 −0
internal/runtime/executor/aistudio_executor.go+28−0
@@ -488,9 +488,37 @@ func (e *AIStudioExecutor) translateRequest(ctx context.Context, req cliproxyexe · 487 unmodified lines
488 }488 }
489 payload, _ = sjson.DeleteBytes(payload, "session_id")489 payload, _ = sjson.DeleteBytes(payload, "session_id")
490 payload = helps.EnsureGeminiLeadingUserContent(payload, "contents")490 payload = helps.EnsureGeminiLeadingUserContent(payload, "contents")
491 payload = normalizeAIStudioThinkingLevel(payload)
491 return payload, translatedPayload{payload: payload, action: action, toFormat: to}, nil492 return payload, translatedPayload{payload: payload, action: action, toFormat: to}, nil
492}493}
493 494
495// normalizeAIStudioThinkingLevel normalizes thinking levels to Google's canonical uppercase enum values.
496// The AI Studio upstream validates generationConfig.thinkingConfig.thinkingLevel case-sensitively
497// and rejects lowercase values with a 400 invalid argument error.
498func normalizeAIStudioThinkingLevel(payload []byte) []byte {
499 const path = "generationConfig.thinkingConfig.thinkingLevel"
500 level := gjson.GetBytes(payload, path)
501 if level.Type != gjson.String {
502 return payload
503 }
504
505 normalized := strings.ToUpper(level.String())
506 switch normalized {
507 case "MINIMAL", "LOW", "MEDIUM", "HIGH":
508 default:
509 return payload
510 }
511 if normalized == level.String() {
512 return payload
513 }
514
515 result, err := sjson.SetBytes(payload, path, normalized)
516 if err != nil {
517 return payload
518 }
519 return result
520}
521
494func (e *AIStudioExecutor) buildEndpoint(model, action, alt string) string {522func (e *AIStudioExecutor) buildEndpoint(model, action, alt string) string {
495 base := fmt.Sprintf("%s/%s/models/%s:%s", glEndpoint, glAPIVersion, model, action)523 base := fmt.Sprintf("%s/%s/models/%s:%s", glEndpoint, glAPIVersion, model, action)
496 if action == "streamGenerateContent" {524 if action == "streamGenerateContent" {
internal/runtime/executor/aistudio_executor_test.go+78−0
@@ -39,6 +39,84 @@ func TestAIStudioTranslateRequestPreservesSummaryFromOriginalRequest(t *testing. · 38 unmodified lines
39 }39 }
40}40}
41 41
42func TestAIStudioTranslateRequestNormalizesThinkingLevel(t *testing.T) {
43 executor := NewAIStudioExecutor(&config.Config{}, "aistudio", nil)
44 req := cliproxyexecutor.Request{
45 Model: "gemini-3.7-flash",
46 Payload: []byte(`{
47 "contents":[{"role":"user","parts":[{"text":"Human: Hi"}]}],
48 "model":"gemini-3.7-flash",
49 "generationConfig":{"thinkingConfig":{"thinkingLevel":"high","includeThoughts":true}}
50 }`),
51 }
52 opts := cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatGemini}
53
54 payload, _, err := executor.translateRequest(context.Background(), req, opts, false)
55 if err != nil {
56 t.Fatalf("translateRequest() error = %v", err)
57 }
58 if got := gjson.GetBytes(payload, "generationConfig.thinkingConfig.thinkingLevel").String(); got != "HIGH" {
59 t.Fatalf("thinkingLevel = %q, want HIGH; payload=%s", got, payload)
60 }
61 if !gjson.GetBytes(payload, "generationConfig.thinkingConfig.includeThoughts").Bool() {
62 t.Fatalf("includeThoughts was lost: %s", payload)
63 }
64}
65
66func TestAIStudioTranslateRequestNormalizesThinkingLevelAfterPayloadOverride(t *testing.T) {
67 executor := NewAIStudioExecutor(&config.Config{Payload: config.PayloadConfig{Override: []config.PayloadRule{{
68 Models: []config.PayloadModelRule{{Name: "gemini-3.7-flash", Protocol: "gemini"}},
69 Params: map[string]any{"generationConfig.thinkingConfig.thinkingLevel": "medium"},
70 }}}}, "aistudio", nil)
71 req := cliproxyexecutor.Request{
72 Model: "gemini-3.7-flash",
73 Payload: []byte(`{"contents":[{"role":"user","parts":[{"text":"hi"}]}],"model":"gemini-3.7-flash"}`),
74 }
75 opts := cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatGemini}
76
77 payload, _, err := executor.translateRequest(context.Background(), req, opts, false)
78 if err != nil {
79 t.Fatalf("translateRequest() error = %v", err)
80 }
81 if got := gjson.GetBytes(payload, "generationConfig.thinkingConfig.thinkingLevel").String(); got != "MEDIUM" {
82 t.Fatalf("thinkingLevel = %q, want MEDIUM; payload=%s", got, payload)
83 }
84}
85
86func TestNormalizeAIStudioThinkingLevel(t *testing.T) {
87 tests := []struct {
88 name string
89 levelJSON string
90 want string
91 unchanged bool
92 }{
93 {name: "minimal", levelJSON: `"minimal"`, want: "MINIMAL"},
94 {name: "low", levelJSON: `"low"`, want: "LOW"},
95 {name: "medium", levelJSON: `"medium"`, want: "MEDIUM"},
96 {name: "mixed case high", levelJSON: `"hIgH"`, want: "HIGH"},
97 {name: "already uppercase", levelJSON: `"HIGH"`, want: "HIGH", unchanged: true},
98 {name: "none", levelJSON: `"none"`, want: "none", unchanged: true},
99 {name: "xhigh", levelJSON: `"xhigh"`, want: "xhigh", unchanged: true},
100 {name: "max", levelJSON: `"max"`, want: "max", unchanged: true},
101 {name: "custom", levelJSON: `"custom"`, want: "custom", unchanged: true},
102 {name: "non-string", levelJSON: `1`, want: "1", unchanged: true},
103 }
104
105 for _, test := range tests {
106 t.Run(test.name, func(t *testing.T) {
107 input := []byte(fmt.Sprintf(`{"generationConfig":{"thinkingConfig":{"thinkingLevel":%s}},"marker":true}`, test.levelJSON))
108 output := normalizeAIStudioThinkingLevel(input)
109 level := gjson.GetBytes(output, "generationConfig.thinkingConfig.thinkingLevel")
110 if got := level.String(); got != test.want {
111 t.Fatalf("thinkingLevel = %q, want %q; payload=%s", got, test.want, output)
112 }
113 if test.unchanged && string(output) != string(input) {
114 t.Fatalf("payload changed from %s to %s", input, output)
115 }
116 })
117 }
118}
119
42func TestAIStudioTranslateRequestPrependsLeadingUserForIssue4959ResponsesHistory(t *testing.T) {120func TestAIStudioTranslateRequestPrependsLeadingUserForIssue4959ResponsesHistory(t *testing.T) {
43 executor := NewAIStudioExecutor(&config.Config{}, "aistudio", nil)121 executor := NewAIStudioExecutor(&config.Config{}, "aistudio", nil)
44 _, body, err := executor.translateRequest(context.Background(), cliproxyexecutor.Request{122 _, body, err := executor.translateRequest(context.Background(), cliproxyexecutor.Request{