Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
40 changes: 39 additions & 1 deletion docs-site/src/content/docs/reference/cli/providers-accounts.md
Original file line number Diff line number Diff line change
Expand Up @@ -874,12 +874,50 @@ openai: { provider, autoSwitchThreshold: number, enabled: boolean }
generic OAuth: { provider, autoSwitchThreshold: number | null, enabled: boolean, poolEnabled: boolean | null, inert: boolean | null }
```

### `ocx account preserve-signed-in openai [on|off|status] [--apply-to-bound] [--json]`

Opt in to **use other eligible accounts first and keep the signed-in ChatGPT/Codex Desktop
identity as fallback**. A bare invocation or `status` reads the running proxy's policy, affected
entry IDs, manual pin and next unbound choice. `on` persists `codexPreserveSignedInAccount: true`;
`off` restores your existing custom account order without rewriting any priorities.

The reserved group includes native `__main__` and ordinary pool entries with the same selected
ChatGPT account/workspace **and** normalized member email. Sharing a workspace alone is insufficient.
The group follows the latest physical login observed by the proxy's native-login lifecycle;
no incoming request credential can define it. If member/workspace evidence is unavailable,
`identityKnown: false` reports that only main can be identified. Status obtains a fresh owned
observation; a busy or unreadable native profile returns 503 without applying the change.

All four Codex strategies prefer other identities with quota headroom, preserving custom order
within each group. Reserved entries remain fallback when preferred tiers drain. **Fallback can
consume the signed-in quota**, subject to existing model access, pause, cooldown, reauthentication,
hard-lock and paid-credit consent checks. This policy does not grant paid-credit consent.

Healthy existing bindings stay in place, and manual pins and exact account-qualified model routes
override the preference. To apply it to ongoing work, run:

```bash
ocx account preserve-signed-in openai on --apply-to-bound
```

This explicitly releases only affected bindings and any affected manual pin for the **next request**;
in-flight requests keep their captured account. Unrelated bindings and pins remain. Moving work may
lose its warm prompt cache, and account-scoped uploads or continuation state can prevent a move.
`codexAccountPriorityFailback` remains a separate opt-in, with its existing quota-strategy,
positive-threshold and fresh-observation conditions; it can also move work toward a preferred group.

For **never use this identity through the pool**, use `ocx account pause openai main` instead:
linked pause excludes main and matching pool entries without changing the desktop login file.
Only requests that reach OpenCodex are controlled. This does not unlock an already-disabled
desktop composer or promise to fix client-side quota gating.

### `ocx account priority <provider> <account-id|alias|main> [<-100..100|first|earlier|normal|later|last|reset>] [--json]`

Reads or sets one Codex pool account's selection order: **higher is used earlier**, the default is
`0`, and the range is `-100` through `100`. Only the `openai` Codex pool is ordered, so other
providers exit 1. `main` targets the Codex Desktop login, which is ordered like any other pool
account — `ocx account priority openai main last` is how you keep it as the reserve.
account. `ocx account priority openai main last` lowers only that entry; use
`preserve-signed-in` above to reserve its matching ordinary pool entries too.

Preset words stand in for small integers: `first` is `+2`, `earlier` is `+1`, `normal` is `0`,
`later` is `-1`, and `last` is `-2`. `reset` returns the account to the default and drops its stored
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ separate. Full request URLs such as `/api/v1/responses` are not provider base UR
| `activeCodexAccountId?` | `string` | — | Manually selected Pool account for the next request. Selection clears thread affinity; in-flight requests keep captured credentials. |
| `codexAccountPriorities?` | `Record<string, number>` | — | 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 at or above its own nonzero effective usage threshold (account override or global default), cooling down, soft-avoided, paused, or needs reauthentication — unknown quota never drains a tier. Ordering never makes an ineligible account selectable and by default, 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`). |
| `codexPreserveSignedInAccount?` | `boolean` | `false` | Prefer other eligible Codex Pool identities before native main and ordinary entries matching its selected workspace and member email. Existing custom priorities are preserved inside each group. Healthy bindings and manual pins remain; exact account routes and Direct mode are unchanged. Fallback may consume signed-in quota under all existing guards. Use [account preserve-signed-in](/reference/cli/providers-accounts/#ocx-account-preserve-signed-in-openai-onoffstatus---apply-to-bound---json) for status or an explicit next-request binding transition. |
| `codexAccountPriorityFailback?` | `boolean` | `false` | Opt-in return of ongoing Codex Pool tasks to a usable higher-priority account under the `quota` strategy. Requests trigger a background quota refresh at most once every five minutes, including failed refresh attempts. When recent quota observations show headroom, the next request may rebind even if its current account is below the threshold. This explicit preference can discard a warm prompt cache; it does not change default cache affinity. No requests means no polling. Manual pins, model access, cooldowns, pauses and reauthentication exclusions still apply; in-flight requests keep their captured account. Requires the current source account's effective threshold to be positive (an override of 0 disables it; a positive override works with global 0); exact account routes and Direct mode are unchanged. |
| `autoSwitchThreshold?` | `number` | `80` | Usage threshold for placing new/unbound work. `quota` can re-evaluate unbound tasks on their next request once usage crosses this threshold. Bound tasks keep their account past the threshold by default (`pool.cacheAffinity`); they leave only when that account is exhausted or otherwise cannot serve, and then only for an account with genuine quota headroom and strictly lower usage. Set `pool.cacheAffinity: false` to re-evaluate bound tasks at this threshold, still only onto such a destination. `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. |
| `codexAccountAutoSwitchThresholds?` | `Record<string, number>` | — | Per-account overrides for `autoSwitchThreshold`: account id → integer from `0` to `100`. An absent entry inherits the global value; `0` disables usage-driven switching only when that account is the source. `quota` uses the current source account's effective threshold for bound-task re-evaluation and unbound switching. Fill-first drain and selection-order tier headroom are evaluated against each account's own effective threshold; main-account pins and subagent fallback also use the relevant account's effective threshold. Supports the main `__main__` account. A malformed map is ignored with a warning. Managed by each account card on the Codex Auth page; disabling an override removes its entry. Enabling the override copies the current global threshold into a fixed account value. The override, including `0`, takes precedence over later global changes. Disabling it sends `threshold: null`, removes the entry, and restores inheritance of the current global threshold and future global changes. |
Expand Down
2 changes: 2 additions & 0 deletions docs-site/src/content/docs/reference/management-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,8 @@ manager. Its routes are:
| `POST /api/codex-auth/accounts/clear-cooldown` | Clear runtime cooldown for one account or all accounts | 400 invalid id |
| `GET, PUT /api/codex-auth/active` | Read or select the active account | 400 invalid or missing account; 409 paused/legacy-row conflict or `account_selection_unavailable` |
| `PUT /api/codex-auth/auto-switch` | Set the global quota threshold with `{ threshold }`, or an account override with `{ id, threshold }`; `null` restores global inheritance, and `__main__` selects the Desktop login | 400 invalid id/threshold; 404 missing account |
| `GET /api/codex-auth/preserve-signed-in` | Read policy, current `affectedAccountIds`, `identityKnown`, `nextAccountId`, `nextUsesSignedInQuota` (actual next choice, even when disabled), `pinnedAccountId`, `bindings`, `priorityFailbackEnabled`, and `fallbackConsumesSignedInQuota` | 503 native profile busy/unreadable |
| `PUT /api/codex-auth/preserve-signed-in` | Strict `{ enabled: boolean, applyToBound?: boolean }`; explicit `applyToBound: true` requires enabled, releases affected bindings/pin for next request; reports `releasedPin` | 400 invalid body; 503 native profile busy/unreadable |
| `PUT, PATCH /api/codex-auth/pool-strategy` | Update Codex account-pool selection strategy | 400 invalid strategy/config |
| `PUT /api/codex-auth/failover` | Set the account failover threshold | 400 invalid threshold |
| `GET /api/codex-auth/quota` | Read cached quota state by account | — |
Expand Down
2 changes: 1 addition & 1 deletion scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"version": 1,
"root": "tests",
"explicitShards": ["layout-clients.json"],
"explicit": { "service-state-isolation.test.ts": "service", "compaction-history-framing.test.ts": "responses", "catalog-native-max-context.test.ts": "codex-integration", "protocol-argon-thought-boundary.test.ts": "responses", "service-log.test.ts": "service", "messages-native-empty-inline.test.ts": "claude-integration",
"explicit": { "codex-preserve-signed-in.test.ts": "codex-integration", "cli-account-preservation.test.ts": "cli", "service-state-isolation.test.ts": "service", "compaction-history-framing.test.ts": "responses", "catalog-native-max-context.test.ts": "codex-integration", "protocol-argon-thought-boundary.test.ts": "responses", "service-log.test.ts": "service", "messages-native-empty-inline.test.ts": "claude-integration",
"owner-registry-isolation.test.ts": "config",
"codex-history-pending-wal.test.ts": "codex-integration", "responses-anthropic-billing-send-budget.test.ts": "responses", "anthropic-billing-ownership.test.ts": "adapters/anthropic", "server-anthropic-billing.test.ts": "server", "server-anthropic-billing-boundaries.test.ts": "server", "router-anthropic-billing-failover.test.ts": "routing", "anthropic-billing-refusal.test.ts": "adapters/anthropic", "messages-request-id-headers.test.ts": "claude-integration", "bounded-file-read.test.ts": "lib", "responses-undeclared-tool-guard-inbound.test.ts": "responses", "explicit-empty-tool-catalog.test.ts": "responses", "explicit-empty-tool-catalog-history.test.ts": "responses",
"messages-request-id-endpoint.test.ts": "claude-integration",
Expand Down
10 changes: 7 additions & 3 deletions skills/ocx/references/01_management_surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ These answer in the CLI head and never reach the proxy, so they work with nothin
|---|---|
| [lifecycle](01_surface_lifecycle.md) | 12 |
| [providers-models](01_surface_providers-models.md) | 47 |
| [accounts](01_surface_accounts.md) | 41 |
| [accounts](01_surface_accounts.md) | 42 |
| [agents-routing](01_surface_agents-routing.md) | 51 |
| [integrations](01_surface_integrations.md) | 41 |
| [observe-system](01_surface_observe-system.md) | 92 |
Expand Down Expand Up @@ -519,6 +519,10 @@ Original invocation order. These headings preserve links to the previous single-

[State-changing task](01_surface_providers-models.md#ocx-models-order-reset)

### `ocx account preserve-signed-in`

[State-changing task](01_surface_accounts.md#ocx-account-preserve-signed-in)

### `ocx logout`

[State-changing task](01_surface_accounts.md#ocx-logout)
Expand Down Expand Up @@ -1397,6 +1401,6 @@ Original invocation order. These headings preserve links to the previous single-

## Counts

- declared capabilities: 337
- of those, state-changing: 206
- declared capabilities: 338
- of those, state-changing: 207
- head-resolved invocations: 2
24 changes: 23 additions & 1 deletion skills/ocx/references/01_surface_accounts.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
Use these declarations to choose a task, then check its flags and authority before execution.
Non-mutating probes may still contact providers, consume quota or refresh caches.

Declared capabilities: 41.
Declared capabilities: 42.

### `ocx account login`

Expand Down Expand Up @@ -355,6 +355,28 @@ JSON mode: `envelope`.
- Without --account the existing pool-wide operation remains. OpenAI per-account status reads override plus effective threshold; on writes80, off0, inherit null, threshold a validated0–100 integer.
- New OpenAI policy writes require successful unambiguous roster evidence and never fall back to a raw unresolved selector. These are routing thresholds, not pool enablement or paid-credit permission.

### `ocx account preserve-signed-in`

Usage: `ocx account preserve-signed-in openai [on|off|status] [--apply-to-bound] [--json]`

Prefer other Codex identities and reserve the native sign-in as fallback.

State-changing: yes.

| Method | Route |
|---|---|
| GET | `/api/codex-auth/preserve-signed-in` |
| PUT | `/api/codex-auth/preserve-signed-in` |

| Flag | Value | Meaning |
|---|---|---|
| `--apply-to-bound` | boolean | With on, release protected bindings and pins for the next request. |
| `--json` | boolean | Emit affected entries, next unbound choice and pin/binding semantics. |

JSON mode: `payload`.

- Default/status reads. Custom priorities are preserved. Bound threads and manual pins remain unless explicitly released. Exact-account models override the preference. Fallback can spend signed-in quota; linked pause excludes it instead.

### `ocx logout`

Usage: `ocx logout <provider> [--live] [--json]`
Expand Down
44 changes: 44 additions & 0 deletions src/cli/account-preservation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { apiError, apiJson, proxyUnreachable, resolveBaseUrl, type AccountDeps } from "./account-api";

/** This operation follows the selected runtime; never read a local account/config roster. */
export async function cmdPreserveSignedIn(args: string[], deps: AccountDeps): Promise<number> {
const provider = args.shift();
let mode = "status";
let hasMode = false;
let json = false;
let applyToBound = false;
for (const arg of args) {
if (arg === "--json" && !json) json = true;
else if (arg === "--apply-to-bound" && !applyToBound) applyToBound = true;
else if (["on", "off", "status"].includes(arg) && !hasMode) { mode = arg; hasMode = true; }
else return usage();
}
if (provider !== "openai" || (applyToBound && mode !== "on")) return usage();
const baseUrl = await resolveBaseUrl(deps);
if (!baseUrl) return proxyUnreachable();
const result = await apiJson(deps, baseUrl, mode === "status" ? "GET" : "PUT", "/api/codex-auth/preserve-signed-in",
mode === "status" ? undefined : { enabled: mode === "on", applyToBound });
if (result.status === 0) return proxyUnreachable(result.transportError);
if (result.status !== 200) return apiError(result.json, "Signed-in quota policy unavailable", result.status);
if (typeof result.json.enabled !== "boolean" || !Array.isArray(result.json.affectedAccountIds)
|| !result.json.affectedAccountIds.every(id => typeof id === "string")
|| (result.json.nextAccountId !== null && typeof result.json.nextAccountId !== "string")) {
return apiError({}, "Invalid signed-in quota policy response", 502);
}
if (json) console.log(JSON.stringify(result.json, null, 2));
else {
console.log(`Preserve signed-in quota: ${result.json.enabled ? "on" : "off"}`);
console.log(`Affected pool entries: ${result.json.affectedAccountIds.join(", ")}`);
console.log(`Next unbound choice: ${result.json.nextAccountId ?? "none"}`);
console.log(`Bindings: ${result.json.bindings}; manual pin: ${result.json.pinnedAccountId ?? "none"}`);
if (result.json.priorityFailbackEnabled === true) console.log("Existing priority failback can move bound work when its quota and freshness conditions hold.");
console.log("Fallback can consume signed-in quota. Exact-account models override this preference.");
console.log("Use on --apply-to-bound to release affected bindings and pins for the next request.");
}
return 0;
}

function usage(): number {
console.error("Usage: ocx account preserve-signed-in openai [on|off|status] [--apply-to-bound] [--json]");
return 1;
}
5 changes: 5 additions & 0 deletions src/cli/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const ACCOUNT_USAGE = `Usage:
ocx account auto-switch <provider> <on|off|status|threshold <0-100>> [--json]
ocx account auto-switch <anthropic|anthropic2> <on|off|status|inherit|threshold <0-100>> --account <id> [--json]
ocx account alias <provider> <account-or-key-id|alias> <display-name|-> [--json]
ocx account preserve-signed-in openai [on|off|status] [--apply-to-bound] [--json]
ocx account priority <provider> <account-id|alias|main> [<-100..100|first|earlier|normal|later|last|reset>] [--json]
ocx account pause <provider> <account-id|alias|main> [--json]
ocx account resume <provider> <account-id|alias|main> [--json]
Expand Down Expand Up @@ -477,6 +478,10 @@ export async function cmdAccount(args: string[], deps: AccountDeps = {}): Promis
if (sub === "refresh") return await cmdRefresh(rest, deps);
if (sub === "auto-switch") return await cmdAutoSwitch(rest, deps);
if (sub === "alias" || sub === "rename") return await cmdAlias(rest, deps);
if (sub === "preserve-signed-in") {
const { cmdPreserveSignedIn } = await import("./account-preservation");
return cmdPreserveSignedIn(rest, deps);
}
if (sub === "priority") return await cmdPriority(rest, deps);
// #2702: the server routes existed and only the CLI caller was missing, so these were
// dashboard-only capabilities.
Expand Down
8 changes: 8 additions & 0 deletions src/cli/capabilities-accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ import type { Capability } from "./capability-types";

/** Account pool operations and physical native-login profiles have separate owners. */
export const ACCOUNT_CAPABILITIES: readonly Capability[] = [
{
command: ["account", "preserve-signed-in"], summary: "Prefer other Codex identities and reserve the native sign-in as fallback.",
usage: "ocx account preserve-signed-in openai [on|off|status] [--apply-to-bound] [--json]",
routes: [{ method: "GET", path: "/api/codex-auth/preserve-signed-in" }, { method: "PUT", path: "/api/codex-auth/preserve-signed-in" }],
flags: [{ name: "--apply-to-bound", value: "boolean", summary: "With on, release protected bindings and pins for the next request." }, { name: "--json", value: "boolean", summary: "Emit affected entries, next unbound choice and pin/binding semantics." }],
mutates: true, json: "payload",
details: ["Default/status reads. Custom priorities are preserved. Bound threads and manual pins remain unless explicitly released. Exact-account models override the preference. Fallback can spend signed-in quota; linked pause excludes it instead."],
},
{
command: ["logout"],
summary: "Remove the active stored OAuth credential for a provider locally.",
Expand Down
Loading
Loading