Merge pull request #3794 from jeremymcs/fix/claude-code-routing-bugs
fix(providers): match subscription block errors and respect claude-code routing
This commit is contained in:
commit
d29d086f6a
4 changed files with 60 additions and 14 deletions
|
|
@ -316,6 +316,28 @@ describe("RetryHandler — long-context entitlement 429 (#2803)", () => {
|
|||
assert.ok(switchEvent!.to.startsWith("claude-code/"), "Should switch to claude-code provider");
|
||||
});
|
||||
|
||||
it("switches to claude-code on 'out of extra usage' error (#3772)", async () => {
|
||||
const ccModel = createMockModel("claude-code", "claude-opus-4-6");
|
||||
const { deps, emittedEvents } = createMockDeps({
|
||||
model: createMockModel("anthropic", "claude-opus-4-6"),
|
||||
findModelResult: (provider: string, modelId: string) => {
|
||||
if (provider === "claude-code" && modelId === "claude-opus-4-6") return ccModel;
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
deps.isClaudeCodeReady = () => true;
|
||||
|
||||
const handler = new RetryHandler(deps);
|
||||
const msg = errorMessage("You're out of extra usage. Add more at claude.ai/settings/usage and keep going.");
|
||||
|
||||
const result = await handler.handleRetryableError(msg);
|
||||
|
||||
assert.equal(result, true, "should retry via claude-code fallback");
|
||||
const switchEvent = emittedEvents.find((e) => e.type === "fallback_provider_switch");
|
||||
assert.ok(switchEvent, "Expected fallback_provider_switch event");
|
||||
assert.ok(switchEvent!.to.startsWith("claude-code/"), "Should switch to claude-code provider");
|
||||
});
|
||||
|
||||
it("does NOT switch to claude-code when current provider is not anthropic", async () => {
|
||||
const ccModel = createMockModel("claude-code", "gpt-4o");
|
||||
const { deps, emittedEvents } = createMockDeps({
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ export class RetryHandler {
|
|||
// generated error from getApiKey() when credentials are in a backoff window.
|
||||
// Re-entering the retry handler for that message creates a cascade of empty
|
||||
// error entries in the session file, breaking resume (#3429).
|
||||
return /overloaded|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|connection.?error|connection.?refused|other side closed|fetch failed|upstream.?connect|reset before headers|terminated|retry delay|network.?(?:is\s+)?unavailable|credentials.*expired|extra usage is required|third.party.*draw from extra|third.party.*not.*available/i.test(
|
||||
return /overloaded|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|connection.?error|connection.?refused|other side closed|fetch failed|upstream.?connect|reset before headers|terminated|retry delay|network.?(?:is\s+)?unavailable|credentials.*expired|extra usage is required|(?:out of|no) extra usage|third.party.*draw from extra|third.party.*not.*available/i.test(
|
||||
err,
|
||||
);
|
||||
}
|
||||
|
|
@ -458,12 +458,13 @@ export class RetryHandler {
|
|||
}
|
||||
|
||||
/**
|
||||
* Detect the Anthropic third-party subscription block error (#3772).
|
||||
* This is a hard policy block, not a transient rate limit — credential
|
||||
* rotation will not help.
|
||||
* Detect Anthropic subscription block errors (#3772).
|
||||
* These are hard policy blocks, not transient rate limits — credential
|
||||
* rotation will not help. Matches both the explicit "third-party" message
|
||||
* and the "out of extra usage" variant that subscription users receive.
|
||||
*/
|
||||
private _isThirdPartyBlock(errorMessage: string): boolean {
|
||||
return /third[- .]party.*(?:draw from extra|not.*available|plan limits|not permitted|cannot be used|not supported)/i.test(errorMessage);
|
||||
return /third[- .]party.*(?:draw from extra|not.*available|plan limits|not permitted|cannot be used|not supported)|(?:out of|no) extra usage/i.test(errorMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -329,8 +329,17 @@ export function resolveModelId<T extends { id: string; provider: string }>(
|
|||
if (candidates.length === 0) return undefined;
|
||||
if (candidates.length === 1) return candidates[0];
|
||||
|
||||
// Extension / CLI-wrapper providers that should never win bare-ID resolution
|
||||
// when a first-class API provider also offers the same model.
|
||||
// When the user's current provider is claude-code (set by startup migration
|
||||
// or explicit selection), honour it for bare IDs. Routing back to anthropic
|
||||
// would undo the migration and hit the third-party subscription block (#3772).
|
||||
if (currentProvider === "claude-code") {
|
||||
const ccMatch = candidates.find(m => m.provider === "claude-code");
|
||||
if (ccMatch) return ccMatch;
|
||||
}
|
||||
|
||||
// Extension / CLI-wrapper providers that should not win bare-ID resolution
|
||||
// when a first-class API provider also offers the same model AND the user
|
||||
// has not explicitly chosen the extension provider.
|
||||
const EXTENSION_PROVIDERS = new Set(["claude-code"]);
|
||||
|
||||
// Prefer currentProvider only when it is a first-class API provider
|
||||
|
|
|
|||
|
|
@ -143,17 +143,17 @@ test("resolvePreferredModelConfig keeps explicit phase models as the ceiling", (
|
|||
|
||||
// ─── resolveModelId tests ─────────────────────────────────────────────────
|
||||
|
||||
test("resolveModelId: bare ID resolves to anthropic over claude-code when session is claude-code (#2905)", () => {
|
||||
test("resolveModelId: bare ID resolves to claude-code when session is claude-code (#3772)", () => {
|
||||
const availableModels = [
|
||||
{ id: "claude-sonnet-4-6", provider: "anthropic" },
|
||||
{ id: "claude-sonnet-4-6", provider: "claude-code" },
|
||||
];
|
||||
|
||||
// Bug: when currentProvider is "claude-code", bare ID "claude-sonnet-4-6"
|
||||
// resolves to claude-code/claude-sonnet-4-6 instead of anthropic/claude-sonnet-4-6
|
||||
// When currentProvider is "claude-code" (set by startup migration for subscription
|
||||
// users), bare IDs must resolve to claude-code to avoid the third-party block (#3772).
|
||||
const result = resolveModelId("claude-sonnet-4-6", availableModels, "claude-code");
|
||||
assert.ok(result, "should resolve a model");
|
||||
assert.equal(result.provider, "anthropic", "bare ID must resolve to anthropic, not claude-code");
|
||||
assert.equal(result.provider, "claude-code", "bare ID must resolve to claude-code when session provider is claude-code");
|
||||
});
|
||||
|
||||
test("resolveModelId: bare ID still prefers current provider when it is a first-class API provider", () => {
|
||||
|
|
@ -227,14 +227,28 @@ test("model change notify in selectAndApplyModel is gated behind verbose flag",
|
|||
);
|
||||
});
|
||||
|
||||
test("resolveModelId: anthropic wins over claude-code regardless of list order", () => {
|
||||
test("resolveModelId: anthropic wins over claude-code when session provider is not claude-code", () => {
|
||||
const availableModels = [
|
||||
{ id: "claude-sonnet-4-6", provider: "claude-code" },
|
||||
{ id: "claude-sonnet-4-6", provider: "anthropic" },
|
||||
];
|
||||
|
||||
// Even when claude-code appears first in the list, anthropic should win
|
||||
// When the session is NOT on claude-code, bare IDs should resolve to
|
||||
// the canonical anthropic provider (original #2905 behavior preserved).
|
||||
const result = resolveModelId("claude-sonnet-4-6", availableModels, undefined);
|
||||
assert.ok(result, "should resolve a model");
|
||||
assert.equal(result.provider, "anthropic", "anthropic must win when session is not claude-code");
|
||||
});
|
||||
|
||||
test("resolveModelId: claude-code wins when session is claude-code regardless of list order", () => {
|
||||
const availableModels = [
|
||||
{ id: "claude-sonnet-4-6", provider: "claude-code" },
|
||||
{ id: "claude-sonnet-4-6", provider: "anthropic" },
|
||||
];
|
||||
|
||||
// When session provider is claude-code (subscription user migration), it must
|
||||
// win regardless of candidate ordering to avoid the third-party block (#3772).
|
||||
const result = resolveModelId("claude-sonnet-4-6", availableModels, "claude-code");
|
||||
assert.ok(result, "should resolve a model");
|
||||
assert.equal(result.provider, "anthropic", "anthropic must win over claude-code regardless of list order");
|
||||
assert.equal(result.provider, "claude-code", "claude-code must win when it is the session provider");
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue