Files
oh-my-opencode/src/hooks/anthropic-effort/hook.ts
T
YeonGyu-Kim 1ae32283ef fix(anthropic-effort): clamp variant=max for github-copilot Claude models
GitHub Copilot proxies Anthropic's API but does not support
output_config.effort: "max" (same constraint as Anthropic OAuth).

Previously the anthropic-effort hook early-returned for github-copilot
provider, skipping all effort clamping. Users on github-copilot/claude-opus-4.6
with variant=max got HTTP 400 'invalid_reasoning_effort'.

Fix: Remove the github-copilot early return. Treat github-copilot as a
constrained provider (alongside Anthropic OAuth), clamping max→high.

Rename isAnthropicOAuth→isConstrainedProvider to reflect the broader scope.

Fixes the remaining user report from #3429.
2026-04-16 18:41:09 +09:00

113 lines
3.8 KiB
TypeScript

import { isProviderUsingOAuth, log, normalizeModelID } from "../../shared"
const OPUS_PATTERN = /claude-.*opus/i
const EFFORT_UNSUPPORTED_PATTERN = /claude-.*haiku/i
const INTERNAL_SKIP_AGENTS = new Set(["title", "summary", "compaction"])
function isClaudeProvider(providerID: string, modelID: string): boolean {
if (["anthropic", "google-vertex-anthropic", "opencode"].includes(providerID)) return true
if (providerID === "github-copilot" && modelID.toLowerCase().includes("claude")) return true
return false
}
function isOpusModel(modelID: string): boolean {
const normalized = normalizeModelID(modelID)
return OPUS_PATTERN.test(normalized)
}
function isEffortUnsupportedModel(modelID: string): boolean {
const normalized = normalizeModelID(modelID)
return EFFORT_UNSUPPORTED_PATTERN.test(normalized)
}
function shouldSkipForInternalAgent(agentName: string | undefined): boolean {
if (!agentName) return false
return INTERNAL_SKIP_AGENTS.has(agentName.trim().toLowerCase())
}
/**
* Providers that expose constrained APIs rejecting `output_config.effort: "max"`
* (supported values: low | medium | high). Includes:
* - Anthropic OAuth (Claude Pro/Max via third-party clients)
* - GitHub Copilot (proxied Anthropic, doesn't support "max")
*/
function isConstrainedProvider(providerID: string): boolean {
if (providerID === "github-copilot") return true
if (providerID === "anthropic") return isProviderUsingOAuth(providerID)
return false
}
interface ChatParamsInput {
sessionID: string
agent: { name?: string }
model: { providerID: string; modelID: string }
provider: { id: string }
message: { variant?: string }
}
interface ChatParamsOutput {
temperature?: number
topP?: number
topK?: number
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, isConstrained: boolean): string {
if (variant !== "max") return variant
if (isConstrained) return MAX_VARIANT_BY_TIER.default
return isOpus ? MAX_VARIANT_BY_TIER.opus : MAX_VARIANT_BY_TIER.default
}
export function createAnthropicEffortHook() {
return {
"chat.params": async (
input: ChatParamsInput,
output: ChatParamsOutput
): Promise<void> => {
const { agent, model, message } = input
if (!model?.modelID || !model?.providerID) return
if (isEffortUnsupportedModel(model.modelID)) return
if (message.variant !== "max") return
if (!isClaudeProvider(model.providerID, model.modelID)) return
if (shouldSkipForInternalAgent(agent?.name)) return
if (output.options.effort !== undefined) return
const opus = isOpusModel(model.modelID)
const constrained = isConstrainedProvider(model.providerID)
const clamped = clampVariant(message.variant, opus, constrained)
output.options.effort = clamped
const shouldOverrideMessageVariant = !opus || constrained
if (shouldOverrideMessageVariant) {
// Override the variant so OpenCode doesn't pass "max" to the API.
// Non-Opus models cap at high; Anthropic OAuth (Claude Pro/Max) also
// caps at high even on Opus because the OAuth API only accepts
// low | medium | high.
;(message as { variant?: string }).variant = clamped
log("anthropic-effort: clamped variant max→high", {
sessionID: input.sessionID,
provider: model.providerID,
model: model.modelID,
reason: constrained ? "constrained-provider" : "non-opus",
})
} else {
log("anthropic-effort: injected effort=max", {
sessionID: input.sessionID,
provider: model.providerID,
model: model.modelID,
})
}
},
}
}