Skip to content
Closed
19 changes: 19 additions & 0 deletions docs-site/src/content/docs/guides/claude-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,25 @@ Operational contract when enabled:

See [Configuration](/reference/configuration/providers/#anthropicaccountpool-experimental).

### Native Messages with account pooling

Enabling the Anthropic account pool also prefers native Claude Messages for eligible direct
Anthropic routes. In Providers → Anthropic → Account pooling, open **How account selection works** to
change **Preserve native Claude requests**. It is on by default; turn it off to use the legacy
translation path for pooled requests. The setting is stored as
`anthropicAccountPool.nativeMessages` and applies when pooling is enabled. With pooling off,
the explicit native Messages rollout settings retain their previous behavior.

Native requests preserve history and cache breakpoints, with sticky account routing and bounded
pre-output recovery. Routes needing proxy-only processing still use translation. An account
switch may cold-start the replacement account's cache; native routing does not share a cache
across accounts or guarantee cache hits for changing prompts. Recognized native CLI and Desktop
Code requests retain their billing/identity preamble and supported feature beta headers. Inline
tool declarations, references and removals use consistent OAuth names.

For first-party Desktop Code, keep the desired picker model bound to its `anthropic/...` route.
The pooling setting does not change the Desktop login, Chat-tab connection mode or account history.

## Quickstart

```bash
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -885,12 +885,21 @@ for the subscription and client conditions the pool is meant for.
| Key | Type | Default | Description |
| --- | --- | --- | --- |
| `anthropicAccountPool.enabled?` | `boolean` | `false` | Enable sticky session affinity and quota-ranked new-session selection. **429 failover is not gated here**: it activates whenever two or more usable accounts are stored, exactly like every other multi-credential provider, and cannot be switched off. |
| `anthropicAccountPool.nativeMessages?` | `boolean` | `true` | Preserve native Claude requests for eligible direct routes when pooling is enabled; `false` uses the legacy bridge. Pool-off retains explicit native rollout settings. |
| `anthropicAccountPool.autoSwitchThreshold?` | `number` | `80` | For new sessions, when the active account reaches this threshold, choose the lowest known cached usage in the configured window; the account chosen does not itself have to be at or above the threshold. `0` disables **proactive** usage-based switching only — new-session selection and routing recovery after an eligible 429 still consult `quotaWindow`. |
| `anthropicAccountPool.strategy?` | `"quota" \| "round-robin" \| "fill-first"` | `"quota"` | New-session strategy; `quota` ranks accounts by the window set by `quotaWindow`, and `fill-first` evaluates its drain threshold in that same window. |
| `anthropicAccountPool.quotaWindow?` | `"five-hour" \| "weekly" \| "max-utilization"` | `"five-hour"` | The cached provider-reported utilization bar used for usage-aware account selection. `five-hour` keeps the original behavior. `weekly` scores the weekly bar and skips accounts whose 5-hour bar is exhausted while another eligible account remains, but falls back to exhausted candidates when none do. `max-utilization` scores the highest known bar, so it can use 5-hour usage before weekly usage is available; if neither is known, the account follows unknown-usage ordering. Known usage ranks before unknown usage under the opt-in `weekly` and `max-utilization` windows only; an omitted or explicit `five-hour` preserves the legacy ordering. If every eligible account is unknown, selection still returns one in eligible order. After the documented lower-5-hour tie-break, exact ties preserve eligible order. A healthy affinity-bound session is not proactively rebalanced. For new-session assignment and routing recovery after an eligible 429 replacement, `quota` ranks eligible candidates directly with this window; `fill-first` advances in stable order using this window's threshold and exhaustion rules; `round-robin` ignores it. Cooldown, failover limits, and reauthentication eligibility remain separate local state. Per-account weekly bars come from usage probes or observed response headers. |
| `anthropicAccountPool.stickyLimit?` | `number` | `1` | Successful new-session binds retained on one round-robin selection. Range 1–100. |
| `anthropicAccountPool.routes?` | `{name, match, accounts, fallback?}[]` | — | Ordered model routes for the enabled Anthropic OAuth pool. `match` is a full, case-sensitive model ID glob (`*` and `?`); first match wins. `accounts` contains stored account IDs, not aliases. Eligible accounts are limited to the route for initial selection and 429 retry. Without fallback, an empty route returns a local 401, or 429 with route-scoped `Retry-After` if all its declared accounts are cooling. With `fallback: true`, an all-cooling ordinary pool returns 429 with its earliest usable cooldown, even if a saved route account was removed; the client response never names the route; the proxy log uses `route:#<n>` for the rule’s 1-based position. `fallback: true` widens only when no routed account is eligible; fill-first then uses ordinary pool order. No matching rule retains normal selection; disabling the pool makes saved routes inactive. Invalid rules fail validated writes and prevent routed dispatch until corrected. `null` clears routes through the settings API. |

#### Native request preservation for the Anthropic pool

`anthropicAccountPool.nativeMessages?: boolean` defaults to `true` when the pool is enabled.
It selects both managed native Messages paths together for eligible direct Anthropic OAuth
requests. Set `false` to retain the legacy bridge for pooled requests. With pooling disabled,
this preference is retained but does not override explicit protocol rollout settings.
The dashboard exposes it under **How account selection works** in the Anthropic account pool.

Anthropic OAuth vision and web-search helpers match these routes using each helper's own model,
independently of the main request model. Each helper authenticates with its routed account, which
can differ from the globally active account. If a strict helper route has no eligible account, the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,7 @@ modes, the preview, and the per-request trace.
| `protocols.unrepresentable?` | `"legacy" \| "reject"` | `"legacy"` | `legacy` sends a request whose path drops a feature and records the loss in the trace. `reject` refuses it with HTTP 400 before any send, naming only the feature keys. |
| `protocols.rollout.nativeChatCombos?` | `boolean` | `false` | Send an eligible Chat candidate inside a combo natively from its own copy of the client body. |
| `protocols.rollout.managedMessagesNative?` | `boolean` | `false` | Send Messages natively to a direct, key-authenticated Anthropic provider instead of through the internal Responses bridge. |
| `protocols.rollout.managedMessagesNativeOAuth?` | `boolean` | `false` | Native Messages for the unpooled `anthropic` OAuth provider on `api.anthropic.com`. Read as off unless `managedMessagesNative` is on; a pooled account set stays on the bridge. Recognized JSON-string `metadata.user_id` account UUIDs are aligned with the serving OAuth credential; device and session fields are preserved. |
| `protocols.rollout.managedMessagesNativeOAuth?` | `boolean` | `false` | Native Messages for the `anthropic` OAuth provider on `api.anthropic.com`. Read as off unless `managedMessagesNative` is on when pooling is disabled. An enabled Anthropic pool instead follows `anthropicAccountPool.nativeMessages` (default true), enabling both native switches together or selecting the legacy bridge. Recognized JSON-string `metadata.user_id` account UUIDs are aligned with the serving OAuth credential; device and session fields are preserved. |
| `protocols.rollout.directEncoders?` | `boolean` | `false` | Encode Chat and Messages answers from a non-Responses upstream directly from adapter events. |
| `protocols.rollout.shadowPlan?` | `boolean` | `false` | Compare each Chat or Messages request's path with the plan a preview predicts and mark a disagreement as `planMismatch` on its log row. Sends nothing extra. |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type PoolState = {
strategy: AccountPoolStrategy;
stickyLimit: number;
quotaWindow: AccountPoolQuotaWindow;
nativeMessages: boolean;
};

/**
Expand Down Expand Up @@ -128,6 +129,7 @@ export default function AnthropicAccountPoolSettings({
strategy: normalizeAccountPoolStrategy(json.strategy),
stickyLimit: nextSticky,
quotaWindow: normalizeAccountPoolQuotaWindow(json.quotaWindow),
nativeMessages: json.nativeMessages,
});
setDraft(String(nextThreshold));
onThresholdChangeRef.current?.(nextThreshold);
Expand All @@ -150,6 +152,7 @@ export default function AnthropicAccountPoolSettings({
strategy: AccountPoolStrategy;
stickyLimit: number;
quotaWindow: AccountPoolQuotaWindow;
nativeMessages: boolean;
}) => {
const requestApiBase = apiBase;
saveAbortRef.current?.abort();
Expand All @@ -164,6 +167,7 @@ export default function AnthropicAccountPoolSettings({
strategy: next.strategy,
stickyLimit: next.stickyLimit,
quotaWindow: next.quotaWindow,
nativeMessages: next.nativeMessages,
});
setSaving(true);
setError(null);
Expand All @@ -176,6 +180,7 @@ export default function AnthropicAccountPoolSettings({
strategy: next.strategy,
stickyLimit: next.stickyLimit,
quotaWindow: next.quotaWindow,
nativeMessages: next.nativeMessages,
}, (input, init) => fetch(input, init), { signal: controller.signal });
if (!currentRequest()) return;
if (!json) throw new Error("save");
Expand All @@ -189,6 +194,7 @@ export default function AnthropicAccountPoolSettings({
strategy: savedStrategy,
stickyLimit: savedSticky,
quotaWindow: savedWindow,
nativeMessages: json.nativeMessages,
});
setDraft(String(savedThreshold));
onThresholdChangeRef.current?.(savedThreshold);
Expand All @@ -213,6 +219,7 @@ export default function AnthropicAccountPoolSettings({
const strategy = state?.strategy ?? DEFAULT_ACCOUNT_POOL_STRATEGY;
const stickyLimit = state?.stickyLimit ?? DEFAULT_ACCOUNT_POOL_STICKY_LIMIT;
const quotaWindow = state?.quotaWindow ?? DEFAULT_ACCOUNT_POOL_QUOTA_WINDOW;
const nativeMessages = state?.nativeMessages ?? true;
// The window is inert ONLY under round-robin, which never scores a usage bar at any stage.
//
// A 0 threshold is not inertness: it disables PROACTIVE usage-based switching, but
Expand Down Expand Up @@ -253,6 +260,7 @@ export default function AnthropicAccountPoolSettings({
strategy,
stickyLimit,
quotaWindow,
nativeMessages,
});
}}
>
Expand All @@ -273,6 +281,21 @@ export default function AnthropicAccountPoolSettings({
<p>{t("anthropicPool.detailsEnabling")}</p>
<p>{t("anthropicPool.detailsFailover")}</p>
<p>{t("anthropicPool.detailsActivity")}</p>
<label className="anthropic-pool-card__native-messages">
<input
type="checkbox"
checked={nativeMessages}
disabled={loading || saving || loadError || !state}
aria-describedby="anthropic-pool-native-messages-help"
onChange={(event) => {
void save({ enabled, threshold, strategy, stickyLimit, quotaWindow, nativeMessages: event.target.checked });
}}
/>
<span className="anthropic-pool-card__native-messages-copy">
<span className="field-label">{t("anthropicPool.nativeMessagesLabel")}</span>
<span id="anthropic-pool-native-messages-help" className="card-sub">{t("anthropicPool.nativeMessagesHelp")}</span>
</span>
</label>
<p>
<a href={ANTHROPIC_POOL_GUIDE_URL} target="_blank" rel="noreferrer">{t("anthropicPool.detailsGuide")}</a>
</p>
Expand Down Expand Up @@ -306,6 +329,7 @@ export default function AnthropicAccountPoolSettings({
strategy,
stickyLimit,
quotaWindow,
nativeMessages,
});
}
}}
Expand All @@ -327,6 +351,7 @@ export default function AnthropicAccountPoolSettings({
strategy: next,
stickyLimit,
quotaWindow,
nativeMessages,
});
}}
onStickyDraftChange={setStickyDraft}
Expand All @@ -347,6 +372,7 @@ export default function AnthropicAccountPoolSettings({
strategy,
stickyLimit: parsed,
quotaWindow,
nativeMessages,
});
}}
/>
Expand All @@ -371,6 +397,7 @@ export default function AnthropicAccountPoolSettings({
strategy,
stickyLimit,
quotaWindow: parsed,
nativeMessages,
});
}}
/>
Expand Down
2 changes: 2 additions & 0 deletions gui/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1937,6 +1937,8 @@ export const de: Record<TKey, string> = {
"anthropicPool.detailsEnabling": "Mit aktivem Pool bleibt eine Sitzung bei ihrem Konto, solange es funktioniert; die gewählte Strategie bestimmt das Konto für neue Sitzungen und nach einer Ablehnung.",
"anthropicPool.detailsFailover": "Ausschalten beendet das Failover nicht: Nach einem 429 oder einer als Kontoablehnung eingestuften Antwort kann eine Anfrage weiterhin zu einem anderen angemeldeten Konto wechseln. Pausiere ein Konto, um es davon auszunehmen.",
"anthropicPool.detailsActivity": "OpenCodex sendet keine Keep-warm-Anfragen. Standardmäßig erneuert es Claude-Tokens nicht im Hintergrund und fragt dort auch keine Nutzung ab; die Nutzung wird nur gelesen, wenn das Dashboard, die Menüleisten-App oder ein ocx-Befehl sie anfordert.",
"anthropicPool.nativeMessagesLabel": `Native Claude-Anfragen beibehalten`,
"anthropicPool.nativeMessagesHelp": `Bei aktiviertem Pool bleiben Claude-Code-Verlauf und Cache-Markierungen auf dem nativen Messages-Pfad, sofern die gewählte Route ihn unterstützt. Andere Routen können Anfragen weiterhin übersetzen. Ein Kontowechsel kann den Cache neu aufbauen; Cache-Treffer sind nicht garantiert und Konten teilen keinen Cache.`,
"anthropicPool.detailsGuide": "Anleitung zum Kontenpool",
"anthropicPool.needTwoAccounts": "Füge mindestens zwei Claude-OAuth-Konten hinzu, bevor du den Pool aktivierst.",
"anthropicPool.threshold": "Nutzungsschwelle für neue Sitzungen",
Expand Down
2 changes: 2 additions & 0 deletions gui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2548,6 +2548,8 @@ export const en = {
"anthropicPool.detailsEnabling": "Turning the pool on keeps each session on its account while that account is healthy, and lets the selected strategy choose accounts for new sessions and after a refusal.",
"anthropicPool.detailsFailover": "Turning it off does not turn off failover: after a 429 or a classified account refusal, a request can still move to another signed-in account. Pause an account to keep it out of failover.",
"anthropicPool.detailsActivity": "OpenCodex sends no keep-warm requests. By default it does not refresh Claude tokens or check usage in the background; usage is read only when the dashboard, the menu bar app, or an ocx command asks for it.",
"anthropicPool.nativeMessagesLabel": `Preserve native Claude requests`,
"anthropicPool.nativeMessagesHelp": `With pooling enabled, keeps Claude Code history and cache markers on the native Messages path when the selected route supports it. Other routes may still translate requests. An account change may start a cold cache; cache hits are not guaranteed, and caches are not shared between accounts.`,
"anthropicPool.detailsGuide": "Account pool guide",
"anthropicPool.needTwoAccounts": "Add at least two Claude OAuth accounts before enabling the pool.",
"anthropicPool.threshold": "New-session usage threshold",
Expand Down
2 changes: 2 additions & 0 deletions gui/src/i18n/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2474,6 +2474,8 @@ export const fr: Record<TKey, string> = {
"anthropicPool.detailsEnabling": "Une fois le groupe activé, chaque session reste sur son compte tant qu’il fonctionne, et la stratégie choisie désigne le compte des nouvelles sessions et celui qui prend le relais après un refus.",
"anthropicPool.detailsFailover": "Le désactiver n’arrête pas la bascule : après un 429 ou un refus de compte identifié, une requête peut encore passer à un autre compte connecté. Mettez un compte en pause pour l’exclure.",
"anthropicPool.detailsActivity": "OpenCodex n’envoie aucune requête de maintien à chaud (keep-warm). Par défaut, il ne rafraîchit pas les jetons Claude et ne consulte pas l’usage en arrière-plan ; l’usage n’est lu que lorsque le tableau de bord, l’app de la barre des menus ou une commande ocx le demande.",
"anthropicPool.nativeMessagesLabel": `Conserver les requêtes Claude natives`,
"anthropicPool.nativeMessagesHelp": `Lorsque le pool est activé, l’historique Claude Code et les marqueurs de cache restent sur le chemin Messages natif si la route choisie le permet. D’autres routes peuvent toujours traduire les requêtes. Un changement de compte peut repartir avec un cache froid ; les résultats du cache ne sont pas garantis et les comptes ne partagent pas leur cache.`,
"anthropicPool.detailsGuide": "Guide du groupe de comptes",
"anthropicPool.needTwoAccounts": "Ajoutez au moins deux comptes OAuth Claude avant d’activer le groupe.",
"anthropicPool.threshold": "Seuil d’utilisation des nouvelles sessions",
Expand Down
2 changes: 2 additions & 0 deletions gui/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2370,6 +2370,8 @@ export const ja: Record<TKey, string> = {
"anthropicPool.detailsEnabling": "プールをオンにすると、各セッションはアカウントが正常な間そのアカウントに留まり、新規セッションと拒否後のアカウントは選択した戦略に従って選ばれます。",
"anthropicPool.detailsFailover": "オフにしてもフェイルオーバーは止まりません。429 やアカウント拒否と判定された応答の後、リクエストはサインイン済みの別アカウントへ移ることがあります。対象から外すにはそのアカウントを一時停止してください。",
"anthropicPool.detailsActivity": "OpenCodex はキープウォーム(keep-warm)用のリクエストを送信しません。既定ではバックグラウンドで Claude トークンを更新したり使用量を確認したりせず、使用量はダッシュボード、メニューバーアプリ、または ocx コマンドが要求したときにのみ読み取ります。",
"anthropicPool.nativeMessagesLabel": `Claude のネイティブリクエストを維持`,
"anthropicPool.nativeMessagesHelp": `プールが有効な場合、選択したルートが対応していれば、Claude Code の履歴とキャッシュマーカーをネイティブ Messages 経路で維持します。他のルートでは引き続きリクエストが変換されることがあります。アカウントを切り替えるとキャッシュが新規状態になる場合があり、キャッシュヒットは保証されず、アカウント間でキャッシュは共有されません。`,
"anthropicPool.detailsGuide": "アカウントプールのガイド",
"anthropicPool.needTwoAccounts": "プールを有効にする前に、Claude OAuth アカウントを 2 つ以上追加してください。",
"anthropicPool.threshold": "新規セッションの使用率しきい値",
Expand Down
Loading
Loading