diff --git a/internal/runtime/executor/claude_executor.go b/internal/runtime/executor/claude_executor.go index c7470954..6af08608 100644 --- a/internal/runtime/executor/claude_executor.go +++ b/internal/runtime/executor/claude_executor.go @@ -450,59 +450,15 @@ func extractAndRemoveBetas(body []byte) ([]string, []byte) { return betas, body } -// injectThinkingConfig adds thinking configuration based on metadata or legacy suffixes. +// injectThinkingConfig adds thinking configuration based on metadata using the unified flow. +// It uses util.ResolveClaudeThinkingConfig which internally calls ResolveThinkingConfigFromMetadata +// and NormalizeThinkingBudget, ensuring consistency with other executors like Gemini. func (e *ClaudeExecutor) injectThinkingConfig(modelName string, metadata map[string]any, body []byte) []byte { - // Only inject if thinking config is not already present - if gjson.GetBytes(body, "thinking").Exists() { + budget, ok := util.ResolveClaudeThinkingConfig(modelName, metadata) + if !ok { return body } - - budgetTokens, ok := resolveClaudeThinkingBudget(modelName, metadata) - if !ok || budgetTokens <= 0 { - return body - } - - body, _ = sjson.SetBytes(body, "thinking.type", "enabled") - body, _ = sjson.SetBytes(body, "thinking.budget_tokens", budgetTokens) - return body -} - -func resolveClaudeThinkingBudget(modelName string, metadata map[string]any) (int, bool) { - budget, include, effort, matched := util.ThinkingFromMetadata(metadata) - if matched { - if include != nil && !*include { - return 0, false - } - if budget != nil { - normalized := util.NormalizeThinkingBudget(modelName, *budget) - if normalized > 0 { - return normalized, true - } - return 0, false - } - if effort != nil { - if derived, ok := util.ThinkingEffortToBudget(modelName, *effort); ok && derived > 0 { - return derived, true - } - } - } - return claudeBudgetFromSuffix(modelName) -} - -func claudeBudgetFromSuffix(modelName string) (int, bool) { - lower := strings.ToLower(strings.TrimSpace(modelName)) - switch { - case strings.HasSuffix(lower, "-thinking-low"): - return 1024, true - case strings.HasSuffix(lower, "-thinking-medium"): - return 8192, true - case strings.HasSuffix(lower, "-thinking-high"): - return 24576, true - case strings.HasSuffix(lower, "-thinking"): - return 8192, true - default: - return 0, false - } + return util.ApplyClaudeThinkingConfig(body, budget) } // ensureMaxTokensForThinking ensures max_tokens > thinking.budget_tokens when thinking is enabled. diff --git a/internal/util/claude_thinking.go b/internal/util/claude_thinking.go new file mode 100644 index 00000000..b0c5a0a2 --- /dev/null +++ b/internal/util/claude_thinking.go @@ -0,0 +1,46 @@ +package util + +import ( + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" +) + +// ApplyClaudeThinkingConfig applies thinking configuration to a Claude API request payload. +// It sets the thinking.type to "enabled" and thinking.budget_tokens to the specified budget. +// If budget is nil or the payload already has thinking config, it returns the payload unchanged. +func ApplyClaudeThinkingConfig(body []byte, budget *int) []byte { + if budget == nil { + return body + } + if gjson.GetBytes(body, "thinking").Exists() { + return body + } + if *budget <= 0 { + return body + } + updated := body + updated, _ = sjson.SetBytes(updated, "thinking.type", "enabled") + updated, _ = sjson.SetBytes(updated, "thinking.budget_tokens", *budget) + return updated +} + +// ResolveClaudeThinkingConfig resolves thinking configuration from metadata for Claude models. +// It uses the unified ResolveThinkingConfigFromMetadata and normalizes the budget. +// Returns the normalized budget (nil if thinking should not be enabled) and whether it matched. +func ResolveClaudeThinkingConfig(modelName string, metadata map[string]any) (*int, bool) { + budget, include, matched := ResolveThinkingConfigFromMetadata(modelName, metadata) + if !matched { + return nil, false + } + if include != nil && !*include { + return nil, true + } + if budget == nil { + return nil, true + } + normalized := NormalizeThinkingBudget(modelName, *budget) + if normalized <= 0 { + return nil, true + } + return &normalized, true +}