Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
304b305
docs(devlog): spec for provider cache affinity
harshav167 Oct 26, 2026
970fe5f
test(providers): cache affinity behavior at the store, selection, and…
harshav167 Oct 26, 2026
acffe64
feat(providers): cache-affinity store, config surface, and sweeper re…
harshav167 Oct 26, 2026
c75f84d
feat(providers): consult cache affinity at the credential-selection seam
harshav167 Oct 26, 2026
4b7857b
feat(server): record cache affinity at attempt finalization
harshav167 Oct 26, 2026
14bd068
docs: document the cacheAffinity provider option
harshav167 Oct 26, 2026
a49ed47
fix(providers): preserve cache affinity across dispatch and fallback
harshav167 Oct 26, 2026
1b2257f
fix(providers): preserve healthy cache-affinity bindings
lidge-jun Oct 28, 2026
9f593b2
merge: integrate origin/dev into provider cache-affinity carry
lidge-jun Oct 28, 2026
7996288
docs: list the account auto-pause page in the configuration sidebar
lidge-jun Oct 28, 2026
cb94b69
Merge origin/dev into provider cache affinity
lidge-jun Oct 28, 2026
ceb4902
fix(providers): preserve request-local affinity recovery and release …
lidge-jun Oct 28, 2026
3c272ec
Merge origin/dev into provider cache affinity finalization
lidge-jun Oct 28, 2026
41a9967
fix(server): preserve affinity on failed turns and local refusals
lidge-jun Oct 28, 2026
85c4301
test(server): use the named budget for key-failover servers
lidge-jun Oct 28, 2026
65d808d
Merge current dev into provider cache affinity
harshav167 Oct 30, 2026
6e8f1ef
Merge remote-tracking branch 'origin/dev' into feat/provider-cache-af…
harshav167 Oct 30, 2026
0438ee8
fix(server): clear prior candidate terminal status and honor session-…
harshav167 Oct 31, 2026
6f10e60
Merge remote-tracking branch 'origin/dev' into temp/pr7738
harshav167 Oct 31, 2026
569daec
test(server): finish e2e teardown when a tracked server stop rejects
harshav167 Oct 31, 2026
cff0878
Merge remote-tracking branch 'origin/dev' into feat/provider-cache-af…
harshav167 Oct 31, 2026
7f98a24
fix(server): clear candidate terminalSource and unify the affinity la…
harshav167 Nov 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ OpenCodex tracks process-retained state in the categories below. Each has a docu
store, which is pinned and never evicted.
- **4 observed buffers** (translator accumulators, image/OAuth/Grok tails) are
monitored for in-flight byte pressure without eviction.
- **31 state-store registrations** handle expiry sweeps (60 s interval) and
- **32 state-store registrations** handle expiry sweeps (60 s interval) and
config-generation reconciliation so stale provider/account keys are removed.
- **Path and fingerprint memos** (workspace metadata, hardened identities, installation
salts, mode-hint capabilities) use insertion-order LRU caps (8–128 entries).
Expand Down
399 changes: 399 additions & 0 deletions devlog/_plan/261019_cache_affinity/000_spec.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs-site/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ export default defineConfig({
items: [
{ label: "Overview", translations: { fr: "Vue d’ensemble", ko: "개요", "zh-CN": "概览", "zh-TW": "概覽", ru: "Обзор", ja: "概要", tr: "Genel Bakış" }, slug: "reference/configuration" },
{ label: "Providers", translations: { fr: "Fournisseurs", ko: "프로바이더", "zh-CN": "提供商", "zh-TW": "供應商", ru: "Провайдеры", ja: "プロバイダー", tr: "Sağlayıcılar" }, slug: "reference/configuration/providers" },
{ label: "Account Auto-pause", slug: "reference/configuration/account-auto-pause" },
{ label: "Catalog", translations: { fr: "Catalogue", ko: "카탈로그", "zh-CN": "目录", "zh-TW": "目錄", ru: "Каталог", ja: "カタログ", tr: "Katalog" }, slug: "reference/configuration/catalog" },
{ label: "Routing", translations: { fr: "Routage", ko: "라우팅", "zh-CN": "路由", "zh-TW": "路由", ru: "Маршрутизация", ja: "ルーティング", tr: "Yönlendirme" }, slug: "reference/configuration/routing" },
{ label: "Agents", translations: { fr: "Agents", ko: "에이전트", "zh-CN": "代理", "zh-TW": "代理", ru: "Агенты", ja: "エージェント", tr: "Ajanlar" }, slug: "reference/configuration/agents" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ sauvegarde dont le contenu diffère, puis réécrit en identifiants sans préfix
| --- | --- | --- |
| `adapter` | `string` | L'un des `openai-chat`, `openai-responses`, `anthropic`, `google`, `kiro`, `cursor`, `ollama-native`, `azure-openai` (ou alias `azure`). |
| `baseUrl` | `string` | URL de base de l'API en amont. La plupart des points de terminaison fixes intégrés ignorent une valeur incompatible ; les préréglages de clés protégés contre les collisions préservent une ancienne destination personnalisée portant le même nom. |
| `cacheAffinity?` | `"auto" \| "session" \| "turn" \| "off"` | Préférence pour la clé du dernier tour réussi ; nécessite au moins deux entrées de clé et exclut OAuth/forward. Par défaut : `auto`. Les refus locaux conservent la liaison ; un refus amont ne libère que la liaison du fournisseur et de l’entrée qui ont servi. Voir le [contrat complet en anglais](/reference/configuration/providers/#provider-entries-ocxproviderconfig). |
| `requestPacing?` | `{ enabled, requestsPerMinute?, minIntervalMs?, maxConcurrentRequests?, models?, keySelection?, keys? }` | Cadencement facultatif du démarrage des requêtes sortantes côté client, distinct de l’utilisation, de la facturation et des indicateurs de limitation en amont. `maxConcurrentRequests` est une limite entière positive des requêtes en cours ; une règle fournisseur ou modèle peut utiliser uniquement cette limite. Les limites du fournisseur s’appliquent à tous ses modèles, tandis que les entrées `models` ciblent les identifiants exacts des modèles en amont et peuvent ajouter du délai ou réduire la concurrence. L’attente dans la file ne consomme pas le délai d’expiration des en-têtes de réponse en amont. Les requêtes HTTP et les distributions explicites `fetchResponse`/`runTurn` des adaptateurs sont couvertes. Avec une limite de concurrence, un tour Responses WebSocket canonique utilise HTTP/SSE afin de libérer la capacité à la fin, en cas d’erreur ou d’annulation du corps de réponse. Pour les adaptateurs `runTurn`, dont Cursor, la limite compte les tours actifs plutôt que les envois physiques : RunSSE et BidiAppend peuvent se chevaucher dans un même tour, tandis qu’un autre tour attend. Les envois suivants respectent toujours les intervalles de démarrage. |
| `requestPacing.keySelection?` | `"active"`, `"round-robin"` | Sélection des clés pour les requêtes normales : `active` par défaut conserve la clé enregistrée et la rotation sur erreur ; `round-robin` répartit les allocations entre les clés disponibles sans modifier la sélection enregistrée. Cela équilibre les allocations, pas les jetons ni les coûts. |
| `requestPacing.keys?` | `{ requestsPerMinute?, minIntervalMs?, maxConcurrentRequests? }` | Limites uniformes par référence de clé : RPM, intervalle minimal et concurrence, partagées entre modèles et cumulées avec les plafonds fournisseur et modèle. Les références identiques partagent leur capacité ; plusieurs références du même projet ne garantissent pas des quotas indépendants. TPM et quotas quotidiens ne sont pas gérés. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ account を削除しても mapping は保持され、同じ id を再追加す
| --- | --- | --- |
| `adapter` | `string` | `openai-chat`、`openai-responses`、`anthropic`、`google`、`kiro`、`cursor`、`ollama-native`、`azure-openai` (または別名 `azure`) のいずれか。 |
| `baseUrl` | `string` |アップストリーム API のベース URL。ほとんどの組み込み固定エンドポイントは不一致を無視します。衝突安全キー プリセットは、古い同じ名前のカスタム宛先を保持します。 |
| `cacheAffinity?` | `"auto" \| "session" \| "turn" \| "off"` | 直前の成功ターンを処理したキーを優先します。キーが2件以上あるプールが対象で、OAuth/forwardは対象外です。既定値は `auto`。ローカル拒否では関連付けを保持し、上流の拒否では処理したプロバイダーとエントリーが一致する関連付けのみ解除します。[英語の完全な仕様](/reference/configuration/providers/#provider-entries-ocxproviderconfig)を参照してください。 |
| `requestPacing?` | `{ enabled, requestsPerMinute?, minIntervalMs?, maxConcurrentRequests?, models?, keySelection?, keys? }` | 上流の使用量、請求、レート制限表示とは別の、クライアント側の送信開始間隔調整です。`maxConcurrentRequests` は実行中リクエスト数を制限する正の整数で、プロバイダー規則またはモデル規則だけに設定できます。プロバイダー制限は全モデルに適用され、`models` は上流の正確なモデル ID に一致し、遅延を増やすか同時実行数を減らします。キュー待機は応答ヘッダーのタイムアウトを消費しません。HTTP と明示的なアダプターの `fetchResponse`/`runTurn` 送信を対象にします。同時実行数の上限がある場合、標準の Responses WebSocket ターンは応答本文の完了・エラー・キャンセル時に枠を解放できるよう HTTP/SSE を使います。 Cursor を含む `runTurn` アダプターでは、上限は物理送信数ではなく進行中のターン数を数えます。同じターンの RunSSE と BidiAppend は重なり得ますが、別のターンは待機します。後続送信にも開始間隔は適用されます。 |
| `requestPacing.keySelection?` | `"active"`, `"round-robin"` | 通常リクエストのキー選択です。既定の `active` は保存済みキーと既存の障害時切替を維持します。`round-robin` は保存済み選択を変えず、利用可能なプールのキーを順に割り当てます。トークンや料金ではなく割当回数を分散します。 |
| `requestPacing.keys?` | `{ requestsPerMinute?, minIntervalMs?, maxConcurrentRequests? }` | キー参照ごとの共通 RPM・開始間隔・同時実行上限です。全モデルで共有し、プロバイダーとモデルの上限も同時に適用します。同一参照は枠を共有しますが、同じ上流プロジェクトの別参照は独立クォータを意味しません。TPM と日次上限は管理しません。 |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ managed map을 활성화하면 privacy-safe selector를 만들고, 이후 계정
| --- | --- | --- |
| `adapter` | `string` | `openai-chat`, `openai-responses`, `anthropic`, `google`, `kiro`, `cursor`, `ollama-native`, `azure-openai` 중 하나이며, `azure`는 별칭입니다. |
| `baseUrl` | `string` | 상위 API 기본 URL입니다. 대부분의 내장 고정 엔드포인트는 불일치를 무시합니다. 충돌 안전 키 프리셋은 같은 이름의 이전 사용자 지정 목적지를 보존합니다. |
| `cacheAffinity?` | `"auto" \| "session" \| "turn" \| "off"` | 마지막 성공 턴을 처리한 키를 우선합니다. 키 풀 항목이 2개 이상일 때 적용하며 OAuth/forward는 제외합니다. 기본값은 `auto`입니다. 로컬 거부는 바인딩을 유지하고 업스트림 거부는 실제 처리한 공급자와 항목이 일치하는 바인딩만 해제합니다. [전체 영어 계약](/reference/configuration/providers/#provider-entries-ocxproviderconfig)을 참고하세요. |
| `requestPacing?` | `{ enabled, requestsPerMinute?, minIntervalMs?, maxConcurrentRequests?, models?, keySelection?, keys? }` | 업스트림 사용량, 과금, rate-limit 지표와 별개인 선택적 클라이언트 측 아웃바운드 요청 시작 속도 조절입니다. `maxConcurrentRequests`는 진행 중 요청 수를 제한하는 양의 정수이며 provider 또는 model 규칙에서 단독으로 사용할 수 있습니다. Provider 제한은 모든 모델에 적용되고 `models` 항목은 정확한 업스트림 모델 ID와 일치하며 지연을 늘리거나 동시성을 줄일 수 있습니다. 큐 대기는 응답 헤더 타임아웃을 소모하지 않습니다. HTTP 및 명시적 어댑터 `fetchResponse`/`runTurn` 전송을 포함합니다. 동시성 제한이 있으면 표준 Responses WebSocket 턴은 응답 본문의 완료·오류·취소 때 점유 슬롯을 해제할 수 있도록 HTTP/SSE를 사용합니다. Cursor를 포함한 `runTurn` 어댑터에서는 물리적 전송 수가 아니라 진행 중인 턴 수를 제한합니다. 같은 턴의 RunSSE와 BidiAppend는 겹칠 수 있지만 다른 턴은 대기합니다. 후속 전송에도 시작 간격은 적용됩니다. |
| `requestPacing.keySelection?` | `"active"`, `"round-robin"` | 정상 요청의 키 선택 방식입니다. 기본 `active`는 저장된 키와 기존 오류 전환을 유지하며, `round-robin`은 저장된 활성 키를 변경하지 않고 사용 가능한 풀 키를 요청별 순서대로 배정합니다. 토큰이나 비용이 아니라 배정 횟수를 분산합니다. |
| `requestPacing.keys?` | `{ requestsPerMinute?, minIntervalMs?, maxConcurrentRequests? }` | 키 참조별 동일한 RPM·최소 시작 간격·동시 요청 제한입니다. 모든 모델에서 공유하며 프로바이더·정확한 모델 상한과 함께 적용합니다. 중복 참조는 한 슬롯을 공유하지만 같은 업스트림 프로젝트의 다른 참조가 독립 할당량을 뜻하지는 않습니다. TPM·일일 한도는 처리하지 않습니다. |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
title: Account Auto-pause
description: Opt-in automatic pausing of accounts after exact permanent refusals.
---

## Auto-pause permanently refused accounts

`providers.<provider>.accountAutoPause` is opt-in and defaults to off. It applies to that
provider's OAuth accounts, Codex pool accounts (including the main account in pool mode),
and configured API-key pool entries. For example:

```sh
ocx config set providers.anthropic.accountAutoPause.enabled true
ocx config set providers.anthropic.accountAutoPause.consecutiveFailures 2
```

`consecutiveFailures` is an integer from 1 to 100; omitting it uses 2. Only exact,
account-scoped HTTP 401/403 refusals qualify: an organization forbidding OAuth, an inactive
or expired subscription, an account denied API/Claude Code access or disabled by Google for a Terms of Service violation, or a downgrade to a free
plan with no credits. A model permission error, expired access token, quota window, 429,
5xx, malformed response or network failure never pauses an account.

The counter is process-local, serialized per account, and resets on a successful HTTP
response, explicit resume or a new login. Token refresh alone does not reset an OAuth
account's streak. Transient errors leave the counter unchanged. Restarting the proxy clears
counters; completed pauses, their fixed reason codes and timestamps persist. Independent
proxy processes maintain independent counters. Changing, disabling or re-enabling the policy, disabling the provider,
or changing its endpoint discards in-flight refusal observations, so a refusal from a request
sent under the earlier policy can never pause an account afterwards. A refusal counts only for the credential that actually
reached the upstream: when a local plugin rewrites the credential or destination of a send, that
send never counts toward a pause. For Codex pool accounts this includes the workspace
(`chatgpt-account-id`) the request was sent for.

At the threshold, OpenCodex excludes the account using its existing pause mechanism and
attempts an eligible sibling for the current request within the existing retry/send budget.
Fixed Codex account selectors and account-bound uploaded files retain their restrictions.
A pause never cancels already dispatched work or changes credentials. It emits one generic
log line without account identifiers or upstream error text.

`ocx account list <provider>` shows the pause reason. The dashboard's Paused badge exposes
the reason and time. Use `ocx account resume <provider> <account-id-or-alias>` to clear the
reason and allow selection again. On an API-key row, clicking Resume also activates the key.
This policy never resumes an account automatically.
38 changes: 2 additions & 36 deletions docs-site/src/content/docs/reference/configuration/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ Providers can expose a built-in shorthand, such as `agy` for `google-antigravity
| `apiKeyTransport?` | `"x-api-key" \| "bearer"` | Anthropic key header style. Defaults to native `x-api-key`; valid only for key-auth `anthropic` providers. |
| `apiKeyPool?` | `ApiKeyPoolEntry[]` | Multi-key pool. `apiKey` mirrors the active entry; each item has `id`, `key`, optional `label`, and optional numeric `addedAt`. |
| `apiKeyPoolStrategy?` | `"round-robin" \| "fill-first" \| "quota"` | How a warm key is chosen **before** the first attempt when the committed key is already cooling. Omitted keeps rotation reactive-only: the pool moves after a 429 or 401 and not before. `round-robin` takes the next key in the pool, `fill-first` keeps the first eligible one, and `quota` prefers the key with the most remaining headroom, falling back to `fill-first` order for a provider whose per-key quota is unknown. A healthy committed key is never overridden, so a manual key selection stands. |
| `cacheAffinity?` | `"auto" \| "session" \| "turn" \| "off"` | Prefers the key-pool entry that served a conversation's last successful turn, to reuse its upstream prompt cache. Applies with 2+ key-pool entries, independently of `apiKeyPoolStrategy`; OAuth/forward routes are excluded. Bindings match the exact provider and routed model: a different model does not inherit the binding. Disabled, removed, cooling, or pacing-saturated entries are not preferred, and 429/401 rotation still proceeds. With round-robin request pacing, recovery stays request-local and does not change the saved active key. Logs report `released` only when the matching binding was removed. Proxy-generated refusals preserve a healthy binding; an upstream refusal releases only the binding matching the serving provider and pool entry. `auto` (default) keeps a warm binding with ≥1024 tokens read or written, **or a detected tool-result continuation even below that threshold**. Warmth lasts 5 minutes, or 1 hour after a reported long-retention write. When no cache-write counter is reported, the sent prompt counts as written; an explicit zero is respected. This also pins providers that do not cache, so use `off` where load spreading matters more. `session` ignores warmth and token thresholds while the entry stays eligible; `turn` prefers only warm tool-result continuations. **Every binding expires 24 hours after its last recorded success**, and restart forgets all bindings. Changing the active key manually does not supersede a live binding; use `off` or disable its pool entry to override affinity. Unrelated dashboard provider saves preserve an omitted mode, including `off`. Log rows expose `cacheAffinity`/`cacheAffinityReason`. |
| `defaultModel?` | `string` | Model used when this provider is selected without an explicit model. |
| `models?` | `string[]` | Seed/fallback model list. With `liveModels: false`, a nonempty `models` list is followed by `retainModels`; an empty or omitted `models` list instead seeds `defaultModel` (if configured), then `retainModels`, removing duplicate ids in first-seen order. |
| `liveModels?` | `boolean` | Fetch the live catalog on start/sync (default `true`). Custom providers use `${baseUrl}/models`; built-ins may use a registry URL and filter. |
Expand Down Expand Up @@ -1927,39 +1928,4 @@ from the tool-call cache for those turns. Preserve their supplied reasoning for

## Auto-pause permanently refused accounts

`providers.<provider>.accountAutoPause` is opt-in and defaults to off. It applies to that
provider's OAuth accounts, Codex pool accounts (including the main account in pool mode),
and configured API-key pool entries. For example:

```sh
ocx config set providers.anthropic.accountAutoPause.enabled true
ocx config set providers.anthropic.accountAutoPause.consecutiveFailures 2
```

`consecutiveFailures` is an integer from 1 to 100; omitting it uses 2. Only exact,
account-scoped HTTP 401/403 refusals qualify: an organization forbidding OAuth, an inactive
or expired subscription, an account denied API/Claude Code access or disabled by Google for a Terms of Service violation, or a downgrade to a free
plan with no credits. A model permission error, expired access token, quota window, 429,
5xx, malformed response or network failure never pauses an account.

The counter is process-local, serialized per account, and resets on a successful HTTP
response, explicit resume or a new login. Token refresh alone does not reset an OAuth
account's streak. Transient errors leave the counter unchanged. Restarting the proxy clears
counters; completed pauses, their fixed reason codes and timestamps persist. Independent
proxy processes maintain independent counters. Changing, disabling or re-enabling the policy, disabling the provider,
or changing its endpoint discards in-flight refusal observations, so a refusal from a request
sent under the earlier policy can never pause an account afterwards. A refusal counts only for the credential that actually
reached the upstream: when a local plugin rewrites the credential or destination of a send, that
send never counts toward a pause. For Codex pool accounts this includes the workspace
(`chatgpt-account-id`) the request was sent for.

At the threshold, OpenCodex excludes the account using its existing pause mechanism and
attempts an eligible sibling for the current request within the existing retry/send budget.
Fixed Codex account selectors and account-bound uploaded files retain their restrictions.
A pause never cancels already dispatched work or changes credentials. It emits one generic
log line without account identifiers or upstream error text.

`ocx account list <provider>` shows the pause reason. The dashboard's Paused badge exposes
the reason and time. Use `ocx account resume <provider> <account-id-or-alias>` to clear the
reason and allow selection again. On an API-key row, clicking Resume also activates the key.
This policy never resumes an account automatically.
See [Account auto-pause](/reference/configuration/account-auto-pause/) for configuration, qualifying refusals, and recovery.
Loading
Loading