mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-09-07 00:24:17 +08:00
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. Closes: #5481
This commit is contained in:
@@ -488,9 +488,37 @@ func (e *AIStudioExecutor) translateRequest(ctx context.Context, req cliproxyexe
|
||||
}
|
||||
payload, _ = sjson.DeleteBytes(payload, "session_id")
|
||||
payload = helps.EnsureGeminiLeadingUserContent(payload, "contents")
|
||||
payload = normalizeAIStudioThinkingLevel(payload)
|
||||
return payload, translatedPayload{payload: payload, action: action, toFormat: to}, nil
|
||||
}
|
||||
|
||||
// normalizeAIStudioThinkingLevel normalizes thinking levels to Google's canonical uppercase enum values.
|
||||
// The AI Studio upstream validates generationConfig.thinkingConfig.thinkingLevel case-sensitively
|
||||
// and rejects lowercase values with a 400 invalid argument error.
|
||||
func normalizeAIStudioThinkingLevel(payload []byte) []byte {
|
||||
const path = "generationConfig.thinkingConfig.thinkingLevel"
|
||||
level := gjson.GetBytes(payload, path)
|
||||
if level.Type != gjson.String {
|
||||
return payload
|
||||
}
|
||||
|
||||
normalized := strings.ToUpper(level.String())
|
||||
switch normalized {
|
||||
case "MINIMAL", "LOW", "MEDIUM", "HIGH":
|
||||
default:
|
||||
return payload
|
||||
}
|
||||
if normalized == level.String() {
|
||||
return payload
|
||||
}
|
||||
|
||||
result, err := sjson.SetBytes(payload, path, normalized)
|
||||
if err != nil {
|
||||
return payload
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (e *AIStudioExecutor) buildEndpoint(model, action, alt string) string {
|
||||
base := fmt.Sprintf("%s/%s/models/%s:%s", glEndpoint, glAPIVersion, model, action)
|
||||
if action == "streamGenerateContent" {
|
||||
|
||||
@@ -39,6 +39,84 @@ func TestAIStudioTranslateRequestPreservesSummaryFromOriginalRequest(t *testing.
|
||||
}
|
||||
}
|
||||
|
||||
func TestAIStudioTranslateRequestNormalizesThinkingLevel(t *testing.T) {
|
||||
executor := NewAIStudioExecutor(&config.Config{}, "aistudio", nil)
|
||||
req := cliproxyexecutor.Request{
|
||||
Model: "gemini-3.7-flash",
|
||||
Payload: []byte(`{
|
||||
"contents":[{"role":"user","parts":[{"text":"Human: Hi"}]}],
|
||||
"model":"gemini-3.7-flash",
|
||||
"generationConfig":{"thinkingConfig":{"thinkingLevel":"high","includeThoughts":true}}
|
||||
}`),
|
||||
}
|
||||
opts := cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatGemini}
|
||||
|
||||
payload, _, err := executor.translateRequest(context.Background(), req, opts, false)
|
||||
if err != nil {
|
||||
t.Fatalf("translateRequest() error = %v", err)
|
||||
}
|
||||
if got := gjson.GetBytes(payload, "generationConfig.thinkingConfig.thinkingLevel").String(); got != "HIGH" {
|
||||
t.Fatalf("thinkingLevel = %q, want HIGH; payload=%s", got, payload)
|
||||
}
|
||||
if !gjson.GetBytes(payload, "generationConfig.thinkingConfig.includeThoughts").Bool() {
|
||||
t.Fatalf("includeThoughts was lost: %s", payload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAIStudioTranslateRequestNormalizesThinkingLevelAfterPayloadOverride(t *testing.T) {
|
||||
executor := NewAIStudioExecutor(&config.Config{Payload: config.PayloadConfig{Override: []config.PayloadRule{{
|
||||
Models: []config.PayloadModelRule{{Name: "gemini-3.7-flash", Protocol: "gemini"}},
|
||||
Params: map[string]any{"generationConfig.thinkingConfig.thinkingLevel": "medium"},
|
||||
}}}}, "aistudio", nil)
|
||||
req := cliproxyexecutor.Request{
|
||||
Model: "gemini-3.7-flash",
|
||||
Payload: []byte(`{"contents":[{"role":"user","parts":[{"text":"hi"}]}],"model":"gemini-3.7-flash"}`),
|
||||
}
|
||||
opts := cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatGemini}
|
||||
|
||||
payload, _, err := executor.translateRequest(context.Background(), req, opts, false)
|
||||
if err != nil {
|
||||
t.Fatalf("translateRequest() error = %v", err)
|
||||
}
|
||||
if got := gjson.GetBytes(payload, "generationConfig.thinkingConfig.thinkingLevel").String(); got != "MEDIUM" {
|
||||
t.Fatalf("thinkingLevel = %q, want MEDIUM; payload=%s", got, payload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeAIStudioThinkingLevel(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
levelJSON string
|
||||
want string
|
||||
unchanged bool
|
||||
}{
|
||||
{name: "minimal", levelJSON: `"minimal"`, want: "MINIMAL"},
|
||||
{name: "low", levelJSON: `"low"`, want: "LOW"},
|
||||
{name: "medium", levelJSON: `"medium"`, want: "MEDIUM"},
|
||||
{name: "mixed case high", levelJSON: `"hIgH"`, want: "HIGH"},
|
||||
{name: "already uppercase", levelJSON: `"HIGH"`, want: "HIGH", unchanged: true},
|
||||
{name: "none", levelJSON: `"none"`, want: "none", unchanged: true},
|
||||
{name: "xhigh", levelJSON: `"xhigh"`, want: "xhigh", unchanged: true},
|
||||
{name: "max", levelJSON: `"max"`, want: "max", unchanged: true},
|
||||
{name: "custom", levelJSON: `"custom"`, want: "custom", unchanged: true},
|
||||
{name: "non-string", levelJSON: `1`, want: "1", unchanged: true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
input := []byte(fmt.Sprintf(`{"generationConfig":{"thinkingConfig":{"thinkingLevel":%s}},"marker":true}`, test.levelJSON))
|
||||
output := normalizeAIStudioThinkingLevel(input)
|
||||
level := gjson.GetBytes(output, "generationConfig.thinkingConfig.thinkingLevel")
|
||||
if got := level.String(); got != test.want {
|
||||
t.Fatalf("thinkingLevel = %q, want %q; payload=%s", got, test.want, output)
|
||||
}
|
||||
if test.unchanged && string(output) != string(input) {
|
||||
t.Fatalf("payload changed from %s to %s", input, output)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAIStudioTranslateRequestPrependsLeadingUserForIssue4959ResponsesHistory(t *testing.T) {
|
||||
executor := NewAIStudioExecutor(&config.Config{}, "aistudio", nil)
|
||||
_, body, err := executor.translateRequest(context.Background(), cliproxyexecutor.Request{
|
||||
|
||||
Reference in New Issue
Block a user