// Package claude implements thinking configuration scaffolding for Claude models. // // Claude models use the thinking.budget_tokens format with values in the range // 1024-128000. Some Claude models support ZeroAllowed (sonnet-4-5, opus-4-5), // while older models do not. // See: _bmad-output/planning-artifacts/architecture.md#Epic-6 package claude import ( "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) // Applier implements thinking.ProviderApplier for Claude models. // This applier is stateless and holds no configuration. type Applier struct{} // NewApplier creates a new Claude thinking applier. func NewApplier() *Applier { return &Applier{} } func init() { thinking.RegisterProvider("claude", NewApplier()) } // Apply applies thinking configuration to Claude request body. // // IMPORTANT: This method expects config to be pre-validated by thinking.ValidateConfig. // ValidateConfig handles: // - Mode conversion (Level→Budget, Auto→Budget) // - Budget clamping to model range // - ZeroAllowed constraint enforcement // // Apply only processes ModeBudget and ModeNone; other modes are passed through unchanged. // // Expected output format when enabled: // // { // "thinking": { // "type": "enabled", // "budget_tokens": 16384 // } // } // // Expected output format when disabled: // // { // "thinking": { // "type": "disabled" // } // } func (a *Applier) Apply(body []byte, config thinking.ThinkingConfig, modelInfo *registry.ModelInfo) ([]byte, error) { if thinking.IsUserDefinedModel(modelInfo) { return applyCompatibleClaude(body, config) } if modelInfo.Thinking == nil { return body, nil } // Only process ModeBudget and ModeNone; other modes pass through // (caller should use ValidateConfig first to normalize modes) if config.Mode != thinking.ModeBudget && config.Mode != thinking.ModeNone { return body, nil } if len(body) == 0 || !gjson.ValidBytes(body) { body = []byte(`{}`) } // Budget is expected to be pre-validated by ValidateConfig (clamped, ZeroAllowed enforced) // Decide enabled/disabled based on budget value if config.Budget == 0 { result, _ := sjson.SetBytes(body, "thinking.type", "disabled") result, _ = sjson.DeleteBytes(result, "thinking.budget_tokens") return result, nil } result, _ := sjson.SetBytes(body, "thinking.type", "enabled") result, _ = sjson.SetBytes(result, "thinking.budget_tokens", config.Budget) return result, nil } func applyCompatibleClaude(body []byte, config thinking.ThinkingConfig) ([]byte, error) { if config.Mode != thinking.ModeBudget && config.Mode != thinking.ModeNone && config.Mode != thinking.ModeAuto { return body, nil } if len(body) == 0 || !gjson.ValidBytes(body) { body = []byte(`{}`) } switch config.Mode { case thinking.ModeNone: result, _ := sjson.SetBytes(body, "thinking.type", "disabled") result, _ = sjson.DeleteBytes(result, "thinking.budget_tokens") return result, nil case thinking.ModeAuto: result, _ := sjson.SetBytes(body, "thinking.type", "enabled") result, _ = sjson.DeleteBytes(result, "thinking.budget_tokens") return result, nil default: result, _ := sjson.SetBytes(body, "thinking.type", "enabled") result, _ = sjson.SetBytes(result, "thinking.budget_tokens", config.Budget) return result, nil } }