diff --git a/docs-site/src/content/docs/reference/cli/providers-accounts.md b/docs-site/src/content/docs/reference/cli/providers-accounts.md index 1c9cac81800..754834c4912 100644 --- a/docs-site/src/content/docs/reference/cli/providers-accounts.md +++ b/docs-site/src/content/docs/reference/cli/providers-accounts.md @@ -337,6 +337,32 @@ grant counts, nullable dates, cooldown, pending operation and journal availabili Empty grants are not the same as unavailable status. It never consumes a grant or resumes a pending spend. Consumption remains a human GUI operation. +### Use remaining included quota + +To keep using a selected account past a proactive switching threshold or the main-account +protection threshold, explicitly enable its remaining-quota preference: + +```bash +ocx account use-remaining openai main on +ocx account use openai main +ocx account use-remaining openai main status --json +ocx account use-remaining openai main off +``` + +Replace `main` with a unique pool-account ID or alias to target an added account. +Enabling this disables proactive usage switching for that account. For the main login, it also +moves both effective hard-lock thresholds to 100%, so a 95% reading can still admit a request. +The preference persists across restarts until turned off; original threshold settings remain +stored and resume applying when it is disabled. It does not select an account itself or override +round-robin scheduling, manual/automatic pause, entitlement checks, reauthentication, or upstream +cooldowns. Paid-credit permission remains a separate setting; enabling this never opts an account +into credits or redeems a reset grant. The default-on main hard lock still refuses at 100%. + +`GET /api/codex-auth/accounts/use-remaining?id=` reads the live policy. +`PUT /api/codex-auth/accounts/use-remaining` accepts `{ id, useRemainingQuota: boolean }`. +Both require the existing management authentication. This control is currently exposed through +the CLI and API. + ### Main-account quota protection In **Codex settings → Multi-auth → Advanced settings**, **Block main account** diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index d23c45edb22..347bd72f08f 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -45,6 +45,7 @@ separate. Full request URLs such as `/api/v1/responses` are not provider base UR | `contextCapValue?` | `number` | `350000` | Default used on first enable. A later enable restores the selected provider value. Updating the global value with `setAll: true` changes enabled caps only; `setAll: true` without a value enables all configured providers at the current global value. | | `codexAccounts?` | `CodexAccount[]` | `[]` | ChatGPT/Codex pool account metadata managed by Codex Auth. Secrets live separately in `codex-accounts.json`. | | `pausedCodexAccountIds?` | `string[]` | `[]` | Accounts excluded from Pool selection until resumed, including the main `__main__` account when paused. | +| `codexUseRemainingQuotaAccountIds?` | `string[]` | `[]` | Accounts opted into using their remaining included quota. Disables proactive usage switching for listed accounts; for `__main__`, effective short/long hard-lock thresholds become 100%. Original thresholds remain stored and apply again after disabling. Pause, authentication, eligibility, cooldown and paid-credit permission still apply. Managed by `ocx account use-remaining openai on`; this persistent preference does not select the account. | | `creditCodexAccountIds?` | `string[]` | `[]` | Accounts allowed to keep serving from ChatGPT credits after a usage limit, including the main `__main__` account. Upstream does not refuse an account that holds credits at 100%; it serves the request and draws the balance. Spending is opt-in: an account not listed here is skipped by selection while one of its usage windows (weekly or monthly, only monthly on 30-day plans, or the 5-hour window) reads 100% with its reset still ahead, and returns once that reset passes. A weekly or monthly reading without a reset time does not hold the account; a 5-hour reading at 100% without a reset holds it only while that reading is still fresh. When no other account is available, automatic selection finds none rather than spending credits. An unlisted `__main__` is also refused, like a hard-lock refusal, when a request names it or carries its credential. Listing `__main__` does not lift the main-account hard lock (on by default at 98%), which still stops the main login first; turn the lock off to let the main account spend credits (a lock at 100% still stops it at 100%). New accounts start unlisted. Managed by the **Use credits** switch in the Codex Auth header and the **Use credits after limit** switch in each account card's **⋯** menu. | | `codexQuotaAutoRefresh?` | `Record` | `{}` | Per-Codex-login-account opt-in for automatic `fiveHour` and `weekly` window activation in Pool mode; Direct mode does not run this worker. In Providers/Codex Auth **Advanced settings**, one control enables or disables both supported windows across all current main and added accounts. New accounts are not opted in automatically. Enable skips windows absent from live WHAM data; disable also clears stale enabled windows. The UI reuses granular `/api/settings` writes, reconciles partial failures, and retries the original ON/OFF intent without replacing unrelated settings or completed reset markers. The API still rejects enabling an unavailable window with HTTP 409. At a reported reset time, opencodex sends one minimal non-stored Codex message using that account's quota and persists the activated timestamp. This does not apply to API-key providers. | | `codexAccountNamespaces?` | `Record` | — | Optional map from an arbitrary public model selector to a stored Codex account target. When account-qualified picker rows are enabled, each selector whose target is present adds separate `/` rows to the Codex picker; each row uses only that account. With any selector active, bare native rows are hidden in the picker, but their ids remain routable and listed by raw `/v1/models` unless explicitly disabled. | diff --git a/docs-site/src/content/docs/zh-cn/reference/cli/providers-accounts.md b/docs-site/src/content/docs/zh-cn/reference/cli/providers-accounts.md index 684040f94e8..204e22636e0 100644 --- a/docs-site/src/content/docs/zh-cn/reference/cli/providers-accounts.md +++ b/docs-site/src/content/docs/zh-cn/reference/cli/providers-accounts.md @@ -5,6 +5,13 @@ description: 提供方配置、凭据、配额,以及模型目录命令。 这些命令用于配置上游提供方、认证账号、管理凭据池,并控制暴露给 Codex 的模型目录。 +使用剩余套餐额度可通过 `ocx account use-remaining openai main on` 开启,再用 +`ocx account use openai main` 选择主账号;`status --json` 查看状态,`off` 关闭。 +将 `main` 替换为唯一的池账号 ID 或别名即可控制该账号。开启后关闭该账号按用量提前切换; +主账号的短、长窗口硬锁阈值延后到 100%。开关跨重启保留,关闭后恢复原有阈值。 +暂停、重新认证、上游冷却和付费 credits 权限仍然生效;此开关不会开启付费额度。 +详见[英文说明](/reference/cli/providers-accounts/#use-remaining-included-quota)。 + ## 提供方 ### `ocx provider ` diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index 1f0d2d4ccaa..d39924059c7 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -2,6 +2,7 @@ "version": 1, "root": "tests", "explicit": { + "codex-use-remaining-quota.test.ts": "codex-integration", "cli-log-view-filter.test.ts": "cli", "cli-usage-model-search.test.ts": "cli", "cli-companion-usage.test.ts": "cli", "cli-account-key-quota.test.ts": "cli", "cli-observe-snapshot.test.ts": "cli", "cli-access-data-plane.test.ts": "cli", "cli-access-key-guard.test.ts": "cli", "cli-access-rename.test.ts": "cli", "cli-access-audio.test.ts": "cli", "cli-access-audio-live.test.ts": "cli", "cli-log-follow.test.ts": "cli", "cli-injection-follow.test.ts": "cli", diff --git a/skills/ocx/references/01_management_surface.md b/skills/ocx/references/01_management_surface.md index 8d9ad48e645..281a6064454 100644 --- a/skills/ocx/references/01_management_surface.md +++ b/skills/ocx/references/01_management_surface.md @@ -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) | 40 | +| [accounts](01_surface_accounts.md) | 41 | | [agents-routing](01_surface_agents-routing.md) | 48 | | [integrations](01_surface_integrations.md) | 40 | | [observe-system](01_surface_observe-system.md) | 92 | @@ -599,6 +599,10 @@ Original invocation order. These headings preserve links to the previous single- [State-changing task](01_surface_accounts.md#ocx-account-pool) +### `ocx account use-remaining` + +[State-changing task](01_surface_accounts.md#ocx-account-use-remaining) + ### `ocx account credits` [State-changing task](01_surface_accounts.md#ocx-account-credits) @@ -1361,6 +1365,6 @@ Original invocation order. These headings preserve links to the previous single- ## Counts -- declared capabilities: 328 -- of those, state-changing: 198 +- declared capabilities: 329 +- of those, state-changing: 199 - head-resolved invocations: 2 diff --git a/skills/ocx/references/01_surface_accounts.md b/skills/ocx/references/01_surface_accounts.md index 0deea60d2c2..e5ee669cbdc 100644 --- a/skills/ocx/references/01_surface_accounts.md +++ b/skills/ocx/references/01_surface_accounts.md @@ -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: 40. +Declared capabilities: 41. ### `ocx account login` @@ -868,6 +868,29 @@ JSON mode: `envelope`. - Reads the target supported-field list before a narrow PUT. Null/unsupported, stored/effective and generic inert policy stay distinct; disabling does not promise that reactive429 rotation stops. - No local classification/defaults are round-tripped into the live target; supported-field reads are not CAS. +### `ocx account use-remaining` + +Usage: `ocx account use-remaining openai [--json]` + +Choose whether one Codex account keeps using its remaining included quota. + +State-changing: yes. + +| Method | Route | +|---|---| +| GET | `/api/codex-auth/accounts` | +| GET | `/api/codex-auth/accounts/use-remaining` | +| PUT | `/api/codex-auth/accounts/use-remaining` | + +| Flag | Value | Meaning | +|---|---|---| +| `--json` | boolean | Emit the validated account policy. | + +JSON mode: `envelope`. + +- On disables proactive usage switching for the selected account and raises main-account protection to 100%. Off restores the configured thresholds. +- This is a persistent preference, not account selection. Pause, reauthentication, cooldown, entitlement and paid-credit policy still apply. + ### `ocx account credits` Usage: `ocx account credits openai [--json]; ocx account credits openai --all [--json]` diff --git a/src/cli/account-policy-dto.ts b/src/cli/account-policy-dto.ts index 6f42734d2e1..c816d64ef9f 100644 --- a/src/cli/account-policy-dto.ts +++ b/src/cli/account-policy-dto.ts @@ -8,6 +8,7 @@ export const threshold = z.number().int().min(0).max(100); const nullableThreshold = threshold.nullable(); const row = z.object({ id: accountId, alias: text.optional(), autoSwitchThresholdOverride: nullableThreshold.optional(), + useRemainingQuota: z.boolean().optional(), quota: z.object({ shortWindowSeconds: z.number().finite().optional(), shortResetAt: z.number().finite().optional(), weeklyResetAt: z.number().finite().optional() }).nullable().optional(), quotaAutoRefresh: z.object({ fiveHourAvailable: z.boolean(), weeklyAvailable: z.boolean(), @@ -39,6 +40,11 @@ export const poolSchema = z.object({ export const thresholdReceipt = z.object({ ok: z.literal(true), id: accountId, autoSwitchThresholdOverride: nullableThreshold, autoSwitchThreshold: threshold }); export const creditReceipt = z.object({ ok: z.literal(true), id: accountId, creditsAfterLimit: z.boolean() }); +export const remainingQuotaReceipt = z.object({ ok: z.literal(true), id: accountId, + useRemainingQuota: z.boolean(), autoSwitchThreshold: threshold, + mainAccountHardLock: z.object({ enabled: z.boolean(), state: z.enum(["off", "unknown", "ready", "blocked"]), + thresholds: z.object({ short: threshold, long: threshold }) }).optional(), +}); export const creditAllReceipt = z.object({ ok: z.literal(true), all: z.boolean(), ids: z.array(accountId).refine(ids => new Set(ids).size === ids.length) }); const window = z.enum(["five_hour", "seven_day", "seven_day_overage_included"]); diff --git a/src/cli/account-policy.ts b/src/cli/account-policy.ts index 4ab90f0d3fb..96755cb3e63 100644 --- a/src/cli/account-policy.ts +++ b/src/cli/account-policy.ts @@ -4,14 +4,15 @@ import { resolveCodexAccountTargetFromRows } from "./account-target"; import { runCatalogAction } from "./catalog-command-result"; import { serializeManagementJson } from "./json-input"; import { creditAllReceipt, creditReceipt, grantsSchema, parseDto, poolSchema, record, - rosterSchema, threshold, thresholdReceipt } from "./account-policy-dto"; + rosterSchema, threshold, thresholdReceipt, remainingQuotaReceipt } from "./account-policy-dto"; import { CliUsageError, RuntimeApiError, printData, runtimeBaseUrl, runtimeRequest, summaryLines, takeFlag, takeOptionWithSyntax, type RuntimeApiDeps } from "./runtime-api"; -type Subcommand = "pool" | "auto-switch" | "credits" | "quota-activation" | "anthropic-reset-grants"; +type Subcommand = "pool" | "auto-switch" | "credits" | "quota-activation" | "anthropic-reset-grants" | "use-remaining"; const USAGE = "Usage: ocx account pool [--enabled on|off] [--threshold 0-100] [--strategy NAME] [--sticky 1-100] [--quota-window NAME] [--json]\n" + " ocx account auto-switch openai --account ID [--json]\n" + " ocx account credits openai [--json]\n" + + " ocx account use-remaining openai ID [--json]\n" + " ocx account quota-activation openai ID --window [--json]\n" + " ocx account anthropic-reset-grants [ID] [--json]"; const READ_ERRORS = { no_account: "No matching Anthropic OAuth account.", auth_failed: "Sign in to this Anthropic account again.", @@ -52,6 +53,32 @@ function output(data: Record, json: boolean, note?: string): nu } function requireEqual(actual: unknown, expected: unknown): void { if (actual !== expected) throw new Error("Unverified account policy result"); } +async function useRemaining(args: string[], json: boolean, deps: RuntimeApiDeps): Promise { + if (args.shift() !== "openai") usage(); + const requested = selector(args.shift()), action = args.shift(); + if (!["on", "off", "status"].includes(action ?? "")) usage(); + done(args); + const pinned = { ...deps, baseUrl: await runtimeBaseUrl(deps) }; + const { id } = await target(pinned, requested); + const path = "/api/codex-auth/accounts/use-remaining"; + const result = parseDto(remainingQuotaReceipt, await request( + action === "status" ? path + "?id=" + encodeURIComponent(id) : path, + pinned, action === "status" ? undefined : { id, useRemainingQuota: action === "on" }, + )); + requireEqual(result.id, id); + if (action !== "status") requireEqual(result.useRemainingQuota, action === "on"); + if (result.useRemainingQuota) { + requireEqual(result.autoSwitchThreshold, 0); + if (id === MAIN_CODEX_ACCOUNT_ID) { + requireEqual(result.mainAccountHardLock?.thresholds.short, 100); + requireEqual(result.mainAccountHardLock?.thresholds.long, 100); + } + } + return output(result, json, result.useRemainingQuota + ? "Use remaining included quota: on. Paid-credit permission is unchanged." + : "Use remaining included quota: off. Configured usage thresholds apply."); +} + async function pool(args: string[], json: boolean, deps: RuntimeApiDeps): Promise { const provider = selector(args.shift()).toLowerCase(); if (!isValidProviderName(provider)) usage(); @@ -121,12 +148,12 @@ async function codex(sub: "auto-switch" | "credits" | "quota-activation", args: const override = selected?.autoSwitchThresholdOverride; if (override === undefined) throw new Error("Missing account threshold evidence"); const active = record(await request("/api/codex-auth/active", pinned)); - const effective = override ?? parseDto(threshold, active.autoSwitchThreshold); + const effective = selected?.useRemainingQuota ? 0 : override ?? parseDto(threshold, active.autoSwitchThreshold); return output({ ok: true, id, autoSwitchThresholdOverride: override, autoSwitchThreshold: effective }, json); } const result = parseDto(thresholdReceipt, await request("/api/codex-auth/auto-switch", pinned, { id, threshold: value })); requireEqual(result.id, id); requireEqual(result.autoSwitchThresholdOverride, value); - if (value !== null) requireEqual(result.autoSwitchThreshold, value); + if (value !== null && !rows.find(row => row.id === id)?.useRemainingQuota) requireEqual(result.autoSwitchThreshold, value); return output(result, json, "Account threshold saved."); } if (sub === "credits") { @@ -156,6 +183,7 @@ export async function handleAccountPolicyCommand(sub: Subcommand, args: string[] const rest = [...args], json = takeFlag(rest, "--json"); if (rest.includes("--json")) usage(); if (sub === "pool") return pool(rest, json, deps); + if (sub === "use-remaining") return useRemaining(rest, json, deps); if (sub !== "anthropic-reset-grants") return codex(sub, rest, json, deps); const id = rest.length ? selector(rest.shift()) : undefined; done(rest); diff --git a/src/cli/account.ts b/src/cli/account.ts index 98973d029a2..ddf5cc061cd 100644 --- a/src/cli/account.ts +++ b/src/cli/account.ts @@ -69,6 +69,7 @@ const ACCOUNT_USAGE = `Usage: ocx account login [--id ] [--reauth] [--open-browser on|off] [--add-account on|off] [--code -] [--no-wait] [--json] ocx account pool [--enabled on|off] [--threshold N] [--strategy NAME] [--sticky N] [--quota-window W] [--json] ocx account credits openai [--json] + ocx account use-remaining openai [--json] ocx account quota-activation openai ID --window fiveHour|weekly [--json] ocx account anthropic-reset-grants [ID] [--json] ocx account code [--flow ] [--json] (reads the code from stdin) @@ -423,11 +424,11 @@ async function cmdClear(rest: string[], deps: AccountDeps): Promise { export async function cmdAccount(args: string[], deps: AccountDeps = {}): Promise { const [sub, ...rest] = args; try { - if (["pool", "credits", "quota-activation", "anthropic-reset-grants"].includes(sub ?? "") + if (["pool", "credits", "quota-activation", "anthropic-reset-grants", "use-remaining"].includes(sub ?? "") || (sub === "auto-switch" && rest[0]?.trim().toLowerCase() === "openai" && rest.some(arg => arg === "--account" || arg.startsWith("--account=")))) { const { handleAccountPolicyCommand } = await import("./account-policy"); - return handleAccountPolicyCommand(sub as "pool" | "auto-switch" | "credits" | "quota-activation" | "anthropic-reset-grants", rest, deps); + return handleAccountPolicyCommand(sub as "pool" | "auto-switch" | "credits" | "quota-activation" | "anthropic-reset-grants" | "use-remaining", rest, deps); } if (sub === "list") return await cmdList(rest, deps); if (sub === "history") { diff --git a/src/cli/capabilities-accounts.ts b/src/cli/capabilities-accounts.ts index f405653c54e..c0f90499e38 100644 --- a/src/cli/capabilities-accounts.ts +++ b/src/cli/capabilities-accounts.ts @@ -162,6 +162,17 @@ export const ACCOUNT_CAPABILITIES: readonly Capability[] = [ json: "envelope", details: ["Reads the target supported-field list before a narrow PUT. Null/unsupported, stored/effective and generic inert policy stay distinct; disabling does not promise that reactive429 rotation stops.","No local classification/defaults are round-tripped into the live target; supported-field reads are not CAS."], }, + { + command: ["account", "use-remaining"], + usage: "ocx account use-remaining openai [--json]", + summary: "Choose whether one Codex account keeps using its remaining included quota.", + routes: [{ method: "GET", path: "/api/codex-auth/accounts" }, + { method: "GET", path: "/api/codex-auth/accounts/use-remaining" }, { method: "PUT", path: "/api/codex-auth/accounts/use-remaining" }], + flags: [{ name: "--json", value: "boolean", summary: "Emit the validated account policy." }], + mutates: true, json: "envelope", + details: ["On disables proactive usage switching for the selected account and raises main-account protection to 100%. Off restores the configured thresholds.", + "This is a persistent preference, not account selection. Pause, reauthentication, cooldown, entitlement and paid-credit policy still apply."], + }, { command: ["account","credits"], usage: "ocx account credits openai [--json]; ocx account credits openai --all [--json]", diff --git a/src/cli/registry.ts b/src/cli/registry.ts index 93d4f244b5b..28d69cd1654 100644 --- a/src/cli/registry.ts +++ b/src/cli/registry.ts @@ -329,6 +329,7 @@ export const CLI_COMMANDS: CliCommandEntry[] = [ "add-key [--label