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")
|
to := sdktranslator.FromString("gemini")
|
||||||
payload := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), stream)
|
payload := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), stream)
|
||||||
payload = ApplyThinkingMetadata(payload, req.Metadata, req.Model)
|
payload = ApplyThinkingMetadata(payload, req.Metadata, req.Model)
|
||||||
|
payload = util.ApplyGemini3ThinkingLevelFromMetadata(req.Model, req.Metadata, payload)
|
||||||
payload = util.ApplyDefaultThinkingIfNeeded(req.Model, payload)
|
payload = util.ApplyDefaultThinkingIfNeeded(req.Model, payload)
|
||||||
payload = util.ConvertThinkingLevelToBudget(payload, req.Model)
|
payload = util.ConvertThinkingLevelToBudget(payload, req.Model)
|
||||||
payload = util.NormalizeGeminiThinkingBudget(req.Model, payload)
|
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 := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)
|
||||||
|
|
||||||
translated = applyThinkingMetadataCLI(translated, req.Metadata, req.Model)
|
translated = applyThinkingMetadataCLI(translated, req.Metadata, req.Model)
|
||||||
|
translated = util.ApplyGemini3ThinkingLevelFromMetadataCLI(req.Model, req.Metadata, translated)
|
||||||
translated = util.ApplyDefaultThinkingIfNeededCLI(req.Model, translated)
|
translated = util.ApplyDefaultThinkingIfNeededCLI(req.Model, translated)
|
||||||
translated = normalizeAntigravityThinking(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 := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)
|
||||||
|
|
||||||
translated = applyThinkingMetadataCLI(translated, req.Metadata, req.Model)
|
translated = applyThinkingMetadataCLI(translated, req.Metadata, req.Model)
|
||||||
|
translated = util.ApplyGemini3ThinkingLevelFromMetadataCLI(req.Model, req.Metadata, translated)
|
||||||
translated = util.ApplyDefaultThinkingIfNeededCLI(req.Model, translated)
|
translated = util.ApplyDefaultThinkingIfNeededCLI(req.Model, translated)
|
||||||
translated = normalizeAntigravityThinking(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 := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)
|
||||||
|
|
||||||
translated = applyThinkingMetadataCLI(translated, req.Metadata, req.Model)
|
translated = applyThinkingMetadataCLI(translated, req.Metadata, req.Model)
|
||||||
|
translated = util.ApplyGemini3ThinkingLevelFromMetadataCLI(req.Model, req.Metadata, translated)
|
||||||
translated = util.ApplyDefaultThinkingIfNeededCLI(req.Model, translated)
|
translated = util.ApplyDefaultThinkingIfNeededCLI(req.Model, translated)
|
||||||
translated = normalizeAntigravityThinking(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")
|
to := sdktranslator.FromString("gemini-cli")
|
||||||
basePayload := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)
|
basePayload := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)
|
||||||
basePayload = applyThinkingMetadataCLI(basePayload, req.Metadata, req.Model)
|
basePayload = applyThinkingMetadataCLI(basePayload, req.Metadata, req.Model)
|
||||||
|
basePayload = util.ApplyGemini3ThinkingLevelFromMetadataCLI(req.Model, req.Metadata, basePayload)
|
||||||
basePayload = util.ApplyDefaultThinkingIfNeededCLI(req.Model, basePayload)
|
basePayload = util.ApplyDefaultThinkingIfNeededCLI(req.Model, basePayload)
|
||||||
basePayload = util.NormalizeGeminiCLIThinkingBudget(req.Model, basePayload)
|
basePayload = util.NormalizeGeminiCLIThinkingBudget(req.Model, basePayload)
|
||||||
basePayload = util.StripThinkingConfigIfUnsupported(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")
|
to := sdktranslator.FromString("gemini-cli")
|
||||||
basePayload := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)
|
basePayload := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)
|
||||||
basePayload = applyThinkingMetadataCLI(basePayload, req.Metadata, req.Model)
|
basePayload = applyThinkingMetadataCLI(basePayload, req.Metadata, req.Model)
|
||||||
|
basePayload = util.ApplyGemini3ThinkingLevelFromMetadataCLI(req.Model, req.Metadata, basePayload)
|
||||||
basePayload = util.ApplyDefaultThinkingIfNeededCLI(req.Model, basePayload)
|
basePayload = util.ApplyDefaultThinkingIfNeededCLI(req.Model, basePayload)
|
||||||
basePayload = util.NormalizeGeminiCLIThinkingBudget(req.Model, basePayload)
|
basePayload = util.NormalizeGeminiCLIThinkingBudget(req.Model, basePayload)
|
||||||
basePayload = util.StripThinkingConfigIfUnsupported(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 {
|
for _, attemptModel := range models {
|
||||||
payload := sdktranslator.TranslateRequest(from, to, attemptModel, bytes.Clone(req.Payload), false)
|
payload := sdktranslator.TranslateRequest(from, to, attemptModel, bytes.Clone(req.Payload), false)
|
||||||
payload = applyThinkingMetadataCLI(payload, req.Metadata, req.Model)
|
payload = applyThinkingMetadataCLI(payload, req.Metadata, req.Model)
|
||||||
|
payload = util.ApplyGemini3ThinkingLevelFromMetadataCLI(req.Model, req.Metadata, payload)
|
||||||
payload = deleteJSONField(payload, "project")
|
payload = deleteJSONField(payload, "project")
|
||||||
payload = deleteJSONField(payload, "model")
|
payload = deleteJSONField(payload, "model")
|
||||||
payload = deleteJSONField(payload, "request.safetySettings")
|
payload = deleteJSONField(payload, "request.safetySettings")
|
||||||
|
|||||||
@@ -274,6 +274,42 @@ func ApplyDefaultThinkingIfNeeded(model string, body []byte) []byte {
|
|||||||
return updated
|
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.
|
// ApplyDefaultThinkingIfNeededCLI injects default thinkingConfig for models that require it.
|
||||||
// For Gemini CLI API format (request.generationConfig.thinkingConfig path).
|
// For Gemini CLI API format (request.generationConfig.thinkingConfig path).
|
||||||
// Returns the modified body if thinkingConfig was added, otherwise returns the original.
|
// 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