From b326ec364150b39207e6c9652643f037a2bb7d26 Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Tue, 16 Dec 2025 14:22:05 +0800 Subject: [PATCH 1/3] feat(iflow): add thinking support for iFlow models --- internal/registry/model_definitions.go | 15 +++++++++++---- internal/runtime/executor/iflow_executor.go | 20 ++++++++++++++++++++ internal/runtime/executor/payload_helpers.go | 2 +- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/internal/registry/model_definitions.go b/internal/registry/model_definitions.go index 7a4bdf0c..ca894ba6 100644 --- a/internal/registry/model_definitions.go +++ b/internal/registry/model_definitions.go @@ -630,6 +630,13 @@ func GetQwenModels() []*ModelInfo { } } +// iFlowThinkingSupport is a shared ThinkingSupport configuration for iFlow models +// that support thinking mode via chat_template_kwargs.enable_thinking (boolean toggle). +// Uses level-based configuration so standard normalization flows apply before conversion. +var iFlowThinkingSupport = &ThinkingSupport{ + Levels: []string{"none", "auto", "minimal", "low", "medium", "high", "xhigh"}, +} + // GetIFlowModels returns supported models for iFlow OAuth accounts. func GetIFlowModels() []*ModelInfo { entries := []struct { @@ -645,9 +652,9 @@ func GetIFlowModels() []*ModelInfo { {ID: "qwen3-vl-plus", DisplayName: "Qwen3-VL-Plus", Description: "Qwen3 multimodal vision-language", Created: 1758672000}, {ID: "qwen3-max-preview", DisplayName: "Qwen3-Max-Preview", Description: "Qwen3 Max preview build", Created: 1757030400}, {ID: "kimi-k2-0905", DisplayName: "Kimi-K2-Instruct-0905", Description: "Moonshot Kimi K2 instruct 0905", Created: 1757030400}, - {ID: "glm-4.6", DisplayName: "GLM-4.6", Description: "Zhipu GLM 4.6 general model", Created: 1759190400}, + {ID: "glm-4.6", DisplayName: "GLM-4.6", Description: "Zhipu GLM 4.6 general model", Created: 1759190400, Thinking: iFlowThinkingSupport}, {ID: "kimi-k2", DisplayName: "Kimi-K2", Description: "Moonshot Kimi K2 general model", Created: 1752192000}, - {ID: "kimi-k2-thinking", DisplayName: "Kimi-K2-Thinking", Description: "Moonshot Kimi K2 thinking model", Created: 1762387200, Thinking: &ThinkingSupport{Levels: []string{"low", "medium", "high"}}}, + {ID: "kimi-k2-thinking", DisplayName: "Kimi-K2-Thinking", Description: "Moonshot Kimi K2 thinking model", Created: 1762387200}, {ID: "deepseek-v3.2-chat", DisplayName: "DeepSeek-V3.2", Description: "DeepSeek V3.2 Chat", Created: 1764576000}, {ID: "deepseek-v3.2-reasoner", DisplayName: "DeepSeek-V3.2", Description: "DeepSeek V3.2 Reasoner", Created: 1764576000}, {ID: "deepseek-v3.2", DisplayName: "DeepSeek-V3.2-Exp", Description: "DeepSeek V3.2 experimental", Created: 1759104000}, @@ -655,10 +662,10 @@ func GetIFlowModels() []*ModelInfo { {ID: "deepseek-r1", DisplayName: "DeepSeek-R1", Description: "DeepSeek reasoning model R1", Created: 1737331200}, {ID: "deepseek-v3", DisplayName: "DeepSeek-V3-671B", Description: "DeepSeek V3 671B", Created: 1734307200}, {ID: "qwen3-32b", DisplayName: "Qwen3-32B", Description: "Qwen3 32B", Created: 1747094400}, - {ID: "qwen3-235b-a22b-thinking-2507", DisplayName: "Qwen3-235B-A22B-Thinking", Description: "Qwen3 235B A22B Thinking (2507)", Created: 1753401600, Thinking: &ThinkingSupport{Levels: []string{"low", "medium", "high"}}}, + {ID: "qwen3-235b-a22b-thinking-2507", DisplayName: "Qwen3-235B-A22B-Thinking", Description: "Qwen3 235B A22B Thinking (2507)", Created: 1753401600}, {ID: "qwen3-235b-a22b-instruct", DisplayName: "Qwen3-235B-A22B-Instruct", Description: "Qwen3 235B A22B Instruct", Created: 1753401600}, {ID: "qwen3-235b", DisplayName: "Qwen3-235B-A22B", Description: "Qwen3 235B A22B", Created: 1753401600}, - {ID: "minimax-m2", DisplayName: "MiniMax-M2", Description: "MiniMax M2", Created: 1758672000, Thinking: &ThinkingSupport{Levels: []string{"low", "medium", "high"}}}, + {ID: "minimax-m2", DisplayName: "MiniMax-M2", Description: "MiniMax M2", Created: 1758672000}, } models := make([]*ModelInfo, 0, len(entries)) for _, entry := range entries { diff --git a/internal/runtime/executor/iflow_executor.go b/internal/runtime/executor/iflow_executor.go index ad0b4d2a..0ed3c111 100644 --- a/internal/runtime/executor/iflow_executor.go +++ b/internal/runtime/executor/iflow_executor.go @@ -66,6 +66,7 @@ func (e *IFlowExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re if errValidate := ValidateThinkingConfig(body, upstreamModel); errValidate != nil { return resp, errValidate } + body = applyIFlowThinkingConfig(body) body = applyPayloadConfig(e.cfg, req.Model, body) endpoint := strings.TrimSuffix(baseURL, "/") + iflowDefaultEndpoint @@ -157,6 +158,7 @@ func (e *IFlowExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au if errValidate := ValidateThinkingConfig(body, upstreamModel); errValidate != nil { return nil, errValidate } + body = applyIFlowThinkingConfig(body) // Ensure tools array exists to avoid provider quirks similar to Qwen's behaviour. toolsResult := gjson.GetBytes(body, "tools") if toolsResult.Exists() && toolsResult.IsArray() && len(toolsResult.Array()) == 0 { @@ -442,3 +444,21 @@ func ensureToolsArray(body []byte) []byte { } return updated } + +// applyIFlowThinkingConfig converts normalized reasoning_effort to iFlow chat_template_kwargs.enable_thinking. +// This should be called after NormalizeThinkingConfig has processed the payload. +// iFlow only supports boolean enable_thinking, so any non-"none" effort enables thinking. +func applyIFlowThinkingConfig(body []byte) []byte { + effort := gjson.GetBytes(body, "reasoning_effort") + if !effort.Exists() { + return body + } + + val := strings.ToLower(strings.TrimSpace(effort.String())) + enableThinking := val != "none" && val != "" + + body, _ = sjson.DeleteBytes(body, "reasoning_effort") + body, _ = sjson.SetBytes(body, "chat_template_kwargs.enable_thinking", enableThinking) + + return body +} diff --git a/internal/runtime/executor/payload_helpers.go b/internal/runtime/executor/payload_helpers.go index b0eafbb7..ff2d6ab4 100644 --- a/internal/runtime/executor/payload_helpers.go +++ b/internal/runtime/executor/payload_helpers.go @@ -273,7 +273,7 @@ func StripThinkingFields(payload []byte, effortOnly bool) []byte { "reasoning.effort", } if !effortOnly { - fieldsToRemove = append([]string{"reasoning"}, fieldsToRemove...) + fieldsToRemove = append([]string{"reasoning", "thinking"}, fieldsToRemove...) } out := payload for _, field := range fieldsToRemove { From 28a428ae2f8b8e2b96d749c07496b51a598eaa70 Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Tue, 16 Dec 2025 18:07:20 +0800 Subject: [PATCH 2/3] fix(thinking): align budget effort mapping across translators Unify thinking budget-to-effort conversion in a shared helper, handle disabled/default thinking cases in translators, adjust zero-budget mapping, and drop the old OpenAI-specific helper with updated tests. --- internal/runtime/executor/payload_helpers.go | 8 +- .../codex/claude/codex_claude_request.go | 9 ++- .../codex/gemini/codex_gemini_request.go | 2 +- .../openai/claude/openai_claude_request.go | 20 ++++- .../openai/gemini/openai_gemini_request.go | 2 +- internal/util/openai_thinking.go | 37 --------- internal/util/thinking.go | 80 +++++++++++++++++++ internal/util/thinking_suffix.go | 30 ------- test/thinking_conversion_test.go | 20 ++--- 9 files changed, 116 insertions(+), 92 deletions(-) delete mode 100644 internal/util/openai_thinking.go diff --git a/internal/runtime/executor/payload_helpers.go b/internal/runtime/executor/payload_helpers.go index ff2d6ab4..adb224a8 100644 --- a/internal/runtime/executor/payload_helpers.go +++ b/internal/runtime/executor/payload_helpers.go @@ -72,13 +72,7 @@ func ApplyReasoningEffortMetadata(payload []byte, metadata map[string]any, model // Fallback: numeric thinking_budget suffix for level-based (OpenAI-style) models. if util.ModelUsesThinkingLevels(baseModel) || allowCompat { if budget, _, _, matched := util.ThinkingFromMetadata(metadata); matched && budget != nil { - if effort, ok := util.OpenAIThinkingBudgetToEffort(baseModel, *budget); ok && effort != "" { - if *budget == 0 && effort == "none" && util.ModelUsesThinkingLevels(baseModel) { - if _, supported := util.NormalizeReasoningEffortLevel(baseModel, effort); !supported { - return StripThinkingFields(payload, false) - } - } - + if effort, ok := util.ThinkingBudgetToEffort(baseModel, *budget); ok && effort != "" { if updated, err := sjson.SetBytes(payload, field, effort); err == nil { return updated } diff --git a/internal/translator/codex/claude/codex_claude_request.go b/internal/translator/codex/claude/codex_claude_request.go index 414efa89..41fd2764 100644 --- a/internal/translator/codex/claude/codex_claude_request.go +++ b/internal/translator/codex/claude/codex_claude_request.go @@ -219,15 +219,20 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool) // Convert thinking.budget_tokens to reasoning.effort for level-based models reasoningEffort := "medium" // default if thinking := rootResult.Get("thinking"); thinking.Exists() && thinking.IsObject() { - if thinking.Get("type").String() == "enabled" { + switch thinking.Get("type").String() { + case "enabled": if util.ModelUsesThinkingLevels(modelName) { if budgetTokens := thinking.Get("budget_tokens"); budgetTokens.Exists() { budget := int(budgetTokens.Int()) - if effort, ok := util.OpenAIThinkingBudgetToEffort(modelName, budget); ok && effort != "" { + if effort, ok := util.ThinkingBudgetToEffort(modelName, budget); ok && effort != "" { reasoningEffort = effort } } } + case "disabled": + if effort, ok := util.ThinkingBudgetToEffort(modelName, 0); ok && effort != "" { + reasoningEffort = effort + } } } template, _ = sjson.Set(template, "reasoning.effort", reasoningEffort) diff --git a/internal/translator/codex/gemini/codex_gemini_request.go b/internal/translator/codex/gemini/codex_gemini_request.go index c2dacd3e..91a38029 100644 --- a/internal/translator/codex/gemini/codex_gemini_request.go +++ b/internal/translator/codex/gemini/codex_gemini_request.go @@ -253,7 +253,7 @@ func ConvertGeminiRequestToCodex(modelName string, inputRawJSON []byte, _ bool) if util.ModelUsesThinkingLevels(modelName) { if thinkingBudget := thinkingConfig.Get("thinkingBudget"); thinkingBudget.Exists() { budget := int(thinkingBudget.Int()) - if effort, ok := util.OpenAIThinkingBudgetToEffort(modelName, budget); ok && effort != "" { + if effort, ok := util.ThinkingBudgetToEffort(modelName, budget); ok && effort != "" { reasoningEffort = effort } } diff --git a/internal/translator/openai/claude/openai_claude_request.go b/internal/translator/openai/claude/openai_claude_request.go index 0ee8c225..e61ec521 100644 --- a/internal/translator/openai/claude/openai_claude_request.go +++ b/internal/translator/openai/claude/openai_claude_request.go @@ -63,10 +63,22 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream // Thinking: Convert Claude thinking.budget_tokens to OpenAI reasoning_effort if thinking := root.Get("thinking"); thinking.Exists() && thinking.IsObject() { - if thinkingType := thinking.Get("type"); thinkingType.Exists() && thinkingType.String() == "enabled" { - if budgetTokens := thinking.Get("budget_tokens"); budgetTokens.Exists() { - budget := int(budgetTokens.Int()) - if effort, ok := util.OpenAIThinkingBudgetToEffort(modelName, budget); ok && effort != "" { + if thinkingType := thinking.Get("type"); thinkingType.Exists() { + switch thinkingType.String() { + case "enabled": + if budgetTokens := thinking.Get("budget_tokens"); budgetTokens.Exists() { + budget := int(budgetTokens.Int()) + if effort, ok := util.ThinkingBudgetToEffort(modelName, budget); ok && effort != "" { + out, _ = sjson.Set(out, "reasoning_effort", effort) + } + } else { + // No budget_tokens specified, default to "auto" for enabled thinking + if effort, ok := util.ThinkingBudgetToEffort(modelName, -1); ok && effort != "" { + out, _ = sjson.Set(out, "reasoning_effort", effort) + } + } + case "disabled": + if effort, ok := util.ThinkingBudgetToEffort(modelName, 0); ok && effort != "" { out, _ = sjson.Set(out, "reasoning_effort", effort) } } diff --git a/internal/translator/openai/gemini/openai_gemini_request.go b/internal/translator/openai/gemini/openai_gemini_request.go index cca6ebf7..032ca60d 100644 --- a/internal/translator/openai/gemini/openai_gemini_request.go +++ b/internal/translator/openai/gemini/openai_gemini_request.go @@ -83,7 +83,7 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream if thinkingConfig := genConfig.Get("thinkingConfig"); thinkingConfig.Exists() && thinkingConfig.IsObject() { if thinkingBudget := thinkingConfig.Get("thinkingBudget"); thinkingBudget.Exists() { budget := int(thinkingBudget.Int()) - if effort, ok := util.OpenAIThinkingBudgetToEffort(modelName, budget); ok && effort != "" { + if effort, ok := util.ThinkingBudgetToEffort(modelName, budget); ok && effort != "" { out, _ = sjson.Set(out, "reasoning_effort", effort) } } diff --git a/internal/util/openai_thinking.go b/internal/util/openai_thinking.go deleted file mode 100644 index 5ce7e6bf..00000000 --- a/internal/util/openai_thinking.go +++ /dev/null @@ -1,37 +0,0 @@ -package util - -// OpenAIThinkingBudgetToEffort maps a numeric thinking budget (tokens) -// into an OpenAI-style reasoning effort level for level-based models. -// -// Ranges: -// - 0 -> "none" -// - -1 -> "auto" -// - 1..1024 -> "low" -// - 1025..8192 -> "medium" -// - 8193..24576 -> "high" -// - 24577.. -> highest supported level for the model (defaults to "xhigh") -// -// Negative values other than -1 are treated as unsupported. -func OpenAIThinkingBudgetToEffort(model string, budget int) (string, bool) { - switch { - case budget == -1: - return "auto", true - case budget < -1: - return "", false - case budget == 0: - return "none", true - case budget > 0 && budget <= 1024: - return "low", true - case budget <= 8192: - return "medium", true - case budget <= 24576: - return "high", true - case budget > 24576: - if levels := GetModelThinkingLevels(model); len(levels) > 0 { - return levels[len(levels)-1], true - } - return "xhigh", true - default: - return "", false - } -} diff --git a/internal/util/thinking.go b/internal/util/thinking.go index 793134fc..77ec16ba 100644 --- a/internal/util/thinking.go +++ b/internal/util/thinking.go @@ -118,3 +118,83 @@ func IsOpenAICompatibilityModel(model string) bool { } return strings.EqualFold(strings.TrimSpace(info.Type), "openai-compatibility") } + +// ThinkingEffortToBudget maps a reasoning effort level to a numeric thinking budget (tokens), +// clamping the result to the model's supported range. +// +// Mappings (values are normalized to model's supported range): +// - "none" -> 0 +// - "auto" -> -1 +// - "minimal" -> 512 +// - "low" -> 1024 +// - "medium" -> 8192 +// - "high" -> 24576 +// - "xhigh" -> 32768 +// +// Returns false when the effort level is empty or unsupported. +func ThinkingEffortToBudget(model, effort string) (int, bool) { + if effort == "" { + return 0, false + } + normalized, ok := NormalizeReasoningEffortLevel(model, effort) + if !ok { + normalized = strings.ToLower(strings.TrimSpace(effort)) + } + switch normalized { + case "none": + return 0, true + case "auto": + return NormalizeThinkingBudget(model, -1), true + case "minimal": + return NormalizeThinkingBudget(model, 512), true + case "low": + return NormalizeThinkingBudget(model, 1024), true + case "medium": + return NormalizeThinkingBudget(model, 8192), true + case "high": + return NormalizeThinkingBudget(model, 24576), true + case "xhigh": + return NormalizeThinkingBudget(model, 32768), true + default: + return 0, false + } +} + +// ThinkingBudgetToEffort maps a numeric thinking budget (tokens) +// to a reasoning effort level for level-based models. +// +// Mappings: +// - 0 -> "none" (or lowest supported level if model doesn't support "none") +// - -1 -> "auto" +// - 1..1024 -> "low" +// - 1025..8192 -> "medium" +// - 8193..24576 -> "high" +// - 24577.. -> highest supported level for the model (defaults to "xhigh") +// +// Returns false when the budget is unsupported (negative values other than -1). +func ThinkingBudgetToEffort(model string, budget int) (string, bool) { + switch { + case budget == -1: + return "auto", true + case budget < -1: + return "", false + case budget == 0: + if levels := GetModelThinkingLevels(model); len(levels) > 0 { + return levels[0], true + } + return "none", true + case budget > 0 && budget <= 1024: + return "low", true + case budget <= 8192: + return "medium", true + case budget <= 24576: + return "high", true + case budget > 24576: + if levels := GetModelThinkingLevels(model); len(levels) > 0 { + return levels[len(levels)-1], true + } + return "xhigh", true + default: + return "", false + } +} diff --git a/internal/util/thinking_suffix.go b/internal/util/thinking_suffix.go index b877e109..ff3b24a6 100644 --- a/internal/util/thinking_suffix.go +++ b/internal/util/thinking_suffix.go @@ -201,36 +201,6 @@ func ReasoningEffortFromMetadata(metadata map[string]any) (string, bool) { return "", true } -// ThinkingEffortToBudget maps reasoning effort levels to approximate budgets, -// clamping the result to the model's supported range. -func ThinkingEffortToBudget(model, effort string) (int, bool) { - if effort == "" { - return 0, false - } - normalized, ok := NormalizeReasoningEffortLevel(model, effort) - if !ok { - normalized = strings.ToLower(strings.TrimSpace(effort)) - } - switch normalized { - case "none": - return 0, true - case "auto": - return NormalizeThinkingBudget(model, -1), true - case "minimal": - return NormalizeThinkingBudget(model, 512), true - case "low": - return NormalizeThinkingBudget(model, 1024), true - case "medium": - return NormalizeThinkingBudget(model, 8192), true - case "high": - return NormalizeThinkingBudget(model, 24576), true - case "xhigh": - return NormalizeThinkingBudget(model, 32768), true - default: - return 0, false - } -} - // ResolveOriginalModel returns the original model name stored in metadata (if present), // otherwise falls back to the provided model. func ResolveOriginalModel(model string, metadata map[string]any) string { diff --git a/test/thinking_conversion_test.go b/test/thinking_conversion_test.go index 6d156954..d93ff648 100644 --- a/test/thinking_conversion_test.go +++ b/test/thinking_conversion_test.go @@ -295,7 +295,7 @@ func TestThinkingConversionsAcrossProtocolsAndModels(t *testing.T) { } // Check numeric budget fallback for allowCompat if budget, _, _, matched := util.ThinkingFromMetadata(metadata); matched && budget != nil { - if mapped, okMap := util.OpenAIThinkingBudgetToEffort(normalizedModel, *budget); okMap && mapped != "" { + if mapped, okMap := util.ThinkingBudgetToEffort(normalizedModel, *budget); okMap && mapped != "" { return true, mapped, false } } @@ -308,7 +308,7 @@ func TestThinkingConversionsAcrossProtocolsAndModels(t *testing.T) { effort, ok := util.ReasoningEffortFromMetadata(metadata) if !ok || strings.TrimSpace(effort) == "" { if budget, _, _, matched := util.ThinkingFromMetadata(metadata); matched && budget != nil { - if mapped, okMap := util.OpenAIThinkingBudgetToEffort(normalizedModel, *budget); okMap { + if mapped, okMap := util.ThinkingBudgetToEffort(normalizedModel, *budget); okMap { effort = mapped ok = true } @@ -336,7 +336,7 @@ func TestThinkingConversionsAcrossProtocolsAndModels(t *testing.T) { return false, "", true } if budget, _, _, matched := util.ThinkingFromMetadata(metadata); matched && budget != nil { - if mapped, okMap := util.OpenAIThinkingBudgetToEffort(normalizedModel, *budget); okMap && mapped != "" { + if mapped, okMap := util.ThinkingBudgetToEffort(normalizedModel, *budget); okMap && mapped != "" { mapped = strings.ToLower(strings.TrimSpace(mapped)) if normalized, okLevel := util.NormalizeReasoningEffortLevel(normalizedModel, mapped); okLevel { return true, normalized, false @@ -609,7 +609,7 @@ func TestRawPayloadThinkingConversions(t *testing.T) { return true, normalized, false } if budget, ok := cs.thinkingParam.(int); ok { - if mapped, okM := util.OpenAIThinkingBudgetToEffort(model, budget); okM && mapped != "" { + if mapped, okM := util.ThinkingBudgetToEffort(model, budget); okM && mapped != "" { return true, mapped, false } } @@ -625,7 +625,7 @@ func TestRawPayloadThinkingConversions(t *testing.T) { return false, "", true // invalid level } if budget, ok := cs.thinkingParam.(int); ok { - if mapped, okM := util.OpenAIThinkingBudgetToEffort(model, budget); okM && mapped != "" { + if mapped, okM := util.ThinkingBudgetToEffort(model, budget); okM && mapped != "" { // Check if the mapped effort is valid for this model if _, validLevel := util.NormalizeReasoningEffortLevel(model, mapped); !validLevel { return true, mapped, true // expect validation error @@ -646,7 +646,7 @@ func TestRawPayloadThinkingConversions(t *testing.T) { return false, "", true } if budget, ok := cs.thinkingParam.(int); ok { - if mapped, okM := util.OpenAIThinkingBudgetToEffort(model, budget); okM && mapped != "" { + if mapped, okM := util.ThinkingBudgetToEffort(model, budget); okM && mapped != "" { // Check if the mapped effort is valid for this model if _, validLevel := util.NormalizeReasoningEffortLevel(model, mapped); !validLevel { return true, mapped, true // expect validation error @@ -721,7 +721,7 @@ func TestRawPayloadThinkingConversions(t *testing.T) { } } -func TestOpenAIThinkingBudgetToEffortRanges(t *testing.T) { +func TestThinkingBudgetToEffortRanges(t *testing.T) { cleanup := registerCoreModels(t) defer cleanup() @@ -733,7 +733,7 @@ func TestOpenAIThinkingBudgetToEffortRanges(t *testing.T) { ok bool }{ {name: "dynamic-auto", model: "gpt-5", budget: -1, want: "auto", ok: true}, - {name: "zero-none", model: "gpt-5", budget: 0, want: "none", ok: true}, + {name: "zero-none", model: "gpt-5", budget: 0, want: "minimal", ok: true}, {name: "low-min", model: "gpt-5", budget: 1, want: "low", ok: true}, {name: "low-max", model: "gpt-5", budget: 1024, want: "low", ok: true}, {name: "medium-min", model: "gpt-5", budget: 1025, want: "medium", ok: true}, @@ -741,14 +741,14 @@ func TestOpenAIThinkingBudgetToEffortRanges(t *testing.T) { {name: "high-min", model: "gpt-5", budget: 8193, want: "high", ok: true}, {name: "high-max", model: "gpt-5", budget: 24576, want: "high", ok: true}, {name: "over-max-clamps-to-highest", model: "gpt-5", budget: 64000, want: "high", ok: true}, - {name: "over-max-xhigh-model", model: "gpt-5.2", budget: 50000, want: "xhigh", ok: true}, + {name: "over-max-xhigh-model", model: "gpt-5.2", budget: 64000, want: "xhigh", ok: true}, {name: "negative-unsupported", model: "gpt-5", budget: -5, want: "", ok: false}, } for _, cs := range cases { cs := cs t.Run(cs.name, func(t *testing.T) { - got, ok := util.OpenAIThinkingBudgetToEffort(cs.model, cs.budget) + got, ok := util.ThinkingBudgetToEffort(cs.model, cs.budget) if ok != cs.ok { t.Fatalf("ok mismatch for model=%s budget=%d: expect %v got %v", cs.model, cs.budget, cs.ok, ok) } From 9df96a4bb406ace21ddc800418051e3130bdcdec Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Tue, 16 Dec 2025 18:29:34 +0800 Subject: [PATCH 3/3] test(thinking): add effort to budget coverage --- test/thinking_conversion_test.go | 40 +++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/test/thinking_conversion_test.go b/test/thinking_conversion_test.go index d93ff648..74a1bd8a 100644 --- a/test/thinking_conversion_test.go +++ b/test/thinking_conversion_test.go @@ -721,7 +721,7 @@ func TestRawPayloadThinkingConversions(t *testing.T) { } } -func TestThinkingBudgetToEffortRanges(t *testing.T) { +func TestThinkingBudgetToEffort(t *testing.T) { cleanup := registerCoreModels(t) defer cleanup() @@ -758,3 +758,41 @@ func TestThinkingBudgetToEffortRanges(t *testing.T) { }) } } + +func TestThinkingEffortToBudget(t *testing.T) { + cleanup := registerCoreModels(t) + defer cleanup() + + cases := []struct { + name string + model string + effort string + want int + ok bool + }{ + {name: "none", model: "gemini-2.5-pro", effort: "none", want: 0, ok: true}, + {name: "auto", model: "gemini-2.5-pro", effort: "auto", want: -1, ok: true}, + {name: "minimal", model: "gemini-2.5-pro", effort: "minimal", want: 512, ok: true}, + {name: "low", model: "gemini-2.5-pro", effort: "low", want: 1024, ok: true}, + {name: "medium", model: "gemini-2.5-pro", effort: "medium", want: 8192, ok: true}, + {name: "high", model: "gemini-2.5-pro", effort: "high", want: 24576, ok: true}, + {name: "xhigh", model: "gemini-2.5-pro", effort: "xhigh", want: 32768, ok: true}, + {name: "empty-unsupported", model: "gemini-2.5-pro", effort: "", want: 0, ok: false}, + {name: "invalid-unsupported", model: "gemini-2.5-pro", effort: "ultra", want: 0, ok: false}, + {name: "case-insensitive", model: "gemini-2.5-pro", effort: "LOW", want: 1024, ok: true}, + {name: "case-insensitive-medium", model: "gemini-2.5-pro", effort: "MEDIUM", want: 8192, ok: true}, + } + + for _, cs := range cases { + cs := cs + t.Run(cs.name, func(t *testing.T) { + got, ok := util.ThinkingEffortToBudget(cs.model, cs.effort) + if ok != cs.ok { + t.Fatalf("ok mismatch for model=%s effort=%s: expect %v got %v", cs.model, cs.effort, cs.ok, ok) + } + if got != cs.want { + t.Fatalf("value mismatch for model=%s effort=%s: expect %d got %d", cs.model, cs.effort, cs.want, got) + } + }) + } +}