Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/oauth/anthropic-account-refusal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,11 @@ async function isAccountRefusal(response: Response, signal?: AbortSignal): Promi
// Whole-message patterns: request-policy/model/resource refusals and quoted diagnostics
// must not acquire account authority merely by containing entitlement keywords.
if ("code" in error && error.code != null
&& (typeof error.code !== "string" || !["subscription_required", "insufficient_quota", "permission_denied"].includes(error.code))) return false;
&& (typeof error.code !== "string" || !["subscription_required", "insufficient_quota", "permission_denied", "oauth_org_not_allowed"].includes(error.code))) return false;
// The organization disabled subscription access: an account-scoped verdict, cooled like its siblings.
if (error.type === "oauth_org_not_allowed" || ("code" in error && error.code === "oauth_org_not_allowed")) return true;
const message = error.message.trim();
if (error.type === "permission_error" && /^(?:Your account does not have access to Claude Code|Your (?:Claude )?subscription (?:has expired|is (?:expired|inactive))|Your account does not have an active subscription)[.!]?$/i.test(message)) return true;
if (error.type === "permission_error" && /^(?:Your account does not have access to Claude Code|Your (?:Claude )?subscription (?:has expired|is (?:expired|inactive))|Your account does not have an active subscription|(?:Your|This) organization has disabled Claude subscription access for Claude Code(?:\. Use an Anthropic API key instead, or ask your admin to enable access)?)[.!]?$/i.test(message)) return true;
return (error.type === "billing_error" || error.type === "permission_error")
&& /^Your credit balance is too low to access the Anthropic API\.(?: Please go to Plans & Billing to upgrade or purchase credits\.)?$/i.test(message);
} catch {
Expand Down
4 changes: 3 additions & 1 deletion structure/providers/anthropic-account-pool.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,9 @@ only canonical Fable, Opus, or Sonnet labels after removing terminal controls; u

`src/oauth/anthropic-account-refusal.ts` accepts an HTTP 403 only when a complete bounded
JSON `error` envelope has `permission_error` or `billing_error` and a whole-message
account subscription/Claude Code entitlement or Anthropic credit-balance refusal.
account subscription/Claude Code entitlement or Anthropic credit-balance refusal, or when
the envelope carries the structured `oauth_org_not_allowed` type or code (organization disabled
Claude subscription access; also matched by its whole message under `permission_error`).
Generic permission, resource/model access, content policy, quoted diagnostics, conflicting
error codes and incomplete/malformed bodies remain terminal. The shared physical dispatch
in `src/server/responses/request-transport.ts` binds the refusal to the stored bearer it sent;
Expand Down
18 changes: 18 additions & 0 deletions tests/adapters/anthropic/anthropic-quota-dispatch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,25 @@ test.each([
expect(sent).toHaveLength(2);
});

const orgDisabled = "Your organization has disabled Claude subscription access for Claude Code. Use an Anthropic API key instead, or ask your admin to enable access.";
test.each([
{ type: "permission_error", message: orgDisabled },
{ type: "oauth_org_not_allowed", message: orgDisabled },
{ type: "permission_error", code: "oauth_org_not_allowed", message: "Access denied." },
])("organization-disabled 403 rotates and cools the refused account (%#)", async error => {
const [a, b] = await seed();
const config = configFor(body => sent.length === 1
? Response.json({ type: "error", error }, { status: 403 }) : answer(body.stream === true));
const response = await post(config);
expect(response.status).toBe(200);
expect(await response.text()).toContain("The answer is complete.");
expect(sent.map(row => row.authorization)).toEqual([`Bearer ${credential(0).access}`, `Bearer ${credential(1).access}`]);
expect(getEligibleAnthropicAccounts()).toEqual([b!]);
expect(getAnthropicAccountHealthSnapshot(a!)).not.toBeNull();
});

test.each([
[`Request denied: ${orgDisabled}`, "permission_error"],
["Request blocked by content policy", "permission_error"],
["You do not have permission to access this resource", "permission_error"],
["Your account does not have access to this model", "permission_error"],
Expand Down
Loading