Skip to content
Closed
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
17 changes: 17 additions & 0 deletions docs-site/src/content/docs/reference/cli/providers-accounts.md
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,23 @@ rotate the request to another eligible Pool account. These failure transitions r

Clear the manual Codex account selection without resolving an account id, so it works even when an account is literally named `auto`. Codex pools only; other provider types have no automatic selection to restore.

### `ocx account pause|resume <provider> <id|alias|main> [--json]`

Pause or resume one account in the Codex pool or a generic OAuth provider pool, including
`google-antigravity`. For the Codex pool, `main` identifies only the built-in Codex account;
generic OAuth accounts must be identified by id or a unique alias. A paused generic OAuth account
is excluded from request selection, 429 failover, and proactive token refresh, and cannot be
selected manually. Pausing the active account switches to the next usable account when one exists.
If every account is paused, requests that need that pool return 403 until an account is resumed.

For a generic OAuth provider, identify the account by id or by a unique exact or case-insensitive
alias. The JSON response reports the account id, pause state, and active account id.

```bash
ocx account pause google-antigravity <account-id-or-alias>
ocx account resume google-antigravity <account-id-or-alias>
```

### `ocx account refresh <provider> [--json]`

For the Codex pool, use `ocx account refresh openai [--json]`. It force-refreshes account quotas and
Expand Down
6 changes: 4 additions & 2 deletions docs-site/src/content/docs/reference/management-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -516,8 +516,10 @@ outcome fields from an older server do not establish successful recovery.
| `POST /api/oauth/login/cancel` | Cancel a public in-progress OAuth flow | 400 unknown provider |
| `GET /api/oauth/status` | Poll one provider's OAuth flow | 400 unknown provider |
| `POST /api/oauth/logout` | Remove the selected provider credential | 400 unknown provider; `oauth_mutation_busy` |
| `GET, DELETE /api/oauth/accounts` | List masked accounts or remove one account. Kiro rows include `autoSelectable` and a closed `skipReason` when excluded from automatic selection; an active singleton may still send. Quota remains opt-in. | 400 invalid provider/id; 404 account missing; `oauth_mutation_busy` |
| `PUT /api/oauth/accounts/active` | Select the active OAuth account | 400 invalid provider/account; `oauth_mutation_busy` |
| `GET /api/oauth/accounts` | List masked accounts; generic OAuth account rows include their `paused` state. Kiro rows include `autoSelectable` and a closed `skipReason` when excluded from automatic selection; an active singleton may still send. Quota remains opt-in. | 400 invalid provider |
| `DELETE /api/oauth/accounts` | Remove one account | 400 invalid provider/id; 404 account missing; `oauth_mutation_busy` |
| `PUT /api/oauth/accounts/active` | Select the active OAuth account | 400 invalid provider/account; 404 account missing; 409 account paused; `oauth_mutation_busy` |
| `PUT /api/oauth/accounts/pause` | Pause or resume one generic OAuth account. Body `{ provider, accountId, paused }`; pausing the active account selects the next usable account when available | 400 unsupported provider or invalid body; 404 account missing; `oauth_mutation_busy` |
| `GET, PUT, PATCH /api/pool/settings` | Read or update pool policy for any kind (codex, anthropic, generic); answers with the same keys for all three and declares in `supported` which the kind honours | 400 unknown provider, a field the kind does not support, or an invalid value |
| `GET, PUT, PATCH /api/oauth/accounts/pool` | Legacy per-pool policy for Anthropic and generic OAuth providers; superseded by `/api/pool/settings` and kept for existing clients | 400 codex or api-key provider, or invalid policy |
| `POST /api/oauth/accounts/clear-cooldown` | Clear one OAuth account's runtime cooldown | 400 invalid provider/account |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,22 @@ Codex 池選擇套用於清除既有親和性後的下一個請求;進行中

不解析帳號 id 即清除 Codex 帳號的手動選擇,即使存在名為 `auto` 的帳號仍有效。僅適用於 Codex 池;其他提供者類型沒有可還原的自動選擇。

### `ocx account pause|resume <provider> <id|alias|main> [--json]`

暫停或恢復 Codex 帳號池或通用 OAuth 供應商池中的單一帳號,包括
`google-antigravity`。在 Codex 池中,`main` 僅代表 Codex 內建帳號;通用 OAuth 帳號必須用 id 或唯一別名識別。
已暫停的通用 OAuth 帳號不會參與請求選帳、429 輪替或主動 Token 刷新,也不能手動選取。
若暫停目前使用中的帳號,系統會在有其他可用帳號時切換過去。若全部帳號都已暫停,
需要該池的請求會回覆 403,直到恢復其中一個帳號。

通用 OAuth 供應商可用帳號 id,或唯一且完全相符/不區分大小寫的別名識別帳號。
JSON 回應會提供帳號 id、暫停狀態與目前 active 帳號 id。

```bash
ocx account pause google-antigravity <account-id-or-alias>
ocx account resume google-antigravity <account-id-or-alias>
```

### `ocx account refresh <provider> [--json]`

對於 Codex 池,請使用 `ocx account refresh openai [--json]`。它強制重新整理帳號配額並印出可用的週/月百分比與重置時間;缺失的配額資料被回報為未知,而非 0%。其 JSON 封裝為 `{ accounts: AccountRow[] }`,每個 Codex 列上有 `quota`。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,10 @@ Aside 設定檔的變更在這種情況下仍會儲存一件事:確認之後
| `POST /api/oauth/login/cancel` | 取消公開進行中的 OAuth 流程 | 400 未知供應商 |
| `GET /api/oauth/status` | 輪詢一個供應商的 OAuth 流程 | 400 未知供應商 |
| `POST /api/oauth/logout` | 移除所選的供應商憑證 | 400 未知供應商;`oauth_mutation_busy` |
| `GET, DELETE /api/oauth/accounts` | 列出遮罩帳號或移除一個帳號 Kiro 列包含自動選取狀態 `autoSelectable`,排除時還包含封閉集合的 `skipReason`。唯一的有效帳號仍可傳送請求,配額查詢仍為選用。 | 400 無效供應商/id;404 帳號缺失;`oauth_mutation_busy` |
| `PUT /api/oauth/accounts/active` | 選擇現用 OAuth 帳號 | 400 無效供應商/帳號;`oauth_mutation_busy` |
| `GET /api/oauth/accounts` | 列出遮罩帳號;通用 OAuth 帳號列也會提供 `paused` 狀態。Kiro 列包含自動選取狀態 `autoSelectable`,排除時還包含封閉集合的 `skipReason`。唯一的有效帳號仍可傳送請求,配額查詢仍為選用。 | 400 無效供應商 |
| `DELETE /api/oauth/accounts` | 移除一個帳號 | 400 無效供應商/id;404 帳號缺失;`oauth_mutation_busy` |
| `PUT /api/oauth/accounts/active` | 選擇現用 OAuth 帳號 | 400 無效供應商/帳號;404 帳號缺失;409 帳號已暫停;`oauth_mutation_busy` |
| `PUT /api/oauth/accounts/pause` | 暫停或恢復一個通用 OAuth 帳號。Body `{ provider, accountId, paused }`;若暫停現用帳號,且有可用帳號,會切換至下一個 | 400 不支援的供應商或無效 body;404 帳號缺失;`oauth_mutation_busy` |
| `GET, PUT, PATCH /api/oauth/accounts/pool` | 讀取或更新 Anthropic OAuth 池政策 | 400 非 Anthropic 供應商或無效政策 |
| `POST /api/oauth/accounts/clear-cooldown` | 清除一個 OAuth 帳號的 runtime 冷卻 | 400 無效供應商/帳號 |
| `PUT /api/oauth/accounts/alias` | 設定或清除 OAuth 帳號別名 | 400 無效供應商/帳號/別名 |
Expand Down
33 changes: 26 additions & 7 deletions gui/src/components/provider-workspace/ProviderAuthPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ function safeCockpitImportResult(value: unknown): CockpitImportResult | null {

export default function ProviderAuthPanel({
item, apiBase, oauth, accounts = EMPTY_OAUTH_ACCOUNTS, keys = EMPTY_API_KEYS, accountLoadState = "ready",
switchingAccountId = null, busy = false, loginHint, authHandlers, onCodexActiveNeedsReauthChange,
switchingAccountId = null, pausingAccountId = null, busy = false, loginHint, authHandlers, onCodexActiveNeedsReauthChange,
codexController, onUpdateProvider,
}: {
item: WorkspaceItem;
Expand All @@ -189,6 +189,7 @@ export default function ProviderAuthPanel({
keys?: ApiKeyRow[];
accountLoadState?: AccountLoadState;
switchingAccountId?: string | null;
pausingAccountId?: string | null;
busy?: boolean;
loginHint?: LoginHint | null;
authHandlers?: ProviderAuthHandlers;
Expand Down Expand Up @@ -536,6 +537,7 @@ export default function ProviderAuthPanel({
{accounts.map(account => {
const label = oauthAccountDisplayLabel(accounts, account, t);
const switching = switchingAccountId === account.id;
const pausing = pausingAccountId === account.id;
const healthStatus = account.health?.status;
const showReauth = accountShowsReauth(account);
const inCooldown = oauthHealthIsCooldown(healthStatus);
Expand All @@ -546,11 +548,11 @@ export default function ProviderAuthPanel({
<li key={account.id} className={`pwi-auth-acct${account.active ? " pwi-auth-acct--active" : ""}`}>
<div className={`pwi-auth-row${account.active ? " pwi-auth-row--active" : ""}`}>
<button type="button" className="pwi-auth-row-main"
onClick={() => { if (!account.active && !showReauth && !inCooldown && !switchingAccountId) void authHandlers.onSwitchAccount(item.name, account); }}
onClick={() => { if (!account.active && !account.paused && !showReauth && !inCooldown && !switchingAccountId && !pausingAccountId) void authHandlers.onSwitchAccount(item.name, account); }}
aria-current={account.active ? "true" : undefined}
aria-label={`${label}${account.active ? ` — ${t("pws.accountCurrent")}` : ""}`}
disabled={Boolean(showReauth || inCooldown || (switchingAccountId && !switching))}>
<span className={`pwi-auth-dot ${showReauth ? "pwi-auth-dot--warn" : account.active ? "pwi-auth-dot--ok" : "pwi-auth-dot--off"}`} aria-hidden="true" />
disabled={Boolean(account.paused || showReauth || inCooldown || switchingAccountId || pausingAccountId)}>
<span className={`pwi-auth-dot ${showReauth ? "pwi-auth-dot--warn" : account.active && !account.paused ? "pwi-auth-dot--ok" : "pwi-auth-dot--off"}`} aria-hidden="true" />
<span className="pwi-auth-row-copy">
<span className="pwi-auth-row-label">{label}</span>
<span className="pwi-auth-row-secondary">{[account.email, `${t("prov.accountId")}: ${maskedId}`].filter(Boolean).join(" · ")}</span>
Expand All @@ -560,20 +562,37 @@ export default function ProviderAuthPanel({
{inCooldown && (
<span className="pwi-auth-row-secondary faint">{t("pws.healthCooldownHint")}</span>
)}
{account.paused && (
<span className="pwi-auth-row-secondary faint">{t("pws.accountPausedHint")}</span>
)}
</span>
{healthLabel && (
<span className={oauthHealthBadgeClass(healthStatus)}>{healthLabel}</span>
)}
{showReauth && !healthLabel && <span className="badge badge-amber">{t("pws.reauth")}</span>}
{kiroSkipReasonKey(account, item.name) && <span className="badge badge-amber">{t(kiroSkipReasonKey(account, item.name)!)}</span>}
{account.active && <span className="badge badge-primary">{t("prov.accountActive")}</span>}
{account.paused && <span className="badge badge-muted">{t("codexAuth.paused")}</span>}
{account.active && !account.paused && <span className="badge badge-primary">{t("prov.accountActive")}</span>}
{switching && <span className="badge badge-muted">{t("pws.accountSwitching")}</span>}
</button>
{typeof account.paused === "boolean" && authHandlers.onPauseAccount && (
<button
type="button"
className="btn btn-ghost btn-sm"
aria-label={`${t(account.paused ? "codexAuth.resume" : "codexAuth.pause")} — ${label}`}
title={account.paused ? t("pws.accountPausedHint") : undefined}
aria-busy={pausing}
disabled={busy || Boolean(switchingAccountId) || Boolean(pausingAccountId)}
onClick={() => void authHandlers.onPauseAccount(item.name, account, !account.paused)}
>
{t(account.paused ? "codexAuth.resume" : "codexAuth.pause")}
</button>
)}
{showReauth && (
<button
type="button"
className="btn btn-ghost btn-sm"
disabled={busy || Boolean(switchingAccountId)}
disabled={busy || Boolean(switchingAccountId) || Boolean(pausingAccountId)}
onClick={() => void authHandlers.onReauth(item.name, account.id)}
>
{t("pws.reauthenticate")}
Expand All @@ -596,7 +615,7 @@ export default function ProviderAuthPanel({
<button type="button" className="btn btn-ghost btn-sm pwi-auth-row-remove"
aria-label={`${t("common.remove")} — ${label}`}
title={`${t("common.remove")} — ${label}`}
disabled={Boolean(switchingAccountId)}
disabled={Boolean(switchingAccountId) || Boolean(pausingAccountId)}
onClick={() => void authHandlers.onRemoveAccount(item.name, account)}>
<IconTrash style={{ width: 13, height: 13 }} aria-hidden="true" />
</button>
Expand Down
3 changes: 3 additions & 0 deletions gui/src/components/provider-workspace/ProviderDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export default function ProviderDetails({
settingsFocusToken = 0,
settingsFocusProvider = null,
switchingAccountId,
pausingAccountId,
keys,
busyProvider,
loginHint,
Expand Down Expand Up @@ -98,6 +99,7 @@ export default function ProviderDetails({
settingsFocusToken?: number;
settingsFocusProvider?: string | null;
switchingAccountId?: string | null;
pausingAccountId?: string | null;
keys?: ApiKeyRow[];
busyProvider?: string | null;
loginHint?: LoginHint | null;
Expand Down Expand Up @@ -357,6 +359,7 @@ export default function ProviderDetails({
keys={keys}
accountLoadState={accountLoadState}
switchingAccountId={switchingAccountId}
pausingAccountId={pausingAccountId}
busy={busyProvider === item.name}
loginHint={loginHint}
authHandlers={authHandlers}
Expand Down
4 changes: 3 additions & 1 deletion gui/src/components/provider-workspace/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ export type OAuthAccountRow = AccountQuotaReading & {
active: boolean;
needsReauth?: boolean;
autoSelectable?: boolean;
skipReason?: "needs_reauth" | "suspended" | "cooldown" | "quota_exhausted";
skipReason?: "needs_reauth" | "paused" | "suspended" | "cooldown" | "quota_exhausted";
paused?: boolean;
health?: { status: OAuthAccountHealthStatus; reason?: string; until?: string };
healthLabel?: string;
healthSummary?: string;
Expand Down Expand Up @@ -89,6 +90,7 @@ export interface ProviderAuthHandlers {
onLogout: (provider: string) => void | Promise<void>;
onReauth: (provider: string, accountId?: string) => void | Promise<void>;
onSwitchAccount: (provider: string, account: OAuthAccountRow) => void | Promise<void>;
onPauseAccount: (provider: string, account: OAuthAccountRow, paused: boolean) => void | Promise<void>;
onRemoveAccount: (provider: string, account: OAuthAccountRow) => void | Promise<void>;
onRetryAccounts?: (provider: string) => void | Promise<void>;
onAddApiKey: (provider: string, key: string) => Promise<boolean>;
Expand Down
Loading
Loading