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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Package version: 6.1.8
| Installer behavior | `scripts/install-oc-codex-multi-auth.js`, `scripts/install-oc-codex-multi-auth-core.js` | npm bin, config merge, cache cleanup, TUI config enablement |
| Plugin orchestration | `index.ts` | OAuth loader, request pipeline, metrics, recovery, `ToolContext` assembly |
| TUI quota status | `tui.ts`, `lib/tui-status.ts`, `lib/tui-quota-cache.ts`, `lib/codex-usage.ts` | prompt quota status, quota details, shared quota cache |
| Tool registry | `lib/tools/index.ts` + `lib/tools/codex-*.ts` | 21 registered `codex-*` tools |
| Tool registry | `lib/tools/index.ts` + `lib/tools/codex-*.ts` | 24 registered `codex-*` tools |
| OAuth flow + PKCE | `lib/auth/auth.ts`, `lib/auth/server.ts`, `lib/auth/device-code.ts`, `lib/auth/login-runner.ts` | browser/device/manual login, token refresh, workspace selection |
| OAuth scopes | `lib/auth/scopes.ts` | connector scope validation and re-auth checks |
| Multi-account rotation | `lib/accounts.ts`, `lib/accounts/`, `lib/rotation.ts` | health scoring, cooldowns, token bucket, recovery |
Expand Down
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ If browser launch is blocked, use the alternate login paths in [docs/getting-sta
| `codex-limits` | What quota or rate-limit state is visible now? |
| `codex-reset` | Do I have a banked rate-limit reset credit, and how do I redeem it? |
| `codex-dashboard` | Can I manage accounts from one interactive surface? |
| `codex-pool` | Which accounts are preferred for each model, and how do I change them? |

Most of these also run as a **direct CLI** with no agent/model involvement (no token cost) — e.g. `oc-codex-multi-auth warm`, `oc-codex-multi-auth status`, or `npx -y oc-codex-multi-auth@latest warm`. Use `oc-codex-multi-auth warm` to open every enabled account's usage window at the start of a session and stagger the rolling quota cooldowns. Add `--json` for scriptable output.

Expand Down Expand Up @@ -244,6 +245,58 @@ Primary config files:
- `~/.config/opencode/tui.json`
- `~/.opencode/openai-codex-auth-config.json`

### Route models to preferred accounts

Use `modelAccountPools` to assign one or more preferred ChatGPT accounts to a
model. Account references use stable account IDs, so adding, removing, or
reordering accounts does not silently change a model's routing.

```json
{
"modelAccountPools": {
"gpt-5.6-sol": [
"org-example-account-id",
"00000000-0000-0000-0000-000000000000"
],
"gpt-5.6-terra": [
"org-another-account-id"
]
}
}
```

Save this configuration in `~/.opencode/openai-codex-auth-config.json`, then
restart OpenCode. Model matching is case-insensitive and uses the effective
model after request model normalization.

Use `codex-pool` to manage these mappings with ordinary 1-based account
numbers. The tool resolves those numbers and writes stable IDs to disk:

```text
codex-pool
codex-pool action="set" model="gpt-5.6-sol" accounts=[7,8]
codex-pool action="add" model="gpt-5.6-sol" accounts=[9]
codex-pool action="remove" model="gpt-5.6-sol" accounts=[7]
codex-pool action="clear" model="gpt-5.6-sol"
```

Add `dryRun=true` to preview a mutation. Use `format="json"` for structured
output; stable IDs remain redacted unless `includeSensitive=true` is also set.
Restart OpenCode after an applied mutation. The plugin configuration is global
while account storage is per-project by default, so a reference unresolved in
the current project is reported but never automatically deleted.

Routing behavior:

- A mapped model uses only healthy, selectable accounts in its preferred pool.
- Existing rotation strategy, quota, cooldown, and token-health rules still apply within the preferred pool.
- If every preferred account is unavailable, disabled, unknown, cooling down, or rate-limited, routing automatically falls back to the healthy general account pool.
- An unmapped model or an empty account list uses the general account pool directly.
- `codex-status`, `codex-dashboard`, and routing diagnostics report the account-pool mode as `preferred`, `general`, or `general-fallback`.

Account IDs are local account metadata but should still be treated as private
configuration. Do not publish a populated configuration file.

Selected runtime/environment overrides:

| Variable | Effect |
Expand Down
6 changes: 3 additions & 3 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,13 @@ Native mode keeps the host payload shape whenever possible. Legacy mode applies

### 4. Tool registry

`lib/tools/index.ts` builds the OpenCode tool map from 21 per-file factories under `lib/tools/`.
`lib/tools/index.ts` builds the OpenCode tool map from 24 per-file factories under `lib/tools/`.

Common groups:

- setup: `codex-setup`, `codex-help`, `codex-next`
- daily account use: `codex-list`, `codex-switch`, `codex-status`, `codex-limits`, `codex-reset`
- account metadata: `codex-label`, `codex-tag`, `codex-note`, `codex-remove`, `codex-refresh`
- daily account use: `codex-list`, `codex-switch`, `codex-warm`, `codex-status`, `codex-limits`, `codex-reset`
- account metadata and routing: `codex-label`, `codex-tag`, `codex-note`, `codex-pool`, `codex-remove`, `codex-refresh`
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- diagnostics and resilience: `codex-health`, `codex-metrics`, `codex-doctor`, `codex-diag`, `codex-diff`
- backup and secrets: `codex-export`, `codex-import`, `codex-keychain`
- interactive surface: `codex-dashboard`
Expand Down
16 changes: 16 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,9 @@ advanced settings go in `~/.opencode/openai-codex-auth-config.json`:
"fastSession": false,
"fastSessionStrategy": "hybrid",
"fastSessionMaxInputItems": 30,
"modelAccountPools": {
"gpt-5.6-sol": ["org-example-account-id"]
},
"retryProfile": "balanced",
"retryBudgetOverrides": {
"network": 2,
Expand Down Expand Up @@ -193,6 +196,7 @@ The sample above intentionally sets `"retryAllAccountsMaxRetries": 3` as a bound
| `fastSession` | `false` | forces low-latency settings per request (`reasoningEffort=none/low`, `reasoningSummary=auto`, `textVerbosity=low`) |
| `fastSessionStrategy` | `hybrid` | `hybrid` speeds simple turns and keeps full-depth for complex prompts; `always` forces fast mode every turn |
| `fastSessionMaxInputItems` | `30` | max input items kept when fast mode is applied |
| `modelAccountPools` | `{}` | optional map of effective model IDs to preferred stable account IDs; selection uses the configured pool while it has a healthy account, then falls back to the general account pool |
| `retryProfile` | `balanced` | retry budget profile for request classes (`conservative`, `balanced`, `aggressive`) |
| `retryBudgetOverrides` | `{}` | optional per-class budget overrides (`authRefresh`, `network`, `server`, `rateLimitShort`, `rateLimitGlobal`, `emptyResponse`) |
| `perProjectAccounts` | `true` | each project gets its own account storage |
Expand All @@ -212,6 +216,18 @@ The sample above intentionally sets `"retryAllAccountsMaxRetries": 3` as a bound
| `fetchTimeoutMs` | `60000` | upstream fetch timeout in ms |
| `streamStallTimeoutMs` | `45000` | max time to wait for next SSE chunk before aborting |

Use `codex-pool action="set" model="gpt-5.6-sol" accounts=[7,8]` to manage a
pool with 1-based account numbers while persisting stable IDs. The tool also
supports `status` (default), `add`, `remove`, `clear`, `dryRun=true`, and JSON
output. Restart OpenCode after an applied mutation. Because this config is
global while account storage is per-project by default, references unavailable
in the current project are reported but not automatically pruned.

Model keys are matched case-insensitively after request model normalization.
Empty lists, unmapped models, and pools with no selectable accounts use the
general account pool. Routing diagnostics expose the resulting mode as
`preferred`, `general`, or `general-fallback`.

### Beginner Safe Mode Behavior

when `beginnerSafeMode` is enabled (`true` or `CODEX_AUTH_BEGINNER_SAFE_MODE=1`), the plugin applies a safer retry profile automatically:
Expand Down
10 changes: 5 additions & 5 deletions docs/development/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ index.ts
|- ToolContext construction
v
lib/tools/index.ts
|- registers 23 OpenCode tools
|- registers 24 OpenCode tools
|- each tool delegates to lib/tools/codex-*.ts

Request path
Expand Down Expand Up @@ -90,7 +90,7 @@ tui.ts
| Storage | `lib/storage.ts`, `lib/storage/` | V3 JSON storage, atomic writes, migrations, per-project paths, backups, import/export, keychain opt-in, flagged accounts |
| Request bridge | `lib/request/fetch-helpers.ts`, `lib/request/request-transformer.ts`, `lib/request/response-handler.ts`, `lib/request/retry-budget.ts`, `lib/request/rate-limit-backoff.ts` | URL/body/header shaping, Codex invariants, SSE conversion, retry budgets, backoff, error mapping |
| Model/prompt mapping | `lib/prompts/codex.ts`, `lib/prompts/opencode-codex.ts`, `lib/prompts/codex-opencode-bridge.ts`, `lib/request/helpers/model-map.ts` | model-family detection, Codex instructions cache, OpenCode prompt adaptation, fallback aliases |
| Tool registry | `lib/tools/index.ts`, `lib/tools/codex-*.ts` | 23 OpenCode tools for setup, account switching, status, health, quota resets, diagnostics, backup, keychain, and recovery |
| Tool registry | `lib/tools/index.ts`, `lib/tools/codex-*.ts` | 24 OpenCode tools for setup, account switching, status, health, quota resets, diagnostics, backup, keychain, and recovery |
| Runtime support | `lib/runtime.ts`, `lib/circuit-breaker.ts`, `lib/proactive-refresh.ts`, `lib/parallel-probe.ts`, `lib/recovery/`, `lib/shutdown.ts` | pure runtime helpers, failure isolation, refresh scheduling, health probing, session recovery, cleanup |
| UI helpers | `lib/ui/` | terminal formatting, auth menu, select/confirm prompts, theme/color handling, beginner checklist |
| Config templates | `config/opencode-modern.json`, `config/opencode-legacy.json`, `config/minimal-opencode.json`, `config/README.md` | copy-paste OpenCode provider templates and model catalog guidance |
Expand Down Expand Up @@ -169,7 +169,7 @@ Legacy mode exists for compatibility with older OpenCode/AI SDK payload behavior

## Tool Registry Architecture

The plugin exposes 23 OpenCode tools through `lib/tools/index.ts`. `index.ts` builds one `ToolContext` from plugin-closure state and helper functions, then passes it to `createToolRegistry(ctx)`.
The plugin exposes 24 OpenCode tools through `lib/tools/index.ts`. `index.ts` builds one `ToolContext` from plugin-closure state and helper functions, then passes it to `createToolRegistry(ctx)`.

Why this shape exists:

Expand All @@ -183,8 +183,8 @@ Tool groups:
| Group | Tools |
| --- | --- |
| Setup and help | `codex-setup`, `codex-help`, `codex-next` |
| Daily account use | `codex-list`, `codex-switch`, `codex-status`, `codex-limits`, `codex-reset`, `codex-dashboard` |
| Account metadata | `codex-label`, `codex-tag`, `codex-note`, `codex-remove`, `codex-refresh` |
| Daily account use | `codex-list`, `codex-switch`, `codex-warm`, `codex-status`, `codex-limits`, `codex-reset`, `codex-dashboard` |
| Account metadata and routing | `codex-label`, `codex-tag`, `codex-note`, `codex-pool`, `codex-remove`, `codex-refresh` |
| Diagnostics | `codex-health`, `codex-metrics`, `codex-doctor`, `codex-diag`, `codex-diff` |
| Backup/secrets | `codex-export`, `codex-import`, `codex-keychain` |

Expand Down
30 changes: 30 additions & 0 deletions docs/development/CONFIG_FIELDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,36 @@ Examples:

This normalization is why legacy aliases and snapshot-like IDs can still route to a stable family while preserving the user-facing config surface.

## `modelAccountPools`

The plugin runtime config at `~/.opencode/openai-codex-auth-config.json` can
map effective model IDs to preferred stable account IDs:

```json
{
"modelAccountPools": {
"gpt-5.6-sol": ["org-example-account-id"]
}
}
```

The request pipeline resolves the pool after model normalization. All rotation
strategies restrict selection to healthy accounts in the preferred pool while
one is available. If the configured IDs are unknown or every preferred account
is disabled, cooling down, rate-limited, or locally depleted, selection falls
back to the general account pool. Empty lists and unmapped models use the
general pool directly.

`codex-pool` is the supported mutation surface. It accepts 1-based account
numbers for `set`, `add`, and `remove`, but resolves and atomically persists
only stable account IDs. `clear` removes a model mapping, and every mutation
supports a dry-run preview. Writes preserve unrelated raw config fields and
refuse to replace malformed JSON or an invalid existing pool.

The config file is global while account storage is per-project by default.
Consequently, status may report unresolved references for the current project;
the tool does not automatically prune them because they may be valid elsewhere.

## Verification Notes

Use these commands when validating config fields:
Expand Down
45 changes: 41 additions & 4 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ import {
getEmptyResponseRetryDelayMs,
getPidOffsetEnabled,
getRotationStrategy,
getModelAccountPool,
getFetchTimeoutMs,
getStreamStallTimeoutMs,
getCodexTuiV2,
Expand Down Expand Up @@ -316,6 +317,8 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
fallbackFrom: snapshot?.fallbackFrom ?? null,
fallbackTo: snapshot?.fallbackTo ?? null,
fallbackReason: snapshot?.fallbackReason ?? null,
accountPoolMode: snapshot?.accountPoolMode ?? null,
configuredAccountPoolSize: snapshot?.configuredAccountPoolSize ?? 0,
selectionExplainability: serializeSelectionExplainability(
options.selectionExplainability ?? snapshot?.explainability ?? [],
),
Expand Down Expand Up @@ -372,6 +375,8 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
lines.push(` Fallback from: ${formatRoutingValue(routing.fallbackFrom)}`);
lines.push(` Fallback to: ${formatRoutingValue(routing.fallbackTo)}`);
lines.push(` Fallback reason: ${formatRoutingValue(routing.fallbackReason)}`);
lines.push(` Account pool: ${formatRoutingValue(routing.accountPoolMode)}`);
lines.push(` Configured pool size: ${routing.configuredAccountPoolSize}`);
if (options.includeExplainability) {
lines.push(" Selection explainability:");
if (routing.selectionExplainability.length === 0) {
Expand Down Expand Up @@ -468,6 +473,22 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
routing.fallbackReason ? "warning" : "muted",
),
);
lines.push(
formatUiKeyValue(
ui,
"Account pool",
formatRoutingValue(routing.accountPoolMode),
routing.accountPoolMode === "general-fallback" ? "warning" : "muted",
),
);
lines.push(
formatUiKeyValue(
ui,
"Configured pool size",
String(routing.configuredAccountPoolSize),
"muted",
),
);
if (options.includeExplainability) {
lines.push("");
lines.push(...formatUiSection(ui, "Selection explainability"));
Expand Down Expand Up @@ -1923,8 +1944,9 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
const attempted = new Set<number>();
let restartAccountTraversalWithFallback = false;
let restartAccountTraversalAfterWorkspaceDeactivation = false;
const preferredAccountIds = getModelAccountPool(pluginConfig, model);

while (attempted.size < Math.max(1, accountCount)) {
while (attempted.size < Math.max(1, accountCount)) {
const selectionExplainability = accountManager.getSelectionExplainability(
modelFamily,
model,
Expand All @@ -1943,8 +1965,15 @@ while (attempted.size < Math.max(1, accountCount)) {
fallbackFrom,
fallbackTo,
fallbackReason,
configuredAccountPoolSize: preferredAccountIds.length,
};
const account = accountManager.getAccountForStrategy(rotationStrategy, modelFamily, model, { pidOffsetEnabled });
const account = accountManager.getAccountForStrategy(
rotationStrategy,
modelFamily,
model,
{ pidOffsetEnabled },
preferredAccountIds,
);
if (!account || attempted.has(account.index)) {
break;
}
Expand All @@ -1961,8 +1990,16 @@ while (attempted.size < Math.max(1, accountCount)) {
fallbackApplied,
fallbackFrom,
fallbackTo,
fallbackReason,
};
fallbackReason,
accountPoolMode:
preferredAccountIds.length === 0
? "general"
: account.accountId !== undefined &&
preferredAccountIds.includes(account.accountId)
? "preferred"
: "general-fallback",
configuredAccountPoolSize: preferredAccountIds.length,
};
}
// Log account selection for debugging rotation
logDebug(
Expand Down
2 changes: 1 addition & 1 deletion lib/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ lib/
├── storage.ts # V3 JSON storage facade
├── storage/ # atomic writes, paths, migrations, keychain, backup/import/export
├── table-formatter.ts # CLI table formatting
├── tools/ # 21 codex-* tool factories + registry
├── tools/ # 24 codex-* tool factories + registry
├── tui-quota-cache.ts # shared quota snapshot cache
├── tui-status.ts # prompt quota status formatting
├── types.ts # TypeScript interfaces
Expand Down
20 changes: 17 additions & 3 deletions lib/accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,14 +225,28 @@ export class AccountManager {
family: ModelFamily,
model?: string | null,
options?: HybridSelectionOptions,
preferredAccountIds?: readonly string[],
): ManagedAccount | null {
switch (strategy) {
case "sticky":
return this.rotation.getCurrentOrNextForFamilySticky(family, model);
return this.rotation.getCurrentOrNextForFamilySticky(
family,
model,
preferredAccountIds,
);
case "round-robin":
return this.rotation.getCurrentOrNextForFamily(family, model);
return this.rotation.getCurrentOrNextForFamily(
family,
model,
preferredAccountIds,
);
default:
return this.rotation.getCurrentOrNextForFamilyHybrid(family, model, options);
return this.rotation.getCurrentOrNextForFamilyHybrid(
family,
model,
options,
preferredAccountIds,
);
}
}

Expand Down
Loading