From f03812dbe9cdca28d938bb20e936b7f0b2bdcf79 Mon Sep 17 00:00:00 2001 From: jun Date: Sat, 5 Sep 2026 05:18:23 +0900 Subject: [PATCH 1/4] docs(guides): the Claude pool guide no longer sells 429 failover The guide is where an operator decides whether to enable the experimental pool at all, so a stale sentence there is the most expensive one in the docs: it sells the pool on recovery that is now unconditional. Corrected in the source locale and the three that translate it, with the guard test extended to cover them. --- .../src/content/docs/fr/guides/claude-code.md | 2 +- docs-site/src/content/docs/guides/claude-code.md | 5 ++++- .../src/content/docs/tr/guides/claude-code.md | 2 +- .../src/content/docs/zh-tw/guides/claude-code.md | 2 +- tests/docs-429-failover-claims.test.ts | 14 ++++++++++++++ 5 files changed, 21 insertions(+), 4 deletions(-) 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..21dc8aa915 100644 --- a/tests/docs-429-failover-claims.test.ts +++ b/tests/docs-429-failover-claims.test.ts @@ -59,4 +59,18 @@ 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. + for (const path of ["", "zh-tw/", "tr/", "fr/"]) { + 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, `${path || "en"} guide`).toContain("429"); + // The carve-out is always emphasised, in every locale, so a silent revert to the old + // one-clause sentence drops the marker. + expect(intro, `${path || "en"} guide lost the carve-out`).toMatch(/\*\*[^*]*\*\*/); + } + }); }); From 292cb15c1bdb9eb45083a1e2852c0223501bc1f5 Mon Sep 17 00:00:00 2001 From: jun Date: Sat, 5 Sep 2026 05:19:02 +0900 Subject: [PATCH 2/4] test(oauth): pin the rotator against being re-gated behind the pool flag The original defect was one line at the top of rotateAnthropicAccountOn429. Restoring it would strand every stock install again while every behavioural test kept passing, because they all seed two accounts and so satisfy the quorum either way. --- tests/routing/always-on-429-failover.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) 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"); + }); }); From f2a78a3bf95f66a07cbb66d7c9a9b67e29f8292e Mon Sep 17 00:00:00 2001 From: jun Date: Sat, 5 Sep 2026 05:29:22 +0900 Subject: [PATCH 3/4] test(layout): move the quorum cache test into tests/routing/ Follows #3511, which relocated its sibling always-on-429-failover.test.ts. The docs claim test stays at the root beside docs-bun-source-requirement.test.ts, which is where doc-contract tests live. --- tests/{ => routing}/anthropic-quorum-cache.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename tests/{ => routing}/anthropic-quorum-cache.test.ts (97%) 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; From fc05738396dda130ef8c36e73c224f90b989e82f Mon Sep 17 00:00:00 2001 From: jun Date: Sat, 5 Sep 2026 05:32:58 +0900 Subject: [PATCH 4/4] test(docs): ban the stale sentence pattern, not just its shape CodeRabbit was right that the guide assertion was too weak: requiring the intro to mention 429 and carry emphasis is satisfied by the ORIGINAL stale sentence, so a revert would have passed. Each locale now bans the phrase pattern that actually attributed failover to the pool. Driven red against the restored sentence before committing. --- tests/docs-429-failover-claims.test.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/tests/docs-429-failover-claims.test.ts b/tests/docs-429-failover-claims.test.ts index 21dc8aa915..a45bd97c44 100644 --- a/tests/docs-429-failover-claims.test.ts +++ b/tests/docs-429-failover-claims.test.ts @@ -64,13 +64,22 @@ describe("429 failover docs", () => { // 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. - for (const path of ["", "zh-tw/", "tr/", "fr/"]) { + // + // 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, `${path || "en"} guide`).toContain("429"); - // The carve-out is always emphasised, in every locale, so a silent revert to the old - // one-clause sentence drops the marker. - expect(intro, `${path || "en"} guide lost the carve-out`).toMatch(/\*\*[^*]*\*\*/); + expect(intro, `${label} guide`).toContain("429"); + expect(banned.test(intro), `${label} guide re-attributes 429 failover to the pool`).toBe(false); } }); });