diff --git a/src/hooks/anthropic-effort/hook.ts b/src/hooks/anthropic-effort/hook.ts index 16e2656c2..ef6c98529 100644 --- a/src/hooks/anthropic-effort/hook.ts +++ b/src/hooks/anthropic-effort/hook.ts @@ -28,6 +28,20 @@ interface ChatParamsOutput { options: Record } +/** + * Valid thinking budget levels per model tier. + * Opus supports "max"; all other Claude models cap at "high". + */ +const MAX_VARIANT_BY_TIER: Record = { + opus: "max", + default: "high", +} + +function clampVariant(variant: string, isOpus: boolean): string { + if (variant !== "max") return variant + return isOpus ? MAX_VARIANT_BY_TIER.opus : MAX_VARIANT_BY_TIER.default +} + export function createAnthropicEffortHook() { return { "chat.params": async ( @@ -38,15 +52,27 @@ export function createAnthropicEffortHook() { if (!model?.modelID || !model?.providerID) return if (message.variant !== "max") return if (!isClaudeProvider(model.providerID, model.modelID)) return - if (!isOpus46(model.modelID)) return if (output.options.effort !== undefined) return - output.options.effort = "max" - log("anthropic-effort: injected effort=max", { - sessionID: input.sessionID, - provider: model.providerID, - model: model.modelID, - }) + const opus = isOpus46(model.modelID) + const clamped = clampVariant(message.variant, opus) + output.options.effort = clamped + + if (!opus) { + // Override the variant so OpenCode doesn't pass "max" to the API + ;(message as { variant?: string }).variant = clamped + log("anthropic-effort: clamped variant max→high for non-Opus model", { + sessionID: input.sessionID, + provider: model.providerID, + model: model.modelID, + }) + } else { + log("anthropic-effort: injected effort=max", { + sessionID: input.sessionID, + provider: model.providerID, + model: model.modelID, + }) + } }, } } diff --git a/src/hooks/anthropic-effort/index.test.ts b/src/hooks/anthropic-effort/index.test.ts index 0ce7e0ccb..666a5c2ac 100644 --- a/src/hooks/anthropic-effort/index.test.ts +++ b/src/hooks/anthropic-effort/index.test.ts @@ -143,8 +143,8 @@ describe("createAnthropicEffortHook", () => { expect(output.options.effort).toBeUndefined() }) - it("should NOT inject effort for non-opus model", async () => { - //#given claude-sonnet-4-6 (not opus) + it("should clamp effort to high for non-opus claude model with variant max", async () => { + //#given claude-sonnet-4-6 (not opus) with variant max const hook = createAnthropicEffortHook() const { input, output } = createMockParams({ modelID: "claude-sonnet-4-6", @@ -153,8 +153,9 @@ describe("createAnthropicEffortHook", () => { //#when chat.params hook is called await hook["chat.params"](input, output) - //#then effort should NOT be injected - expect(output.options.effort).toBeUndefined() + //#then effort should be clamped to high (not max) + expect(output.options.effort).toBe("high") + expect(input.message.variant).toBe("high") }) it("should NOT inject effort for non-anthropic provider with non-claude model", async () => {