diff --git a/docs-site/src/content/docs/fr/guides/claude-code.md b/docs-site/src/content/docs/fr/guides/claude-code.md index 671c22c233..797f9d3554 100644 --- a/docs-site/src/content/docs/fr/guides/claude-code.md +++ b/docs-site/src/content/docs/fr/guides/claude-code.md @@ -13,7 +13,7 @@ Vous pouvez vous connecter à plusieurs comptes Claude via le tableau de bord de ajouter un compte). Par défaut, chaque requête utilise uniquement le compte **actif**. Un groupe de comptes Claude **expérimental et facultatif** (`anthropicAccountPool.enabled`) ajoute l'affinité de -session et le basculement en cas de délai de récupération 429 entre ces comptes OAuth. Pour les **nouvelles** +session et la sélection des nouvelles sessions basée sur l'usage entre ces comptes OAuth. Il ne contrôle **pas** le basculement sur 429 : dès que deux comptes utilisables sont enregistrés, une requête limitée bascule vers un autre compte que l'option soit activée ou non, et cela ne peut pas être désactivé. Pour les **nouvelles** sessions uniquement, `anthropicAccountPool.strategy` sélectionne un compte éligible : `quota` (par défaut) choisit la plus faible utilisation connue dans la fenêtre configurée par `anthropicAccountPool.quotaWindow` (`five-hour` par défaut, `weekly` ou `max-utilization`) lorsqu'elle dépasse `autoSwitchThreshold` ; `round-robin` diff --git a/docs-site/src/content/docs/guides/claude-code.md b/docs-site/src/content/docs/guides/claude-code.md index d991c3a889..264d5d6fea 100644 --- a/docs-site/src/content/docs/guides/claude-code.md +++ b/docs-site/src/content/docs/guides/claude-code.md @@ -13,7 +13,10 @@ You can log in multiple Claude accounts via the Providers dashboard (`ocx login add-account). By default every request uses the **active** account only. An **experimental, opt-in** Claude account pool (`anthropicAccountPool.enabled`) adds sticky -session affinity and 429 cooldown failover across those OAuth accounts. For **new** sessions, +session affinity and usage-aware new-session selection across those OAuth accounts. It does +**not** gate 429 failover: with two or more usable accounts stored, a rate-limited request moves +to another account whether the pool is on or off, and that cannot be switched off. For **new** +sessions, `anthropicAccountPool.strategy` selects among eligible accounts: `quota` (default) picks the lowest known usage in the window set by `quotaWindow` (`five-hour` by default, or `weekly` / `max-utilization`) when above `autoSwitchThreshold`; `round-robin` spreads evenly diff --git a/docs-site/src/content/docs/tr/guides/claude-code.md b/docs-site/src/content/docs/tr/guides/claude-code.md index 1fe4fd0393..bf955fba97 100644 --- a/docs-site/src/content/docs/tr/guides/claude-code.md +++ b/docs-site/src/content/docs/tr/guides/claude-code.md @@ -16,7 +16,7 @@ fazla Claude hesabına giriş yapabilirsiniz. Varsayılan olarak her istek yaln **Deneysel, isteğe bağlı** bir Claude hesap havuzu (`anthropicAccountPool.enabled`), bu OAuth hesapları arasında yapışkan oturum -bağlılığı ve 429 bekleme süresi (cooldown) yük devretmesi ekler. Yalnızca +bağlılığı ve kullanıma dayalı yeni oturum seçimi ekler. 429 yük devretmesini **kontrol etmez**: iki veya daha fazla kullanılabilir hesap saklandığında, hız sınırına takılan istek bu anahtar açık da kapalı da olsa başka bir hesaba geçer ve bu kapatılamaz. Yalnızca **yeni** oturumlar için `anthropicAccountPool.strategy` uygun hesaplar arasından seçim yapar: `quota` (varsayılan), `autoSwitchThreshold` üzerinde olduğunda `anthropicAccountPool.quotaWindow` ile yapılandırılan penceredeki bilinen en düşük kullanımı diff --git a/docs-site/src/content/docs/zh-tw/guides/claude-code.md b/docs-site/src/content/docs/zh-tw/guides/claude-code.md index 2663cb3647..7aea715953 100644 --- a/docs-site/src/content/docs/zh-tw/guides/claude-code.md +++ b/docs-site/src/content/docs/zh-tw/guides/claude-code.md @@ -13,7 +13,7 @@ Code 可以使用每一個已路由的供應商——包括 OAuth 登入、帳 每個請求只使用**作用中**帳號。 **實驗性、opt-in** 的 Claude 帳號池(`anthropicAccountPool.enabled`)會在這些 OAuth 帳號之間加入 -sticky session affinity 與 429 冷卻故障轉移。僅對**新**工作階段,`anthropicAccountPool.strategy` +sticky session affinity 與依用量的新工作階段選擇。它**不**控制 429 容錯移轉:只要儲存了兩個以上可用帳號,被限流的請求無論此開關開或關都會切換到另一個帳號,且無法關閉。僅對**新**工作階段,`anthropicAccountPool.strategy` 會在合格帳號之間選擇:`quota`(預設)在用量高於 `autoSwitchThreshold` 時,依 `anthropicAccountPool.quotaWindow` 所設定的視窗挑選已知用量最低者(`five-hour` 為預設,亦可選 `weekly` 或 `max-utilization`); diff --git a/tests/docs-429-failover-claims.test.ts b/tests/docs-429-failover-claims.test.ts index 5a0adb2914..a45bd97c44 100644 --- a/tests/docs-429-failover-claims.test.ts +++ b/tests/docs-429-failover-claims.test.ts @@ -59,4 +59,27 @@ describe("429 failover docs", () => { expect(row, `${locale} lost the 429 carve-out`).toContain("429"); } }); + + test("the Claude Code guide does not attribute 429 failover to the pool", async () => { + // The guide is where an operator decides whether to enable the experimental pool at all, so + // a stale sentence here is the most expensive one in the docs: it sells the pool on recovery + // that is now unconditional. Checked in the source locale and the three that translate it. + // + // Asserting only that the intro mentions 429 and carries emphasis is too weak: the ORIGINAL + // stale sentence would satisfy both. So each locale bans the phrase pattern that actually + // attributed failover to the pool, which is the thing that has to stay gone. + const bannedByLocale: Record = { + "": /adds sticky[\s\S]{0,80}429/i, + "zh-tw/": /加入[\s\S]{0,40}429\s*冷卻(故障轉移|容錯移轉)/, + "tr/": /bağlılığı ve 429[\s\S]{0,60}yük devretmesi ekler/i, + "fr/": /ajoute l['’]affinité de[\s\S]{0,80}basculement[\s\S]{0,40}429/i, + }; + for (const [path, banned] of Object.entries(bannedByLocale)) { + const label = path || "en"; + const source = await Bun.file(`docs-site/src/content/docs/${path}guides/claude-code.md`).text(); + const intro = source.slice(0, source.indexOf("anthropicAccountPool.strategy")); + expect(intro, `${label} guide`).toContain("429"); + expect(banned.test(intro), `${label} guide re-attributes 429 failover to the pool`).toBe(false); + } + }); }); diff --git a/tests/routing/always-on-429-failover.test.ts b/tests/routing/always-on-429-failover.test.ts index 59cbe084ca..b4d825983d 100644 --- a/tests/routing/always-on-429-failover.test.ts +++ b/tests/routing/always-on-429-failover.test.ts @@ -139,4 +139,21 @@ describe("proactive Anthropic routing stays opt-in", () => { ); expect(picks.every(id => id === ids[0]!)).toBe(true); }); + test("the rotator cannot be re-gated behind the pool flag", async () => { + // The original defect was ONE line at the top of rotateAnthropicAccountOn429: + // if (!isAnthropicAccountPoolEnabled(config)) return null; + // Restoring it would strand every stock install again, and nothing else in this file would + // fail -- every behavioural test seeds two accounts, which satisfies the quorum either way, + // so they would keep passing while the feature was dead for the users who never opted in. + // + // Pin the shape instead: the flag may still appear in the rotator, but only alongside the + // presence check, never as a gate of its own. + const source = await Bun.file("src/oauth/anthropic-routing.ts").text(); + const start = source.indexOf("export function rotateAnthropicAccountOn429"); + expect(start).toBeGreaterThan(-1); + const body = source.slice(start, source.indexOf("\n}", start)); + const gate = body.split("\n").find(line => line.includes("isAnthropicAccountPoolEnabled")); + expect(gate, "the rotator no longer references the pool flag at all").toBeDefined(); + expect(gate, "the pool flag became a gate of its own again").toContain("hasAnthropicFailoverQuorum"); + }); }); diff --git a/tests/anthropic-quorum-cache.test.ts b/tests/routing/anthropic-quorum-cache.test.ts similarity index 97% rename from tests/anthropic-quorum-cache.test.ts rename to tests/routing/anthropic-quorum-cache.test.ts index 87e35aff2e..17a6db6096 100644 --- a/tests/anthropic-quorum-cache.test.ts +++ b/tests/routing/anthropic-quorum-cache.test.ts @@ -23,9 +23,9 @@ import { hasAnthropicFailoverQuorum, resetAnthropicRoutingForManualSelection, rotateAnthropicAccountOn429, -} from "../src/oauth/anthropic-routing"; -import { getAccountSet, saveCredential } from "../src/oauth/store"; -import { removeTreeWithRetry } from "./helpers/remove-tree"; +} from "../../src/oauth/anthropic-routing"; +import { getAccountSet, saveCredential } from "../../src/oauth/store"; +import { removeTreeWithRetry } from "../helpers/remove-tree"; const originalHome = process.env.OPENCODEX_HOME; let home: string;