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 c7879dfa9c..baa5f48ff7 100644 --- a/docs-site/src/content/docs/fr/reference/configuration/providers.md +++ b/docs-site/src/content/docs/fr/reference/configuration/providers.md @@ -39,7 +39,7 @@ Après une inscription ou une connexion OAuth dans l’interface, une boîte de | `codexAccountPriorities?` | `Record` | — | Ordre de sélection par compte pour le pool Codex : identifiant de compte → entier de `-100` à `100`, **les valeurs élevées sont prioritaires**, une valeur absente équivaut à `0`. Cette limite porte sur le classement, et non sur l'admissibilité : la sélection retient, parmi les comptes déjà admissibles, le niveau prioritaire le plus élevé qui dispose encore d'une marge de quota, puis `accountPoolStrategy` choisit un compte dans ce niveau. Un niveau est ignoré uniquement lorsque chacun de ses membres dépasse `autoSwitchThreshold`, est en temporisation, est temporairement évité, est suspendu ou doit être réauthentifié ; un quota inconnu ne suffit jamais à considérer un niveau comme épuisé. L'ordre ne rend jamais admissible un compte qui ne l'est pas et ne réaffecte jamais une tâche déjà liée à un compte. Le compte principal `__main__` participe selon les mêmes règles ; la connexion Codex Desktop peut ainsi être configurée pour être utilisée en dernier. Sans entrée, le pool se comporte exactement comme auparavant. Un mappage mal formé est ignoré avec un avertissement dans la console : l'ordre est désactivé et la configuration n'est pas réparée. Ce champ est géré par `ocx account priority` et la page Codex Auth. | | `activeCodexAccountPinned?` | `string` | — | Identifiant du compte du dernier opérateur sélectionné manuellement. Lorsqu'il est défini, un niveau `codexAccountPriorities` supérieur ne peut pas le préempter jusqu'à ce que la broche soit libérée par drainage, exclusion, suppression ou un failover/promotion explicite. Un mouvement circulaire ordinaire à l’intérieur du niveau plafonné ne le libère pas. L'écriture d'une entrée `codexAccountPriorities` libère également le pin, donc un pin créé avant qu'un ordre n'existe ne peut pas surpasser un ensemble par la suite. `GET /api/codex-auth/active` indique à la fois si le compte effectif est épinglé (`pinned`) et le compte portant le plafond (`pinnedAccountId`). | | `autoSwitchThreshold?` | `number` | `80` | Seuil d'utilisation pour la commutation proactive. `quota` peut réévaluer les tâches liées et non liées lors de leur prochaine requête ; `fill-first` ne l'utilise que comme seuil d'évacuation pour l'affectation des requêtes non liées ; la sélection `round-robin` normale ne l'utilise pas. Le score retient la plus élevée des fenêtres de quota connues sur 5 heures, une semaine ou 30 jours. `0` désactive uniquement la commutation proactive fondée sur l'utilisation, pas l'affectation des requêtes non liées ni la récupération après incident. | -| `accountPoolStrategy?` | `"quota" \| "round-robin" \| "fill-first"` | `"quota"` | Stratégie d'affectation des requêtes Codex nouvelles ou non liées. Une requête est non liée lorsqu'elle ne possède aucune affinité active, définie par l'identifiant de la tâche parente et la portée du quota ; une tâche existante visible peut perdre son lien après le redémarrage du proxy ou la réinitialisation de l'affinité. `quota` sélectionne le compte admissible le moins utilisé lorsqu'aucun compte actif n'existe, conserve un compte actif admissible sous `autoSwitchThreshold` et, une fois le seuil franchi, peut déplacer une requête non liée ou relier de manière proactive une tâche liée à un compte admissible moins utilisé. `round-robin` répartit équitablement les requêtes non liées ; `fill-first` continue de les attribuer au compte actif jusqu'à sa temporisation, son indisponibilité ou le seuil d'évacuation configuré. | +| `accountPoolStrategy?` | `"quota" \| "round-robin" \| "fill-first" \| "reset-first"` | `"quota"` | Stratégie d'affectation des requêtes Codex nouvelles ou non liées. Une requête est non liée lorsqu'elle ne possède aucune affinité active, définie par l'identifiant de la tâche parente et la portée du quota ; une tâche existante visible peut perdre son lien après le redémarrage du proxy ou la réinitialisation de l'affinité. `quota` sélectionne le compte admissible le moins utilisé lorsqu'aucun compte actif n'existe, conserve un compte actif admissible sous `autoSwitchThreshold` et, une fois le seuil franchi, peut déplacer une requête non liée ou relier de manière proactive une tâche liée à un compte admissible moins utilisé. `round-robin` répartit équitablement les requêtes non liées ; `fill-first` continue de les attribuer au compte actif jusqu'à sa temporisation, son indisponibilité ou le seuil d'évacuation configuré. | | `accountPoolStickyLimit?` | `number` | `1` | Nombre d'affectations de tâches nouvelles ou non liées conservées sur une même sélection tournante avant de passer à la suivante ; le compteur avance lorsqu'une tâche est liée, et non après une réponse réussie en amont. Plage : 1–100. | | `upstreamFailoverThreshold?` | `number` | `3` | Nombre d'échecs transitoires consécutifs avant le basculement des futures nouvelles sessions. Réglez `0` pour désactiver ce mécanisme. Pour les requêtes Responses ordinaires et les envois compacts natifs, les échecs avérés d'accessibilité DNS/TCP avant connexion sont suivis au niveau du couple fournisseur-hôte : ils n'affectent jamais l'état ni la temporisation du compte, l'affinité de tâche ou de session, la sélection du compte actif ou le routage du pool, et ne sont jamais comptabilisés dans ce seuil. | | `upstreamHostCircuitThreshold?` | `number` | `0` | Seuil facultatif du coupe-circuit pour les échecs DNS/TCP avérés avant connexion sur les requêtes Responses OpenAI natives en mode transfert et les envois compacts. `0` le désactive ; `1`–`20` ouvre, après ce nombre de requêtes logiques arrivées à leur terme, une temporisation de 30 secondes propre à l'origine du fournisseur. Tant que le circuit est ouvert, les requêtes reçoivent `503` avec `Retry-After` avant la sélection du compte ou l'envoi en amont ; après la temporisation, une requête est admise en état semi-ouvert. Les délais d'attente et les réponses HTTP ne sont jamais comptabilisés, et toute réponse HTTP ferme le circuit. Ce mécanisme s'applique uniquement au routage du pool Codex sans compte épinglé ; il reste inactif pour `codexAccountMode: "direct"` et les sélecteurs qualifiés par compte. | @@ -47,6 +47,8 @@ Après une inscription ou une connexion OAuth dans l’interface, une boîte de | `cacheRetention?` | `"none" \| "short" \| "long"` | `"short"` | Politique Anthropic de mise en cache des invites : désactivée, éphémère pendant 5 minutes ou étendue à 1 heure. | | `tokenGuardian?` | `OcxTokenGuardianConfig` | désactivé | Politique facultative d'actualisation proactive OAuth et de préchauffage des comptes Codex. | +`reset-first` (Codex only) selects the account with the earliest future 5-hour or weekly reset among eligible accounts below `autoSwitchThreshold`. Both windows participate; the existing maximum-usage score still enforces the threshold. Priority tiers and account health remain authoritative. Bound tasks keep affinity until the threshold or failure requires a switch. Missing, invalid, or elapsed reset timestamps rank last; ties use lowest usage, then stable pool order. If all accounts exceed the threshold, existing best-effort lowest-usage fallback applies. With threshold `0`, reset ordering remains enabled. Configure it with `ocx account strategy codex reset-first`. + Les noms des sélecteurs sont des étiquettes publiques choisies par l'utilisateur ; opencodex ne leur attribue aucune sémantique de rôle de compte. Les clés `codexAccountNamespaces` comportent de 1 à 64 caractères. Elles commencent et se terminent par une lettre ou un chiffre ASCII et ne contiennent que des lettres, des chiffres, `.`, `_` ou `-`. Les noms réservés 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 608e66deb2..889786d9b7 100644 --- a/docs-site/src/content/docs/ja/reference/configuration/providers.md +++ b/docs-site/src/content/docs/ja/reference/configuration/providers.md @@ -37,7 +37,7 @@ GUI で登録または OAuth ログインが完了すると、Models ページ | `activeCodexAccountId?` | `string` | — |次のリクエスト用に手動で選択されたプール アカウント。選択するとスレッドのアフィニティがクリアされます。実行中のリクエストでは、取得された資格情報が保持されます。 | | `codexAccountPriorities?` | `Record` | — | Codex pool のアカウント別選択順。アカウント ID → `-100` から `100` の整数で、**大きいほど先に使われ**、未設定は `0` です。これは eligibility ではなく順序の境界です。選択は適格なアカウントを、まだ quota に余裕がある最上位 tier に絞り込み、その tier の中を `accountPoolStrategy` が選びます。tier が飛ばされるのは、そのメンバー全員が `autoSwitchThreshold` 超過、cooldown 中、soft-avoid、一時停止、または再認証待ちのときだけで、usage 不明が tier を drain させることはありません。順序付けが不適格なアカウントを選択可能にすることはなく、すでにアカウントが結び付いた thread を再 bind することもありません。メインの `__main__` も同じ条件で参加するため、Codex Desktop ログインを最後に使わせられます。エントリが 1 つもなければ挙動は従来どおりです。map が不正な場合は警告を出して順序付けを無効にします(config の修復処理は走りません)。`ocx account priority` と Codex Auth ページで管理します。 | | `autoSwitchThreshold?` | `number` | `80` | 使用量ベースのプロアクティブ切り替えしきい値。`quota` は紐付け済み/未紐付けタスクの次のリクエストを再評価でき、`fill-first` は未紐付け割り当ての使い切り基準としてのみ使用し、通常の `round-robin` 選択は使用しません。既知の 5 時間、週次、30 日 quota window の最大スコアを使います。`0` は使用量ベースの切り替えだけを無効にし、未紐付け割り当てや障害回復は無効にしません。 | -| `accountPoolStrategy?` | `"quota" \| "round-robin" \| "fill-first"` | `"quota"` | 新規/未紐付け Codex リクエストの割り当て戦略。live な `(parent thread id, quota scope)` affinity がなければ未紐付けで、プロキシ再起動や affinity リセット後は既存の表示タスクも未紐付けになり得ます。`quota` はアクティブアカウントがなければ既知 usage 最小の適格アカウントを選び、適格なアクティブアカウントが `autoSwitchThreshold` 未満なら維持します。しきい値到達後は、未紐付けリクエストまたは紐付け済みタスクの次のリクエストを usage の低い適格アカウントへ移せます。`round-robin` は未紐付けリクエストを均等分散し、`fill-first` は cooldown、使用不可、または drain threshold までアクティブアカウントへ割り当てます。 | +| `accountPoolStrategy?` | `"quota" \| "round-robin" \| "fill-first" \| "reset-first"` | `"quota"` | 新規/未紐付け Codex リクエストの割り当て戦略。live な `(parent thread id, quota scope)` affinity がなければ未紐付けで、プロキシ再起動や affinity リセット後は既存の表示タスクも未紐付けになり得ます。`quota` はアクティブアカウントがなければ既知 usage 最小の適格アカウントを選び、適格なアクティブアカウントが `autoSwitchThreshold` 未満なら維持します。しきい値到達後は、未紐付けリクエストまたは紐付け済みタスクの次のリクエストを usage の低い適格アカウントへ移せます。`round-robin` は未紐付けリクエストを均等分散し、`fill-first` は cooldown、使用不可、または drain threshold までアクティブアカウントへ割り当てます。 | | `accountPoolStickyLimit?` | `number` | `1` | 1 回の round-robin 選択で次へ進む前に保持する新規/未紐付けタスク割り当て数。カウンターは上流の成功後ではなくタスクの紐付け時に増えます。範囲 1–100。`accountPoolStrategy` が `round-robin` のときのみ。 | | `upstreamFailoverThreshold?` | `number` | `3` |今後の新しいセッションがフェイルオーバーする前に一時的なエラーが連続して発生する。 `0` を無効に設定します。通常のResponses送信とネイティブcompact送信では、実証済みの接続前DNS/TCP到達不能障害はprovider-host単位で記録され、アカウントの健全性、アカウントのクールダウン、スレッド/セッションの親和性、アクティブアカウントの選択、Poolルーティングには影響せず、この閾値にもカウントされません。 | | `upstreamHostCircuitThreshold?` | `number` | `0` | ネイティブOpenAI forwardのResponses送信とcompact送信で、実証済みの接続前DNS/TCP障害に適用するオプトインのサーキットしきい値です。`0`で無効、`1`〜`20`ではその回数の終端論理リクエストが失敗するとprovider-originを30秒間遮断します。遮断中はアカウント選択やupstream送信の前に`Retry-After`付き`503`を返し、時間経過後はhalf-openリクエストを1件だけ許可します。タイムアウトとHTTP応答は数えず、HTTP応答が1件でもあれば回路を閉じます。 Codex Pool ルーティングでアカウントが固定されていない場合にのみ適用され、`codexAccountMode: "direct"` とアカウント修飾セレクターでは動作しません。 | @@ -45,6 +45,8 @@ GUI で登録または OAuth ログインが完了すると、Models ページ | `cacheRetention?` | `"none" \| "short" \| "long"` | `"short"` | Anthropic プロンプト キャッシュ ポリシー: 無効、5 分間の一時的、または 1 時間の延長。 | | `tokenGuardian?` | `OcxTokenGuardianConfig` |オフ |オプションのプロアクティブな OAuth 更新および Codex アカウントのウォームアップ ポリシー。 | +`reset-first` (Codex only) selects the account with the earliest future 5-hour or weekly reset among eligible accounts below `autoSwitchThreshold`. Both windows participate; the existing maximum-usage score still enforces the threshold. Priority tiers and account health remain authoritative. Bound tasks keep affinity until the threshold or failure requires a switch. Missing, invalid, or elapsed reset timestamps rank last; ties use lowest usage, then stable pool order. If all accounts exceed the threshold, existing best-effort lowest-usage fallback applies. With threshold `0`, reset ordering remains enabled. Configure it with `ocx account strategy codex reset-first`. + selector 名はユーザーが選ぶ公開 label であり、opencodex はアカウント role の意味を付与しません。 `codexAccountNamespaces` のキーは長さ 1〜64 文字、先頭と末尾は ASCII 英数字、内部には英数字、`.`、`_`、`-` を使用でき、予約済み JavaScript object 名は拒否されます。 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 342441eca1..239c10cd20 100644 --- a/docs-site/src/content/docs/ko/reference/configuration/providers.md +++ b/docs-site/src/content/docs/ko/reference/configuration/providers.md @@ -37,7 +37,7 @@ GUI에서 등록이나 OAuth 로그인을 마치면 Models 페이지로 이동 | `activeCodexAccountId?` | `string` | — | 다음 요청에 수동으로 선택한 Pool 계정입니다. 선택하면 thread 결속이 해제되며, 진행 중인 요청은 캡처한 자격 증명을 유지합니다. | | `codexAccountPriorities?` | `Record` | — | Codex pool의 계정별 선택 순서. 계정 ID → `-100`부터 `100`까지의 정수이며 **값이 클수록 먼저** 쓰이고, 항목이 없으면 `0`입니다. 이는 eligibility 경계가 아니라 순서 경계입니다. 선택은 이미 적격한 계정들을 quota 여유가 남은 최상위 tier로 좁히고, 그 tier 안에서 `accountPoolStrategy`가 계정을 고릅니다. tier를 건너뛰는 경우는 그 구성원 전부가 `autoSwitchThreshold` 초과, cooldown, soft-avoid, 일시 중지 또는 재인증 대기일 때뿐이며, usage를 알 수 없다고 해서 tier가 소진되지는 않습니다. 순서는 부적격 계정을 선택 가능하게 만들지 않고, 이미 계정에 묶인 thread를 다시 bind하지도 않습니다. 메인 `__main__` 계정도 동일한 조건으로 참여하므로 Codex Desktop 로그인을 마지막에 쓰도록 둘 수 있습니다. 항목이 하나도 없으면 동작은 이전과 같습니다. map이 잘못된 경우 경고를 출력하고 순서 지정을 끕니다(config 복구는 하지 않습니다). `ocx account priority`와 Codex Auth 페이지에서 관리합니다. | | `autoSwitchThreshold?` | `number` | `80` | 사용량 기반 선제 전환 임계값입니다. `quota`는 바인딩된 작업과 바인딩 없는 작업의 다음 요청을 모두 재평가할 수 있고, `fill-first`는 바인딩 없는 작업 배정의 소진 기준으로만 사용하며, 기본 `round-robin` 선택은 이 값을 사용하지 않습니다. 알려진 5시간, 주간, 30일 quota window 중 가장 높은 점수를 씁니다. `0`은 사용량 기반 전환만 끄며 바인딩 없는 작업 배정이나 실패 복구는 끄지 않습니다. | -| `accountPoolStrategy?` | `"quota" \| "round-robin" \| "fill-first"` | `"quota"` | 새 작업/바인딩 없는 Codex 요청의 계정 배정 전략입니다. `(parent thread id, quota scope)`의 live affinity가 없으면 바인딩 없는 요청이며, 프록시 재시작이나 affinity 초기화 뒤에는 기존에 보이던 작업도 바인딩이 없어질 수 있습니다. `quota`는 활성 계정이 없을 때 알려진 usage가 가장 낮은 적격 계정을 선택하고, 적격 활성 계정이 `autoSwitchThreshold` 미만이면 유지합니다. 임계값 도달 뒤에는 바인딩 없는 요청이나 바인딩된 작업의 다음 요청을 usage가 더 낮은 적격 계정으로 옮길 수 있습니다. `round-robin`은 바인딩 없는 요청을 균등 분배하고, `fill-first`는 cooldown, 사용 불가 또는 drain threshold까지 활성 계정에 배정합니다. | +| `accountPoolStrategy?` | `"quota" \| "round-robin" \| "fill-first" \| "reset-first"` | `"quota"` | 새 작업/바인딩 없는 Codex 요청의 계정 배정 전략입니다. `(parent thread id, quota scope)`의 live affinity가 없으면 바인딩 없는 요청이며, 프록시 재시작이나 affinity 초기화 뒤에는 기존에 보이던 작업도 바인딩이 없어질 수 있습니다. `quota`는 활성 계정이 없을 때 알려진 usage가 가장 낮은 적격 계정을 선택하고, 적격 활성 계정이 `autoSwitchThreshold` 미만이면 유지합니다. 임계값 도달 뒤에는 바인딩 없는 요청이나 바인딩된 작업의 다음 요청을 usage가 더 낮은 적격 계정으로 옮길 수 있습니다. `round-robin`은 바인딩 없는 요청을 균등 분배하고, `fill-first`는 cooldown, 사용 불가 또는 drain threshold까지 활성 계정에 배정합니다. | | `accountPoolStickyLimit?` | `number` | `1` | 한 round-robin 선택이 다음으로 넘어가기 전에 유지하는 새 작업/바인딩 없는 작업 배정 수입니다. 카운터는 업스트림 성공 뒤가 아니라 작업을 바인딩할 때 증가합니다. 범위 1–100이며 `accountPoolStrategy`가 `round-robin`일 때만 적용됩니다. | | `upstreamFailoverThreshold?` | `number` | `3` | 연속된 일시적 실패가 이 횟수에 도달하면 이후 새 세션은 failover됩니다. `0`으로 두면 비활성화됩니다. 일반 Responses와 네이티브 compact 전송에서 입증된 연결 전 DNS/TCP 도달 불가 실패는 provider-host 범위로 기록되며 계정 상태, 계정 쿨다운, 스레드/세션 선호도, 활성 계정 선택 또는 Pool 라우팅에 영향을 주지 않고 이 임계값에도 집계되지 않습니다. | | `upstreamHostCircuitThreshold?` | `number` | `0` | 네이티브 OpenAI forward Responses와 compact 전송에서 입증된 연결 전 DNS/TCP 실패에 적용하는 선택적 회로 차단 임계값입니다. `0`은 비활성화하며, `1`~`20`은 이 횟수만큼 최종 논리 요청이 실패하면 provider-origin을 30초 동안 차단합니다. 차단 중에는 계정 선택이나 업스트림 전송 전에 `Retry-After`가 포함된 `503`을 반환하고, 시간이 지나면 반개방 요청 하나만 허용합니다. 타임아웃과 HTTP 응답은 집계하지 않으며, HTTP 응답이 하나라도 오면 회로를 닫습니다. Codex Pool 라우팅에서 계정이 고정되지 않은 경우에만 적용되며, `codexAccountMode: "direct"` 및 계정 한정 선택자에서는 동작하지 않습니다. | @@ -45,6 +45,8 @@ GUI에서 등록이나 OAuth 로그인을 마치면 Models 페이지로 이동 | `cacheRetention?` | `"none" \| "short" \| "long"` | `"short"` | Anthropic 프롬프트 캐시 정책입니다. 비활성, 5분짜리 임시, 1시간짜리 확장 중 하나입니다. | | `tokenGuardian?` | `OcxTokenGuardianConfig` | 꺼짐 | 선택적 선제 OAuth 갱신과 Codex 계정 워밍업 정책입니다. | +`reset-first` (Codex only) selects the account with the earliest future 5-hour or weekly reset among eligible accounts below `autoSwitchThreshold`. Both windows participate; the existing maximum-usage score still enforces the threshold. Priority tiers and account health remain authoritative. Bound tasks keep affinity until the threshold or failure requires a switch. Missing, invalid, or elapsed reset timestamps rank last; ties use lowest usage, then stable pool order. If all accounts exceed the threshold, existing best-effort lowest-usage fallback applies. With threshold `0`, reset ordering remains enabled. Configure it with `ocx account strategy codex reset-first`. + selector 이름은 사용자가 정하는 공개 label이며, opencodex는 여기에 계정 역할 의미를 부여하지 않습니다. `codexAccountNamespaces` 키는 길이가 1~64자이고 시작과 끝은 ASCII 영숫자여야 하며, 내부에는 영숫자, `.`, `_`, `-`를 사용할 수 있습니다. 예약된 JavaScript object 이름은 거부됩니다. diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index f7e8f16abe..aba4bcb689 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -40,7 +40,7 @@ After GUI registration or OAuth login, the confirmation dialog lets you open the | `codexAccountPriorities?` | `Record` | — | Per-account selection order for the Codex pool: account id → integer from `-100` to `100`, **higher is used earlier**, absent means `0`. This is an ordering boundary, not an eligibility one: selection narrows the already-eligible accounts to the highest tier that still has quota headroom, and `accountPoolStrategy` then picks within that tier. A tier is skipped only when every member is over `autoSwitchThreshold`, cooling down, soft-avoided, paused, or needs reauthentication — unknown quota never drains a tier. Ordering never makes an ineligible account selectable and never re-binds a thread that already has an account. The main `__main__` account participates on equal terms, which is how the Codex Desktop login can be set to drain last. With no entries the pool behaves exactly as before. A malformed map is ignored with a console warning (ordering off, no config repair). Managed by `ocx account priority` and the Codex Auth page. | | `activeCodexAccountPinned?` | `string` | — | Account id the operator last selected by hand. While set, a higher `codexAccountPriorities` tier cannot preempt it until the pin is released by drain, exclusion, deletion, or an explicit failover/promotion away. Ordinary round-robin movement inside the capped tier does not release it. Writing any `codexAccountPriorities` entry also releases the pin, so a pin made before an order existed cannot outrank one set afterward. `GET /api/codex-auth/active` reports both whether the effective account is pinned (`pinned`) and the account carrying the ceiling (`pinnedAccountId`). | | `autoSwitchThreshold?` | `number` | `80` | Usage threshold for proactive switching. `quota` can re-evaluate both bound and unbound tasks on their next request; `fill-first` uses it only as the drain point for unbound assignment; normal `round-robin` selection does not use it. The score uses the hottest known 5h, weekly, or 30d quota window. `0` disables usage-based proactive switching only, not unbound assignment or failure recovery. | -| `accountPoolStrategy?` | `"quota" \| "round-robin" \| "fill-first"` | `"quota"` | Assignment strategy for new/unbound Codex requests. A request is unbound when it has no live (parent thread id, quota scope) affinity; a visible existing task can become unbound after proxy restart or affinity reset. `quota` picks the lowest-usage eligible account when no active account exists, keeps an eligible active account below `autoSwitchThreshold`, and after the threshold may move an unbound request or proactively rebind a bound task to a lower-usage eligible account. `round-robin` distributes unbound requests evenly; `fill-first` keeps assigning unbound requests to the active account until cooldown, unavailability, or the configured drain threshold. | +| `accountPoolStrategy?` | `"quota" \| "round-robin" \| "fill-first" \| "reset-first"` | `"quota"` | Assignment strategy for new/unbound Codex requests. A request is unbound when it has no live (parent thread id, quota scope) affinity; a visible existing task can become unbound after proxy restart or affinity reset. `quota` picks the lowest-usage eligible account when no active account exists, keeps an eligible active account below `autoSwitchThreshold`, and after the threshold may move an unbound request or proactively rebind a bound task to a lower-usage eligible account. `round-robin` distributes unbound requests evenly; `fill-first` keeps assigning unbound requests to the active account until cooldown, unavailability, or the configured drain threshold. | | `accountPoolStickyLimit?` | `number` | `1` | New/unbound task assignments retained on one round-robin selection before advancing; the counter advances when a task is bound, not after an upstream success. Range 1–100. | | `upstreamFailoverThreshold?` | `number` | `3` | Consecutive transient failures before future new sessions fail over. Set `0` to disable. For regular Responses and native compact sends, proven pre-connection DNS/TCP reachability failures are tracked at the provider-host level: they never affect account health, account cooldowns, thread/session affinity, active-account selection, or Pool routing, and never count toward this threshold. | | `upstreamHostCircuitThreshold?` | `number` | `0` | Opt-in circuit threshold for proven pre-connection DNS/TCP failures on native OpenAI forward Responses and compact sends. `0` disables it; `1`–`20` opens a 30-second provider-origin cooldown after that many terminal logical requests. While open, requests receive `503` with `Retry-After` before account selection or upstream send; after cooldown, one half-open request is admitted. Timeouts and HTTP responses never count, and any HTTP response closes the circuit. Applies only to Codex Pool routing with no pinned account; it is inert for `codexAccountMode: "direct"` and account-qualified selectors. | @@ -49,6 +49,8 @@ After GUI registration or OAuth login, the confirmation dialog lets you open the | `cacheRetention?` | `"none" \| "short" \| "long"` | `"short"` | Anthropic prompt-cache policy: disabled, 5-minute ephemeral, or 1-hour extended. | | `tokenGuardian?` | `OcxTokenGuardianConfig` | off | Optional proactive OAuth refresh and Codex-account warmup policy. | +`reset-first` (Codex only) selects the account with the earliest future 5-hour or weekly reset among eligible accounts below `autoSwitchThreshold`. Both windows participate; the existing maximum-usage score still enforces the threshold. Priority tiers and account health remain authoritative. Bound tasks keep affinity until the threshold or failure requires a switch. Missing, invalid, or elapsed reset timestamps rank last; ties use lowest usage, then stable pool order. If all accounts exceed the threshold, existing best-effort lowest-usage fallback applies. With threshold `0`, reset ordering remains enabled. Configure it with `ocx account strategy codex reset-first`. + Selector names are user-chosen public labels; opencodex assigns no account-role semantics to them. `codexAccountNamespaces` keys are 1–64 characters, starting and ending with an ASCII letter or number, with letters, numbers, `.`, `_`, or `-` inside. Reserved JavaScript object 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 78bca4d40d..ae7eea4f3e 100644 --- a/docs-site/src/content/docs/ru/reference/configuration/providers.md +++ b/docs-site/src/content/docs/ru/reference/configuration/providers.md @@ -38,7 +38,7 @@ ocx models provider openrouter on | `activeCodexAccountId?` | `string` | — | Вручную выбранный аккаунт Pool для следующего запроса. Выбор очищает thread affinity; in-flight-запросы сохраняют уже захваченные credential'ы. | | `codexAccountPriorities?` | `Record` | — | Порядок выбора для каждого аккаунта пула Codex: id аккаунта → целое число от `-100` до `100`, **больше — используется раньше**, отсутствие означает `0`. Это граница порядка, а не пригодности: выбор сужает уже подходящие аккаунты до самого высокого уровня, у которого ещё есть запас квоты, а внутри этого уровня аккаунт выбирает `accountPoolStrategy`. Уровень пропускается, только когда все его аккаунты превысили `autoSwitchThreshold`, находятся в cooldown, под soft-avoid, на паузе или требуют повторной аутентификации; неизвестный usage никогда не исчерпывает уровень. Порядок не делает выбираемым непригодный аккаунт и не перепривязывает поток, у которого аккаунт уже есть. Основной аккаунт `__main__` участвует на равных — именно так логин Codex Desktop можно оставить на самый конец. Без записей поведение остаётся прежним. Некорректная map игнорируется с предупреждением в консоли (порядок отключается, восстановление config не запускается). Управляется через `ocx account priority` и страницу Codex Auth. | | `autoSwitchThreshold?` | `number` | `80` | Порог проактивного переключения по использованию. `quota` может повторно оценить следующий запрос как привязанной, так и непривязанной задачи; `fill-first` использует его только как точку исчерпания для непривязанных назначений; обычный `round-robin` его не использует. Оценка берёт самое горячее из окон 5 часов, недели и 30 дней. `0` отключает только переключение по использованию, но не назначение непривязанных задач и не восстановление после сбоев. | -| `accountPoolStrategy?` | `"quota" \| "round-robin" \| "fill-first"` | `"quota"` | Стратегия назначения для новых/непривязанных запросов Codex. Запрос непривязан, если у него нет live affinity `(parent thread id, quota scope)`; видимая существующая задача может стать непривязанной после перезапуска прокси или сброса affinity. `quota` выбирает подходящий аккаунт с наименьшим известным usage, когда активного аккаунта нет, сохраняет подходящий активный аккаунт ниже `autoSwitchThreshold`, а после порога может перевести непривязанный запрос или следующий запрос привязанной задачи на подходящий аккаунт с меньшим usage. `round-robin` равномерно распределяет непривязанные запросы; `fill-first` назначает их активному аккаунту до cooldown, недоступности или порога исчерпания. | +| `accountPoolStrategy?` | `"quota" \| "round-robin" \| "fill-first" \| "reset-first"` | `"quota"` | Стратегия назначения для новых/непривязанных запросов Codex. Запрос непривязан, если у него нет live affinity `(parent thread id, quota scope)`; видимая существующая задача может стать непривязанной после перезапуска прокси или сброса affinity. `quota` выбирает подходящий аккаунт с наименьшим известным usage, когда активного аккаунта нет, сохраняет подходящий активный аккаунт ниже `autoSwitchThreshold`, а после порога может перевести непривязанный запрос или следующий запрос привязанной задачи на подходящий аккаунт с меньшим usage. `round-robin` равномерно распределяет непривязанные запросы; `fill-first` назначает их активному аккаунту до cooldown, недоступности или порога исчерпания. | | `accountPoolStickyLimit?` | `number` | `1` | Число назначений новых/непривязанных задач на одном выборе round-robin перед переходом дальше. Счётчик растёт при привязке задачи, а не после успеха upstream. Диапазон 1–100; только при `accountPoolStrategy` = `round-robin`. | | `upstreamFailoverThreshold?` | `number` | `3` | Сколько подряд transient failure допустить, прежде чем новые сессии начнут делать failover. `0` отключает эту логику. Для обычных Responses-запросов и нативных compact-отправок доказанные ошибки доступности DNS/TCP до соединения учитываются на уровне пары «провайдер, хост» и не влияют на здоровье аккаунта, кулдауны аккаунта, привязку потока/сессии, выбор активного аккаунта или маршрутизацию пула, а также не учитываются в этом пороге. | | `upstreamHostCircuitThreshold?` | `number` | `0` | Опциональный порог circuit breaker для доказанных DNS/TCP-сбоев до соединения в нативных OpenAI forward Responses- и compact-отправках. `0` отключает его; `1`–`20` открывает 30-секундный cooldown для provider-origin после такого числа завершившихся логических запросов. Пока circuit открыт, до выбора аккаунта и upstream-отправки возвращается `503` с `Retry-After`; после cooldown допускается один half-open запрос. Таймауты и HTTP-ответы не учитываются, а любой HTTP-ответ закрывает circuit. Применяется только к маршрутизации Codex Pool без закреплённого аккаунта; при `codexAccountMode: "direct"` и для селекторов с указанием аккаунта схема не активна. | @@ -46,6 +46,8 @@ ocx models provider openrouter on | `cacheRetention?` | `"none" \| "short" \| "long"` | `"short"` | Политика prompt-cache Anthropic: отключено, 5-минутный ephemeral или 1-часовой extended. | | `tokenGuardian?` | `OcxTokenGuardianConfig` | off | Необязательная политика proactive OAuth refresh и warmup'а аккаунтов Codex. | +`reset-first` (Codex only) selects the account with the earliest future 5-hour or weekly reset among eligible accounts below `autoSwitchThreshold`. Both windows participate; the existing maximum-usage score still enforces the threshold. Priority tiers and account health remain authoritative. Bound tasks keep affinity until the threshold or failure requires a switch. Missing, invalid, or elapsed reset timestamps rank last; ties use lowest usage, then stable pool order. If all accounts exceed the threshold, existing best-effort lowest-usage fallback applies. With threshold `0`, reset ordering remains enabled. Configure it with `ocx account strategy codex reset-first`. + Имена селекторов — выбранные пользователем публичные метки; opencodex не придаёт им семантики ролей аккаунтов. Ключи `codexAccountNamespaces` имеют длину 1–64 символа. Они должны начинаться и заканчиваться ASCII-буквой или цифрой; внутри разрешены буквы, цифры, `.`, `_` и `-`. Зарезервированные 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 36c183bf11..3c84885f2d 100644 --- a/docs-site/src/content/docs/tr/reference/configuration/providers.md +++ b/docs-site/src/content/docs/tr/reference/configuration/providers.md @@ -39,7 +39,7 @@ Arayüzde kayıt veya OAuth girişi tamamlanınca Models sayfasını açan bir b | `codexAccountPriorities?` | `Record` | — | Codex havuzu için hesap başına seçim sırası: hesap kimliği → `-100` ile `100` arası tam sayı, **daha yüksek olan daha önce kullanılır**, yoksa `0` anlamına gelir. Bu bir öncelik sırası sınırıdır, bir uygunluk sınırı değildir: seçim, zaten uygun olan hesapları hala kota payı bulunan en yüksek katmana daraltır ve `accountPoolStrategy` daha sonra bu katman içinde seçim yapar. Bir katman, yalnızca her üye `autoSwitchThreshold` üzerinde olduğunda, soğumada olduğunda, yumuşak kaçınıldığında, duraklatıldığında veya yeniden kimlik doğrulama gerektiğinde atlanır — bilinmeyen kota asla bir katmanı boşaltmaz. Sıralama asla uygun olmayan bir hesabı seçilebilir yapmaz ve zaten bir hesabı olan bir iş parçacığını asla yeniden bağlamaz. Ana `__main__` hesap eşit şartlarda katılır, bu sayede Codex Desktop girişi en son tükenecek şekilde ayarlanabilir. Hiçbir girdi olmadığında havuz tam olarak eskisi gibi davranır. Hatalı biçimlendirilmiş bir harita bir konsol uyarısıyla yok sayılır (sıralama kapalı, yapılandırma onarımı yok). `ocx account priority` ve Codex Auth sayfası tarafından yönetilir. | | `activeCodexAccountPinned?` | `string` | — | Operatörün en son elle seçtiği hesap kimliği. Ayarlandığı sürece, pin tükenme, hariç tutma, silme veya açık bir yük devretme/yükseltme ile serbest bırakılana kadar daha yüksek bir `codexAccountPriorities` katmanı onu öncelikleyemez. Sınırlı katman içindeki sıradan round-robin hareketi onu serbest bırakmaz. Herhangi bir `codexAccountPriorities` girdisi yazmak da pini serbest bırakır, böylece bir sıra var olmadan önce yapılan bir pin daha sonra ayarlanan bir pinin önüne geçemez. `GET /api/codex-auth/active`, hem geçerli hesabın sabitlenip sabitlenmediğini (`pinned`) hem de tavanı taşıyan hesabı (`pinnedAccountId`) bildirir. | | `autoSwitchThreshold?` | `number` | `80` | Proaktif geçiş için kullanım eşiği. `quota`, bir sonraki isteklerinde hem bağlı hem de bağımsız görevleri yeniden değerlendirebilir; `fill-first` bunu yalnızca bağımsız atama için tükenme noktası olarak kullanır; normal `round-robin` seçimi bunu kullanmaz. Puan, bilinen en sıcak 5 saatlik, haftalık veya 30 günlük kota penceresini kullanır. `0`, yalnızca kullanıma dayalı proaktif geçişi devre dışı bırakır, bağımsız atamayı veya arıza kurtarmayı devre dışı bırakmaz. | -| `accountPoolStrategy?` | `"quota" \| "round-robin" \| "fill-first"` | `"quota"` | Yeni/bağımsız Codex istekleri için atama stratejisi. Bir istek, canlı (üst iş parçacığı kimliği, kota kapsamı) bağlılığı olmadığında bağımsızdır; görünür mevcut bir görev, proxy yeniden başlatmasından veya bağlılık sıfırlamasından sonra bağımsız hale gelebilir. `quota`, aktif bir hesap olmadığında en düşük kullanımlı uygun hesabı seçer, `autoSwitchThreshold` altında uygun bir aktif hesabı tutar ve eşikten sonra bağımsız bir isteği taşıyabilir veya bağlı bir görevi proaktif olarak daha düşük kullanımlı uygun bir hesaba yeniden bağlayabilir. `round-robin`, bağımsız istekleri eşit olarak dağıtır; `fill-first`, soğuma, kullanılamama veya yapılandırılmış tükenme eşiğine kadar bağımsız istekleri aktif hesaba atamaya devam eder. | +| `accountPoolStrategy?` | `"quota" \| "round-robin" \| "fill-first" \| "reset-first"` | `"quota"` | Yeni/bağımsız Codex istekleri için atama stratejisi. Bir istek, canlı (üst iş parçacığı kimliği, kota kapsamı) bağlılığı olmadığında bağımsızdır; görünür mevcut bir görev, proxy yeniden başlatmasından veya bağlılık sıfırlamasından sonra bağımsız hale gelebilir. `quota`, aktif bir hesap olmadığında en düşük kullanımlı uygun hesabı seçer, `autoSwitchThreshold` altında uygun bir aktif hesabı tutar ve eşikten sonra bağımsız bir isteği taşıyabilir veya bağlı bir görevi proaktif olarak daha düşük kullanımlı uygun bir hesaba yeniden bağlayabilir. `round-robin`, bağımsız istekleri eşit olarak dağıtır; `fill-first`, soğuma, kullanılamama veya yapılandırılmış tükenme eşiğine kadar bağımsız istekleri aktif hesaba atamaya devam eder. | | `accountPoolStickyLimit?` | `number` | `1` | İlerlemeden önce bir round-robin seçiminde tutulan yeni/bağımsız görev atamaları; sayaç yukarı akış başarısından sonra değil, bir görev bağlandığında ilerler. Aralık 1–100. | | `upstreamFailoverThreshold?` | `number` | `3` | Gelecekteki yeni oturumların yük devretmesinden önceki ardışık geçici arızalar. Devre dışı bırakmak için `0` ayarlayın. Düzenli Responses ve yerel sıkıştırma gönderimleri için kanıtlanmış bağlantı öncesi DNS/TCP erişilebilirlik arızaları sağlayıcı-ana bilgisayar düzeyinde izlenir: hesap sağlığını, hesap soğuma sürelerini, iş parçacığı/oturum bağlılığını, aktif hesap seçimini veya Havuz yönlendirmesini asla etkilemez ve bu eşiğe asla sayılmaz. | | `upstreamHostCircuitThreshold?` | `number` | `0` | Yerel OpenAI iletme Responses ve sıkıştırma gönderimlerinde kanıtlanmış bağlantı öncesi DNS/TCP arızaları için isteğe bağlı devre eşiği. `0` devre dışı bırakır; `1`–`20`, bu kadar terminal mantıksal istekten sonra 30 saniyelik bir sağlayıcı-kaynak soğuma süresi açar. Açıkken istekler, hesap seçiminden veya yukarı akış gönderiminden önce `Retry-After` ile `503` alır; soğuma süresinden sonra bir yarı açık isteğe izin verilir. Zaman aşımları ve HTTP yanıtları asla sayılmaz ve herhangi bir HTTP yanıtı devreyi kapatır. Yalnızca sabitlenmiş hesabı olmayan Codex Havuz yönlendirmesi için geçerlidir; `codexAccountMode: "direct"` ve hesap nitelikli seçiciler için etkisizdir. | @@ -47,6 +47,8 @@ Arayüzde kayıt veya OAuth girişi tamamlanınca Models sayfasını açan bir b | `cacheRetention?` | `"none" \| "short" \| "long"` | `"short"` | Anthropic istem önbelleği politikası: devre dışı, 5 dakikalık kısa ömürlü veya 1 saatlik uzatılmış. | | `tokenGuardian?` | `OcxTokenGuardianConfig` | kapalı | İsteğe bağlı proaktif OAuth yenileme ve Codex hesabı ısınma politikası. | +`reset-first` (Codex only) selects the account with the earliest future 5-hour or weekly reset among eligible accounts below `autoSwitchThreshold`. Both windows participate; the existing maximum-usage score still enforces the threshold. Priority tiers and account health remain authoritative. Bound tasks keep affinity until the threshold or failure requires a switch. Missing, invalid, or elapsed reset timestamps rank last; ties use lowest usage, then stable pool order. If all accounts exceed the threshold, existing best-effort lowest-usage fallback applies. With threshold `0`, reset ordering remains enabled. Configure it with `ocx account strategy codex reset-first`. + Seçici adları kullanıcı tarafından seçilen genel etiketlerdir; opencodex bunlara hiçbir hesap rolü anlambilimi atamaz. `codexAccountNamespaces` anahtarları 1–64 karakterdir, başında ve sonunda bir ASCII harf veya rakam bulunur, içinde 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 99fba1fbb4..28341ef9c8 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 @@ -37,7 +37,7 @@ ocx models provider openrouter on | `activeCodexAccountId?` | `string` | — | 为下一次请求手动选定的 Pool 账户。选择会清除线程亲和性;进行中的请求会保留捕获到的凭据。 | | `codexAccountPriorities?` | `Record` | — | Codex pool 各账号的选择顺序:账号 ID → `-100` 到 `100` 的整数,**数值越大越先使用**,未设置即为 `0`。这是顺序边界而非资格边界:选择会把已经合格的账号收窄到仍有 quota 余量的最高 tier,再由 `accountPoolStrategy` 在该 tier 内挑选。只有当某个 tier 的所有成员都超过 `autoSwitchThreshold`、处于 cooldown、被 soft-avoid、已暂停或需要重新认证时,该 tier 才会被跳过;usage 未知不会让 tier 耗尽。顺序不会让不合格的账号变得可选,也不会重新绑定已经绑定账号的 thread。主账号 `__main__` 同样参与排序,因此可以让 Codex Desktop 登录账号最后才被用到。没有任何条目时,行为与以往完全一致。映射格式非法时会打印警告并关闭排序(不会触发 config 修复)。可通过 `ocx account priority` 和 Codex Auth 页面管理。 | | `autoSwitchThreshold?` | `number` | `80` | 基于用量的主动切换阈值。`quota` 可在下一次请求中重新评估已绑定和未绑定任务;`fill-first` 仅把它用作未绑定分配的耗尽点;正常 `round-robin` 不使用它。分数取已知 5 小时、周或 30 天 quota window 的最高值。`0` 只关闭基于用量的主动切换,不关闭未绑定任务分配或故障恢复。 | -| `accountPoolStrategy?` | `"quota" \| "round-robin" \| "fill-first"` | `"quota"` | 新建/未绑定 Codex 请求的分配策略。没有 live `(parent thread id, quota scope)` affinity 的请求属于未绑定;代理重启或 affinity 重置后,已有可见任务也可能未绑定。`quota` 在没有活跃账号时选择已知 usage 最低的合格账号;活跃账号合格且低于 `autoSwitchThreshold` 时继续使用;达到阈值后,可把未绑定请求或已绑定任务的下一次请求切换到 usage 更低的合格账号。`round-robin` 均匀分配未绑定请求;`fill-first` 在 cooldown、不可用或耗尽阈值前持续分配给活跃账号。 | +| `accountPoolStrategy?` | `"quota" \| "round-robin" \| "fill-first" \| "reset-first"` | `"quota"` | 新建/未绑定 Codex 请求的分配策略。没有 live `(parent thread id, quota scope)` affinity 的请求属于未绑定;代理重启或 affinity 重置后,已有可见任务也可能未绑定。`quota` 在没有活跃账号时选择已知 usage 最低的合格账号;活跃账号合格且低于 `autoSwitchThreshold` 时继续使用;达到阈值后,可把未绑定请求或已绑定任务的下一次请求切换到 usage 更低的合格账号。`round-robin` 均匀分配未绑定请求;`fill-first` 在 cooldown、不可用或耗尽阈值前持续分配给活跃账号。 | | `accountPoolStickyLimit?` | `number` | `1` | 一次 round-robin 选择在推进前保留的新建/未绑定任务分配数。计数在任务绑定时增加,而不是在上游成功后增加。范围 1–100;仅当 `accountPoolStrategy` 为 `round-robin` 时生效。 | | `upstreamFailoverThreshold?` | `number` | `3` | 连续发生多少次瞬态故障后,后续新会话会切换到备用上游。设为 `0` 可禁用。对于常规 Responses 和原生 compact 发送,已证明的连接前 DNS/TCP 不可达故障按 provider-host 粒度记录,不影响账户健康、账户冷却、线程/会话亲和性、活动账户选择或 Pool 路由,也不会计入此阈值。 | | `upstreamHostCircuitThreshold?` | `number` | `0` | 原生 OpenAI forward Responses 与 compact 发送的可选断路器阈值,仅统计已证明的连接前 DNS/TCP 故障。`0` 表示禁用;`1`–`20` 表示在这么多个终止逻辑请求失败后,对 provider-origin 冷却 30 秒。断路期间会在账户选择和上游发送之前返回带 `Retry-After` 的 `503`;冷却结束后只允许一个半开请求。超时和 HTTP 响应不计数,任意 HTTP 响应都会关闭断路器。 仅适用于未固定账户的 Codex Pool 路由;在 `codexAccountMode: "direct"` 或使用账户限定选择器时不会启用。 | @@ -45,6 +45,8 @@ ocx models provider openrouter on | `cacheRetention?` | `"none" \| "short" \| "long"` | `"short"` | Anthropic 提示缓存策略:禁用、5 分钟临时缓存,或 1 小时扩展缓存。 | | `tokenGuardian?` | `OcxTokenGuardianConfig` | 关闭 | 可选的主动 OAuth 刷新与 Codex 账户预热策略。 | +`reset-first` (Codex only) selects the account with the earliest future 5-hour or weekly reset among eligible accounts below `autoSwitchThreshold`. Both windows participate; the existing maximum-usage score still enforces the threshold. Priority tiers and account health remain authoritative. Bound tasks keep affinity until the threshold or failure requires a switch. Missing, invalid, or elapsed reset timestamps rank last; ties use lowest usage, then stable pool order. If all accounts exceed the threshold, existing best-effort lowest-usage fallback applies. With threshold `0`, reset ordering remains enabled. Configure it with `ocx account strategy codex reset-first`. + selector 名称是用户自定的公开 label;opencodex 不会为其赋予账户角色语义。 `codexAccountNamespaces` 的 key 长度为 1–64 个字符,首尾必须是 ASCII 字母或数字, 中间可使用字母、数字、`.`、`_` 或 `-`;保留的 JavaScript object 名称会被拒绝。value 必须是有效的 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 74ee860ff1..35a21cf35b 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 @@ -35,13 +35,15 @@ ocx models provider openrouter on | `codexAccountNamespaces?` | `Record` | — | 公開模型選擇器命名空間到已儲存 Codex 帳號目標。這會驗證並持久化映射,但不會自行新增 picker 列或變更路由。 | | `activeCodexAccountId?` | `string` | — | 為下一個請求手動選擇的池帳號。選擇清除執行緒親和性;進行中的請求保留擷取的憑證。 | | `autoSwitchThreshold?` | `number` | `80` | 主動切換的用量閾值。`quota` 可在其下一個請求時重新評估綁定與未綁定任務;`fill-first` 僅將其用作未綁定指派的排空點;一般 `round-robin` 選擇不使用它。分數使用最熱的已知 5h、週或 30d 配額視窗。`0` 僅停用基於用量的主動切換,而非未綁定指派或失敗復原。 | -| `accountPoolStrategy?` | `"quota" \| "round-robin" \| "fill-first"` | `"quota"` | 新/未綁定 Codex 請求的指派策略。當請求沒有即時(父執行緒 id、配額 scope)親和性時即為未綁定;可見的既有任務在代理重啟或親和性重置後可變為未綁定。`quota` 在無現用帳號時選擇最低用量的合格帳號,將合格現用帳號保持在 `autoSwitchThreshold` 以下,且在閾值後可將未綁定請求或主動重新綁定綁定任務到較低用量的合格帳號。`round-robin` 均勻分配未綁定請求;`fill-first` 持續將未綁定請求指派到現用帳號直到冷卻、不可用或設定的排空閾值。 | +| `accountPoolStrategy?` | `"quota" \| "round-robin" \| "fill-first" \| "reset-first"` | `"quota"` | 新/未綁定 Codex 請求的指派策略。當請求沒有即時(父執行緒 id、配額 scope)親和性時即為未綁定;可見的既有任務在代理重啟或親和性重置後可變為未綁定。`quota` 在無現用帳號時選擇最低用量的合格帳號,將合格現用帳號保持在 `autoSwitchThreshold` 以下,且在閾值後可將未綁定請求或主動重新綁定綁定任務到較低用量的合格帳號。`round-robin` 均勻分配未綁定請求;`fill-first` 持續將未綁定請求指派到現用帳號直到冷卻、不可用或設定的排空閾值。 | | `accountPoolStickyLimit?` | `number` | `1` | 在前進一個 round-robin 選擇前保留的新/未綁定任務指派;計數器在任務綁定時前進,而非在上游成功後。範圍 1–100。 | | `upstreamFailoverThreshold?` | `number` | `3` | 未來新 session 容錯移轉前的連續暫時性失敗。設 `0` 停用。 | | `modelCacheTtlMs?` | `number` | `300000` | Per-供應商 `/models` 快取的新鮮度視窗。 | | `cacheRetention?` | `"none" \| "short" \| "long"` | `"short"` | Anthropic prompt-cache 政策:停用、5 分鐘臨時或 1 小時延長。 | | `tokenGuardian?` | `OcxTokenGuardianConfig` | off | 可選的主動 OAuth refresh 與 Codex 帳號暖機政策。 | +`reset-first` (Codex only) selects the account with the earliest future 5-hour or weekly reset among eligible accounts below `autoSwitchThreshold`. Both windows participate; the existing maximum-usage score still enforces the threshold. Priority tiers and account health remain authoritative. Bound tasks keep affinity until the threshold or failure requires a switch. Missing, invalid, or elapsed reset timestamps rank last; ties use lowest usage, then stable pool order. If all accounts exceed the threshold, existing best-effort lowest-usage fallback applies. With threshold `0`, reset ordering remains enabled. Configure it with `ocx account strategy codex reset-first`. + `codexAccountNamespaces` key 是公開選擇器:1–64 字元,以 ASCII 字母或數字開頭與結尾,中間為字母、數字、`.`、`_` 或 `-`。保留的 JavaScript 物件名稱被拒絕。每個值是有效的池帳號 id(絕非內部 `__main__`)或代表 Codex Desktop 帳號的 `"@main"`。供應商與保留的 `openai` / `combo` 衝突以不區分大小寫方式檢查。保持原始帳號 id 與電子郵件私密;選擇器是公開名稱。 ## 保留的 OpenAI 供應商 diff --git a/gui/src/account-pool-strategy.ts b/gui/src/account-pool-strategy.ts index 4dbc7b9e2b..78a99cd5cc 100644 --- a/gui/src/account-pool-strategy.ts +++ b/gui/src/account-pool-strategy.ts @@ -1,9 +1,10 @@ -export type AccountPoolStrategy = "quota" | "round-robin" | "fill-first"; +export type AccountPoolStrategy = "quota" | "round-robin" | "fill-first" | "reset-first"; export const ACCOUNT_POOL_STRATEGIES: readonly AccountPoolStrategy[] = [ "quota", "round-robin", "fill-first", + "reset-first", ] as const; /** Which cached usage bar the `quota` strategy scores. Mirrors `OcxAccountPoolQuotaWindow`. */ diff --git a/gui/src/components/AccountPoolStrategyControls.tsx b/gui/src/components/AccountPoolStrategyControls.tsx index d5023ca43f..2a813bcc4e 100644 --- a/gui/src/components/AccountPoolStrategyControls.tsx +++ b/gui/src/components/AccountPoolStrategyControls.tsx @@ -8,12 +8,14 @@ import { NumberStepper } from "./NumberStepper"; import { Select } from "../ui"; const STRATEGY_LABEL_KEYS = { + "reset-first": "accountPool.strategyResetFirst", quota: "accountPool.strategyQuota", "round-robin": "accountPool.strategyRoundRobin", "fill-first": "accountPool.strategyFillFirst", } as const; const STRATEGY_HINT_KEYS = { + "reset-first": "accountPool.strategyHintResetFirst", quota: "accountPool.strategyHintQuota", "round-robin": "accountPool.strategyHintRoundRobin", "fill-first": "accountPool.strategyHintFillFirst", @@ -21,6 +23,7 @@ const STRATEGY_HINT_KEYS = { export interface AccountPoolStrategyControlsProps { strategy: AccountPoolStrategy; + codex?: boolean; stickyDraft: string; disabled?: boolean; strategySelectId?: string; @@ -41,6 +44,7 @@ export interface AccountPoolStrategyControlsProps { */ export default function AccountPoolStrategyControls({ strategy, + codex = false, stickyDraft, disabled = false, strategySelectId = "account-pool-strategy", @@ -50,7 +54,7 @@ export default function AccountPoolStrategyControls({ onStickyCommit, }: AccountPoolStrategyControlsProps) { const t = useT(); - const strategyOptions = ACCOUNT_POOL_STRATEGIES.map((value) => ({ + const strategyOptions = ACCOUNT_POOL_STRATEGIES.filter(value => codex || value !== "reset-first").map((value) => ({ value, label: t(STRATEGY_LABEL_KEYS[value]), })); diff --git a/gui/src/components/CodexAccountPool.tsx b/gui/src/components/CodexAccountPool.tsx index c01bd8b9d2..1d4d4a4828 100644 --- a/gui/src/components/CodexAccountPool.tsx +++ b/gui/src/components/CodexAccountPool.tsx @@ -63,7 +63,7 @@ export default function CodexAccountPool({ apiBase, accountModeState = null, ban invalid: t("codexAuth.autoSwitchThresholdInvalid"), }); const [poolStrategy, setPoolStrategy] = useState< - typeof DEFAULT_ACCOUNT_POOL_STRATEGY | "round-robin" | "fill-first" | null + typeof DEFAULT_ACCOUNT_POOL_STRATEGY | "round-robin" | "fill-first" | "reset-first" | null >(null); const { beginServerRead, acceptServerRead, rejectServerRead, hydrateServerValue } = autoSwitch; // A hook cannot be called conditionally, so the fallback instance is always created diff --git a/gui/src/components/CodexAutoSwitchSetting.tsx b/gui/src/components/CodexAutoSwitchSetting.tsx index 76d10825d5..cf1bcfcdf2 100644 --- a/gui/src/components/CodexAutoSwitchSetting.tsx +++ b/gui/src/components/CodexAutoSwitchSetting.tsx @@ -7,6 +7,10 @@ import { NumberStepper } from "./NumberStepper"; export type AutoSwitchFeedback = { tone: "ok" | "err"; message: string } | null; const AUTO_SWITCH_DESCRIPTION_KEYS = { + "reset-first": { + on: "accountPool.strategyHintResetFirst", + off: "codexAuth.autoSwitchQuotaOffDesc", + }, quota: { on: "codexAuth.autoSwitchQuotaDesc", off: "codexAuth.autoSwitchQuotaOffDesc", diff --git a/gui/src/components/CodexPoolStrategySetting.tsx b/gui/src/components/CodexPoolStrategySetting.tsx index b0acdab3f4..3adab38828 100644 --- a/gui/src/components/CodexPoolStrategySetting.tsx +++ b/gui/src/components/CodexPoolStrategySetting.tsx @@ -212,6 +212,7 @@ export default function CodexPoolStrategySetting({ )} {!loadError && ( = { "accountPool.strategy": "Rotationsstrategie", "accountPool.strategyDesc": "Wie OpenCodex einer neuen/ungebundenen Aufgabe ein Konto zuweist.", + "accountPool.strategyResetFirst": "Nächste Rücksetzung zuerst", + "accountPool.strategyHintResetFirst": "Unterhalb der Nutzungsschwelle wird das Konto mit der nächsten 5-Stunden- oder Wochenrücksetzung bevorzugt. Gebundene Aufgaben wechseln erst an der Schwelle oder bei Fehlern.", "accountPool.strategyQuota": "Kontingent", "accountPool.strategyRoundRobin": "Round-Robin", "accountPool.strategyFillFirst": "Fill-first", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 0cf7469f56..df41282181 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -1961,6 +1961,8 @@ export const en = { "accountPool.strategy": "Rotation strategy", "accountPool.strategyDesc": "How OpenCodex assigns an account to a new/unbound task.", + "accountPool.strategyResetFirst": "Soonest reset first", + "accountPool.strategyHintResetFirst": "Prefer the nearest future 5-hour or weekly reset among accounts below the usage threshold. Bound tasks switch only at the threshold or on failure.", "accountPool.strategyQuota": "Quota", "accountPool.strategyRoundRobin": "Round-robin", "accountPool.strategyFillFirst": "Fill-first", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 576c3b7f23..9efc6314e0 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -1891,6 +1891,8 @@ export const fr: Record = { "anthropicPool.off": "Désactivé", "accountPool.strategy": "Stratégie de rotation", "accountPool.strategyDesc": "Méthode utilisée par OpenCodex pour affecter un compte à une tâche nouvelle/non liée.", + "accountPool.strategyResetFirst": "Réinitialisation la plus proche", + "accountPool.strategyHintResetFirst": "Parmi les comptes sous le seuil, privilégier la prochaine réinitialisation de 5 heures ou hebdomadaire. Les tâches liées changent au seuil ou en cas d’échec.", "accountPool.strategyQuota": "Quota", "accountPool.strategyRoundRobin": "Rotation", "accountPool.strategyFillFirst": "Remplissage prioritaire", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 1e01aea545..bc05679aab 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -1820,6 +1820,8 @@ export const ja: Record = { "accountPool.strategy": "ローテーション戦略", "accountPool.strategyDesc": "OpenCodex が新規/未紐付けタスクへアカウントを割り当てる方法です。", + "accountPool.strategyResetFirst": "リセットが近い順", + "accountPool.strategyHintResetFirst": "使用率のしきい値未満のアカウントから、5時間枠または週次枠の次回リセットが最も近いものを優先します。紐付け済みタスクはしきい値到達時または失敗時のみ切り替わります。", "accountPool.strategyQuota": "クォータ", "accountPool.strategyRoundRobin": "ラウンドロビン", "accountPool.strategyFillFirst": "フィルファースト", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index f1d20bf65e..c9a2d4c719 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -1435,6 +1435,8 @@ export const ko: Record = { "accountPool.strategy": "로테이션 전략", "accountPool.strategyDesc": "OpenCodex가 새 작업/바인딩 없는 작업에 계정을 배정하는 방식입니다.", + "accountPool.strategyResetFirst": "가장 가까운 초기화 우선", + "accountPool.strategyHintResetFirst": "사용량 임계값 미만인 계정 중 5시간 또는 주간 한도의 다음 초기화가 가장 가까운 계정을 우선합니다. 연결된 작업은 임계값 도달 또는 실패 시에만 전환됩니다.", "accountPool.strategyQuota": "할당량", "accountPool.strategyRoundRobin": "라운드로빈", "accountPool.strategyFillFirst": "필 퍼스트", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index c583bccb99..b7abc078ee 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -1890,6 +1890,8 @@ export const ru: Record = { "accountPool.strategy": "Стратегия ротации", "accountPool.strategyDesc": "Как OpenCodex назначает аккаунт новой/непривязанной задаче.", + "accountPool.strategyResetFirst": "Ближайший сброс первым", + "accountPool.strategyHintResetFirst": "Среди аккаунтов ниже порога выбирается ближайший будущий сброс 5-часовой или недельной квоты. Привязанные задачи переключаются при достижении порога или ошибке.", "accountPool.strategyQuota": "Квота", "accountPool.strategyRoundRobin": "Round-robin", "accountPool.strategyFillFirst": "Fill-first", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index ca39260677..00efd11055 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -1909,6 +1909,8 @@ export const tr: Record = { "accountPool.strategy": "Rotasyon stratejisi", "accountPool.strategyDesc": "OpenCodex'in yeni bir göreve nasıl hesap atayacağı.", + "accountPool.strategyResetFirst": "En yakın sıfırlama önce", + "accountPool.strategyHintResetFirst": "Kullanım eşiğinin altındaki hesaplardan 5 saatlik veya haftalık kotası en erken sıfırlanacak olanı seçer. Bağlı görevler yalnızca eşiğe ulaşınca veya hata durumunda geçiş yapar.", "accountPool.strategyQuota": "Kota", "accountPool.strategyRoundRobin": "Round-robin", "accountPool.strategyFillFirst": "İlk doldurma", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index 6462f6c4b5..7830ec71cf 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -1454,6 +1454,8 @@ export const zhTW: Record = { "anthropicPool.off": "關", "accountPool.strategy": "輪換策略", "accountPool.strategyDesc": "新會話如何從帳號池中選擇帳號。", + "accountPool.strategyResetFirst": "額度即將重設優先", + "accountPool.strategyHintResetFirst": "在低於用量門檻的帳號中,優先選擇 5 小時或週額度最早重設的帳號。已綁定任務僅在達到門檻或失敗時切換。", "accountPool.strategyQuota": "配額", "accountPool.strategyRoundRobin": "輪詢", "accountPool.strategyFillFirst": "填滿優先", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 685227abc0..d31da5d491 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -1416,6 +1416,8 @@ export const zh: Record = { "accountPool.strategy": "轮换策略", "accountPool.strategyDesc": "OpenCodex 如何为新建/未绑定任务分配账号。", + "accountPool.strategyResetFirst": "额度即将刷新优先", + "accountPool.strategyHintResetFirst": "在低于用量阈值的账号中,优先选择 5 小时或周额度最早刷新的账号。已绑定任务仅在达到阈值或失败时切换。", "accountPool.strategyQuota": "配额", "accountPool.strategyRoundRobin": "轮询", "accountPool.strategyFillFirst": "填满优先", diff --git a/gui/tests/account-pool-strategy.test.tsx b/gui/tests/account-pool-strategy.test.tsx index 856c44af8c..c78d17c9e1 100644 --- a/gui/tests/account-pool-strategy.test.tsx +++ b/gui/tests/account-pool-strategy.test.tsx @@ -90,6 +90,7 @@ describe("account pool strategy helpers", () => { expect(normalizeAccountPoolStrategy("quota")).toBe("quota"); expect(normalizeAccountPoolStrategy("round-robin")).toBe("round-robin"); expect(normalizeAccountPoolStrategy("fill-first")).toBe("fill-first"); + expect(normalizeAccountPoolStrategy("reset-first")).toBe("reset-first"); expect(normalizeAccountPoolStrategy("weighted")).toBe(DEFAULT_ACCOUNT_POOL_STRATEGY); expect(normalizeAccountPoolStrategy(undefined)).toBe("quota"); }); @@ -177,6 +178,19 @@ describe("AccountPoolStrategyControls", () => { expect(rr).toContain('value="2"'); }); + test("reset-first renders the dual-window threshold explanation", () => { + const markup = renderToStaticMarkup( + + {}} onStickyDraftChange={() => {}} onStickyCommit={() => {}} /> + , + ); + expect(markup).toContain("Soonest reset first"); + expect(markup).toContain("nearest future 5-hour or weekly reset"); + expect(markup).toContain("Bound tasks switch only at the threshold or on failure"); + expect(markup).not.toContain("New/unbound assignments before rotate"); + }); + test("renders a canonical setting row: visible name, control beside it, no sr-only label", () => { const markup = renderToStaticMarkup( diff --git a/src/cli/account-extended.ts b/src/cli/account-extended.ts index 38179aca42..75a5aa9c41 100644 --- a/src/cli/account-extended.ts +++ b/src/cli/account-extended.ts @@ -44,7 +44,7 @@ const EXTENDED_USAGE = `Usage: ocx account pause [--json] ocx account resume [--json] ocx account pause-exhausted [--json] - ocx account strategy [] [--json] + ocx account strategy [] [--json] ocx account sticky [<1-100>] [--json] ocx account remove --yes [--json] ocx account clear-cooldown [--json] diff --git a/src/cli/account.ts b/src/cli/account.ts index acda3639b8..3d37b4cc76 100644 --- a/src/cli/account.ts +++ b/src/cli/account.ts @@ -50,7 +50,7 @@ const ACCOUNT_USAGE = `Usage: ocx account pause [--json] ocx account resume [--json] ocx account pause-exhausted [--json] - ocx account strategy [] [--json] + ocx account strategy [] [--json] ocx account sticky [<1-100>] [--json] ocx account remove --yes [--json] ocx account clear-cooldown [--json] diff --git a/src/codex/auth-api.ts b/src/codex/auth-api.ts index 1a1e66d88b..eea0c95d37 100644 --- a/src/codex/auth-api.ts +++ b/src/codex/auth-api.ts @@ -57,9 +57,9 @@ import { MAX_ACCOUNT_PRIORITY, MIN_ACCOUNT_PRIORITY, normalizeAccountPoolStickyLimit, - normalizeAccountPoolStrategy, + normalizeCodexAccountPoolStrategy, parseAccountPoolStickyLimit, - parseAccountPoolStrategy, + parseCodexAccountPoolStrategy, parseAccountPriority, } from "./pool-rotation"; import { checkAccountIdCollision, getMainChatgptAccountId, readCodexTokens, readCodexTokensResult } from "./auth-collision"; @@ -2342,7 +2342,7 @@ export async function handleCodexAuthAPI( pinnedAccountId: pinnedCodexAccountId(runtimeConfig) ?? null, autoSwitchThreshold: runtimeConfig.autoSwitchThreshold ?? 80, upstreamFailoverThreshold: runtimeConfig.upstreamFailoverThreshold ?? 3, - accountPoolStrategy: normalizeAccountPoolStrategy(runtimeConfig.accountPoolStrategy), + accountPoolStrategy: normalizeCodexAccountPoolStrategy(runtimeConfig.accountPoolStrategy), accountPoolStickyLimit: normalizeAccountPoolStickyLimit(runtimeConfig.accountPoolStickyLimit), }); } @@ -2373,12 +2373,12 @@ export async function handleCodexAuthAPI( return jsonResponse({ error: "strategy or stickyLimit required" }, 400); } const runtimeConfig = getRuntimeConfig(config); - let nextStrategy: NonNullable> | undefined; + let nextStrategy: NonNullable> | undefined; let nextSticky: NonNullable> | undefined; if (body.strategy !== undefined) { - const parsed = parseAccountPoolStrategy(body.strategy); + const parsed = parseCodexAccountPoolStrategy(body.strategy); if (parsed === null) { - return jsonResponse({ error: 'strategy must be one of: quota, round-robin, fill-first' }, 400); + return jsonResponse({ error: 'strategy must be one of: quota, round-robin, fill-first, reset-first' }, 400); } nextStrategy = parsed; } @@ -2394,7 +2394,7 @@ export async function handleCodexAuthAPI( saveRuntimeConfig(config, runtimeConfig); return jsonResponse({ ok: true, - accountPoolStrategy: normalizeAccountPoolStrategy(runtimeConfig.accountPoolStrategy), + accountPoolStrategy: normalizeCodexAccountPoolStrategy(runtimeConfig.accountPoolStrategy), accountPoolStickyLimit: normalizeAccountPoolStickyLimit(runtimeConfig.accountPoolStickyLimit), }); } diff --git a/src/codex/pool-rotation.ts b/src/codex/pool-rotation.ts index d0d032be06..04253c9b85 100644 --- a/src/codex/pool-rotation.ts +++ b/src/codex/pool-rotation.ts @@ -32,6 +32,15 @@ export function parseAccountPoolStrategy(raw: unknown): OcxAccountPoolRotationSt return null; } +/** Codex alone supports ordering by the next quota reset. */ +export function parseCodexAccountPoolStrategy(raw: unknown): OcxAccountPoolRotationStrategy | "reset-first" | null { + return raw === "reset-first" ? raw : parseAccountPoolStrategy(raw); +} + +export function normalizeCodexAccountPoolStrategy(raw: unknown): OcxAccountPoolRotationStrategy | "reset-first" { + return parseCodexAccountPoolStrategy(raw) ?? DEFAULT_STRATEGY; +} + /** Strict parse for management APIs — returns null instead of defaulting. */ export function parseAccountPoolStickyLimit(raw: unknown): number | null { if (typeof raw === "number" && Number.isInteger(raw) && raw >= MIN_STICKY_LIMIT && raw <= MAX_STICKY_LIMIT) { diff --git a/src/codex/routing.ts b/src/codex/routing.ts index 5d8cc17d15..77fddb4b7d 100644 --- a/src/codex/routing.ts +++ b/src/codex/routing.ts @@ -10,7 +10,7 @@ import { clearAccountNeedsReauth, isAccountNeedsReauth, markAccountNeedsReauth } import { POOL_KEY_CODEX, normalizeAccountPoolStickyLimit, - normalizeAccountPoolStrategy, + normalizeCodexAccountPoolStrategy, notePoolRotationFailure, notePoolRotationSuccess, peekRoundRobinAccount, @@ -1316,6 +1316,33 @@ function hasCodexQuotaHeadroom( return usage < threshold; } +/** Earliest future 5h/weekly reset wins; ties and absent resets use existing usage order. */ +function pickResetFirstCodexAccount( + config: OcxConfig, + ids: readonly string[], + now: number, + selectionOptions?: CodexAccountUsabilityOptions, +): string | null { + const available = ids.filter(id => hasCodexQuotaHeadroom(config, id, selectionOptions, now)); + // Preserve the pool's existing best-effort behavior when every account is over threshold. + if (available.length === 0) return pickLowestUsageAmong(config, ids, selectionOptions, now); + let earliest = Number.POSITIVE_INFINITY; + let candidates: string[] = []; + for (const id of available) { + const quota = getAccountQuota(id); + const resets = [quota?.shortResetAt, quota?.weeklyResetAt] + .filter((reset): reset is number => typeof reset === "number" && Number.isFinite(reset) && reset * 1000 > now); + const next = Math.min(...resets); + if (next < earliest) { + earliest = next; + candidates = [id]; + } else if (next === earliest) { + candidates.push(id); + } + } + return pickLowestUsageAmong(config, candidates, selectionOptions, now); +} + /** * Fill-first: keep selectable active under threshold; otherwise advance to the next * eligible id in stable sorted order after the current active (wrapping). @@ -1382,7 +1409,7 @@ function pickNextFillFirstCodexAccount( } /** - * Unbound new-session pick for round-robin / fill-first. Returns null to fall through + * Unbound new-session pick for round-robin / fill-first / reset-first. Returns null to fall through * to the legacy quota path (or when the strategy is quota). * * When `commit` is true (resolve path), advances RR state. `commitSharedActive` @@ -1406,7 +1433,7 @@ function pickUnboundStrategyAccount( commitSharedActive = commit, commitAffinity = commit, ): string | null { - const strategy = normalizeAccountPoolStrategy(config.accountPoolStrategy); + const strategy = normalizeCodexAccountPoolStrategy(config.accountPoolStrategy); if (strategy === "quota") return null; const poolKey = codexPoolKeyForScope(quotaScope); @@ -1427,8 +1454,10 @@ function pickUnboundStrategyAccount( return picked; } - if (strategy === "fill-first") { - picked = pickFillFirstCodexAccount(config, now, quotaScope, selectionOptions); + if (strategy === "fill-first" || strategy === "reset-first") { + picked = strategy === "reset-first" + ? pickResetFirstCodexAccount(config, listEligibleCodexAccountIds(config, now, quotaScope, selectionOptions), now, selectionOptions) + : pickFillFirstCodexAccount(config, now, quotaScope, selectionOptions); if (!picked) return null; if (commitSharedActive) { if (!isIndependentCodexQuotaScope(quotaScope)) rememberActiveCodexAccount(config, picked); @@ -1558,7 +1587,7 @@ export function pickAlternateCodexAccount( quotaScope?: CodexQuotaScope, selectionOptions?: CodexAccountUsabilityOptions, ): string | null { - const strategy = normalizeAccountPoolStrategy(config.accountPoolStrategy); + const strategy = normalizeCodexAccountPoolStrategy(config.accountPoolStrategy); // The exclusion is passed into eligibility rather than post-filtered off its // result: when the excluded account is the only healthy member of the top // tier, the tier walk must be free to descend instead of selecting that tier @@ -1571,6 +1600,9 @@ export function pickAlternateCodexAccount( const eligible = getEligiblePoolAccounts(config, excludeId, now, quotaScope, selectionOptions); return pickNextFillFirstCodexAccount(config, excludeId, eligible, now, selectionOptions); } + if (strategy === "reset-first") { + return pickResetFirstCodexAccount(config, getEligiblePoolAccounts(config, excludeId, now, quotaScope, selectionOptions), now, selectionOptions); + } return pickLowestUsageCodexAccount(config, excludeId, now, quotaScope, selectionOptions); } @@ -1618,9 +1650,9 @@ function setActiveCodexAccount(config: OcxConfig, accountId: string): void { saveConfigPreservingClaudeCode(config); } -/** Quota strategy persists; RR/fill-first keep a process-local cursor only. */ +/** Quota strategy persists; other strategies keep a process-local cursor only. */ function promoteActiveCodexAccount(config: OcxConfig, accountId: string): void { - if (normalizeAccountPoolStrategy(config.accountPoolStrategy) === "quota") { + if (normalizeCodexAccountPoolStrategy(config.accountPoolStrategy) === "quota") { setActiveCodexAccount(config, accountId); return; } @@ -1861,9 +1893,12 @@ function previewReusableAffinityAccount( ) { return null; } - // Quota strategy only: non-quota strategies keep affinity for ongoing threads + if (normalizeCodexAccountPoolStrategy(config.accountPoolStrategy) === "reset-first") { + return resetFirstAffinityReplacement(entry, config, now, quotaScope, selectionOptions) ?? entry.accountId; + } + // Legacy quota strategy: RR/fill-first keep affinity for ongoing threads // (new-session-only rotation — docs / affinity policy A). - if (normalizeAccountPoolStrategy(config.accountPoolStrategy) === "quota") { + if (normalizeCodexAccountPoolStrategy(config.accountPoolStrategy) === "quota") { const threshold = config.autoSwitchThreshold ?? 80; if (threshold > 0) { const usage = computeCodexUsageScore( @@ -1888,10 +1923,21 @@ function previewReusableAffinityAccount( return entry.accountId; } -/** - * Re-evaluate an affined account under the quota strategy. Returns a strictly - * cooler replacement, or null when the current binding should remain. - */ +/** Keep a bound account until threshold, then use reset order among accounts with headroom. */ +function resetFirstAffinityReplacement( + entry: ThreadAffinityEntry, + config: OcxConfig, + now: number, + quotaScope?: CodexQuotaScope, + selectionOptions?: CodexAccountUsabilityOptions, +): string | null { + if (hasCodexQuotaHeadroom(config, entry.accountId, selectionOptions, now)) return null; + const candidates = getEligiblePoolAccounts(config, entry.accountId, now, quotaScope, selectionOptions, true) + .filter(id => hasCodexQuotaHeadroom(config, id, selectionOptions, now)); + return pickResetFirstCodexAccount(config, candidates, now, selectionOptions); +} + +/** Re-evaluate usage-aware strategies without disturbing healthy thread affinity. */ function reevaluateAffinityQuota( entry: ThreadAffinityEntry, config: OcxConfig, @@ -1899,7 +1945,10 @@ function reevaluateAffinityQuota( quotaScope?: CodexQuotaScope, selectionOptions?: CodexAccountUsabilityOptions, ): string | null { - if (normalizeAccountPoolStrategy(config.accountPoolStrategy) !== "quota") return null; + if (normalizeCodexAccountPoolStrategy(config.accountPoolStrategy) === "reset-first") { + return resetFirstAffinityReplacement(entry, config, now, quotaScope, selectionOptions); + } + if (normalizeCodexAccountPoolStrategy(config.accountPoolStrategy) !== "quota") return null; const threshold = config.autoSwitchThreshold ?? 80; const usage = threshold > 0 ? computeCodexUsageScore( @@ -2117,7 +2166,7 @@ export function resolveCodexAccountForThreadDetailed( const cooler = reevaluateAffinityQuota(entry, config, now, quotaScope, selectionOptions); if (cooler) { if (!isIndependentCodexQuotaScope(quotaScope)) { - setActiveCodexAccount(config, cooler); + promoteActiveCodexAccount(config, cooler); } bindThreadAffinity(threadId, cooler, now, quotaScope); // rebinds + resets clocks return { status: "selected", accountId: cooler }; diff --git a/src/types/config.ts b/src/types/config.ts index fc9a55a8fa..428d294dda 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -774,7 +774,7 @@ export interface OcxConfig { /** Auto-switch threshold (0-100). Default 80. 0 = disabled. */ autoSwitchThreshold?: number; /** New-session account rotation strategy for the Codex pool. Default quota (today's behaviour). */ - accountPoolStrategy?: OcxAccountPoolRotationStrategy; + accountPoolStrategy?: OcxAccountPoolRotationStrategy | "reset-first"; /** Successful new-session binds retained on one round-robin selection. Default 1; range 1..100. */ accountPoolStickyLimit?: number; /** Consecutive non-2xx upstream responses before switching future new threads. Default 3. 0 = disabled. */ diff --git a/tests/codex-integration/codex-pool-rotation.test.ts b/tests/codex-integration/codex-pool-rotation.test.ts index 1a69a0e269..89f9217596 100644 --- a/tests/codex-integration/codex-pool-rotation.test.ts +++ b/tests/codex-integration/codex-pool-rotation.test.ts @@ -4,6 +4,8 @@ import { normalizeAccountPriority, notePoolRotationSuccess, parseAccountPriority, + parseAccountPoolStrategy, + parseCodexAccountPoolStrategy, peekRoundRobinAccount, pickRoundRobinAccount, selectPriorityTier, @@ -30,6 +32,7 @@ import { import { saveCodexAccountCredential } from "../../src/codex/account-store"; import { MAIN_CODEX_ACCOUNT_ID } from "../../src/codex/account-id"; import { clearAccountQuota, updateAccountQuota } from "../../src/codex/auth-api"; +import { setAccountQuotaFromParsed } from "../../src/codex/quota"; import { getConfigPath } from "../../src/config"; import type { OcxConfig } from "../../src/types"; import { existsSync, mkdirSync, rmSync } from "node:fs"; @@ -337,6 +340,75 @@ describe("accountPoolStrategy new-session routing", () => { if (existsSync(TEST_DIR)) removeTreeWithRetry(TEST_DIR); }); + test("reset-first is accepted only by the Codex strategy parser", () => { + expect(parseCodexAccountPoolStrategy("reset-first")).toBe("reset-first"); + expect(parseAccountPoolStrategy("reset-first")).toBeNull(); + expect(parseCodexAccountPoolStrategy("invalid")).toBeNull(); + }); + + test("reset-first compares both windows, previews without writes, and uses the same failover order", () => { + const config = makeThreeAccountConfig({ accountPoolStrategy: "reset-first" }); + const now = Date.now(); + const seconds = now / 1000; + setAccountQuotaFromParsed("a", { weeklyPercent: 10, weeklyResetAt: seconds + 600, shortPercent: 10, shortResetAt: seconds + 300 }); + setAccountQuotaFromParsed("b", { weeklyPercent: 60, weeklyResetAt: seconds + 100, shortPercent: 20, shortResetAt: seconds + 500 }); + setAccountQuotaFromParsed("c", { weeklyPercent: 20, weeklyResetAt: seconds + 900, shortPercent: 30, shortResetAt: seconds + 200 }); + expect(previewCodexAccountForRequest("reset-task", config, now)).toBe("b"); + expect(config.activeCodexAccountId).toBe("a"); + expect(getEffectiveActiveCodexAccountId(config)).toBe("a"); + expect(resolveCodexAccountForThread("reset-task", config, now)).toBe("b"); + expect(config.activeCodexAccountId).toBe("a"); + expect(pickAlternateCodexAccount(config, "b", now)).toBe("c"); + }); + + test("reset-first keeps affinity until either window reaches the threshold", () => { + const config = makeThreeAccountConfig({ accountPoolStrategy: "reset-first" }); + const now = Date.now(); + const seconds = now / 1000; + setAccountQuotaFromParsed("a", { weeklyPercent: 10, weeklyResetAt: seconds + 100 }); + setAccountQuotaFromParsed("b", { weeklyPercent: 20, weeklyResetAt: seconds + 200 }); + setAccountQuotaFromParsed("c", { weeklyPercent: 30, weeklyResetAt: seconds + 300 }); + expect(resolveCodexAccountForThread("bound", config, now)).toBe("a"); + setAccountQuotaFromParsed("b", { weeklyPercent: 20, weeklyResetAt: seconds + 50 }); + expect(resolveCodexAccountForThread("bound", config, now)).toBe("a"); + expect(resolveCodexAccountForThread("new", config, now)).toBe("b"); + setAccountQuotaFromParsed("a", { weeklyPercent: 10, shortPercent: 80, shortResetAt: seconds + 10 }); + expect(previewCodexAccountForRequest("bound", config, now)).toBe("b"); + expect(resolveCodexAccountForThread("bound", config, now)).toBe("b"); + setAccountQuotaFromParsed("b", { weeklyPercent: 80 }); + expect(resolveCodexAccountForThread("bound", config, now)).toBe("c"); + }); + + test("reset-first ignores past/missing resets and breaks ties by usage", () => { + const config = makeThreeAccountConfig({ accountPoolStrategy: "reset-first" }); + const now = Date.now(); + setAccountQuotaFromParsed("a", { weeklyPercent: 10, weeklyResetAt: now / 1000 - 1 }); + setAccountQuotaFromParsed("b", { weeklyPercent: 30, weeklyResetAt: now / 1000 + 20 }); + setAccountQuotaFromParsed("c", { weeklyPercent: 20, shortPercent: 10, shortResetAt: now / 1000 + 20 }); + expect(resolveCodexAccountForThread(null, config, now)).toBe("c"); + expect(resolveCodexAccountForThread(null, config, now + 20_000)).toBe("a"); + clearAccountQuota(); + expect(resolveCodexAccountForThread(null, config, now)).toBe("a"); + }); + + test("reset-first preserves priority and availability and honors disabled thresholds", () => { + const config = makeThreeAccountConfig({ accountPoolStrategy: "reset-first" }); + const now = Date.now(); + setAccountQuotaFromParsed("a", { weeklyPercent: 90, weeklyResetAt: now / 1000 + 10 }); + setAccountQuotaFromParsed("b", { weeklyPercent: 20, weeklyResetAt: now / 1000 + 20 }); + setAccountQuotaFromParsed("c", { weeklyPercent: 10, weeklyResetAt: now / 1000 + 30 }); + expect(resolveCodexAccountForThread(null, config, now)).toBe("b"); + config.autoSwitchThreshold = 0; + expect(resolveCodexAccountForThread(null, config, now)).toBe("a"); + config.autoSwitchThreshold = 80; + setCodexAccountPriority(config, "c", 2); + expect(resolveCodexAccountForThread(null, config, now)).toBe("c"); + expect(pickAlternateCodexAccount(config, "c", now)).toBe("b"); + setAccountQuotaFromParsed("b", { weeklyPercent: 95 }); + setAccountQuotaFromParsed("c", { weeklyPercent: 99 }); + expect(resolveCodexAccountForThread(null, config, now)).toBe("a"); + }); + test("round-robin strategy rotates unbound new sessions", () => { const config = makeThreeAccountConfig({ accountPoolStrategy: "round-robin" }); updateAccountQuota("a", 10); diff --git a/tests/server/account-pool-management-api.test.ts b/tests/server/account-pool-management-api.test.ts index e8eed3c05d..7089695732 100644 --- a/tests/server/account-pool-management-api.test.ts +++ b/tests/server/account-pool-management-api.test.ts @@ -4,7 +4,7 @@ import { mkdtempSync, readFileSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { handleCodexAuthAPI } from "../../src/codex/auth-api"; -import { saveConfig } from "../../src/config"; +import { loadConfig, saveConfig } from "../../src/config"; import { startServer } from "../../src/server"; import type { OcxConfig } from "../../src/types"; import { installIsolatedCodexHome, type IsolatedCodexHome } from "../helpers/isolated-codex-home"; @@ -100,6 +100,22 @@ describe("Codex account pool strategy management API", () => { expect(config.accountPoolStickyLimit).toBe(7); }); + test("reset-first survives management save and config reload", async () => { + const config = makeCodexConfig(); + const req = new Request("http://localhost/api/codex-auth/pool-strategy", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ strategy: "reset-first" }), + }); + const resp = await handleCodexAuthAPI(req, new URL(req.url), config); + expect(resp!.status).toBe(200); + expect((await resp!.json()).accountPoolStrategy).toBe("reset-first"); + expect(loadConfig().accountPoolStrategy).toBe("reset-first"); + const read = new Request("http://localhost/api/codex-auth/active"); + const active = await handleCodexAuthAPI(read, new URL(read.url), loadConfig()); + expect((await active!.json()).accountPoolStrategy).toBe("reset-first"); + }); + test("PATCH /api/codex-auth/pool-strategy accepts round-robin", async () => { const config = makeCodexConfig({ accountPoolStrategy: "quota", accountPoolStickyLimit: 1 }); const req = new Request("http://localhost/api/codex-auth/pool-strategy", {