fix: clamp variant "max" to "high" for non-Opus Claude models on fallback
When an agent configured with variant: "max" falls back from Opus to Sonnet (or Haiku), the "max" variant was passed through unchanged. OpenCode sends this as level: "max" to the Anthropic API, which rejects it with: level "max" not supported, valid levels: low, medium, high The anthropic-effort hook previously only handled Opus (inject effort=max) and skipped all other Claude models. Now it actively clamps "max" → "high" for non-Opus Claude models and mutates message.variant so OpenCode doesn't pass the unsupported level to the API.
This commit is contained in:
@@ -28,6 +28,20 @@ interface ChatParamsOutput {
|
|||||||
options: Record<string, unknown>
|
options: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valid thinking budget levels per model tier.
|
||||||
|
* Opus supports "max"; all other Claude models cap at "high".
|
||||||
|
*/
|
||||||
|
const MAX_VARIANT_BY_TIER: Record<string, string> = {
|
||||||
|
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() {
|
export function createAnthropicEffortHook() {
|
||||||
return {
|
return {
|
||||||
"chat.params": async (
|
"chat.params": async (
|
||||||
@@ -38,15 +52,27 @@ export function createAnthropicEffortHook() {
|
|||||||
if (!model?.modelID || !model?.providerID) return
|
if (!model?.modelID || !model?.providerID) return
|
||||||
if (message.variant !== "max") return
|
if (message.variant !== "max") return
|
||||||
if (!isClaudeProvider(model.providerID, model.modelID)) return
|
if (!isClaudeProvider(model.providerID, model.modelID)) return
|
||||||
if (!isOpus46(model.modelID)) return
|
|
||||||
if (output.options.effort !== undefined) return
|
if (output.options.effort !== undefined) return
|
||||||
|
|
||||||
output.options.effort = "max"
|
const opus = isOpus46(model.modelID)
|
||||||
log("anthropic-effort: injected effort=max", {
|
const clamped = clampVariant(message.variant, opus)
|
||||||
sessionID: input.sessionID,
|
output.options.effort = clamped
|
||||||
provider: model.providerID,
|
|
||||||
model: model.modelID,
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,8 +143,8 @@ describe("createAnthropicEffortHook", () => {
|
|||||||
expect(output.options.effort).toBeUndefined()
|
expect(output.options.effort).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should NOT inject effort for non-opus model", async () => {
|
it("should clamp effort to high for non-opus claude model with variant max", async () => {
|
||||||
//#given claude-sonnet-4-6 (not opus)
|
//#given claude-sonnet-4-6 (not opus) with variant max
|
||||||
const hook = createAnthropicEffortHook()
|
const hook = createAnthropicEffortHook()
|
||||||
const { input, output } = createMockParams({
|
const { input, output } = createMockParams({
|
||||||
modelID: "claude-sonnet-4-6",
|
modelID: "claude-sonnet-4-6",
|
||||||
@@ -153,8 +153,9 @@ describe("createAnthropicEffortHook", () => {
|
|||||||
//#when chat.params hook is called
|
//#when chat.params hook is called
|
||||||
await hook["chat.params"](input, output)
|
await hook["chat.params"](input, output)
|
||||||
|
|
||||||
//#then effort should NOT be injected
|
//#then effort should be clamped to high (not max)
|
||||||
expect(output.options.effort).toBeUndefined()
|
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 () => {
|
it("should NOT inject effort for non-anthropic provider with non-claude model", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user