From 42958c42a2443f6f9db28cd96f8fce131d6e2796 Mon Sep 17 00:00:00 2001 From: Ingwannu Date: Fri, 4 Sep 2026 18:06:31 +0000 Subject: [PATCH 1/2] fix(oauth): repair post-merge 429 failover boundaries --- .../src/content/docs/fr/guides/claude-code.md | 17 +- .../fr/reference/cli/providers-accounts.md | 2 +- .../fr/reference/configuration/providers.md | 2 +- .../src/content/docs/guides/claude-code.md | 19 +- .../ja/reference/cli/providers-accounts.md | 2 +- .../ja/reference/configuration/providers.md | 2 +- .../ko/reference/cli/providers-accounts.md | 2 +- .../ko/reference/configuration/providers.md | 2 +- .../docs/reference/cli/providers-accounts.md | 3 +- .../docs/reference/configuration/providers.md | 17 +- .../ru/reference/cli/providers-accounts.md | 2 +- .../ru/reference/configuration/providers.md | 2 +- .../src/content/docs/tr/guides/claude-code.md | 23 +- .../tr/reference/cli/providers-accounts.md | 3 +- .../tr/reference/configuration/providers.md | 2 +- .../zh-cn/reference/cli/providers-accounts.md | 2 +- .../reference/configuration/providers.md | 2 +- .../content/docs/zh-tw/guides/claude-code.md | 19 +- .../zh-tw/reference/cli/providers-accounts.md | 2 +- .../reference/configuration/providers.md | 2 +- src/oauth/anthropic-routing.ts | 8 +- src/oauth/generic-account-failover.ts | 15 +- src/server/responses/core.ts | 16 +- src/types/config.ts | 2 +- src/types/provider.ts | 2 +- structure/04_transports-and-sidecars.md | 9 + tests/adapter-event-oauth-failover.test.ts | 5 +- tests/always-on-429-failover.test.ts | 15 +- ...anthropic-sidecar-account-failover.test.ts | 277 ++++++++++++++++++ tests/generic-oauth-failover.test.ts | 15 +- 30 files changed, 419 insertions(+), 72 deletions(-) create mode 100644 tests/anthropic-sidecar-account-failover.test.ts 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..ef481a1815 100644 --- a/docs-site/src/content/docs/fr/guides/claude-code.md +++ b/docs-site/src/content/docs/fr/guides/claude-code.md @@ -10,10 +10,11 @@ clé et les services auxiliaires, sans configuration d'authentification supplém ## Groupe de comptes OAuth Claude (expérimental) Vous pouvez vous connecter à plusieurs comptes Claude via le tableau de bord des fournisseurs (`ocx login anthropic` / -ajouter un compte). Par défaut, chaque requête utilise uniquement le compte **actif**. +ajouter un compte). Avec un seul compte éligible, chaque requête utilise ce compte actif. Avec au moins deux comptes, +une erreur 429 en amont peut relancer la requête sur un autre compte même si le routage proactif est désactivé. 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 proactive et la sélection des nouvelles sessions entre ces comptes OAuth. 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` @@ -23,18 +24,20 @@ un délai de récupération, une réauthentification ou le seuil, puis passe au Anthropic peut restreindre les comptes dont l'activité ressemble à une rotation automatisée ; la rotation ne protège pas contre l'application des règles du fournisseur. -Comportement lorsque cette option est activée : +Récupération réactive avec au moins deux comptes éligibles : - Un **429** en amont place le compte en temporisation selon `Retry-After` lorsqu'il est présent, ou selon un délai de repli, - efface ses affinités et peut faire basculer la requête vers un autre compte admissible, dans les limites prévues. + et peut faire basculer la requête vers un autre compte admissible, dans les limites prévues. Cette récupération reste active + lorsque `anthropicAccountPool.enabled` est absent ou vaut `false`. Ne conservez qu'un compte éligible si vous refusez tout + changement automatique de compte. +- Lorsque le routage proactif est activé, un 429 efface aussi l'affinité de ce compte. - L'affinité est **locale au processus** et disparaît au redémarrage du proxy. - Les erreurs d'identification **401/403** mettent le compte en quarantaine (`needsReauth`) afin de l'exclure de la sélection jusqu'à sa réauthentification. - Si chaque compte éligible est en temporisation, le proxy renvoie **429** (et non 401) avec `Retry-After` lorsqu'il est connu. -- La récupération, y compris le basculement 429, utilise `quotaWindow` pour classer les comptes de - remplacement admissibles, sans modifier les limites existantes de temporisation ou de basculement ; - `round-robin` ignore `quotaWindow`. +- Lorsque le routage proactif est désactivé, la récupération utilise l'ordre du quota. Lorsqu'il est activé, + la stratégie du groupe choisit les remplacements admissibles ; `round-robin` ignore `quotaWindow`. Voir [Configuration](/fr/reference/configuration/providers/#anthropicaccountpool-expérimental). diff --git a/docs-site/src/content/docs/fr/reference/cli/providers-accounts.md b/docs-site/src/content/docs/fr/reference/cli/providers-accounts.md index 9f865c7f0d..e895fd79a8 100644 --- a/docs-site/src/content/docs/fr/reference/cli/providers-accounts.md +++ b/docs-site/src/content/docs/fr/reference/cli/providers-accounts.md @@ -149,7 +149,7 @@ sont ignorés à moins que `--all` soit présent. Avec un fournisseur, répertor La sortie destinée aux utilisateurs utilise `PROVIDER TYPE ID PLAN/LABEL PRIORITY STATUS` ; une ligne Codex sélectionnée manuellement porte la mention `selected`. `PRIORITY` est l'ordre de sélection Codex signé (`0` lorsqu'il n'est pas défini) et affiche `-` pour les lignes où l'ordre ne s'applique pas, comme les comptes OAuth et les clés API. Avec au moins deux comptes Kiro enregistrés et éligibles, par défaut une réponse 429 entraîne automatiquement une rotation vers un autre -compte, en privilégiant celui dont l'allocation restante connue est la plus élevée ; la rotation est activée par la présence de plusieurs comptes et peut être désactivée avec `oauthAccountFailover.enabled: false` ; `ocx account login kiro` ajoute les comptes au pool un par un. Un résultat vide est toujours un succès. `--json` +compte, en privilégiant celui dont l'allocation restante connue est la plus élevée ; la rotation réactive est activée par la présence de plusieurs comptes. `oauthAccountFailover.enabled: false` désactive seulement la préférence proactive avant envoi, pas cette nouvelle tentative ; `ocx account login kiro` ajoute les comptes au pool un par un. Un résultat vide est toujours un succès. `--json` renvoie : ```text diff --git a/docs-site/src/content/docs/fr/reference/configuration/providers.md b/docs-site/src/content/docs/fr/reference/configuration/providers.md index ed6658c5e3..4f7cb4278a 100644 --- a/docs-site/src/content/docs/fr/reference/configuration/providers.md +++ b/docs-site/src/content/docs/fr/reference/configuration/providers.md @@ -198,7 +198,7 @@ rotation automatique peut déclencher des restrictions du fournisseur. | Clé | Type | Par défaut | Description | | --- | --- | --- | --- | -| `anthropicAccountPool.enabled?` | `boolean` | `false` | Active l'affinité persistante et le basculement après une temporisation 429. | +| `anthropicAccountPool.enabled?` | `boolean` | `false` | Active l’affinité proactive persistante et la sélection des nouvelles sessions. Avec au moins deux comptes éligibles, la récupération réactive après 429 reste active même avec `false`. | | `anthropicAccountPool.autoSwitchThreshold?` | `number` | `80` | Pour les nouvelles sessions, lorsque le compte actif atteint ce seuil, choisir la plus faible utilisation connue et mise en cache dans la fenêtre configurée. `0` désactive la sélection selon le quota. | | `anthropicAccountPool.strategy?` | `"quota" \| "round-robin" \| "fill-first"` | `"quota"` | Stratégie des nouvelles sessions ; `quota` classe les comptes selon la fenêtre définie par `quotaWindow`, par défaut les barres sur 5 heures, et `fill-first` évalue son seuil d'évacuation dans cette même fenêtre. | | `anthropicAccountPool.quotaWindow?` | `"five-hour" \| "weekly" \| "max-utilization"` | `"five-hour"` | Barre d'utilisation signalée par le fournisseur, mise en cache et utilisée pour la sélection selon l'utilisation. `five-hour` conserve le comportement actuel. `weekly` utilise la barre hebdomadaire et ignore les comptes dont la barre sur 5 heures est épuisée tant qu'un autre compte admissible reste disponible, mais y revient si aucun autre ne reste. `max-utilization` utilise la valeur connue la plus élevée et peut donc employer la barre sur 5 heures avant que la barre hebdomadaire soit disponible ; si aucune n'est connue, le compte suit l'ordre des utilisations inconnues. Les utilisations connues précèdent les inconnues, mais si tous les comptes admissibles sont inconnus, la sélection en renvoie tout de même un dans leur ordre admissible. Après le départage documenté par la plus faible utilisation sur 5 heures, une égalité exacte conserve cet ordre. Une session saine avec affinité n'est pas rééquilibrée de manière proactive. Pour l'affectation des nouvelles sessions et la reprise du routage après un remplacement admissible à la suite d'un 429, `quota` classe directement les candidats admissibles avec cette fenêtre ; `fill-first` avance dans un ordre stable selon le seuil et les règles d'épuisement de cette fenêtre ; `round-robin` l'ignore. Le délai de récupération, les limites de basculement et l'éligibilité de réauthentification restent des états locaux distincts. Les barres hebdomadaires ne sont connues qu'après leur interrogation dans la page Fournisseurs du tableau de bord. | diff --git a/docs-site/src/content/docs/guides/claude-code.md b/docs-site/src/content/docs/guides/claude-code.md index d991c3a889..7f5a7f78d4 100644 --- a/docs-site/src/content/docs/guides/claude-code.md +++ b/docs-site/src/content/docs/guides/claude-code.md @@ -10,10 +10,11 @@ included — with zero extra auth work. ## Claude OAuth account pool (experimental) You can log in multiple Claude accounts via the Providers dashboard (`ocx login anthropic` / -add-account). By default every request uses the **active** account only. +add-account). With one eligible account, every request uses that active account. With two or more, +an upstream 429 may retry another account even while proactive pooling is disabled. -An **experimental, opt-in** Claude account pool (`anthropicAccountPool.enabled`) adds sticky -session affinity and 429 cooldown failover across those OAuth accounts. For **new** sessions, +An **experimental, opt-in** Claude account pool (`anthropicAccountPool.enabled`) adds proactive +sticky session affinity and new-session selection across those OAuth accounts. 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 @@ -22,18 +23,20 @@ reauthentication, or threshold, then advances. It is **off by default**, shows a and is not battle-tested — Anthropic may restrict accounts that look like automated rotation; rotation does not protect against provider enforcement. -Operational contract when enabled: +Reactive recovery with two or more eligible accounts: - Upstream **429** cools that account using `Retry-After` when present (else a default backoff), - clears its affinities, and may rotate to another eligible account within the same request - (bounded). + and may rotate to another eligible account within the same request (bounded). This remains active + when `anthropicAccountPool.enabled` is absent or `false`; keep one eligible account if you do not + accept automatic account switching. +- When proactive pooling is enabled, a 429 also clears that account's affinity. - Affinity is **process-local** (lost on proxy restart). - **401/403** credential failures quarantine the account (`needsReauth`) so it is excluded from selection until re-authenticated. - If every eligible account is cooling, the proxy returns **429** (not 401) with `Retry-After` when known. -- Recovery, including 429 failover, uses `quotaWindow` to rank eligible replacements without - changing the existing cooldown or failover limits; `round-robin` ignores `quotaWindow`. +- Recovery uses quota ordering while proactive pooling is disabled. When it is enabled, the chosen + pool strategy governs eligible replacements; `round-robin` ignores `quotaWindow`. - `autoSwitchThreshold: 0` turns off **proactive** usage-based switching only. New-session selection and 429 recovery still consult `quotaWindow`, so the window is inert only under `round-robin`. `fill-first` evaluates its drain threshold in the selected window. diff --git a/docs-site/src/content/docs/ja/reference/cli/providers-accounts.md b/docs-site/src/content/docs/ja/reference/cli/providers-accounts.md index babd71c61f..7f2bb65651 100644 --- a/docs-site/src/content/docs/ja/reference/cli/providers-accounts.md +++ b/docs-site/src/content/docs/ja/reference/cli/providers-accounts.md @@ -113,7 +113,7 @@ Codex pool selection applies to the next request after clearing existing affinit ### `ocx account list [provider] [--json] [--all] [--quota [--refresh]]` -プロバイダーを使用しない場合、Codex プール、OAuth アカウント、および設定された API キー プールが一覧表示されます。 `--all` が存在しない限り、空のプロバイダーはスキップされます。プロバイダーを使用すると、その資格情報ファミリーのみがリストされます。人間の出力では `PROVIDER TYPE ID PLAN/LABEL PRIORITY STATUS` を使用します。手動で選択した Codex 行には `selected` というマークが付けられます。利用可能な Kiro アカウントが 2 つ以上保存されている場合、既定では 429 を受けると別のアカウントへ自動的に切り替え、既知の残り利用枠が最も多いアカウントを優先します。この切り替えはアカウントの存在によって有効になり、`oauthAccountFailover.enabled: false` で無効にできます。`ocx account login kiro` はアカウントを 1 件ずつプールへ追加します。結果が空であっても成功です。 `--json` は次を返します: +プロバイダーを使用しない場合、Codex プール、OAuth アカウント、および設定された API キー プールが一覧表示されます。 `--all` が存在しない限り、空のプロバイダーはスキップされます。プロバイダーを使用すると、その資格情報ファミリーのみがリストされます。人間の出力では `PROVIDER TYPE ID PLAN/LABEL PRIORITY STATUS` を使用します。手動で選択した Codex 行には `selected` というマークが付けられます。利用可能な Kiro アカウントが 2 つ以上保存されている場合、既定では 429 を受けると別のアカウントへ自動的に切り替え、既知の残り利用枠が最も多いアカウントを優先します。この反応型の切り替えはアカウントの存在によって有効になります。`oauthAccountFailover.enabled: false` は送信前のプロアクティブな選択だけを無効にし、この再試行は無効にしません。`ocx account login kiro` はアカウントを 1 件ずつプールへ追加します。結果が空であっても成功です。 `--json` は次を返します: ```text { accounts: AccountRow[], notes: string[] } diff --git a/docs-site/src/content/docs/ja/reference/configuration/providers.md b/docs-site/src/content/docs/ja/reference/configuration/providers.md index b837a57017..491e276b76 100644 --- a/docs-site/src/content/docs/ja/reference/configuration/providers.md +++ b/docs-site/src/content/docs/ja/reference/configuration/providers.md @@ -165,7 +165,7 @@ affinity を維持します。これらの戦略は provider enforcement を回 |キー |タイプ |デフォルト |説明 | | --- | --- | --- | --- | -| `anthropicAccountPool.enabled?` | `boolean` | `false` |スティッキー アフィニティと 429 クールダウン フェイルオーバーを有効にします。 | +| `anthropicAccountPool.enabled?` | `boolean` | `false` |プロアクティブなスティッキー アフィニティと新規セッション選択を有効にします。利用可能なアカウントが 2 つ以上ある場合、`false` でも反応型の 429 クールダウンとフェイルオーバーは有効です。 | | `anthropicAccountPool.autoSwitchThreshold?` | `number` | `80` |新しいセッションでは、アクティブなアカウントがこのしきい値に達すると、設定した期間で既知のキャッシュ使用量が最も低いアカウントを選択します。 `0` はクォータ選択を無効にします。 | | `anthropicAccountPool.strategy?` | `"quota" \| "round-robin" \| "fill-first"` | `"quota"` |新しいセッション戦略。`quota` は `quotaWindow` で指定した期間(既定は 5 時間足)でアカウントを順位付けし、`fill-first` も同じ期間で使い切りのしきい値を判定します。 | | `anthropicAccountPool.quotaWindow?` | `"five-hour" \| "weekly" \| "max-utilization"` | `"five-hour"` |使用量ベースのアカウント選択で使う、プロバイダー報告のキャッシュ済み使用率です。`five-hour` は従来の動作を維持します。`weekly` は週次使用量を使い、他に対象アカウントが残る間だけ 5 時間使用量が上限に達したアカウントを除外し、残らない場合はそれらへフォールバックします。`max-utilization` は判明している値のうち最も高いものを使うため、週次使用量が未取得でも 5 時間使用量を利用できます。どちらも不明なら unknown の順位付けに従います。既知の使用量は unknown より先ですが、対象がすべて unknown でも対象順の先頭を選択します。記載した 5 時間使用量による同点判定後も完全に同点なら、対象順を維持します。正常な affinity セッションを先回りして再配置することはありません。新規セッションの割り当てと、対象となる 429 代替後のルーティング復旧では、`quota` はこの期間で対象候補を直接順位付けし、`fill-first` はこの期間のしきい値と上限到達ルールを使って安定順に進み、`round-robin` はこの設定を無視します。クールダウン、フェイルオーバー上限、再認証の適格性は別のローカル状態です。アカウント別の週次使用量は、ダッシュボードのプロバイダーページで取得した後にのみ利用できます。 | diff --git a/docs-site/src/content/docs/ko/reference/cli/providers-accounts.md b/docs-site/src/content/docs/ko/reference/cli/providers-accounts.md index 67f4562617..36f8108df0 100644 --- a/docs-site/src/content/docs/ko/reference/cli/providers-accounts.md +++ b/docs-site/src/content/docs/ko/reference/cli/providers-accounts.md @@ -113,7 +113,7 @@ Codex pool selection applies to the next request after clearing existing affinit ### `ocx account list [provider] [--json] [--all] [--quota [--refresh]]` -제공자를 지정하지 않으면 Codex 풀, OAuth 계정, 설정된 API 키 풀을 나열합니다. `--all`이 없으면 비어 있는 제공자는 건너뜁니다. 제공자를 지정하면 해당 자격 증명 계열만 나열합니다. 사람이 보는 출력은 `PROVIDER TYPE ID PLAN/LABEL PRIORITY STATUS` 형식을 사용하며, 수동으로 선택한 Codex 행에는 `selected`가 표시됩니다. 사용 가능한 Kiro 계정이 두 개 이상 저장되어 있으면 기본적으로 429 응답 시 다른 계정으로 자동 전환하며, 알려진 잔여 할당량이 가장 많은 계정을 우선합니다. 이 전환은 계정 존재만으로 활성화되며 `oauthAccountFailover.enabled: false`로 끌 수 있습니다. `ocx account login kiro`는 계정을 한 번에 하나씩 풀에 추가합니다. 빈 결과도 성공입니다. `--json`은 다음을 반환합니다: +제공자를 지정하지 않으면 Codex 풀, OAuth 계정, 설정된 API 키 풀을 나열합니다. `--all`이 없으면 비어 있는 제공자는 건너뜁니다. 제공자를 지정하면 해당 자격 증명 계열만 나열합니다. 사람이 보는 출력은 `PROVIDER TYPE ID PLAN/LABEL PRIORITY STATUS` 형식을 사용하며, 수동으로 선택한 Codex 행에는 `selected`가 표시됩니다. 사용 가능한 Kiro 계정이 두 개 이상 저장되어 있으면 기본적으로 429 응답 시 다른 계정으로 자동 전환하며, 알려진 잔여 할당량이 가장 많은 계정을 우선합니다. 이 반응형 전환은 계정 존재만으로 활성화됩니다. `oauthAccountFailover.enabled: false`는 요청 전 선제 선택만 끄며 이 재시도는 끄지 않습니다. `ocx account login kiro`는 계정을 한 번에 하나씩 풀에 추가합니다. 빈 결과도 성공입니다. `--json`은 다음을 반환합니다: ```text { accounts: AccountRow[], notes: string[] } diff --git a/docs-site/src/content/docs/ko/reference/configuration/providers.md b/docs-site/src/content/docs/ko/reference/configuration/providers.md index 877085782d..ab6d588ebe 100644 --- a/docs-site/src/content/docs/ko/reference/configuration/providers.md +++ b/docs-site/src/content/docs/ko/reference/configuration/providers.md @@ -169,7 +169,7 @@ affinity 초기화 뒤의 기존 작업도 포함될 수 있습니다. 출력 | 키 | 타입 | 기본값 | 설명 | | --- | --- | --- | --- | -| `anthropicAccountPool.enabled?` | `boolean` | `false` | sticky 결속과 429 쿨다운 failover를 켭니다. | +| `anthropicAccountPool.enabled?` | `boolean` | `false` | 선제 sticky 결속과 새 세션 선택을 켭니다. 사용 가능한 계정이 두 개 이상이면 `false`여도 반응형 429 쿨다운과 failover는 동작합니다. | | `anthropicAccountPool.autoSwitchThreshold?` | `number` | `80` | 새 세션에서는 활성 계정이 이 임계값에 도달하면 설정된 창의 알려진 캐시 사용량이 가장 낮은 계정을 고릅니다. `0`이면 quota 선택을 끕니다. | | `anthropicAccountPool.strategy?` | `"quota" \| "round-robin" \| "fill-first"` | `"quota"` | 새 세션 전략입니다. `quota`는 `quotaWindow`로 지정한 창(기본값은 5시간 막대)으로 계정 순위를 매기고, `fill-first`도 같은 창에서 소진 임계값을 판정합니다. | | `anthropicAccountPool.quotaWindow?` | `"five-hour" \| "weekly" \| "max-utilization"` | `"five-hour"` | 사용량 기반 계정 선택에 사용하는, 공급자가 보고한 캐시 사용률 막대입니다. `five-hour`는 기존 동작을 유지합니다. `weekly`는 주간 막대를 사용하며 다른 사용 가능한 계정이 남아 있을 때만 5시간 막대가 소진된 계정을 건너뛰고, 아무 계정도 남지 않으면 해당 계정으로 폴백합니다. `max-utilization`은 알려진 값 중 가장 높은 값을 사용하므로 주간 사용량을 알기 전에도 5시간 사용량을 쓸 수 있고, 둘 다 모르면 unknown 순서를 따릅니다. 알려진 사용량은 unknown보다 앞서지만, 사용 가능한 계정이 모두 unknown이어도 사용 가능한 순서의 계정을 선택합니다. 앞서 설명한 5시간 사용량 동점 판정 뒤에도 완전히 같으면 사용 가능한 순서를 유지합니다. 정상 affinity 세션을 선제적으로 재배치하지 않습니다. 새 세션 배정과 가능한 429 대체 이후 라우팅 복구에서 `quota`는 이 창으로 사용 가능한 후보의 순위를 직접 매기고, `fill-first`는 이 창의 임계값과 소진 규칙에 따라 안정 순서로 이동하며, `round-robin`은 이 설정을 무시합니다. 쿨다운, failover 한도, 재인증 가능 여부는 별도의 로컬 상태로 유지됩니다. 계정별 주간 막대는 대시보드의 프로바이더 페이지에서 조회한 뒤에만 알 수 있습니다. | diff --git a/docs-site/src/content/docs/reference/cli/providers-accounts.md b/docs-site/src/content/docs/reference/cli/providers-accounts.md index 98b5972bf6..52f804e56c 100644 --- a/docs-site/src/content/docs/reference/cli/providers-accounts.md +++ b/docs-site/src/content/docs/reference/cli/providers-accounts.md @@ -148,7 +148,8 @@ Human output uses `PROVIDER TYPE ID PLAN/LABEL PRIORITY STATUS`; a manually chos `selected`. `PRIORITY` is the signed Codex selection order (`0` when unset) and shows `-` for rows where ordering does not apply, such as OAuth accounts and API keys. By default, with two or more eligible stored Kiro accounts, a 429 rotates automatically to another account and prefers the one with the most known remaining allowance; rotation is -presence-driven and can be turned off with `oauthAccountFailover.enabled: false`; `ocx account login kiro` +presence-driven. `oauthAccountFailover.enabled: false` disables only proactive pre-dispatch +preference, not this reactive retry; `ocx account login kiro` adds accounts to the pool one at a time. An empty result is still success. `--json` returns: diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index cfd5135ae9..6f9f82fcd2 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -352,7 +352,7 @@ rotation may trigger provider restrictions. | Key | Type | Default | Description | | --- | --- | --- | --- | -| `anthropicAccountPool.enabled?` | `boolean` | `false` | Enable sticky affinity and 429 cooldown failover. | +| `anthropicAccountPool.enabled?` | `boolean` | `false` | Enable proactive sticky affinity and new-session selection. With 2+ eligible accounts, reactive 429 cooldown/failover remains presence-driven even when this is `false`. | | `anthropicAccountPool.autoSwitchThreshold?` | `number` | `80` | For new sessions, when the active account reaches this threshold, choose the lowest known cached usage in the configured window; the account chosen does not itself have to be at or above the threshold. `0` disables **proactive** usage-based switching only — new-session selection and routing recovery after an eligible 429 still consult `quotaWindow`. | | `anthropicAccountPool.strategy?` | `"quota" \| "round-robin" \| "fill-first"` | `"quota"` | New-session strategy; `quota` ranks accounts by the window set by `quotaWindow`, and `fill-first` evaluates its drain threshold in that same window. | | `anthropicAccountPool.quotaWindow?` | `"five-hour" \| "weekly" \| "max-utilization"` | `"five-hour"` | The cached provider-reported utilization bar used for usage-aware account selection. `five-hour` keeps the original behavior. `weekly` scores the weekly bar and skips accounts whose 5-hour bar is exhausted while another eligible account remains, but falls back to exhausted candidates when none do. `max-utilization` scores the highest known bar, so it can use 5-hour usage before weekly usage is available; if neither is known, the account follows unknown-usage ordering. Known usage ranks before unknown usage under the opt-in `weekly` and `max-utilization` windows only; an omitted or explicit `five-hour` preserves the legacy ordering. If every eligible account is unknown, selection still returns one in eligible order. After the documented lower-5-hour tie-break, exact ties preserve eligible order. A healthy affinity-bound session is not proactively rebalanced. For new-session assignment and routing recovery after an eligible 429 replacement, `quota` ranks eligible candidates directly with this window; `fill-first` advances in stable order using this window's threshold and exhaustion rules; `round-robin` ignores it. Cooldown, failover limits, and reauthentication eligibility remain separate local state. Per-account weekly bars are only known once the dashboard Providers page has polled them. | @@ -381,12 +381,12 @@ behaves exactly as before. | Key | Type | Default | Description | | --- | --- | --- | --- | -| `oauthAccountFailover.enabled?` | `boolean` | presence-driven | Global override. `false` forces single-account behaviour everywhere; `true` forces rotation on. | -| `providers..oauthAccountFailover.enabled?` | `boolean` | inherits | Per-provider override; beats the global setting and beats account presence. | +| `oauthAccountFailover.enabled?` | `boolean` | presence-driven | Global control for proactive pre-dispatch preference. `false` disables that preference but does not disable reactive 429 rotation. | +| `providers..oauthAccountFailover.enabled?` | `boolean` | inherits | Per-provider proactive override; beats the global setting. It does not disable presence-driven reactive 429 rotation. | | `providers..oauthAccountFailover.strategy?` | `"quota" \| "round-robin" \| "fill-first"` | — | Declared pool strategy for a generic OAuth provider (#695). Persisted through `ocx account strategy ` or `PUT /api/oauth/accounts/pool`; the generic selector does not act on it yet, so omitted and set behave the same today. | | `providers..oauthAccountFailover.autoSwitchThreshold?` | `number` | — | Declared 0–100 usage percent for a proactive switch on a generic OAuth provider (#695). Set with `ocx account auto-switch threshold `; inert until the selector consumes it. | -To keep strict single-account behaviour for one provider whose terms you would rather not test: +To disable proactive pre-dispatch preference for one provider while retaining reactive 429 recovery: ```json { @@ -398,7 +398,8 @@ To keep strict single-account behaviour for one provider whose terms you would r } ``` -That setting survives logging in, adding an account, and reauthenticating. +That setting survives logging in, adding an account, and reauthenticating. To prevent any automatic +account switch, keep only one eligible account for that provider. Generic OAuth providers (Google Antigravity, xAI, Cursor, Kimi, GitHub Copilot, Nous, and any other OAuth provider outside the Codex and Anthropic pools) also accept `strategy` and @@ -425,9 +426,9 @@ Rotation carries the alternate account's **full** credential snapshot, not just provider that pairs routing metadata with its token — Antigravity's Cloud Code Assist project id, for example — cannot end up sending one account's token with another account's metadata. -Current scope is the ordinary Responses request paths. Cursor reports rate limits as adapter -events rather than an HTTP status, and the standalone Antigravity image endpoint has its own -request path; neither rotates yet. +Current scope covers the ordinary Responses request paths, pre-output Cursor adapter-event 429s, +terminal continuations, and the shared web-search/image sidecar retry hook. A failure after client +output has started remains terminal because replaying it would duplicate visible output or effects. :::caution[Experimental] Rotating across subscription accounts spends a second account's quota and may violate some diff --git a/docs-site/src/content/docs/ru/reference/cli/providers-accounts.md b/docs-site/src/content/docs/ru/reference/cli/providers-accounts.md index 1dd33a4da4..8f5a75cdde 100644 --- a/docs-site/src/content/docs/ru/reference/cli/providers-accounts.md +++ b/docs-site/src/content/docs/ru/reference/cli/providers-accounts.md @@ -138,7 +138,7 @@ label и masked key. Пустые провайдеры пропускаются, если не задан `--all`. С провайдером выводится только это семейство credential'ов. Human-output использует формат `PROVIDER TYPE ID PLAN/LABEL PRIORITY STATUS`; строка Codex, выбранная вручную, помечается `selected`. -При наличии двух или более подходящих сохранённых аккаунтов Kiro по умолчанию ответ 429 автоматически переключает запрос на другой аккаунт, предпочитая аккаунт с наибольшим известным остатком лимита; ротация включается самим наличием аккаунтов и отключается через `oauthAccountFailover.enabled: false`; `ocx account login kiro` добавляет аккаунты в пул по одному. Пустой результат всё равно считается успехом. +При наличии двух или более подходящих сохранённых аккаунтов Kiro по умолчанию ответ 429 автоматически переключает запрос на другой аккаунт, предпочитая аккаунт с наибольшим известным остатком лимита; реактивная ротация включается самим наличием аккаунтов. `oauthAccountFailover.enabled: false` отключает только проактивный выбор до отправки, но не эту повторную попытку; `ocx account login kiro` добавляет аккаунты в пул по одному. Пустой результат всё равно считается успехом. `--json` возвращает: ```text diff --git a/docs-site/src/content/docs/ru/reference/configuration/providers.md b/docs-site/src/content/docs/ru/reference/configuration/providers.md index ce41f34940..318c8cc0c3 100644 --- a/docs-site/src/content/docs/ru/reference/configuration/providers.md +++ b/docs-site/src/content/docs/ru/reference/configuration/providers.md @@ -200,7 +200,7 @@ reauth или порога исчерпания; здоровые привяза | Ключ | Тип | По умолчанию | Описание | | --- | --- | --- | --- | -| `anthropicAccountPool.enabled?` | `boolean` | `false` | Включить sticky affinity и cooldown failover на 429. | +| `anthropicAccountPool.enabled?` | `boolean` | `false` | Включить проактивную sticky affinity и выбор новых сессий. При двух или более подходящих аккаунтах реактивные cooldown и failover после 429 работают и при `false`. | | `anthropicAccountPool.autoSwitchThreshold?` | `number` | `80` | Для новых сессий выбирать аккаунт с наименьшим известным cached usage в настроенном окне, если активный аккаунт достиг порога. `0` отключает выбор по quota. | | `anthropicAccountPool.strategy?` | `"quota" \| "round-robin" \| "fill-first"` | `"quota"` | Стратегия для новых сессий; `quota` ранжирует аккаунты по окну, заданному в `quotaWindow` (по умолчанию это 5-hour bar'ы), а `fill-first` в этом же окне оценивает свой порог исчерпания. | | `anthropicAccountPool.quotaWindow?` | `"five-hour" \| "weekly" \| "max-utilization"` | `"five-hour"` | Кешированная полоса использования, сообщённая провайдером и применяемая при выборе по использованию. `five-hour` сохраняет прежнее поведение. `weekly` использует недельный bar и пропускает аккаунты с исчерпанным 5-hour bar, пока остаётся другой доступный аккаунт, но возвращается к ним, если других нет. `max-utilization` использует наибольшее известное значение, поэтому до появления недельных данных может использовать 5-hour usage; если неизвестны оба значения, аккаунт следует порядку unknown usage. Известное использование ранжируется раньше unknown, но если у всех доступных аккаунтов оно неизвестно, выбирается аккаунт в доступном порядке. После описанного сравнения по меньшему значению 5-hour usage полное равенство также сохраняет этот порядок. Здоровая сессия с affinity не перебалансируется заранее. При назначении новой сессии и восстановлении маршрутизации после допустимой замены при 429 `quota` напрямую ранжирует доступных кандидатов по этому окну, `fill-first` идёт в стабильном порядке с учётом порога и правил исчерпания этого окна, а `round-robin` игнорирует настройку. Cooldown, лимиты failover и допустимость повторной аутентификации остаются отдельным локальным состоянием. Недельные bar'ы аккаунтов известны только после опроса на странице Providers в dashboard. | 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..398f538740 100644 --- a/docs-site/src/content/docs/tr/guides/claude-code.md +++ b/docs-site/src/content/docs/tr/guides/claude-code.md @@ -11,12 +11,13 @@ anahtar yük devretme ve sidecar'lar dahil — kullanabilir. ## Claude OAuth hesap havuzu (deneysel) Sağlayıcılar kontrol panelinden (`ocx login anthropic` / hesap ekle) birden -fazla Claude hesabına giriş yapabilirsiniz. Varsayılan olarak her istek yalnızca -**aktif** hesabı kullanır. +fazla Claude hesabına giriş yapabilirsiniz. Tek uygun hesap varsa her istek aktif +hesabı kullanır. İki veya daha fazla hesap varsa proaktif havuz kapalıyken bile +yukarı akış 429 yanıtı isteği başka bir hesapla yeniden deneyebilir. **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 +(`anthropicAccountPool.enabled`), bu OAuth hesapları arasında proaktif yapışkan +oturum bağlılığı ve yeni oturum seçimi ekler. 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ı @@ -27,20 +28,22 @@ olarak kapalıdır**, bir GUI uyarısı gösterir ve sahada kapsamlı olarak tes edilmemiştir — Anthropic otomatik rotasyona benzeyen hesapları kısıtlayabilir; rotasyon sağlayıcı yaptırımlarına karşı koruma sağlamaz. -Etkinleştirildiğinde operasyonel sözleşme: +İki veya daha fazla uygun hesapla tepkisel kurtarma: - Yukarı akıştan gelen **429**, varsa `Retry-After` (yoksa varsayılan bir geri - çekilme) kullanarak o hesabı soğutur, bağlılıklarını temizler ve aynı istek - içinde uygun başka bir hesaba dönebilir (sınırlı). + çekilme) kullanarak o hesabı soğutur ve aynı istek içinde uygun başka bir + hesaba dönebilir (sınırlı). Bu davranış `anthropicAccountPool.enabled` yokken + veya `false` iken de etkindir. Otomatik hesap geçişini kabul etmiyorsanız + yalnızca bir uygun hesap bırakın. +- Proaktif havuz etkinse 429, o hesabın oturum bağlılığını da temizler. - Bağlılık **işleme özeldir (process-local)** (proxy yeniden başlatıldığında kaybolur). - **401/403** kimlik bilgisi hataları hesabı karantinaya alır (`needsReauth`), böylece yeniden kimlik doğrulanana kadar seçimden hariç tutulur. - Uygun tüm hesaplar soğutuluyorsa, proxy bilindiğinde `Retry-After` ile birlikte **429** (401 değil) döndürür. -- 429 yük devretmesi dahil kurtarma, mevcut soğuma ve yük devretme sınırlarını - değiştirmeden uygun yedek hesapları sıralamak için `quotaWindow` kullanır; - `round-robin` ise `quotaWindow` ayarını yok sayar. +- Proaktif havuz kapalıyken kurtarma kota sırasını kullanır. Etkinken uygun + yedekleri havuz stratejisi seçer; `round-robin` `quotaWindow` ayarını yok sayar. Bkz. [Yapılandırma](/tr/reference/configuration/#anthropicaccountpool-experimental). diff --git a/docs-site/src/content/docs/tr/reference/cli/providers-accounts.md b/docs-site/src/content/docs/tr/reference/cli/providers-accounts.md index d9881286b3..9c811ba269 100644 --- a/docs-site/src/content/docs/tr/reference/cli/providers-accounts.md +++ b/docs-site/src/content/docs/tr/reference/cli/providers-accounts.md @@ -164,7 +164,7 @@ bir Codex satırı `selected` olarak işaretlenir. `PRIORITY`, imzalı Codex se sırasıdır (ayarlanmadığında `0`) ve OAuth hesapları ve API anahtarları gibi sıralamanın geçerli olmadığı satırlar için `-` gösterir. İki veya daha fazla uygun Kiro hesabı saklandığında, varsayılan olarak 429 yanıtı otomatik olarak başka bir hesaba geçer ve bilinen kalan -kotası en yüksek hesabı tercih eder; rotasyon hesapların varlığıyla etkinleşir ve `oauthAccountFailover.enabled: false` ile kapatılabilir; `ocx account login kiro` hesapları havuza teker teker ekler. Boş bir sonuç +kotası en yüksek hesabı tercih eder; tepkisel rotasyon hesapların varlığıyla etkinleşir. `oauthAccountFailover.enabled: false` yalnızca gönderim öncesi proaktif tercihi kapatır, bu yeniden denemeyi kapatmaz; `ocx account login kiro` hesapları havuza teker teker ekler. Boş bir sonuç yine de başarıdır. `--json` şunu döndürür: ```text @@ -450,4 +450,3 @@ kataloğu reddeder, bu nedenle `add`, `edit` ve yönetim API'si katalog yazıcısının daha sonra çıkarması gereken bir şeyi saklamak yerine hatalı değeri reddeder (#759). - diff --git a/docs-site/src/content/docs/tr/reference/configuration/providers.md b/docs-site/src/content/docs/tr/reference/configuration/providers.md index b84e783c22..bd2e297d74 100644 --- a/docs-site/src/content/docs/tr/reference/configuration/providers.md +++ b/docs-site/src/content/docs/tr/reference/configuration/providers.md @@ -224,7 +224,7 @@ ve otomatik rotasyon sağlayıcı kısıtlamalarını tetikleyebilir. | Anahtar | Tip | Varsayılan | Açıklama | | --- | --- | --- | --- | -| `anthropicAccountPool.enabled?` | `boolean` | `false` | Yapışkan bağlılığı ve 429 soğuma yük devretmesini etkinleştirin. | +| `anthropicAccountPool.enabled?` | `boolean` | `false` | Proaktif yapışkan bağlılığı ve yeni oturum seçimini etkinleştirin. İki veya daha fazla uygun hesap varsa tepkisel 429 bekletme ve geçişi `false` iken de çalışır. | | `anthropicAccountPool.autoSwitchThreshold?` | `number` | `80` | Yeni oturumlarda etkin hesap bu eşiğe ulaştığında, yapılandırılan penceredeki bilinen en düşük önbelleğe alınmış kullanımı seçin. `0` kota seçimini devre dışı bırakır. | | `anthropicAccountPool.strategy?` | `"quota" \| "round-robin" \| "fill-first"` | `"quota"` | Yeni oturum stratejisi; `quota`, `quotaWindow` ile belirlenen pencereye (varsayılan 5 saatlik çubuklar) göre hesapları sıralar ve `fill-first` de tükenme eşiğini aynı pencerede değerlendirir. | | `anthropicAccountPool.quotaWindow?` | `"five-hour" \| "weekly" \| "max-utilization"` | `"five-hour"` | Kullanıma dayalı hesap seçiminde kullanılan, sağlayıcının bildirdiği önbelleğe alınmış kullanım çubuğu. `five-hour` mevcut davranışı korur. `weekly` haftalık çubuğu kullanır ve başka uygun hesap kaldığı sürece 5 saatlik çubuğu tükenmiş hesapları atlar; hiçbiri kalmazsa bu hesaplara geri döner. `max-utilization` bilinen en yüksek değeri kullanır; haftalık değer henüz yokken 5 saatlik değeri kullanabilir, ikisi de bilinmiyorsa hesap unknown kullanım sırasını izler. Bilinen kullanım unknown değerlerden önce gelir; tüm uygun hesaplar unknown olsa bile uygun sıradaki bir hesap seçilir. Belgelenen daha düşük 5 saatlik kullanım eşitlik bozmasından sonra tam eşitlikte de uygun sıra korunur. Sağlıklı affinity oturumları önceden yeniden dengelenmez. Yeni oturum ataması ve uygun bir 429 yedeğine geçildikten sonraki yönlendirme kurtarmasında `quota`, uygun adayları doğrudan bu pencereye göre sıralar; `fill-first`, bu pencerenin eşik ve tükenme kurallarıyla kararlı sırada ilerler; `round-robin` ayarı yok sayar. Cooldown, yük devretme sınırları ve yeniden kimlik doğrulama uygunluğu ayrı yerel durum olarak kalır. Hesap başına haftalık çubuklar ancak dashboard Sağlayıcılar sayfasında sorgulandıktan sonra bilinir. | diff --git a/docs-site/src/content/docs/zh-cn/reference/cli/providers-accounts.md b/docs-site/src/content/docs/zh-cn/reference/cli/providers-accounts.md index 984ac8c2ec..7b76d9be1e 100644 --- a/docs-site/src/content/docs/zh-cn/reference/cli/providers-accounts.md +++ b/docs-site/src/content/docs/zh-cn/reference/cli/providers-accounts.md @@ -126,7 +126,7 @@ OAuth 账号会显示为 `Account N`,而 plan/label 列会在 plan、屏蔽后 不指定提供方时,会列出 Codex 池、OAuth 账号和已配置的 API 密钥池。除非提供 `--all`,否则会跳过空的提供方。指定提供方时,只列出该凭据家族。人类可读输出 使用 `PROVIDER TYPE ID PLAN/LABEL PRIORITY STATUS`;手动选中的 Codex 行会标记为 `selected`。 -当存有两个或更多符合条件的 Kiro 账号时,默认情况下 429 会自动轮换到另一个账号,并优先选择已知剩余额度最多的账号;轮换由账号存在与否驱动,可通过 `oauthAccountFailover.enabled: false` 关闭。`ocx account login kiro` 每次向池中添加一个账号。空结果仍然算成功。`--json` 返回: +当存有两个或更多符合条件的 Kiro 账号时,默认情况下 429 会自动轮换到另一个账号,并优先选择已知剩余额度最多的账号;这种响应式轮换由账号存在与否驱动。`oauthAccountFailover.enabled: false` 仅关闭发送前的主动选择,不会关闭此次重试。`ocx account login kiro` 每次向池中添加一个账号。空结果仍然算成功。`--json` 返回: ```text { accounts: AccountRow[], notes: string[] } diff --git a/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md b/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md index d3cc8d9c14..814ee5653c 100644 --- a/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md +++ b/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md @@ -163,7 +163,7 @@ affinity。这些策略不能规避 provider enforcement。 | 键 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | -| `anthropicAccountPool.enabled?` | `boolean` | `false` | 启用粘性亲和性和 429 冷却故障转移。 | +| `anthropicAccountPool.enabled?` | `boolean` | `false` | 启用主动粘性亲和性和新会话选择。有两个或更多可用账户时,即使为 `false`,响应式 429 冷却和故障转移仍会运行。 | | `anthropicAccountPool.autoSwitchThreshold?` | `number` | `80` | 对于新会话,当活动账户达到此阈值时,选择配置窗口中已知缓存使用率最低的账户。`0` 会禁用配额选择。 | | `anthropicAccountPool.strategy?` | `"quota" \| "round-robin" \| "fill-first"` | `"quota"` | 新会话策略;`quota` 按 `quotaWindow` 指定的窗口(默认是 5 小时条形数据)对账户排序,`fill-first` 也在同一窗口中判定其耗尽阈值。 | | `anthropicAccountPool.quotaWindow?` | `"five-hour" \| "weekly" \| "max-utilization"` | `"five-hour"` | 基于用量选择账户时使用的、由提供商报告并缓存的用量条。`five-hour` 保持原有行为。`weekly` 使用每周用量条,并在仍有其他可用账户时跳过 5 小时用量已耗尽的账户;若没有其他账户,则回退使用这些账户。`max-utilization` 使用已知值中的最高值,因此每周用量尚不可用时仍可使用 5 小时用量;两者都未知时,账户遵循 unknown 用量排序。已知用量排在 unknown 之前,但如果所有可用账户都未知,仍会按可用顺序选择一个账户。在前述较低 5 小时用量的同分判定之后,完全相同时也保留可用顺序。不会主动重新平衡健康且已建立亲和性的会话。在新会话分配和符合条件的 429 替代后的路由恢复中,`quota` 直接按此窗口对可用候选账户排序;`fill-first` 按此窗口的阈值和耗尽规则以稳定顺序前进;`round-robin` 忽略此设置。冷却状态、故障转移上限和重新认证资格仍是独立的本地状态。各账户的每周用量只有在控制面板的提供商页面完成查询后才可用。 | 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..605cf6ac68 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 @@ -9,11 +9,12 @@ Code 可以使用每一個已路由的供應商——包括 OAuth 登入、帳 ## Claude OAuth 帳號池(實驗性) -你可以透過 Providers 儀表板登入多個 Claude 帳號(`ocx login anthropic` / add-account)。預設 -每個請求只使用**作用中**帳號。 +你可以透過 Providers 儀表板登入多個 Claude 帳號(`ocx login anthropic` / add-account)。只有一個 +合格帳號時,每個請求都使用該作用中帳號;有兩個以上帳號時,即使主動帳號池已關閉,上游 429 +仍可能改用另一個帳號重試。 **實驗性、opt-in** 的 Claude 帳號池(`anthropicAccountPool.enabled`)會在這些 OAuth 帳號之間加入 -sticky session affinity 與 429 冷卻故障轉移。僅對**新**工作階段,`anthropicAccountPool.strategy` +主動 sticky session affinity 與新工作階段選擇。僅對**新**工作階段,`anthropicAccountPool.strategy` 會在合格帳號之間選擇:`quota`(預設)在用量高於 `autoSwitchThreshold` 時,依 `anthropicAccountPool.quotaWindow` 所設定的視窗挑選已知用量最低者(`five-hour` 為預設,亦可選 `weekly` 或 `max-utilization`); @@ -21,15 +22,17 @@ sticky session affinity 與 429 冷卻故障轉移。僅對**新**工作階段 或達到閾值,然後前進。它**預設關閉**、會在 GUI 顯示警告,而且尚未經過實戰驗證——Anthropic 可能 限制看起來像自動輪換的帳號;輪換並不能保護你免受供應商執行機制的處置。 -啟用時的營運契約: +有兩個以上合格帳號時的反應式復原: -- 上游 **429** 會讓該帳號冷卻(有 `Retry-After` 時使用它,否則用預設 backoff)、清除其 affinity, - 並可能在同一個請求內輪換到另一個合格帳號(有上限)。 +- 上游 **429** 會讓該帳號冷卻(有 `Retry-After` 時使用它,否則用預設 backoff), + 並可能在同一個請求內輪換到另一個合格帳號(有上限)。即使 `anthropicAccountPool.enabled` 不存在 + 或為 `false`,此行為仍會運作;如果不接受自動帳號切換,請只保留一個合格帳號。 +- 主動帳號池啟用時,429 也會清除該帳號的 affinity。 - Affinity 是**程序本機**的(proxy 重啟後就會遺失)。 - **401/403** 憑證失敗會隔離該帳號(`needsReauth`),直到重新認證前都不會參與選擇。 - 如果每個合格帳號都在冷卻,proxy 會回傳 **429**(不是 401),並在已知時附上 `Retry-After`。 -- 復原(包括 429 容錯移轉)會使用 `quotaWindow` 為合格的替代帳號排序,且不改變現有的冷卻或 - 容錯移轉上限;`round-robin` 會忽略 `quotaWindow`。 +- 主動帳號池關閉時,復原使用 quota 排序;啟用時則由帳號池策略選擇合格替代帳號, + `round-robin` 會忽略 `quotaWindow`。 請見 [Configuration](/zh-tw/reference/configuration/#anthropicaccountpool-experimental)。 diff --git a/docs-site/src/content/docs/zh-tw/reference/cli/providers-accounts.md b/docs-site/src/content/docs/zh-tw/reference/cli/providers-accounts.md index 33722358f3..baba42dd66 100644 --- a/docs-site/src/content/docs/zh-tw/reference/cli/providers-accounts.md +++ b/docs-site/src/content/docs/zh-tw/reference/cli/providers-accounts.md @@ -94,7 +94,7 @@ Codex 池選擇套用於清除既有親和性後的下一個請求;進行中 ### `ocx account list [provider] [--json] [--all] [--quota [--refresh]]` -未指定供應商時,列出 Codex 池、OAuth 帳號與已設定的 API-key 池。除非存在 `--all`,否則空的供應商會被跳過。指定供應商時,僅列出該憑證家族。人類輸出使用 `PROVIDER TYPE ID PLAN/LABEL STATUS`;手動選擇的 Codex 列標記為 `selected`。當儲存了兩個以上符合資格的 Kiro 帳號時,預設情況下 429 會自動輪換至另一個帳號,並優先選擇已知剩餘額度最多的帳號;輪換由帳號存在與否驅動,可透過 `oauthAccountFailover.enabled: false` 關閉。`ocx account login kiro` 每次將一個帳號加入池中。空結果仍為成功。`--json` 回傳: +未指定供應商時,列出 Codex 池、OAuth 帳號與已設定的 API-key 池。除非存在 `--all`,否則空的供應商會被跳過。指定供應商時,僅列出該憑證家族。人類輸出使用 `PROVIDER TYPE ID PLAN/LABEL STATUS`;手動選擇的 Codex 列標記為 `selected`。當儲存了兩個以上符合資格的 Kiro 帳號時,預設情況下 429 會自動輪換至另一個帳號,並優先選擇已知剩餘額度最多的帳號;這種反應式輪換由帳號存在與否驅動。`oauthAccountFailover.enabled: false` 只會關閉送出前的主動選擇,不會關閉這次重試。`ocx account login kiro` 每次將一個帳號加入池中。空結果仍為成功。`--json` 回傳: ```text { accounts: AccountRow[], notes: string[] } diff --git a/docs-site/src/content/docs/zh-tw/reference/configuration/providers.md b/docs-site/src/content/docs/zh-tw/reference/configuration/providers.md index 509d4c88c4..13c0fb230e 100644 --- a/docs-site/src/content/docs/zh-tw/reference/configuration/providers.md +++ b/docs-site/src/content/docs/zh-tw/reference/configuration/providers.md @@ -131,7 +131,7 @@ API-key 供應商可持有字面值金鑰或環境參考。OAuth 供應商使用 | Key | 型別 | 預設值 | 說明 | | --- | --- | --- | --- | -| `anthropicAccountPool.enabled?` | `boolean` | `false` | 啟用 sticky 親和性與 429 冷卻容錯移轉。 | +| `anthropicAccountPool.enabled?` | `boolean` | `false` | 啟用主動 sticky 親和性與新工作階段選擇。有兩個以上可用帳號時,即使為 `false`,反應式 429 冷卻與容錯移轉仍會運作。 | | `anthropicAccountPool.autoSwitchThreshold?` | `number` | `80` | 對於新 session,當目前帳號達到此閾值時,選擇設定視窗中最低的已知快取用量。`0` 停用配額挑選。 | | `anthropicAccountPool.strategy?` | `"quota" \| "round-robin" \| "fill-first"` | `"quota"` | 新 session 策略;`quota` 依 `quotaWindow` 指定的視窗(預設為 5 小時列)為帳號排序,`fill-first` 也在同一視窗中判定其排空閾值。 | | `anthropicAccountPool.quotaWindow?` | `"five-hour" \| "weekly" \| "max-utilization"` | `"five-hour"` | 使用量型帳號選擇所採用、由供應商回報並快取的用量列。`five-hour` 保留原有行為。`weekly` 使用每週用量列,並在仍有其他可用帳號時略過 5 小時用量已用盡的帳號;若沒有其他帳號,則退回使用這些帳號。`max-utilization` 使用已知值中的最高值,因此每週用量尚未取得時仍可使用 5 小時用量;兩者都未知時,帳號遵循 unknown 用量排序。已知用量排在 unknown 之前,但若所有可用帳號都是 unknown,仍會依可用順序選出一個。完成前述較低 5 小時用量的同分判定後,完全相同時也保留可用順序。不會主動重新平衡健康且已有 affinity 的 session。在分配新 session 與符合條件的 429 替代後進行路由復原時,`quota` 直接依此視窗排序可用候選帳號;`fill-first` 依此視窗的門檻與用盡規則按穩定順序前進;`round-robin` 忽略此設定。冷卻狀態、容錯移轉上限與重新驗證資格仍是獨立的本機狀態。每個帳號的每週用量只有在 dashboard 的供應商頁面完成查詢後才可得知。 | diff --git a/src/oauth/anthropic-routing.ts b/src/oauth/anthropic-routing.ts index f2c1edc342..6dbaafb877 100644 --- a/src/oauth/anthropic-routing.ts +++ b/src/oauth/anthropic-routing.ts @@ -621,7 +621,13 @@ export function rotateAnthropicAccountOn429( clearAnthropicSessionAffinityForAccount(failedAccountId); notePoolRotationFailure(POOL_KEY_ANTHROPIC, failedAccountId); - const next = pickAlternateAnthropicAccount(config, failedAccountId, now); + // The pool's strategy is a PROACTIVE policy. When the pool is disabled, reactive + // presence-only recovery must not silently reactivate round-robin/fill-first merely + // because those dormant values remain in config. The quota picker is the neutral + // recovery policy already used by the default strategy. + const next = isAnthropicAccountPoolEnabled(config) + ? pickAlternateAnthropicAccount(config, failedAccountId, now) + : pickLowestUsage(config, failedAccountId, now); if (!next) { console.warn("[anthropic-pool] all eligible Anthropic OAuth accounts are in cooldown; returning 429"); return null; diff --git a/src/oauth/generic-account-failover.ts b/src/oauth/generic-account-failover.ts index 9a7a2566f9..321b5f92a1 100644 --- a/src/oauth/generic-account-failover.ts +++ b/src/oauth/generic-account-failover.ts @@ -178,15 +178,20 @@ export function isGenericOAuthFailoverEnabled( * Whether the pre-dispatch account PREFERENCE may run for this provider. * * Unlike reactive rotation, this moves a request that upstream has not refused, so it stays - * refusable: an explicit `false` — per provider first, then global — turns it off. `true` adds - * nothing over presence, so only `false` is honoured; that keeps the predicate identical to the - * old behaviour for every operator who never wrote the key, and a malformed value falls through - * rather than taking a provider out of service. + * refusable: an explicit provider value wins over the global default, and a global `false` + * turns it off only when the provider has no override. A malformed value falls through rather + * than taking a provider out of service. */ function isProactivePreferenceEnabled(config: OcxConfig, providerName: string, now: number): boolean { const provider = config.providers?.[providerName]; if (!provider || !isGenericFailoverProvider(providerName, provider)) return false; - if (provider.oauthAccountFailover?.enabled === false) return false; + const perProvider = provider.oauthAccountFailover?.enabled; + // Preserve the published narrow-over-broad precedence. A provider-specific true may + // opt this provider into proactive preference even when the global default is false; + // a provider-specific false refuses it even when the global setting is true. + if (typeof perProvider === "boolean") { + return perProvider && hasFailoverAccountQuorum(providerName, now); + } if (config.oauthAccountFailover?.enabled === false) return false; return hasFailoverAccountQuorum(providerName, now); } diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts index cb1e00678e..e8afe21836 100644 --- a/src/server/responses/core.ts +++ b/src/server/responses/core.ts @@ -3364,7 +3364,10 @@ async function handleResponsesInner( * tolerates project discovery failing, so a stored account can legitimately have no project; * sending that account's bearer with the FAILED account's project is worse than not rotating. */ - const applyFailoverSnapshot = (snapshot: OAuthAccessSnapshot): boolean => { + const applyFailoverSnapshot = ( + snapshot: OAuthAccessSnapshot, + retryParsed: OcxParsedRequest = parsed, + ): boolean => { if (route.provider.googleMode === "cloud-code-assist" && !snapshot.projectId) return false; let rotatedProvider: OcxProviderConfig = { ...route.provider, apiKey: snapshot.accessToken }; if (route.providerName === "github-copilot") { @@ -3377,7 +3380,14 @@ async function handleResponsesInner( } if (snapshot.projectId) rotatedProvider = { ...rotatedProvider, project: snapshot.projectId }; route.provider = rotatedProvider; - if (route.providerName === "kiro") parsed._kiroAuthContext = { ...(snapshot.kiro ?? {}) }; + if (route.providerName === "kiro") { + const kiroContext = { ...(snapshot.kiro ?? {}) }; + // Terminal-guard continuations are rebuilt from a shallow clone. Updating only the + // outer request pairs the new bearer with the failed account's region/profile on + // the retry. Keep both owners synchronized; for ordinary paths they are identical. + parsed._kiroAuthContext = kiroContext; + if (retryParsed !== parsed) retryParsed._kiroAuthContext = { ...kiroContext }; + } // Re-stamp: a request that rotated accounts must be attributed to the account that actually // served it. All three rotation sites funnel through here, so this is the only re-stamp // needed -- and putting it anywhere else would let one of the three drift. @@ -6683,7 +6693,7 @@ async function handleResponsesInner( const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId); genericFailoverAccountId = nextAccountId; genericFailovers += 1; - if (applyFailoverSnapshot(snapshot)) { + if (applyFailoverSnapshot(snapshot, nextParsed)) { invalidateSameTargetRequest(); activeAdapter = resolveAdapter( resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire), diff --git a/src/types/config.ts b/src/types/config.ts index fdbec5ba25..114a743980 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -791,7 +791,7 @@ export interface OcxConfig { * What `enabled: false` still refuses is the PRE-DISPATCH preference: steering a request * upstream has not refused toward the account with more known headroom. That moves a healthy * request, so it stays a real choice. `providers..oauthAccountFailover` overrides this - * per provider, and only `false` is meaningful — `true` adds nothing over presence. + * per provider in either direction; reactive 429 rotation remains presence-driven. */ oauthAccountFailover?: { enabled?: boolean; diff --git a/src/types/provider.ts b/src/types/provider.ts index 459634fd16..46082c9620 100644 --- a/src/types/provider.ts +++ b/src/types/provider.ts @@ -430,7 +430,7 @@ export interface OcxProviderConfig { * activate it, and a 429 with an idle second account is a defect rather than a preference. * What an explicit `false` still refuses is the pre-dispatch preference that steers a HEALTHY * request toward the account with more known headroom. It beats the global - * `oauthAccountFailover`; only `false` is meaningful, since `true` adds nothing over presence. + * `oauthAccountFailover` in either direction; reactive 429 rotation remains presence-driven. */ oauthAccountFailover?: { enabled?: boolean; diff --git a/structure/04_transports-and-sidecars.md b/structure/04_transports-and-sidecars.md index d460d1f4d5..5bf7039048 100644 --- a/structure/04_transports-and-sidecars.md +++ b/structure/04_transports-and-sidecars.md @@ -1410,9 +1410,18 @@ surface is listed here so a maintainer can find the owner without grepping: | Image/video generation loop | `src/images/loop.ts`, `src/images/plan.ts`, `src/images/fulfill.ts`, `src/images/xai-client.ts`, `src/images/xai-video-client.ts`, `src/images/artifacts.ts` | A provider-returned image URL is downloaded into a local artifact once, then served locally; warnings stay URL-free because provider CDN URLs may embed credentials. | | GitHub Copilot | `src/providers/xai-transport.ts` (`resolveProviderTransport`), `src/providers/github-copilot-transport.ts` | `resolveProviderTransport` selects the Copilot transport when the routed provider name is `github-copilot`; the Copilot module then resolves its headers and base URL, and the registry seeds the provider row and model fallback. | | API-key pools | `src/providers/key-failover.ts` | A 429 rotates the active key and records a cooldown; `provider.apiKey` keeps mirroring the active entry so routing stays single-key. | +| OAuth account failover | `src/oauth/generic-account-failover.ts`, `src/oauth/anthropic-routing.ts`, `src/server/responses/core.ts` | Reactive pre-output 429 recovery is presence-driven with 2+ eligible accounts. Pool flags govern proactive routing, not the reactive retry. A rotated credential and all account-owned routing metadata must be applied to the exact request object being retried; sidecars and terminal continuations share the same bounded request-local counters. | | Alibaba regions | `src/providers/alibaba-region-backup.ts`, `src/providers/alibaba-region-migration.ts`, `src/providers/alibaba-region-startup.ts` | Region migration backs up before rewriting and is idempotent across restarts. | | Discovery and quota | `src/providers/model-discovery.ts`, `src/providers/quota.ts` | Discovery rejects a response over 4 MiB or past 2,000 raw rows before caching it. | +[Decision Log] +- 목적과 의도: Keep reactive OAuth 429 recovery available without silently enabling proactive account-routing policy, while preserving account-owned credential metadata across every retry surface. +- 기존 구현 및 제약 조건: #3495 made reactive recovery presence-driven, but a disabled Anthropic pool still consulted its dormant strategy, generic provider overrides no longer beat the global setting, and a Kiro terminal continuation rebuilt from a shallow request clone that retained the failed account's region/profile. +- 검토한 주요 대안: Restore the old all-or-nothing enable flag; leave the merged behavior and document the gaps; or keep the reactive/proactive split and repair the exact policy and snapshot boundaries. +- 선택한 방식: Keep presence-driven reactive recovery, apply proactive precedence only before dispatch, use quota ordering for disabled-pool Anthropic recovery, and synchronize Kiro metadata onto both the outer request and the continuation clone. +- 다른 대안 대신 이 방식을 선택한 이유: This preserves the owner's merged product decision without allowing disabled proactive settings or stale account metadata to influence a retry. +- 장점, 단점 및 영향: 429 recovery remains automatic for operators with multiple eligible accounts and is consistent across main, continuation, and sidecar paths; operators who require no automatic account switch must keep one eligible account, which the GUI and public docs state explicitly. + ## Sidecars Web search and vision sidecars run only when the main request needs that capability and a usable diff --git a/tests/adapter-event-oauth-failover.test.ts b/tests/adapter-event-oauth-failover.test.ts index bb3b4c37ea..6a26a41a0d 100644 --- a/tests/adapter-event-oauth-failover.test.ts +++ b/tests/adapter-event-oauth-failover.test.ts @@ -137,9 +137,12 @@ describe("#2568 adapter-event OAuth failover", () => { [{ type: "text", text: "ok" }], ]; - const body = await (await handleResponses(request(true), config(false), { model: "", provider: "" })).text(); + const response = await handleResponses(request(true), config(false), { model: "", provider: "" }); + const body = await response.text(); + expect(response.status).toBe(200); expect(attemptKeys).toEqual(["cursor-access-1", "cursor-access-0"]); + expect(body).toContain("ok"); expect(body).not.toContain("rate_limit_exceeded"); }); diff --git a/tests/always-on-429-failover.test.ts b/tests/always-on-429-failover.test.ts index 1ab40da0ad..a548792af1 100644 --- a/tests/always-on-429-failover.test.ts +++ b/tests/always-on-429-failover.test.ts @@ -24,7 +24,7 @@ import { } from "../src/oauth/anthropic-routing"; import { clearPoolRotationState } from "../src/codex/pool-rotation"; import { getAccountSet, saveCredential, setActiveAccount } from "../src/oauth/store"; -import { clearAccountQuotaCache } from "../src/providers/quota"; +import { clearAccountQuotaCache, setCachedProviderAccountQuotaForTests } from "../src/providers/quota"; import type { OcxConfig } from "../src/types"; import { removeTreeWithRetry } from "./helpers/remove-tree"; @@ -99,6 +99,19 @@ describe("Anthropic reactive 429 failover without the pool flag", () => { expect(rotateAnthropicAccountOn429(poolDisabled(), ids[0]!, null)).toBe(ids[1]); }); + test("a disabled pool does not apply its dormant proactive strategy to reactive recovery", async () => { + const ids = await seedAccounts(3); + setCachedProviderAccountQuotaForTests("anthropic", ids[1]!, { fiveHourPercent: 90 }); + setCachedProviderAccountQuotaForTests("anthropic", ids[2]!, { fiveHourPercent: 10 }); + const disabledRoundRobin = { + ...poolAbsent(), + anthropicAccountPool: { enabled: false, strategy: "round-robin" }, + } as OcxConfig; + + // Reactive recovery uses quota ordering while the proactive pool is disabled. + expect(rotateAnthropicAccountOn429(disabledRoundRobin, ids[0]!, null)).toBe(ids[2]); + }); + test("a single account is still a strict no-op", async () => { // Rotating to itself would replay the same 429 on the same credential, and cooling the only // account would take the provider out of service for nothing. diff --git a/tests/anthropic-sidecar-account-failover.test.ts b/tests/anthropic-sidecar-account-failover.test.ts new file mode 100644 index 0000000000..b7c57ed17d --- /dev/null +++ b/tests/anthropic-sidecar-account-failover.test.ts @@ -0,0 +1,277 @@ +import { afterAll, afterEach, beforeAll, beforeEach, expect, mock, test } from "bun:test"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import type { ProviderAdapter } from "../src/adapters/base"; +import { + clearAnthropicAccountPoolState, +} from "../src/oauth/anthropic-routing"; +import { clearGenericFailoverHealth } from "../src/oauth/generic-account-failover"; +import { getAccountSet, saveCredential, setActiveAccount } from "../src/oauth/store"; +import type { AdapterEvent, OcxConfig, OcxParsedRequest, OcxProviderConfig } from "../src/types"; +import { removeTreeWithRetry } from "./helpers/remove-tree"; + +const previousHome = process.env.OPENCODEX_HOME; +let testHome = ""; +let handleResponses: typeof import("../src/server/responses")["handleResponses"]; +let observedKeys: string[] = []; +let sidecarMode = false; +let kiroBuilds: Array<{ key: string; profileArn?: string; apiRegion?: string }> = []; + +function fixtureAdapter(provider: OcxProviderConfig): ProviderAdapter { + return { + name: "anthropic", + buildRequest() { + return { + url: provider.baseUrl, + method: "POST", + headers: { authorization: `Bearer ${provider.apiKey ?? ""}` }, + body: "{}", + }; + }, + async *parseStream() { + yield { type: "done" as const }; + }, + }; +} + +function kiroContinuationEvents(phase: string): AdapterEvent[] { + if (phase === "plan") { + return [ + { type: "text_delta", text: "I will modify the file now." }, + { type: "done", stopReason: "end_turn" }, + ]; + } + if (phase === "complete") { + return [ + { type: "tool_call_start", id: "call_read", name: "read_file" }, + { type: "tool_call_delta", arguments: "{}" }, + { type: "tool_call_end" }, + { type: "done", stopReason: "tool_use" }, + ]; + } + throw new Error(`unexpected phase: ${phase}`); +} + +function kiroFixtureAdapter(provider: OcxProviderConfig): ProviderAdapter { + return { + // Anthropic enables the bounded terminal continuation, while the provider id remains + // Kiro so generic OAuth snapshot pairing is exercised. + name: "anthropic", + buildRequest(parsed: OcxParsedRequest) { + kiroBuilds.push({ + key: provider.apiKey ?? "", + ...(parsed._kiroAuthContext?.profileArn + ? { profileArn: parsed._kiroAuthContext.profileArn } + : {}), + ...(parsed._kiroAuthContext?.apiRegion + ? { apiRegion: parsed._kiroAuthContext.apiRegion } + : {}), + }); + return { + url: provider.baseUrl, + method: "POST", + headers: { authorization: `Bearer ${provider.apiKey ?? ""}` }, + body: "{}", + }; + }, + async *parseStream(response: Response): AsyncGenerator { + yield* kiroContinuationEvents(response.headers.get("x-test-phase") ?? ""); + }, + }; +} + +beforeAll(async () => { + const actualResolver = await import("../src/server/adapter-resolve"); + const actualResolveAdapter = actualResolver.resolveAdapter; + mock.module("../src/server/adapter-resolve", () => ({ + ...actualResolver, + resolveAdapter(provider: OcxProviderConfig, cacheRetention?: "none" | "short" | "long") { + if (provider.adapter === "test-anthropic-sidecar") return fixtureAdapter(provider); + if ( + provider.adapter === "test-kiro-continuation" + || (provider.adapter === "kiro" && provider.apiKey?.startsWith("kiro-access-")) + ) return kiroFixtureAdapter(provider); + return actualResolveAdapter(provider, cacheRetention); + }, + })); + + mock.module("../src/web-search", () => ({ + buildWebSearchTool: () => ({ + name: "web_search", + parameters: { type: "object", properties: {} }, + }), + planWebSearch: () => sidecarMode + ? { + backend: "anthropic", + hostedTool: { type: "web_search" }, + settings: { model: "claude-haiku-4-5", reasoning: "low", timeoutMs: 1_000 }, + maxSearches: 1, + } + : undefined, + shouldResolveOpenAiWebSearchSidecar: () => false, + runWithWebSearch: async (args: { + parsed: OcxParsedRequest; + adapter: ProviderAdapter; + on429?: (retryAfter: string | null) => Promise; + }) => { + const first = await args.adapter.buildRequest(args.parsed); + observedKeys.push(new Headers(first.headers).get("authorization") ?? ""); + const rotated = await args.on429?.("30"); + if (!rotated) throw new Error("Anthropic sidecar did not rotate after 429"); + const second = await rotated.buildRequest(args.parsed); + observedKeys.push(new Headers(second.headers).get("authorization") ?? ""); + return new Response("sidecar-ok", { status: 200 }); + }, + })); + + ({ handleResponses } = await import("../src/server/responses")); +}); + +beforeEach(() => { + testHome = mkdtempSync(join(tmpdir(), "ocx-oauth-429-boundaries-")); + process.env.OPENCODEX_HOME = testHome; + observedKeys = []; + kiroBuilds = []; + sidecarMode = false; + clearAnthropicAccountPoolState(); + clearGenericFailoverHealth(); +}); + +afterEach(() => { + clearAnthropicAccountPoolState(); + clearGenericFailoverHealth(); + removeTreeWithRetry(testHome); +}); + +afterAll(() => { + if (previousHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = previousHome; + mock.restore(); +}); + +test("Anthropic web-search sidecar rotates on 429 when proactive pooling is disabled", async () => { + sidecarMode = true; + for (let index = 0; index < 2; index += 1) { + await saveCredential("anthropic", { + access: `anthropic-access-${index}`, + refresh: `anthropic-refresh-${index}`, + expires: Date.now() + 3_600_000, + accountId: `anthropic-account-${index}`, + } as never, { addAccount: true }); + } + const ids = getAccountSet("anthropic")!.accounts.map(account => account.id); + await setActiveAccount("anthropic", ids[0]!); + + const config = { + port: 0, + defaultProvider: "anthropic", + anthropicAccountPool: { enabled: false, strategy: "round-robin" }, + providers: { + anthropic: { + adapter: "test-anthropic-sidecar", + baseUrl: "https://anthropic-sidecar.test/v1", + authMode: "oauth", + models: ["model"], + }, + }, + } as unknown as OcxConfig; + + const response = await handleResponses(new Request("http://localhost/v1/responses", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + model: "anthropic/model", + input: "search", + stream: true, + tools: [{ type: "web_search" }], + }), + }), config, { model: "", provider: "" }); + + expect(response.status).toBe(200); + expect(await response.text()).toBe("sidecar-ok"); + expect(observedKeys).toEqual([ + "Bearer anthropic-access-0", + "Bearer anthropic-access-1", + ]); +}); + +test("Kiro continuation 429 keeps the rotated bearer and routing metadata together", async () => { + const profiles = [ + "arn:aws:codewhisperer:us-east-1:123456789012:profile/account-a", + "arn:aws:codewhisperer:eu-west-1:123456789012:profile/account-b", + ]; + const regions = ["us-east-1", "eu-west-1"]; + for (let index = 0; index < 2; index += 1) { + await saveCredential("kiro", { + access: `kiro-access-${index}`, + refresh: `kiro-refresh-${index}`, + expires: Date.now() + 3_600_000, + accountId: `kiro-account-${index}`, + kiro: { + profileArn: profiles[index], + apiRegion: regions[index], + ssoRegion: regions[index], + }, + } as never, { addAccount: true }); + } + const ids = getAccountSet("kiro")!.accounts.map(account => account.id); + await setActiveAccount("kiro", ids[0]!); + + const config = { + port: 0, + defaultProvider: "kiro", + providers: { + kiro: { + adapter: "test-kiro-continuation", + baseUrl: "https://kiro-continuation.test/v1", + authMode: "oauth", + models: ["model"], + }, + }, + } as unknown as OcxConfig; + + const phases = ["plan", "rate-limit", "complete"]; + const originalFetch = globalThis.fetch; + globalThis.fetch = (async () => { + const phase = phases.shift(); + if (phase === "rate-limit") { + return Response.json( + { error: { message: "rate limited" } }, + { status: 429, headers: { "retry-after": "30" } }, + ); + } + if (!phase) throw new Error("unexpected extra request"); + return new Response("", { status: 200, headers: { "x-test-phase": phase } }); + }) as typeof fetch; + + try { + const response = await handleResponses(new Request("http://localhost/v1/responses", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + model: "kiro/model", + input: "Please modify the file", + stream: true, + tools: [{ + type: "function", + name: "read_file", + description: "Read one file", + parameters: { type: "object", properties: {} }, + }], + }), + }), config, { model: "", provider: "" }); + + expect(response.status).toBe(200); + expect(await response.text()).toContain("read_file"); + } finally { + globalThis.fetch = originalFetch; + } + + expect(kiroBuilds).toEqual([ + { key: "kiro-access-0", profileArn: profiles[0], apiRegion: regions[0] }, + { key: "kiro-access-0", profileArn: profiles[0], apiRegion: regions[0] }, + { key: "kiro-access-1", profileArn: profiles[1], apiRegion: regions[1] }, + ]); + expect(phases).toEqual([]); +}); diff --git a/tests/generic-oauth-failover.test.ts b/tests/generic-oauth-failover.test.ts index 7bbfc57951..72bdbb31f0 100644 --- a/tests/generic-oauth-failover.test.ts +++ b/tests/generic-oauth-failover.test.ts @@ -12,7 +12,7 @@ import { preferredInitialAccount, rotateGenericOAuthAccountOn429, } from "../src/oauth/generic-account-failover"; -import { getAccountSet, markAccountNeedsReauth, saveCredential } from "../src/oauth/store"; +import { getAccountSet, markAccountNeedsReauth, saveCredential, setActiveAccount } from "../src/oauth/store"; import { clearAccountQuotaCache, setCachedProviderAccountQuotaForTests } from "../src/providers/quota"; import { resolveCopilotApiBaseUrl } from "../src/oauth/github-copilot"; import { resolveProviderTransport } from "../src/providers/xai-transport"; @@ -136,6 +136,16 @@ describe("#2568 generic OAuth account failover", () => { expect(preferredInitialAccount(config(true, false), "xai")).toBeNull(); }); + test("a provider-level true overrides a global proactive opt-out", async () => { + const ids = await seed(2); + await setActiveAccount("xai", ids[0]!); + clearGenericFailoverHealth("xai"); + setCachedProviderAccountQuotaForTests("xai", ids[0]!, { fiveHourPercent: 99 }); + setCachedProviderAccountQuotaForTests("xai", ids[1]!, { fiveHourPercent: 1 }); + + expect(preferredInitialAccount(config(false, true), "xai")).toBe(ids[1]); + }); + test("a second account flagged for reauth is not a quorum", async () => { // A revoked account cannot serve the replay, so counting it would arm the failover machinery // for a user who still has exactly one usable credential. @@ -288,7 +298,7 @@ describe("sidecar on429 wiring", () => { // Kiro's routing metadata) live in exactly one place. A fourth rotation site that swaps the // bearer by hand would reintroduce the mixed-identity bug this helper exists to prevent. const snapshotUses = coreSource.match(/failoverAccountSnapshot\(/g) ?? []; - const helperUses = coreSource.match(/applyFailoverSnapshot\(snapshot\)/g) ?? []; + const helperUses = coreSource.match(/applyFailoverSnapshot\(snapshot(?:, nextParsed)?\)/g) ?? []; // Four since the continuation loop gained its own generic-OAuth arm: the streaming loop grew // one with #2568 and the continuation loop did not, so an xAI/Cursor continuation 429 stayed // terminal. Bumping this count is the deliberate act of admitting a fourth rotation site -- @@ -326,6 +336,7 @@ describe("sidecar on429 wiring", () => { // Kiro routing metadata still travels with its own token. expect(body).toContain("_kiroAuthContext"); + expect(coreSource).toContain("applyFailoverSnapshot(snapshot, nextParsed)"); }); test("pre-dispatch selection replaces the CCA project instead of inheriting one", () => { From 6671a16238c464a4e650e95d97c05b6d8ab6b0f7 Mon Sep 17 00:00:00 2001 From: Ingwannu Date: Fri, 4 Sep 2026 18:36:39 +0000 Subject: [PATCH 2/2] docs(oauth): align reactive failover guidance --- .../fr/reference/configuration/providers.md | 10 ++++++---- .../ja/reference/configuration/providers.md | 4 ++-- .../ko/reference/configuration/providers.md | 4 ++-- .../docs/reference/configuration/providers.md | 14 ++++++++------ .../ru/reference/configuration/providers.md | 16 +++++++++------- .../tr/reference/configuration/providers.md | 18 +++++++++++------- .../zh-cn/reference/configuration/providers.md | 4 ++-- .../zh-tw/reference/configuration/providers.md | 4 ++-- 8 files changed, 42 insertions(+), 32 deletions(-) diff --git a/docs-site/src/content/docs/fr/reference/configuration/providers.md b/docs-site/src/content/docs/fr/reference/configuration/providers.md index 4f7cb4278a..e0c888677d 100644 --- a/docs-site/src/content/docs/fr/reference/configuration/providers.md +++ b/docs-site/src/content/docs/fr/reference/configuration/providers.md @@ -204,14 +204,16 @@ rotation automatique peut déclencher des restrictions du fournisseur. | `anthropicAccountPool.quotaWindow?` | `"five-hour" \| "weekly" \| "max-utilization"` | `"five-hour"` | Barre d'utilisation signalée par le fournisseur, mise en cache et utilisée pour la sélection selon l'utilisation. `five-hour` conserve le comportement actuel. `weekly` utilise la barre hebdomadaire et ignore les comptes dont la barre sur 5 heures est épuisée tant qu'un autre compte admissible reste disponible, mais y revient si aucun autre ne reste. `max-utilization` utilise la valeur connue la plus élevée et peut donc employer la barre sur 5 heures avant que la barre hebdomadaire soit disponible ; si aucune n'est connue, le compte suit l'ordre des utilisations inconnues. Les utilisations connues précèdent les inconnues, mais si tous les comptes admissibles sont inconnus, la sélection en renvoie tout de même un dans leur ordre admissible. Après le départage documenté par la plus faible utilisation sur 5 heures, une égalité exacte conserve cet ordre. Une session saine avec affinité n'est pas rééquilibrée de manière proactive. Pour l'affectation des nouvelles sessions et la reprise du routage après un remplacement admissible à la suite d'un 429, `quota` classe directement les candidats admissibles avec cette fenêtre ; `fill-first` avance dans un ordre stable selon le seuil et les règles d'épuisement de cette fenêtre ; `round-robin` l'ignore. Le délai de récupération, les limites de basculement et l'éligibilité de réauthentification restent des états locaux distincts. Les barres hebdomadaires ne sont connues qu'après leur interrogation dans la page Fournisseurs du tableau de bord. | | `anthropicAccountPool.stickyLimit?` | `number` | `1` | Liaisons de nouvelle session réussies conservées sur une sélection à tour de rôle. Portée 1–100. | -Lorsque cette option est activée, un 429 enregistre une temporisation bornée à partir de `Retry-After` ou d'un délai de repli, puis peut -faire basculer la requête vers un autre compte. L'affinité est locale au processus et de taille bornée. Un 401/403 lié aux identifiants marque le compte +Avec au moins deux comptes admissibles, un 429 enregistre une temporisation bornée à partir de `Retry-After` ou d'un délai de repli, puis peut +faire basculer la requête vers un autre compte même si cette option est absente ou vaut `false`. L'activation du groupe ajoute l'affinité proactive +et la sélection des nouvelles sessions. L'affinité est locale au processus et de taille bornée. Un 401/403 lié aux identifiants marque le compte comme devant être réauthentifié. Si tous les comptes admissibles sont en temporisation, les clients reçoivent un 429 accompagné de `Retry-After` lorsqu'il est connu, et non une erreur d'authentification. :::caution[Expérimental] -Laissez cette option désactivée, sauf si vous comprenez les risques liés aux règles d'Anthropic concernant les comptes. En cas de doute, -préférez le changement manuel avec `ocx account use anthropic `. +Laissez le regroupement proactif désactivé, sauf si vous comprenez les risques liés aux règles d'Anthropic concernant les comptes. Cela ne +désactive pas la rotation réactive après 429 ; ne conservez qu'un seul compte admissible si vous refusez tout changement automatique. En cas +de doute, préférez le changement manuel avec `ocx account use anthropic `. ::: ### Formes d'enregistrement gérées diff --git a/docs-site/src/content/docs/ja/reference/configuration/providers.md b/docs-site/src/content/docs/ja/reference/configuration/providers.md index 491e276b76..775f8dfeee 100644 --- a/docs-site/src/content/docs/ja/reference/configuration/providers.md +++ b/docs-site/src/content/docs/ja/reference/configuration/providers.md @@ -171,10 +171,10 @@ affinity を維持します。これらの戦略は provider enforcement を回 | `anthropicAccountPool.quotaWindow?` | `"five-hour" \| "weekly" \| "max-utilization"` | `"five-hour"` |使用量ベースのアカウント選択で使う、プロバイダー報告のキャッシュ済み使用率です。`five-hour` は従来の動作を維持します。`weekly` は週次使用量を使い、他に対象アカウントが残る間だけ 5 時間使用量が上限に達したアカウントを除外し、残らない場合はそれらへフォールバックします。`max-utilization` は判明している値のうち最も高いものを使うため、週次使用量が未取得でも 5 時間使用量を利用できます。どちらも不明なら unknown の順位付けに従います。既知の使用量は unknown より先ですが、対象がすべて unknown でも対象順の先頭を選択します。記載した 5 時間使用量による同点判定後も完全に同点なら、対象順を維持します。正常な affinity セッションを先回りして再配置することはありません。新規セッションの割り当てと、対象となる 429 代替後のルーティング復旧では、`quota` はこの期間で対象候補を直接順位付けし、`fill-first` はこの期間のしきい値と上限到達ルールを使って安定順に進み、`round-robin` はこの設定を無視します。クールダウン、フェイルオーバー上限、再認証の適格性は別のローカル状態です。アカウント別の週次使用量は、ダッシュボードのプロバイダーページで取得した後にのみ利用できます。 | | `anthropicAccountPool.stickyLimit?` | `number` | `1` |成功した新しいセッションのバインドは 1 つのラウンドロビン選択で保持されます。範囲は 1 ~ 100。 | -有効にすると、429 レコードは `Retry-After` またはデフォルトのバックオフからの制限されたクールダウンを記録し、リクエスト内でローテーションする可能性があります。アフィニティはプロセスローカルであり、サイズ制限があります。資格情報 401/403 は、アカウントに再認証が必要であることをマークします。すべての対象となるアカウントが冷却されている場合、クライアントは、既知の場合、認証エラーではなく、`Retry-After` を含む 429 を受け取ります。 +利用可能なアカウントが 2 つ以上ある場合、429 は `Retry-After` またはデフォルトのバックオフから制限付きクールダウンを記録し、このフラグが未設定または `false` でもリクエスト内で別アカウントへ切り替わることがあります。プールを有効にすると、これに加えてプロアクティブなスティッキー affinity と新規セッション選択が有効になります。Affinity はプロセスローカルであり、サイズ制限があります。資格情報 401/403 は、アカウントに再認証が必要であることをマークします。すべての対象アカウントがクールダウン中の場合、クライアントは認証エラーではなく、既知であれば `Retry-After` を含む 429 を受け取ります。 :::caution[実験的] -Anthropic アカウント ポリシーのリスクを理解していない限り、これは無効のままにしてください。不明な場合は、`ocx account use anthropic ` を手動で切り替えることをお勧めします。 +Anthropic アカウントポリシーのリスクを理解していない限り、プロアクティブなプールは無効のままにしてください。これだけでは反応型の 429 ローテーションは無効になりません。自動切り替えを一切許容しない場合は、利用可能なアカウントを 1 つだけにしてください。不明な場合は、`ocx account use anthropic ` による手動切り替えをお勧めします。 ::: ### 管理されたレコードの形状 diff --git a/docs-site/src/content/docs/ko/reference/configuration/providers.md b/docs-site/src/content/docs/ko/reference/configuration/providers.md index ab6d588ebe..d3bdd2e47e 100644 --- a/docs-site/src/content/docs/ko/reference/configuration/providers.md +++ b/docs-site/src/content/docs/ko/reference/configuration/providers.md @@ -175,10 +175,10 @@ affinity 초기화 뒤의 기존 작업도 포함될 수 있습니다. 출력 | `anthropicAccountPool.quotaWindow?` | `"five-hour" \| "weekly" \| "max-utilization"` | `"five-hour"` | 사용량 기반 계정 선택에 사용하는, 공급자가 보고한 캐시 사용률 막대입니다. `five-hour`는 기존 동작을 유지합니다. `weekly`는 주간 막대를 사용하며 다른 사용 가능한 계정이 남아 있을 때만 5시간 막대가 소진된 계정을 건너뛰고, 아무 계정도 남지 않으면 해당 계정으로 폴백합니다. `max-utilization`은 알려진 값 중 가장 높은 값을 사용하므로 주간 사용량을 알기 전에도 5시간 사용량을 쓸 수 있고, 둘 다 모르면 unknown 순서를 따릅니다. 알려진 사용량은 unknown보다 앞서지만, 사용 가능한 계정이 모두 unknown이어도 사용 가능한 순서의 계정을 선택합니다. 앞서 설명한 5시간 사용량 동점 판정 뒤에도 완전히 같으면 사용 가능한 순서를 유지합니다. 정상 affinity 세션을 선제적으로 재배치하지 않습니다. 새 세션 배정과 가능한 429 대체 이후 라우팅 복구에서 `quota`는 이 창으로 사용 가능한 후보의 순위를 직접 매기고, `fill-first`는 이 창의 임계값과 소진 규칙에 따라 안정 순서로 이동하며, `round-robin`은 이 설정을 무시합니다. 쿨다운, failover 한도, 재인증 가능 여부는 별도의 로컬 상태로 유지됩니다. 계정별 주간 막대는 대시보드의 프로바이더 페이지에서 조회한 뒤에만 알 수 있습니다. | | `anthropicAccountPool.stickyLimit?` | `number` | `1` | 성공한 새 세션 결속이 한 번의 라운드로빈 선택에 유지되는 횟수입니다. 범위는 1–100입니다. | -활성화되면 429 레코드가 `Retry-After` 또는 기본 backoff에서 제한된 쿨다운을 기록하고, 요청 안에서 회전할 수 있습니다. 결속은 프로세스 로컬이며 크기가 제한됩니다. 자격 증명 401/403은 해당 계정이 재인증이 필요함을 표시합니다. 적격한 계정이 모두 쿨다운 중이면, 클라이언트는 인증 오류가 아니라 알려진 경우 `Retry-After`가 포함된 429를 받습니다. +사용 가능한 계정이 두 개 이상이면 이 플래그가 없거나 `false`여도 429가 `Retry-After` 또는 기본 backoff에 따른 제한된 쿨다운을 기록하고 요청 안에서 다른 계정으로 전환할 수 있습니다. 풀을 켜면 여기에 선제 sticky 결속과 새 세션 선택이 추가됩니다. 결속은 프로세스 로컬이며 크기가 제한됩니다. 자격 증명 401/403은 해당 계정에 재인증이 필요함을 표시합니다. 사용 가능한 계정이 모두 쿨다운 중이면 클라이언트는 인증 오류가 아니라, 알려진 경우 `Retry-After`가 포함된 429를 받습니다. :::caution[실험적 기능] -Anthropic 계정 정책 위험을 이해하지 못한다면 이 기능은 꺼두십시오. 확신이 없으면 수동 `ocx account use anthropic ` 전환을 우선하십시오. +Anthropic 계정 정책 위험을 이해하지 못한다면 선제 풀링은 꺼두십시오. 이것만으로 반응형 429 전환이 꺼지지는 않습니다. 자동 전환을 전혀 허용하지 않으려면 사용 가능한 계정을 하나만 유지하십시오. 확신이 없으면 수동 `ocx account use anthropic ` 전환을 우선하십시오. ::: ### 관리되는 레코드 구조 diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index 6f9f82fcd2..7abb9fbc08 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -358,14 +358,16 @@ rotation may trigger provider restrictions. | `anthropicAccountPool.quotaWindow?` | `"five-hour" \| "weekly" \| "max-utilization"` | `"five-hour"` | The cached provider-reported utilization bar used for usage-aware account selection. `five-hour` keeps the original behavior. `weekly` scores the weekly bar and skips accounts whose 5-hour bar is exhausted while another eligible account remains, but falls back to exhausted candidates when none do. `max-utilization` scores the highest known bar, so it can use 5-hour usage before weekly usage is available; if neither is known, the account follows unknown-usage ordering. Known usage ranks before unknown usage under the opt-in `weekly` and `max-utilization` windows only; an omitted or explicit `five-hour` preserves the legacy ordering. If every eligible account is unknown, selection still returns one in eligible order. After the documented lower-5-hour tie-break, exact ties preserve eligible order. A healthy affinity-bound session is not proactively rebalanced. For new-session assignment and routing recovery after an eligible 429 replacement, `quota` ranks eligible candidates directly with this window; `fill-first` advances in stable order using this window's threshold and exhaustion rules; `round-robin` ignores it. Cooldown, failover limits, and reauthentication eligibility remain separate local state. Per-account weekly bars are only known once the dashboard Providers page has polled them. | | `anthropicAccountPool.stickyLimit?` | `number` | `1` | Successful new-session binds retained on one round-robin selection. Range 1–100. | -When enabled, 429 records bounded cooldown from `Retry-After` or a default backoff and may rotate -within the request. Affinity is process-local and size-bounded. Credential 401/403 marks the account -as needing reauthentication. If all eligible accounts are cooling, clients receive 429 with -`Retry-After` when known, not an authentication error. +With two or more eligible accounts, a 429 records bounded cooldown from `Retry-After` or a default +backoff and may rotate within the request even when this flag is absent or `false`. Enabling the pool +additionally activates proactive sticky affinity and new-session selection. Affinity is process-local +and size-bounded. Credential 401/403 marks the account as needing reauthentication. If all eligible +accounts are cooling, clients receive 429 with `Retry-After` when known, not an authentication error. :::caution[Experimental] -Leave this disabled unless you understand Anthropic account policy risk. Prefer manual -`ocx account use anthropic ` switching when unsure. +Leave proactive pooling disabled unless you understand Anthropic account policy risk. This does not +disable reactive 429 rotation; keep only one eligible account if you require no automatic switching. +Prefer manual `ocx account use anthropic ` switching when unsure. ::: ### `oauthAccountFailover` diff --git a/docs-site/src/content/docs/ru/reference/configuration/providers.md b/docs-site/src/content/docs/ru/reference/configuration/providers.md index 318c8cc0c3..f7760287b6 100644 --- a/docs-site/src/content/docs/ru/reference/configuration/providers.md +++ b/docs-site/src/content/docs/ru/reference/configuration/providers.md @@ -206,15 +206,17 @@ reauth или порога исчерпания; здоровые привяза | `anthropicAccountPool.quotaWindow?` | `"five-hour" \| "weekly" \| "max-utilization"` | `"five-hour"` | Кешированная полоса использования, сообщённая провайдером и применяемая при выборе по использованию. `five-hour` сохраняет прежнее поведение. `weekly` использует недельный bar и пропускает аккаунты с исчерпанным 5-hour bar, пока остаётся другой доступный аккаунт, но возвращается к ним, если других нет. `max-utilization` использует наибольшее известное значение, поэтому до появления недельных данных может использовать 5-hour usage; если неизвестны оба значения, аккаунт следует порядку unknown usage. Известное использование ранжируется раньше unknown, но если у всех доступных аккаунтов оно неизвестно, выбирается аккаунт в доступном порядке. После описанного сравнения по меньшему значению 5-hour usage полное равенство также сохраняет этот порядок. Здоровая сессия с affinity не перебалансируется заранее. При назначении новой сессии и восстановлении маршрутизации после допустимой замены при 429 `quota` напрямую ранжирует доступных кандидатов по этому окну, `fill-first` идёт в стабильном порядке с учётом порога и правил исчерпания этого окна, а `round-robin` игнорирует настройку. Cooldown, лимиты failover и допустимость повторной аутентификации остаются отдельным локальным состоянием. Недельные bar'ы аккаунтов известны только после опроса на странице Providers в dashboard. | | `anthropicAccountPool.stickyLimit?` | `number` | `1` | Сколько успешных bind'ов новых сессий удерживать на одном выборе round-robin. Диапазон 1–100. | -Если функция включена, 429 записывает ограниченный cooldown из `Retry-After` или из default -backoff и может переключить аккаунт уже внутри текущего запроса. Affinity локальна для процесса и -ограничена по размеру. Credential 401/403 помечает аккаунт как нуждающийся в переавторизации. -Если все eligible-аккаунты в cooldown, клиент получает 429 с `Retry-After`, если он известен, -а не authentication error. +При двух или более подходящих аккаунтах ответ 429 записывает ограниченный cooldown из +`Retry-After` или default backoff и может переключить аккаунт уже внутри текущего запроса, даже +если флаг отсутствует или равен `false`. Включение пула дополнительно включает проактивную sticky +affinity и выбор новых сессий. Affinity локальна для процесса и ограничена по размеру. Credential +401/403 помечает аккаунт как нуждающийся в переавторизации. Если все eligible-аккаунты в cooldown, +клиент получает 429 с `Retry-After`, если он известен, а не authentication error. :::caution[Experimental] -Оставляйте эту функцию выключенной, если не понимаете policy-risk аккаунтов Anthropic. Если -сомневаетесь, безопаснее переключать аккаунты вручную через +Оставляйте проактивный пул выключенным, если не понимаете policy-risk аккаунтов Anthropic. Это не +отключает реактивную ротацию после 429; если автоматическое переключение недопустимо, оставьте +подходящим только один аккаунт. Если сомневаетесь, безопаснее переключать аккаунты вручную через `ocx account use anthropic `. ::: diff --git a/docs-site/src/content/docs/tr/reference/configuration/providers.md b/docs-site/src/content/docs/tr/reference/configuration/providers.md index bd2e297d74..efb6976243 100644 --- a/docs-site/src/content/docs/tr/reference/configuration/providers.md +++ b/docs-site/src/content/docs/tr/reference/configuration/providers.md @@ -230,15 +230,19 @@ ve otomatik rotasyon sağlayıcı kısıtlamalarını tetikleyebilir. | `anthropicAccountPool.quotaWindow?` | `"five-hour" \| "weekly" \| "max-utilization"` | `"five-hour"` | Kullanıma dayalı hesap seçiminde kullanılan, sağlayıcının bildirdiği önbelleğe alınmış kullanım çubuğu. `five-hour` mevcut davranışı korur. `weekly` haftalık çubuğu kullanır ve başka uygun hesap kaldığı sürece 5 saatlik çubuğu tükenmiş hesapları atlar; hiçbiri kalmazsa bu hesaplara geri döner. `max-utilization` bilinen en yüksek değeri kullanır; haftalık değer henüz yokken 5 saatlik değeri kullanabilir, ikisi de bilinmiyorsa hesap unknown kullanım sırasını izler. Bilinen kullanım unknown değerlerden önce gelir; tüm uygun hesaplar unknown olsa bile uygun sıradaki bir hesap seçilir. Belgelenen daha düşük 5 saatlik kullanım eşitlik bozmasından sonra tam eşitlikte de uygun sıra korunur. Sağlıklı affinity oturumları önceden yeniden dengelenmez. Yeni oturum ataması ve uygun bir 429 yedeğine geçildikten sonraki yönlendirme kurtarmasında `quota`, uygun adayları doğrudan bu pencereye göre sıralar; `fill-first`, bu pencerenin eşik ve tükenme kurallarıyla kararlı sırada ilerler; `round-robin` ayarı yok sayar. Cooldown, yük devretme sınırları ve yeniden kimlik doğrulama uygunluğu ayrı yerel durum olarak kalır. Hesap başına haftalık çubuklar ancak dashboard Sağlayıcılar sayfasında sorgulandıktan sonra bilinir. | | `anthropicAccountPool.stickyLimit?` | `number` | `1` | Bir round-robin seçiminde tutulan başarılı yeni oturum bağlamaları. Aralık 1–100. | -Etkinleştirildiğinde 429, `Retry-After`'dan veya varsayılan bir geri çekilmeden -sınırlı soğuma kaydeder ve istek içinde dönebilir. Bağlılık işleme özeldir ve -boyut sınırlıdır. Kimlik bilgisi 401/403, hesabı yeniden kimlik doğrulama -gerektiriyor olarak işaretler. Uygun tüm hesaplar soğuyorsa istemciler bir -kimlik doğrulama hatası değil, bilindiğinde `Retry-After` ile 429 alır. +İki veya daha fazla uygun hesap varsa 429, `Retry-After`'dan veya varsayılan bir +geri çekilmeden sınırlı soğuma kaydeder ve bu bayrak yokken ya da `false` iken +bile istek içinde başka hesaba geçebilir. Havuzu etkinleştirmek ayrıca proaktif +yapışkan bağlılığı ve yeni oturum seçimini açar. Bağlılık işleme özeldir ve boyut +sınırlıdır. Kimlik bilgisi 401/403, hesabı yeniden kimlik doğrulama gerektiriyor +olarak işaretler. Uygun tüm hesaplar soğuyorsa istemciler bir kimlik doğrulama +hatası değil, bilindiğinde `Retry-After` ile 429 alır. :::caution[Deneysel] -Anthropic hesap politikası riskini anlamadığınız sürece bunu devre dışı bırakın. -Emin olmadığınızda manuel `ocx account use anthropic ` geçişini tercih edin. +Anthropic hesap politikası riskini anlamadığınız sürece proaktif havuzu devre dışı +bırakın. Bu, tepkisel 429 geçişini devre dışı bırakmaz; otomatik geçiş istemiyorsanız +yalnızca bir uygun hesap bırakın. Emin olmadığınızda manuel +`ocx account use anthropic ` geçişini tercih edin. ::: ### Yönetilen kayıt biçimleri diff --git a/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md b/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md index 814ee5653c..ca813198df 100644 --- a/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md +++ b/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md @@ -169,10 +169,10 @@ affinity。这些策略不能规避 provider enforcement。 | `anthropicAccountPool.quotaWindow?` | `"five-hour" \| "weekly" \| "max-utilization"` | `"five-hour"` | 基于用量选择账户时使用的、由提供商报告并缓存的用量条。`five-hour` 保持原有行为。`weekly` 使用每周用量条,并在仍有其他可用账户时跳过 5 小时用量已耗尽的账户;若没有其他账户,则回退使用这些账户。`max-utilization` 使用已知值中的最高值,因此每周用量尚不可用时仍可使用 5 小时用量;两者都未知时,账户遵循 unknown 用量排序。已知用量排在 unknown 之前,但如果所有可用账户都未知,仍会按可用顺序选择一个账户。在前述较低 5 小时用量的同分判定之后,完全相同时也保留可用顺序。不会主动重新平衡健康且已建立亲和性的会话。在新会话分配和符合条件的 429 替代后的路由恢复中,`quota` 直接按此窗口对可用候选账户排序;`fill-first` 按此窗口的阈值和耗尽规则以稳定顺序前进;`round-robin` 忽略此设置。冷却状态、故障转移上限和重新认证资格仍是独立的本地状态。各账户的每周用量只有在控制面板的提供商页面完成查询后才可用。 | | `anthropicAccountPool.stickyLimit?` | `number` | `1` | 在一次轮询选择中保留的成功新会话绑定次数。范围 1–100。 | -启用后,429 会根据 `Retry-After` 记录有界冷却,或者使用默认退避,并且可能在同一请求内轮换。亲和性是进程本地的,并且有大小上限。凭据 401/403 会将账户标记为需要重新认证。如果所有合格账户都在冷却,客户端会在已知时收到带 `Retry-After` 的 429,而不是身份验证错误。 +有两个或更多可用账户时,即使此标志未设置或为 `false`,429 也会根据 `Retry-After` 或默认退避记录有界冷却,并可能在同一请求内切换账户。启用账户池会额外开启主动粘性亲和性和新会话选择。亲和性是进程本地的,并且有大小上限。凭据 401/403 会将账户标记为需要重新认证。如果所有合格账户都在冷却,客户端会在已知时收到带 `Retry-After` 的 429,而不是身份验证错误。 :::caution[Experimental] -除非你理解 Anthropic 账户策略风险,否则请保持关闭。若不确定,优先手动使用 `ocx account use anthropic ` 切换。 +除非你理解 Anthropic 账户策略风险,否则请保持主动账户池关闭。这并不会关闭响应式 429 轮换;如果完全不允许自动切换,请只保留一个可用账户。若不确定,优先手动使用 `ocx account use anthropic ` 切换。 ::: ### 托管记录形状 diff --git a/docs-site/src/content/docs/zh-tw/reference/configuration/providers.md b/docs-site/src/content/docs/zh-tw/reference/configuration/providers.md index 13c0fb230e..db831404bc 100644 --- a/docs-site/src/content/docs/zh-tw/reference/configuration/providers.md +++ b/docs-site/src/content/docs/zh-tw/reference/configuration/providers.md @@ -137,10 +137,10 @@ API-key 供應商可持有字面值金鑰或環境參考。OAuth 供應商使用 | `anthropicAccountPool.quotaWindow?` | `"five-hour" \| "weekly" \| "max-utilization"` | `"five-hour"` | 使用量型帳號選擇所採用、由供應商回報並快取的用量列。`five-hour` 保留原有行為。`weekly` 使用每週用量列,並在仍有其他可用帳號時略過 5 小時用量已用盡的帳號;若沒有其他帳號,則退回使用這些帳號。`max-utilization` 使用已知值中的最高值,因此每週用量尚未取得時仍可使用 5 小時用量;兩者都未知時,帳號遵循 unknown 用量排序。已知用量排在 unknown 之前,但若所有可用帳號都是 unknown,仍會依可用順序選出一個。完成前述較低 5 小時用量的同分判定後,完全相同時也保留可用順序。不會主動重新平衡健康且已有 affinity 的 session。在分配新 session 與符合條件的 429 替代後進行路由復原時,`quota` 直接依此視窗排序可用候選帳號;`fill-first` 依此視窗的門檻與用盡規則按穩定順序前進;`round-robin` 忽略此設定。冷卻狀態、容錯移轉上限與重新驗證資格仍是獨立的本機狀態。每個帳號的每週用量只有在 dashboard 的供應商頁面完成查詢後才可得知。 | | `anthropicAccountPool.stickyLimit?` | `number` | `1` | 在一次 round-robin 選擇上保留的成功新 session 綁定。範圍 1–100。 | -啟用時,429 記錄來自 `Retry-After` 或預設 backoff 的有界冷卻,並可能在請求內輪換。親和性為行程本地且有界。憑證 401/403 將帳號標記為需要重新認證。若所有合格帳號都在冷卻,客戶端收到附帶已知 `Retry-After` 的 429,而非認證錯誤。 +有兩個以上可用帳號時,即使此旗標未設定或為 `false`,429 也會依 `Retry-After` 或預設 backoff 記錄有界冷卻,並可能在同一請求內切換帳號。啟用帳號池會額外開啟主動 sticky 親和性與新工作階段選擇。親和性為行程本地且有界。憑證 401/403 將帳號標記為需要重新認證。若所有合格帳號都在冷卻,客戶端會收到附帶已知 `Retry-After` 的 429,而非認證錯誤。 :::caution[實驗性] -除非你了解 Anthropic 帳號政策風險,否則保持停用。不確定時偏好手動 `ocx account use anthropic ` 切換。 +除非你了解 Anthropic 帳號政策風險,否則請保持主動帳號池停用。這不會停用反應式 429 輪換;如果完全不允許自動切換,請只保留一個可用帳號。不確定時偏好手動 `ocx account use anthropic ` 切換。 ::: ### 受管記錄結構