mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-02 20:40:52 +08:00
fix: apply thinkingLevel from model suffix metadata for Gemini 3
The previous commit added thinkingLevel support but didn't apply it when the reasoning effort came from model name suffix (e.g., model(minimal)). This was because ResolveThinkingConfigFromMetadata returns nil for level-based models, bypassing the metadata application. Changes: - Add ApplyGemini3ThinkingLevelFromMetadata for standard Gemini API - Add ApplyGemini3ThinkingLevelFromMetadataCLI for CLI API format - Update gemini_cli_executor to apply Gemini 3 thinkingLevel from metadata - Update antigravity_executor to apply Gemini 3 thinkingLevel from metadata - Update aistudio_executor to apply Gemini 3 thinkingLevel from metadata - Add comprehensive test coverage for Gemini 3 thinkingLevel functions
This commit is contained in:
@@ -323,6 +323,7 @@ func (e *AIStudioExecutor) translateRequest(req cliproxyexecutor.Request, opts c
|
||||
to := sdktranslator.FromString("gemini")
|
||||
payload := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), stream)
|
||||
payload = ApplyThinkingMetadata(payload, req.Metadata, req.Model)
|
||||
payload = util.ApplyGemini3ThinkingLevelFromMetadata(req.Model, req.Metadata, payload)
|
||||
payload = util.ApplyDefaultThinkingIfNeeded(req.Model, payload)
|
||||
payload = util.ConvertThinkingLevelToBudget(payload, req.Model)
|
||||
payload = util.NormalizeGeminiThinkingBudget(req.Model, payload)
|
||||
|
||||
@@ -89,6 +89,7 @@ func (e *AntigravityExecutor) Execute(ctx context.Context, auth *cliproxyauth.Au
|
||||
translated := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)
|
||||
|
||||
translated = applyThinkingMetadataCLI(translated, req.Metadata, req.Model)
|
||||
translated = util.ApplyGemini3ThinkingLevelFromMetadataCLI(req.Model, req.Metadata, translated)
|
||||
translated = util.ApplyDefaultThinkingIfNeededCLI(req.Model, translated)
|
||||
translated = normalizeAntigravityThinking(req.Model, translated)
|
||||
|
||||
@@ -182,6 +183,7 @@ func (e *AntigravityExecutor) executeClaudeNonStream(ctx context.Context, auth *
|
||||
translated := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)
|
||||
|
||||
translated = applyThinkingMetadataCLI(translated, req.Metadata, req.Model)
|
||||
translated = util.ApplyGemini3ThinkingLevelFromMetadataCLI(req.Model, req.Metadata, translated)
|
||||
translated = util.ApplyDefaultThinkingIfNeededCLI(req.Model, translated)
|
||||
translated = normalizeAntigravityThinking(req.Model, translated)
|
||||
|
||||
@@ -514,6 +516,7 @@ func (e *AntigravityExecutor) ExecuteStream(ctx context.Context, auth *cliproxya
|
||||
translated := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)
|
||||
|
||||
translated = applyThinkingMetadataCLI(translated, req.Metadata, req.Model)
|
||||
translated = util.ApplyGemini3ThinkingLevelFromMetadataCLI(req.Model, req.Metadata, translated)
|
||||
translated = util.ApplyDefaultThinkingIfNeededCLI(req.Model, translated)
|
||||
translated = normalizeAntigravityThinking(req.Model, translated)
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth
|
||||
to := sdktranslator.FromString("gemini-cli")
|
||||
basePayload := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)
|
||||
basePayload = applyThinkingMetadataCLI(basePayload, req.Metadata, req.Model)
|
||||
basePayload = util.ApplyGemini3ThinkingLevelFromMetadataCLI(req.Model, req.Metadata, basePayload)
|
||||
basePayload = util.ApplyDefaultThinkingIfNeededCLI(req.Model, basePayload)
|
||||
basePayload = util.NormalizeGeminiCLIThinkingBudget(req.Model, basePayload)
|
||||
basePayload = util.StripThinkingConfigIfUnsupported(req.Model, basePayload)
|
||||
@@ -217,6 +218,7 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut
|
||||
to := sdktranslator.FromString("gemini-cli")
|
||||
basePayload := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)
|
||||
basePayload = applyThinkingMetadataCLI(basePayload, req.Metadata, req.Model)
|
||||
basePayload = util.ApplyGemini3ThinkingLevelFromMetadataCLI(req.Model, req.Metadata, basePayload)
|
||||
basePayload = util.ApplyDefaultThinkingIfNeededCLI(req.Model, basePayload)
|
||||
basePayload = util.NormalizeGeminiCLIThinkingBudget(req.Model, basePayload)
|
||||
basePayload = util.StripThinkingConfigIfUnsupported(req.Model, basePayload)
|
||||
@@ -418,6 +420,7 @@ func (e *GeminiCLIExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.
|
||||
for _, attemptModel := range models {
|
||||
payload := sdktranslator.TranslateRequest(from, to, attemptModel, bytes.Clone(req.Payload), false)
|
||||
payload = applyThinkingMetadataCLI(payload, req.Metadata, req.Model)
|
||||
payload = util.ApplyGemini3ThinkingLevelFromMetadataCLI(req.Model, req.Metadata, payload)
|
||||
payload = deleteJSONField(payload, "project")
|
||||
payload = deleteJSONField(payload, "model")
|
||||
payload = deleteJSONField(payload, "request.safetySettings")
|
||||
|
||||
@@ -274,6 +274,42 @@ func ApplyDefaultThinkingIfNeeded(model string, body []byte) []byte {
|
||||
return updated
|
||||
}
|
||||
|
||||
// ApplyGemini3ThinkingLevelFromMetadata applies thinkingLevel from metadata for Gemini 3 models.
|
||||
// For standard Gemini API format (generationConfig.thinkingConfig path).
|
||||
// This handles the case where reasoning_effort is specified via model name suffix (e.g., model(minimal)).
|
||||
func ApplyGemini3ThinkingLevelFromMetadata(model string, metadata map[string]any, body []byte) []byte {
|
||||
if !IsGemini3Model(model) {
|
||||
return body
|
||||
}
|
||||
effort, ok := ReasoningEffortFromMetadata(metadata)
|
||||
if !ok || effort == "" {
|
||||
return body
|
||||
}
|
||||
// Validate and apply the thinkingLevel
|
||||
if level, valid := ValidateGemini3ThinkingLevel(model, effort); valid {
|
||||
return ApplyGeminiThinkingLevel(body, level, nil)
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
// ApplyGemini3ThinkingLevelFromMetadataCLI applies thinkingLevel from metadata for Gemini 3 models.
|
||||
// For Gemini CLI API format (request.generationConfig.thinkingConfig path).
|
||||
// This handles the case where reasoning_effort is specified via model name suffix (e.g., model(minimal)).
|
||||
func ApplyGemini3ThinkingLevelFromMetadataCLI(model string, metadata map[string]any, body []byte) []byte {
|
||||
if !IsGemini3Model(model) {
|
||||
return body
|
||||
}
|
||||
effort, ok := ReasoningEffortFromMetadata(metadata)
|
||||
if !ok || effort == "" {
|
||||
return body
|
||||
}
|
||||
// Validate and apply the thinkingLevel
|
||||
if level, valid := ValidateGemini3ThinkingLevel(model, effort); valid {
|
||||
return ApplyGeminiCLIThinkingLevel(body, level, nil)
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
// ApplyDefaultThinkingIfNeededCLI injects default thinkingConfig for models that require it.
|
||||
// For Gemini CLI API format (request.generationConfig.thinkingConfig path).
|
||||
// Returns the modified body if thinkingConfig was added, otherwise returns the original.
|
||||
|
||||
423
test/gemini3_thinking_level_test.go
Normal file
423
test/gemini3_thinking_level_test.go
Normal file
@@ -0,0 +1,423 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// registerGemini3Models loads Gemini 3 models into the registry for testing.
|
||||
func registerGemini3Models(t *testing.T) func() {
|
||||
t.Helper()
|
||||
reg := registry.GetGlobalRegistry()
|
||||
uid := fmt.Sprintf("gemini3-test-%d", time.Now().UnixNano())
|
||||
reg.RegisterClient(uid+"-gemini", "gemini", registry.GetGeminiModels())
|
||||
reg.RegisterClient(uid+"-aistudio", "aistudio", registry.GetAIStudioModels())
|
||||
return func() {
|
||||
reg.UnregisterClient(uid + "-gemini")
|
||||
reg.UnregisterClient(uid + "-aistudio")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsGemini3Model(t *testing.T) {
|
||||
cases := []struct {
|
||||
model string
|
||||
expected bool
|
||||
}{
|
||||
{"gemini-3-pro-preview", true},
|
||||
{"gemini-3-flash-preview", true},
|
||||
{"gemini_3_pro_preview", true},
|
||||
{"gemini-3-pro", true},
|
||||
{"gemini-3-flash", true},
|
||||
{"GEMINI-3-PRO-PREVIEW", true},
|
||||
{"gemini-2.5-pro", false},
|
||||
{"gemini-2.5-flash", false},
|
||||
{"gpt-5", false},
|
||||
{"claude-sonnet-4-5", false},
|
||||
{"", false},
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.model, func(t *testing.T) {
|
||||
got := util.IsGemini3Model(cs.model)
|
||||
if got != cs.expected {
|
||||
t.Fatalf("IsGemini3Model(%q) = %v, want %v", cs.model, got, cs.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsGemini3ProModel(t *testing.T) {
|
||||
cases := []struct {
|
||||
model string
|
||||
expected bool
|
||||
}{
|
||||
{"gemini-3-pro-preview", true},
|
||||
{"gemini_3_pro_preview", true},
|
||||
{"gemini-3-pro", true},
|
||||
{"GEMINI-3-PRO-PREVIEW", true},
|
||||
{"gemini-3-flash-preview", false},
|
||||
{"gemini-3-flash", false},
|
||||
{"gemini-2.5-pro", false},
|
||||
{"", false},
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.model, func(t *testing.T) {
|
||||
got := util.IsGemini3ProModel(cs.model)
|
||||
if got != cs.expected {
|
||||
t.Fatalf("IsGemini3ProModel(%q) = %v, want %v", cs.model, got, cs.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsGemini3FlashModel(t *testing.T) {
|
||||
cases := []struct {
|
||||
model string
|
||||
expected bool
|
||||
}{
|
||||
{"gemini-3-flash-preview", true},
|
||||
{"gemini_3_flash_preview", true},
|
||||
{"gemini-3-flash", true},
|
||||
{"GEMINI-3-FLASH-PREVIEW", true},
|
||||
{"gemini-3-pro-preview", false},
|
||||
{"gemini-3-pro", false},
|
||||
{"gemini-2.5-flash", false},
|
||||
{"", false},
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.model, func(t *testing.T) {
|
||||
got := util.IsGemini3FlashModel(cs.model)
|
||||
if got != cs.expected {
|
||||
t.Fatalf("IsGemini3FlashModel(%q) = %v, want %v", cs.model, got, cs.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateGemini3ThinkingLevel(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
model string
|
||||
level string
|
||||
wantOK bool
|
||||
wantVal string
|
||||
}{
|
||||
// Gemini 3 Pro: supports "low", "high"
|
||||
{"pro-low", "gemini-3-pro-preview", "low", true, "low"},
|
||||
{"pro-high", "gemini-3-pro-preview", "high", true, "high"},
|
||||
{"pro-minimal-invalid", "gemini-3-pro-preview", "minimal", false, ""},
|
||||
{"pro-medium-invalid", "gemini-3-pro-preview", "medium", false, ""},
|
||||
|
||||
// Gemini 3 Flash: supports "minimal", "low", "medium", "high"
|
||||
{"flash-minimal", "gemini-3-flash-preview", "minimal", true, "minimal"},
|
||||
{"flash-low", "gemini-3-flash-preview", "low", true, "low"},
|
||||
{"flash-medium", "gemini-3-flash-preview", "medium", true, "medium"},
|
||||
{"flash-high", "gemini-3-flash-preview", "high", true, "high"},
|
||||
|
||||
// Case insensitivity
|
||||
{"flash-LOW-case", "gemini-3-flash-preview", "LOW", true, "low"},
|
||||
{"flash-High-case", "gemini-3-flash-preview", "High", true, "high"},
|
||||
{"pro-HIGH-case", "gemini-3-pro-preview", "HIGH", true, "high"},
|
||||
|
||||
// Invalid levels
|
||||
{"flash-invalid", "gemini-3-flash-preview", "xhigh", false, ""},
|
||||
{"flash-invalid-auto", "gemini-3-flash-preview", "auto", false, ""},
|
||||
{"flash-empty", "gemini-3-flash-preview", "", false, ""},
|
||||
|
||||
// Non-Gemini 3 models
|
||||
{"non-gemini3", "gemini-2.5-pro", "high", false, ""},
|
||||
{"gpt5", "gpt-5", "high", false, ""},
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.name, func(t *testing.T) {
|
||||
got, ok := util.ValidateGemini3ThinkingLevel(cs.model, cs.level)
|
||||
if ok != cs.wantOK {
|
||||
t.Fatalf("ValidateGemini3ThinkingLevel(%q, %q) ok = %v, want %v", cs.model, cs.level, ok, cs.wantOK)
|
||||
}
|
||||
if got != cs.wantVal {
|
||||
t.Fatalf("ValidateGemini3ThinkingLevel(%q, %q) = %q, want %q", cs.model, cs.level, got, cs.wantVal)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestThinkingBudgetToGemini3Level(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
model string
|
||||
budget int
|
||||
wantOK bool
|
||||
wantVal string
|
||||
}{
|
||||
// Gemini 3 Pro: maps to "low" or "high"
|
||||
{"pro-dynamic", "gemini-3-pro-preview", -1, true, "high"},
|
||||
{"pro-zero", "gemini-3-pro-preview", 0, true, "low"},
|
||||
{"pro-small", "gemini-3-pro-preview", 1000, true, "low"},
|
||||
{"pro-medium", "gemini-3-pro-preview", 8000, true, "low"},
|
||||
{"pro-large", "gemini-3-pro-preview", 20000, true, "high"},
|
||||
{"pro-huge", "gemini-3-pro-preview", 50000, true, "high"},
|
||||
|
||||
// Gemini 3 Flash: maps to "minimal", "low", "medium", "high"
|
||||
{"flash-dynamic", "gemini-3-flash-preview", -1, true, "high"},
|
||||
{"flash-zero", "gemini-3-flash-preview", 0, true, "minimal"},
|
||||
{"flash-tiny", "gemini-3-flash-preview", 500, true, "minimal"},
|
||||
{"flash-small", "gemini-3-flash-preview", 1000, true, "low"},
|
||||
{"flash-medium-val", "gemini-3-flash-preview", 8000, true, "medium"},
|
||||
{"flash-large", "gemini-3-flash-preview", 20000, true, "high"},
|
||||
{"flash-huge", "gemini-3-flash-preview", 50000, true, "high"},
|
||||
|
||||
// Non-Gemini 3 models should return false
|
||||
{"gemini25-budget", "gemini-2.5-pro", 8000, false, ""},
|
||||
{"gpt5-budget", "gpt-5", 8000, false, ""},
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.name, func(t *testing.T) {
|
||||
got, ok := util.ThinkingBudgetToGemini3Level(cs.model, cs.budget)
|
||||
if ok != cs.wantOK {
|
||||
t.Fatalf("ThinkingBudgetToGemini3Level(%q, %d) ok = %v, want %v", cs.model, cs.budget, ok, cs.wantOK)
|
||||
}
|
||||
if got != cs.wantVal {
|
||||
t.Fatalf("ThinkingBudgetToGemini3Level(%q, %d) = %q, want %q", cs.model, cs.budget, got, cs.wantVal)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyGemini3ThinkingLevelFromMetadata(t *testing.T) {
|
||||
cleanup := registerGemini3Models(t)
|
||||
defer cleanup()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
model string
|
||||
metadata map[string]any
|
||||
inputBody string
|
||||
wantLevel string
|
||||
wantInclude bool
|
||||
wantNoChange bool
|
||||
}{
|
||||
{
|
||||
name: "flash-minimal-from-suffix",
|
||||
model: "gemini-3-flash-preview",
|
||||
metadata: map[string]any{"reasoning_effort": "minimal"},
|
||||
inputBody: `{"generationConfig":{"thinkingConfig":{"includeThoughts":true}}}`,
|
||||
wantLevel: "minimal",
|
||||
wantInclude: true,
|
||||
},
|
||||
{
|
||||
name: "flash-medium-from-suffix",
|
||||
model: "gemini-3-flash-preview",
|
||||
metadata: map[string]any{"reasoning_effort": "medium"},
|
||||
inputBody: `{"generationConfig":{"thinkingConfig":{"includeThoughts":true}}}`,
|
||||
wantLevel: "medium",
|
||||
wantInclude: true,
|
||||
},
|
||||
{
|
||||
name: "pro-high-from-suffix",
|
||||
model: "gemini-3-pro-preview",
|
||||
metadata: map[string]any{"reasoning_effort": "high"},
|
||||
inputBody: `{"generationConfig":{"thinkingConfig":{"includeThoughts":true}}}`,
|
||||
wantLevel: "high",
|
||||
wantInclude: true,
|
||||
},
|
||||
{
|
||||
name: "no-metadata-no-change",
|
||||
model: "gemini-3-flash-preview",
|
||||
metadata: nil,
|
||||
inputBody: `{"generationConfig":{"thinkingConfig":{"includeThoughts":true}}}`,
|
||||
wantNoChange: true,
|
||||
},
|
||||
{
|
||||
name: "non-gemini3-no-change",
|
||||
model: "gemini-2.5-pro",
|
||||
metadata: map[string]any{"reasoning_effort": "high"},
|
||||
inputBody: `{"generationConfig":{"thinkingConfig":{"thinkingBudget":-1}}}`,
|
||||
wantNoChange: true,
|
||||
},
|
||||
{
|
||||
name: "invalid-level-no-change",
|
||||
model: "gemini-3-flash-preview",
|
||||
metadata: map[string]any{"reasoning_effort": "xhigh"},
|
||||
inputBody: `{"generationConfig":{"thinkingConfig":{"includeThoughts":true}}}`,
|
||||
wantNoChange: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.name, func(t *testing.T) {
|
||||
input := []byte(cs.inputBody)
|
||||
result := util.ApplyGemini3ThinkingLevelFromMetadata(cs.model, cs.metadata, input)
|
||||
|
||||
if cs.wantNoChange {
|
||||
if string(result) != cs.inputBody {
|
||||
t.Fatalf("expected no change, but got: %s", string(result))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
level := gjson.GetBytes(result, "generationConfig.thinkingConfig.thinkingLevel")
|
||||
if !level.Exists() {
|
||||
t.Fatalf("thinkingLevel not set in result: %s", string(result))
|
||||
}
|
||||
if level.String() != cs.wantLevel {
|
||||
t.Fatalf("thinkingLevel = %q, want %q", level.String(), cs.wantLevel)
|
||||
}
|
||||
|
||||
include := gjson.GetBytes(result, "generationConfig.thinkingConfig.includeThoughts")
|
||||
if cs.wantInclude && (!include.Exists() || !include.Bool()) {
|
||||
t.Fatalf("includeThoughts should be true, got: %s", string(result))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyGemini3ThinkingLevelFromMetadataCLI(t *testing.T) {
|
||||
cleanup := registerGemini3Models(t)
|
||||
defer cleanup()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
model string
|
||||
metadata map[string]any
|
||||
inputBody string
|
||||
wantLevel string
|
||||
wantInclude bool
|
||||
wantNoChange bool
|
||||
}{
|
||||
{
|
||||
name: "flash-minimal-from-suffix-cli",
|
||||
model: "gemini-3-flash-preview",
|
||||
metadata: map[string]any{"reasoning_effort": "minimal"},
|
||||
inputBody: `{"request":{"generationConfig":{"thinkingConfig":{"includeThoughts":true}}}}`,
|
||||
wantLevel: "minimal",
|
||||
wantInclude: true,
|
||||
},
|
||||
{
|
||||
name: "flash-low-from-suffix-cli",
|
||||
model: "gemini-3-flash-preview",
|
||||
metadata: map[string]any{"reasoning_effort": "low"},
|
||||
inputBody: `{"request":{"generationConfig":{"thinkingConfig":{"includeThoughts":true}}}}`,
|
||||
wantLevel: "low",
|
||||
wantInclude: true,
|
||||
},
|
||||
{
|
||||
name: "pro-low-from-suffix-cli",
|
||||
model: "gemini-3-pro-preview",
|
||||
metadata: map[string]any{"reasoning_effort": "low"},
|
||||
inputBody: `{"request":{"generationConfig":{"thinkingConfig":{"includeThoughts":true}}}}`,
|
||||
wantLevel: "low",
|
||||
wantInclude: true,
|
||||
},
|
||||
{
|
||||
name: "no-metadata-no-change-cli",
|
||||
model: "gemini-3-flash-preview",
|
||||
metadata: nil,
|
||||
inputBody: `{"request":{"generationConfig":{"thinkingConfig":{"includeThoughts":true}}}}`,
|
||||
wantNoChange: true,
|
||||
},
|
||||
{
|
||||
name: "non-gemini3-no-change-cli",
|
||||
model: "gemini-2.5-pro",
|
||||
metadata: map[string]any{"reasoning_effort": "high"},
|
||||
inputBody: `{"request":{"generationConfig":{"thinkingConfig":{"thinkingBudget":-1}}}}`,
|
||||
wantNoChange: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.name, func(t *testing.T) {
|
||||
input := []byte(cs.inputBody)
|
||||
result := util.ApplyGemini3ThinkingLevelFromMetadataCLI(cs.model, cs.metadata, input)
|
||||
|
||||
if cs.wantNoChange {
|
||||
if string(result) != cs.inputBody {
|
||||
t.Fatalf("expected no change, but got: %s", string(result))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
level := gjson.GetBytes(result, "request.generationConfig.thinkingConfig.thinkingLevel")
|
||||
if !level.Exists() {
|
||||
t.Fatalf("thinkingLevel not set in result: %s", string(result))
|
||||
}
|
||||
if level.String() != cs.wantLevel {
|
||||
t.Fatalf("thinkingLevel = %q, want %q", level.String(), cs.wantLevel)
|
||||
}
|
||||
|
||||
include := gjson.GetBytes(result, "request.generationConfig.thinkingConfig.includeThoughts")
|
||||
if cs.wantInclude && (!include.Exists() || !include.Bool()) {
|
||||
t.Fatalf("includeThoughts should be true, got: %s", string(result))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeGeminiThinkingBudget_Gemini3Conversion(t *testing.T) {
|
||||
cleanup := registerGemini3Models(t)
|
||||
defer cleanup()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
model string
|
||||
inputBody string
|
||||
wantLevel string
|
||||
wantBudget bool // if true, expect thinkingBudget instead of thinkingLevel
|
||||
}{
|
||||
{
|
||||
name: "gemini3-flash-budget-to-level",
|
||||
model: "gemini-3-flash-preview",
|
||||
inputBody: `{"generationConfig":{"thinkingConfig":{"thinkingBudget":8000}}}`,
|
||||
wantLevel: "medium",
|
||||
},
|
||||
{
|
||||
name: "gemini3-pro-budget-to-level",
|
||||
model: "gemini-3-pro-preview",
|
||||
inputBody: `{"generationConfig":{"thinkingConfig":{"thinkingBudget":20000}}}`,
|
||||
wantLevel: "high",
|
||||
},
|
||||
{
|
||||
name: "gemini25-keeps-budget",
|
||||
model: "gemini-2.5-pro",
|
||||
inputBody: `{"generationConfig":{"thinkingConfig":{"thinkingBudget":8000}}}`,
|
||||
wantBudget: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.name, func(t *testing.T) {
|
||||
result := util.NormalizeGeminiThinkingBudget(cs.model, []byte(cs.inputBody))
|
||||
|
||||
if cs.wantBudget {
|
||||
budget := gjson.GetBytes(result, "generationConfig.thinkingConfig.thinkingBudget")
|
||||
if !budget.Exists() {
|
||||
t.Fatalf("thinkingBudget should exist for non-Gemini3 model: %s", string(result))
|
||||
}
|
||||
level := gjson.GetBytes(result, "generationConfig.thinkingConfig.thinkingLevel")
|
||||
if level.Exists() {
|
||||
t.Fatalf("thinkingLevel should not exist for non-Gemini3 model: %s", string(result))
|
||||
}
|
||||
} else {
|
||||
level := gjson.GetBytes(result, "generationConfig.thinkingConfig.thinkingLevel")
|
||||
if !level.Exists() {
|
||||
t.Fatalf("thinkingLevel should exist for Gemini3 model: %s", string(result))
|
||||
}
|
||||
if level.String() != cs.wantLevel {
|
||||
t.Fatalf("thinkingLevel = %q, want %q", level.String(), cs.wantLevel)
|
||||
}
|
||||
budget := gjson.GetBytes(result, "generationConfig.thinkingConfig.thinkingBudget")
|
||||
if budget.Exists() {
|
||||
t.Fatalf("thinkingBudget should be removed for Gemini3 model: %s", string(result))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user