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.
This commit is contained in:
@@ -26,13 +26,15 @@ function shouldSkipForInternalAgent(agentName: string | undefined): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* Claude Pro/Max subscriptions expose a constrained OAuth API that rejects
|
||||
* `output_config.effort: "max"` (supported values: low | medium | high) even on
|
||||
* Opus models. Detect OAuth auth by inspecting OpenCode's auth.json.
|
||||
* 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 isAnthropicOAuth(providerID: string): boolean {
|
||||
if (providerID !== "anthropic") return false
|
||||
return isProviderUsingOAuth(providerID)
|
||||
function isConstrainedProvider(providerID: string): boolean {
|
||||
if (providerID === "github-copilot") return true
|
||||
if (providerID === "anthropic") return isProviderUsingOAuth(providerID)
|
||||
return false
|
||||
}
|
||||
|
||||
interface ChatParamsInput {
|
||||
@@ -59,9 +61,9 @@ const MAX_VARIANT_BY_TIER: Record<string, string> = {
|
||||
default: "high",
|
||||
}
|
||||
|
||||
function clampVariant(variant: string, isOpus: boolean, isOAuth: boolean): string {
|
||||
function clampVariant(variant: string, isOpus: boolean, isConstrained: boolean): string {
|
||||
if (variant !== "max") return variant
|
||||
if (isOAuth) return MAX_VARIANT_BY_TIER.default
|
||||
if (isConstrained) return MAX_VARIANT_BY_TIER.default
|
||||
return isOpus ? MAX_VARIANT_BY_TIER.opus : MAX_VARIANT_BY_TIER.default
|
||||
}
|
||||
|
||||
@@ -76,16 +78,15 @@ export function createAnthropicEffortHook() {
|
||||
if (isEffortUnsupportedModel(model.modelID)) return
|
||||
if (message.variant !== "max") return
|
||||
if (!isClaudeProvider(model.providerID, model.modelID)) return
|
||||
if (model.providerID === "github-copilot") return
|
||||
if (shouldSkipForInternalAgent(agent?.name)) return
|
||||
if (output.options.effort !== undefined) return
|
||||
|
||||
const opus = isOpusModel(model.modelID)
|
||||
const oauth = isAnthropicOAuth(model.providerID)
|
||||
const clamped = clampVariant(message.variant, opus, oauth)
|
||||
const constrained = isConstrainedProvider(model.providerID)
|
||||
const clamped = clampVariant(message.variant, opus, constrained)
|
||||
output.options.effort = clamped
|
||||
|
||||
const shouldOverrideMessageVariant = !opus || oauth
|
||||
const shouldOverrideMessageVariant = !opus || constrained
|
||||
|
||||
if (shouldOverrideMessageVariant) {
|
||||
// Override the variant so OpenCode doesn't pass "max" to the API.
|
||||
@@ -97,7 +98,7 @@ export function createAnthropicEffortHook() {
|
||||
sessionID: input.sessionID,
|
||||
provider: model.providerID,
|
||||
model: model.modelID,
|
||||
reason: oauth ? "anthropic-oauth" : "non-opus",
|
||||
reason: constrained ? "constrained-provider" : "non-opus",
|
||||
})
|
||||
} else {
|
||||
log("anthropic-effort: injected effort=max", {
|
||||
|
||||
@@ -153,7 +153,7 @@ describe("createAnthropicEffortHook", () => {
|
||||
expect(output.options.effort).toBeUndefined()
|
||||
})
|
||||
|
||||
it("#given github-copilot + claude model #then effort NOT injected", async () => {
|
||||
it("#given github-copilot + claude opus model #then effort clamped to high (constrained provider)", async () => {
|
||||
// given
|
||||
const hook = createAnthropicEffortHook()
|
||||
const { input, output } = createMockParams({
|
||||
@@ -164,9 +164,25 @@ describe("createAnthropicEffortHook", () => {
|
||||
// when
|
||||
await hook["chat.params"](input, output)
|
||||
|
||||
// then — github-copilot is a constrained provider, clamps max→high
|
||||
expect(output.options.effort).toBe("high")
|
||||
expect(input.message.variant).toBe("high")
|
||||
})
|
||||
|
||||
it("#given github-copilot + claude sonnet model #then effort clamped to high", async () => {
|
||||
// given
|
||||
const hook = createAnthropicEffortHook()
|
||||
const { input, output } = createMockParams({
|
||||
providerID: "github-copilot",
|
||||
modelID: "claude-sonnet-4-6",
|
||||
})
|
||||
|
||||
// when
|
||||
await hook["chat.params"](input, output)
|
||||
|
||||
// then
|
||||
expect(output.options.effort).toBeUndefined()
|
||||
expect(input.message.variant).toBe("max")
|
||||
expect(output.options.effort).toBe("high")
|
||||
expect(input.message.variant).toBe("high")
|
||||
})
|
||||
|
||||
describe("#given haiku models (effort unsupported)", () => {
|
||||
|
||||
Reference in New Issue
Block a user