Skip to content

Commit c380ef7

Browse files
authored
feat(auth): account pool round-robin and fill-first strategies (#593)
* feat(codex): add account pool round-robin selector * feat(codex): honor accountPoolStrategy for new sessions * feat(anthropic): account pool rotation strategies * feat(api): expose account pool rotation strategy * feat(gui): account pool rotation strategy controls * fix(gui): optimistic account pool strategy select * docs: account pool rotation strategies * docs: clarify Anthropic pool strategy quota window * test(codex): assert round-robin fairness histogram * fix(auth): peek RR preview and harden pool strategy tests * fix(auth): address Codex pool-strategy review findings Seed RR on manual select, gate affinity re-eval to quota, validate before mutate, keep automatic picks in-memory, and advance fill-first/RR on 429. * fix(gui): keep auto-switch tests mounting with strategy card Mock shared /active defaults and pool-strategy so CodexPoolStrategySetting no longer races the auto-switch harness. * fix(auth): typecheck and remaining pool-strategy P2s * fix(auth): restore #584 retry and harden pool rotation edges * fix(api): reject non-object pool-strategy bodies Return 400 for null/array/scalar JSON instead of throwing 500, and use POOL_KEY_ANTHROPIC in the sticky-release test.
1 parent 2a404c7 commit c380ef7

32 files changed

Lines changed: 2695 additions & 105 deletions

docs-site/src/content/docs/getting-started/how-it-works.mdx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,11 @@ account before the request is forwarded upstream. The rule is intentionally spli
3232
- **Existing thread ids keep affinity.** A thread is bound to the account generation that started it,
3333
so a long SSH, tmux, or mobile-attached Codex session keeps using one account instead of being
3434
rebalanced mid-conversation.
35-
- **New sessions can rebalance.** For a new thread, opencodex compares known quota usage across 5h,
36-
weekly, and 30d windows, skips accounts that need reauthentication or are in cooldown, and can
37-
switch to a lower-usage eligible account when the active account crosses the configured threshold.
35+
- **New sessions can rebalance.** For a new thread, opencodex picks among eligible accounts using
36+
`accountPoolStrategy` (`quota` by default, or `round-robin` / `fill-first`). The `quota` strategy
37+
compares known usage across 5h, weekly, and 30d windows and can switch to a lower-usage account
38+
when the active account crosses `autoSwitchThreshold`. Accounts in cooldown or needing
39+
reauthentication are skipped regardless of strategy.
3840
- **Quota and failure signals feed routing.** The dashboard can force a quota refresh with
3941
`GET /api/codex-auth/accounts?refresh=1`; successful upstream responses capture quota headers,
4042
429 puts an account in cooldown, and 401/403 marks it for reauthentication.

docs-site/src/content/docs/guides/claude-code.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@ You can log in multiple Claude accounts via the Providers dashboard (`ocx login
1313
add-account). By default every request uses the **active** account only.
1414

1515
An **experimental, opt-in** Claude account pool (`anthropicAccountPool.enabled`) adds sticky
16-
session affinity and 429 cooldown failover across those OAuth accounts, with optional
17-
new-session lowest-usage pick from the 5-hour quota bars. It is **off by default**, shows a
18-
GUI warning, and is not battle-tested — Anthropic may restrict accounts that look like
19-
automated rotation.
16+
session affinity and 429 cooldown failover across those OAuth accounts. For **new** sessions
17+
only, `anthropicAccountPool.strategy` selects among eligible accounts: `quota` (default) picks
18+
lowest known 5-hour usage when above `autoSwitchThreshold`; `round-robin` spreads evenly
19+
(`stickyLimit`, default `1`); `fill-first` drains the active account until cooldown,
20+
reauthentication, or threshold, then advances. It is **off by default**, shows a GUI warning,
21+
and is not battle-tested — Anthropic may restrict accounts that look like automated rotation;
22+
rotation does not protect against provider enforcement.
2023

2124
Operational contract when enabled:
2225

docs-site/src/content/docs/ja/reference/configuration.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ namespaced selected id を bare id に変えます。
5252
| `syncResumeHistory?` | `boolean` | `true` | 戻せる Codex App 履歴互換モード。opencodex は元の Codex thread metadata をバックアップし、旧 OpenAI interactive row を `opencodex` に再マッピングし、opencodex が作成した `exec` row を App に見えるソースとして一時的に昇格します。`ocx stop` / `ocx restore` はバックアップした OpenAI row を復元し、残った opencodex user thread を OpenAI に戻し、ネイティブ Codex が `config.toml` からプロキシを削除した後でも開き続けられるようにします。オフにするには `false` に設定します。 |
5353
| `codexAccounts?` | `CodexAccount[]` | `[]` | Codex Auth ダッシュボードが管理する ChatGPT/Codex pool アカウント metadata。secret は `codex-accounts.json` に別途置きます。 |
5454
| `activeCodexAccountId?` | `string` || 手動選択した pool アカウント。既存 thread affinity を消去して次のリクエストから適用し、処理中のリクエストは現在のアカウントを維持します。 |
55-
| `autoSwitchThreshold?` | `number` | `80` | 新しいセッション自動切替用の使用量百分率 threshold。既知の 5 時間、週次、30 日 quota window のうち最も高いスコアを使います。`0` なら quota 自動切替をオフにします。 |
55+
| `autoSwitchThreshold?` | `number` | `80` | 新しいセッション自動切替用の使用量百分率 threshold。既知の 5 時間、週次、30 日 quota window のうち最も高いスコアを使います。`0` なら quota 自動切替をオフにします。`quota` 戦略と `fill-first` の drain threshold にも使います。 |
56+
| `accountPoolStrategy?` | `"quota" \| "round-robin" \| "fill-first"` | `"quota"` | Codex pool の新しいセッション rotation 戦略。**新しいセッションのみ**に適用され、既存 thread id は affinity を維持します。`quota`(既定)— アクティブアカウントが `autoSwitchThreshold` を超えたら既知 usage 最小を選択。`round-robin` — 適格アカウント間を smooth weighted で均等分散。`fill-first` — cooldown、使用不可、または(設定時)`autoSwitchThreshold` までアクティブアカウントを使い切り(未知 usage は強制切替しない)、安定ソート順で次へ。 |
57+
| `accountPoolStickyLimit?` | `number` | `1` | 1 回の round-robin 選択で次へ進む前に保持する成功的新セッション bind 数。範囲 1–100。`accountPoolStrategy``round-robin` のときのみ。 |
5658
| `upstreamFailoverThreshold?` | `number` | `3` | 一時的な上流失敗が連続して起きたのち、以降の新しいセッションを別の適合 pool アカウントに failover する回数。`0` なら失敗ベースの failover をオフにします。 |
5759
| `modelCacheTtlMs?` | `number` | `300000` | プロバイダー別 `/models` キャッシュの有効期間(5 分)。 |
5860
| `cacheRetention?` | `"none" \| "short" \| "long"` | `"short"` | Anthropic prompt cache ポリシー。オフ、5 分 ephemeral、1 時間 extended のいずれか。 |
@@ -71,8 +73,10 @@ namespaced selected id を bare id に変えます。
7173
:::note[Codex アカウントプール]
7274
pool アカウントの追加と quota 更新はダッシュボードの **Codex Auth** ページで処理してください。設定には secret で
7375
ないアカウント metadata だけを保存し、access/refresh token は強化された Codex アカウント credential store に別途
74-
保管します。既存 thread id はアカウント affinity を維持し、新しいセッションは quota、cooldown、health に
75-
応じて自動ルーティングされる場合があります。
76+
保管します。既存 thread id はアカウント affinity を維持し、新しいセッションは `accountPoolStrategy`、quota、cooldown、health に
77+
応じて自動ルーティングされます。
78+
79+
**rotation 戦略**(新しいセッションのみ;bound thread は不変):`quota`(既定)— `autoSwitchThreshold` 超過時に最小 usage を選択;`round-robin` — 均等分散、`accountPoolStickyLimit`(既定 `1`、1–100)で 1 選択あたりの成功 bind 数;`fill-first` — アクティブアカウントを cooldown、再認証、または threshold まで使い切り(未知 usage は強制切替しない)後、安定ソート順で次へ。rotation は provider enforcement を回避しません — 複数アカウント利用は ToS 違反の可能性があります。
7680
:::
7781

7882
### 管理型レコード形式

docs-site/src/content/docs/ko/reference/configuration.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ namespaced selected id를 bare id로 바꿉니다.
5353
| `syncResumeHistory?` | `boolean` | `true` | 되돌릴 수 있는 Codex App 기록 호환 모드. opencodex가 원래 Codex thread metadata를 백업하고, 예전 OpenAI interactive row를 `opencodex`로 재매핑하며, opencodex가 만든 `exec` row를 App에 보이는 source로 잠시 승격합니다. `ocx stop` / `ocx restore`는 백업한 OpenAI row를 복원하고 남은 opencodex user thread를 OpenAI로 돌려 네이티브 Codex가 `config.toml`에서 프록시를 제거한 뒤에도 이어서 열 수 있게 합니다. 끄려면 `false`로 설정합니다. |
5454
| `codexAccounts?` | `CodexAccount[]` | `[]` | Codex Auth 대시보드에서 관리하는 ChatGPT/Codex pool 계정 metadata. secret은 `codex-accounts.json`에 따로 둡니다. |
5555
| `activeCodexAccountId?` | `string` || 수동으로 선택한 pool 계정. 선택 시 기존 thread affinity를 지우고 다음 요청부터 적용하며, 진행 중인 요청은 기존 계정을 유지합니다. |
56-
| `autoSwitchThreshold?` | `number` | `80` | 새 세션 자동 전환용 사용량 백분율 threshold. 알려진 5시간, 주간, 30일 quota window 중 가장 높은 점수를 씁니다. `0`이면 quota 자동 전환을 끕니다. |
56+
| `autoSwitchThreshold?` | `number` | `80` | 새 세션 자동 전환용 사용량 백분율 threshold. 알려진 5시간, 주간, 30일 quota window 중 가장 높은 점수를 씁니다. `0`이면 quota 자동 전환을 끕니다. `quota` 전략과 `fill-first` drain threshold에도 사용됩니다. |
57+
| `accountPoolStrategy?` | `"quota" \| "round-robin" \| "fill-first"` | `"quota"` | Codex pool의 새 세션 rotation 전략. **새 세션에만** 적용되며 기존 thread id는 affinity를 유지합니다. `quota`(기본) — 활성 계정이 `autoSwitchThreshold`를 넘으면 알려진 usage가 가장 낮은 계정 선택. `round-robin` — 적격 계정 간 smooth weighted 균등 분배. `fill-first` — cooldown, 사용 불가 또는(설정 시) `autoSwitchThreshold`까지 활성 계정을 소진(알 수 없는 usage는 강제 전환하지 않음)한 뒤 안정 정렬 순으로 다음 계정. |
58+
| `accountPoolStickyLimit?` | `number` | `1` | 한 round-robin 선택이 다음으로 넘어가기 전에 유지하는 성공적 새 세션 bind 수. 범위 1–100. `accountPoolStrategy``round-robin`일 때만 적용. |
5759
| `upstreamFailoverThreshold?` | `number` | `3` | 일시적인 업스트림 실패가 연속으로 발생한 뒤, 이후 새 세션을 다른 적합한 pool 계정으로 failover할 횟수. `0`이면 실패 기반 failover를 끕니다. |
5860
| `modelCacheTtlMs?` | `number` | `300000` | 프로바이더별 `/models` 캐시의 유효 기간(5분). |
5961
| `cacheRetention?` | `"none" \| "short" \| "long"` | `"short"` | Anthropic prompt cache 정책. 끔, 5분 ephemeral, 1시간 extended 중 하나입니다. |
@@ -73,8 +75,14 @@ namespaced selected id를 bare id로 바꿉니다.
7375
:::note[Codex 계정 풀]
7476
pool 계정 추가와 quota 갱신은 대시보드의 **Codex Auth** 페이지에서 처리하세요. 설정에는 secret이
7577
아닌 계정 metadata만 저장하고, access/refresh token은 강화된 Codex 계정 credential store에 따로
76-
보관합니다. 기존 thread id는 계정 affinity를 유지하며, 새 세션은 quota, cooldown, health에 따라
77-
자동 라우팅될 수 있습니다.
78+
보관합니다. 기존 thread id는 계정 affinity를 유지하며, 새 세션은 `accountPoolStrategy`, quota,
79+
cooldown, health에 따라 자동 라우팅됩니다.
80+
81+
**rotation 전략**(새 세션만; bound thread는 변경 없음): `quota`(기본) — `autoSwitchThreshold` 초과 시
82+
최저 usage 선택; `round-robin` — 균등 분배, `accountPoolStickyLimit`(기본 `1`, 1–100)로 한 선택당
83+
성공 bind 수; `fill-first` — 활성 계정을 cooldown, 재인증 또는 threshold까지 소진(알 수 없는 usage는
84+
강제 전환하지 않음)한 뒤 안정 정렬 순으로 다음 계정. rotation은 provider enforcement를 우회하지
85+
않습니다 — 다계정 사용은 ToS 위반일 수 있습니다.
7886
:::
7987

8088
### 관리형 레코드 형태

docs-site/src/content/docs/reference/configuration.md

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ differing backup and rewrites known legacy namespaced selected ids to bare ids.
5858
| `syncResumeHistory?` | `boolean` | `true` | Reversible Codex App history compatibility mode. opencodex backs up original Codex thread metadata, remaps old OpenAI interactive rows to `opencodex`, and temporarily promotes opencodex-created `exec` rows to an app-visible source. `ocx stop` / `ocx restore` restore backed-up OpenAI rows and eject remaining opencodex user threads to OpenAI so native Codex can resume them after the proxy is removed from `config.toml`. Set `false` to opt out. |
5959
| `codexAccounts?` | `CodexAccount[]` | `[]` | ChatGPT/Codex pool account metadata managed by the Codex Auth dashboard. Secrets live separately in `codex-accounts.json`. |
6060
| `activeCodexAccountId?` | `string` || Manually selected Pool account. Selection clears existing thread affinity and applies to the next request; in-flight requests keep their captured account. |
61-
| `autoSwitchThreshold?` | `number` | `80` | Usage percent threshold for new-session auto-switching. The score uses the hottest known 5h, weekly, or 30d quota window. Set `0` to disable quota auto-switching. |
61+
| `autoSwitchThreshold?` | `number` | `80` | Usage percent threshold for new-session auto-switching. The score uses the hottest known 5h, weekly, or 30d quota window. Set `0` to disable quota auto-switching. Used by the `quota` strategy and as the drain threshold for `fill-first`. |
62+
| `accountPoolStrategy?` | `"quota" \| "round-robin" \| "fill-first"` | `"quota"` | New-session rotation strategy for the Codex pool. Applies to **new sessions only**; existing thread ids keep affinity. `quota` — today's default: pick the lowest known usage when the active account crosses `autoSwitchThreshold`. `round-robin` — even spread across eligible accounts via smooth weighted selection. `fill-first` — keep the active account until it cools down, becomes unusable, or crosses `autoSwitchThreshold` when set (unknown usage does not force a switch), then advance to the next eligible account in stable sorted order. |
63+
| `accountPoolStickyLimit?` | `number` | `1` | Successful new-session binds retained on one round-robin selection before advancing. Range 1–100; only applies when `accountPoolStrategy` is `round-robin`. |
6264
| `upstreamFailoverThreshold?` | `number` | `3` | Consecutive transient upstream failures before future new sessions fail over to another eligible pool account. Set `0` to disable failure failover. |
6365
| `modelCacheTtlMs?` | `number` | `300000` | Freshness window for the per-provider `/models` cache (5 min). |
6466
| `cacheRetention?` | `"none" \| "short" \| "long"` | `"short"` | Anthropic prompt-cache policy: disabled, 5-minute ephemeral, or 1-hour extended. |
@@ -99,12 +101,23 @@ also force the same native-provider recovery with `ocx recover-history --legacy-
99101
:::note[Codex account pool]
100102
Use the dashboard's **Codex Auth** page to add pool accounts and refresh quotas. The config stores
101103
non-secret account metadata only; access and refresh tokens are kept in the hardened Codex account
102-
credential store. Existing thread ids keep account affinity, while new sessions can auto-route based
103-
on quota, cooldown, and health. A pre-stream upstream **429**/**402** on one pool account is retried
104-
once on an eligible alternate account in the same request (so Codex CLI does not stall on a depleted
105-
primary while another account still has quota).
104+
credential store. Existing thread ids keep account affinity, while new sessions auto-route based on
105+
`accountPoolStrategy`, quota, cooldown, and health. A pre-stream upstream **429**/**402** on one pool
106+
account is retried once on an eligible alternate account in the same request (so Codex CLI does not
107+
stall on a depleted primary while another account still has quota).
106108
:::
107109

110+
**Rotation strategies** (new sessions only; bound threads are unchanged):
111+
112+
| Strategy | Behaviour |
113+
| --- | --- |
114+
| `quota` (default) | When the active account's known usage crosses `autoSwitchThreshold`, pick the lowest-usage eligible account across 5h, weekly, and 30d windows. `autoSwitchThreshold: 0` disables quota-based picking. |
115+
| `round-robin` | Even spread across eligible accounts. `accountPoolStickyLimit` (default `1`, range 1–100) keeps that many successful new-session binds on one pick before advancing. |
116+
| `fill-first` | Drain the active account until cooldown, reauthentication, or (when set) `autoSwitchThreshold`; unknown usage does not force a switch. Then advance to the next eligible account in stable sorted order. |
117+
118+
Rotation strategies do not protect against provider enforcement — multi-account use may violate
119+
provider terms of service.
120+
108121
### anthropicAccountPool (experimental)
109122

110123
Opt-in routing across **multiple Anthropic OAuth accounts** already stored in `auth.json`
@@ -116,7 +129,9 @@ organization can share quota; pooling those will not help.
116129
| Key | Type | Default | Description |
117130
| --- | --- | --- | --- |
118131
| `anthropicAccountPool.enabled?` | `boolean` | `false` | When true, sticky session affinity + 429 cooldown failover across eligible Anthropic OAuth accounts. |
119-
| `anthropicAccountPool.autoSwitchThreshold?` | `number` | `80` | For **new** sessions only: if the active account's **known** cached 5-hour usage is at/above this percent, pick the lowest-usage eligible account. Unknown usage does not force a switch. `0` disables quota-based picking (affinity + active only). |
132+
| `anthropicAccountPool.autoSwitchThreshold?` | `number` | `80` | For **new** sessions only: if the active account's **known** cached 5-hour usage is at/above this percent, pick the lowest-usage eligible account. Unknown usage does not force a switch. `0` disables quota-based picking (affinity + active only). Also used as the drain threshold for `fill-first`. |
133+
| `anthropicAccountPool.strategy?` | `"quota" \| "round-robin" \| "fill-first"` | `"quota"` | New-session rotation strategy when the pool is enabled. Same round-robin/fill-first semantics as `accountPoolStrategy`; `quota` uses 5-hour bars only (not Codex multi-window scoring). Applies to **new** sessions only. |
134+
| `anthropicAccountPool.stickyLimit?` | `number` | `1` | Successful new-session binds retained on one round-robin selection before advancing. Range 1–100; only when `strategy` is `round-robin`. |
120135

121136
Reliability contract when enabled:
122137

0 commit comments

Comments
 (0)