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 f4ba4e6f112..ba5e4187e0d 100644 --- a/docs-site/src/content/docs/reference/cli/providers-accounts.md +++ b/docs-site/src/content/docs/reference/cli/providers-accounts.md @@ -795,7 +795,9 @@ Clear the manual Codex account selection without resolving an account id, so it ### `ocx account pause|resume [--json]` -Pause or resume one account in the Codex, Anthropic, or generic OAuth provider pool, including +Accounts paused by the opt-in [per-provider auto-pause policy](/reference/configuration/providers/#auto-pause-permanently-refused-accounts) use the same commands. Listing shows the fixed pause reason; resume clears it. + +Pause or resume one account in the Codex, Anthropic, generic OAuth, or API-key provider pool, including `google-antigravity`. For the Codex pool, `main` identifies only the built-in Codex account; OAuth accounts must be identified by id or a unique alias. A paused OAuth account is excluded from request selection, 429 failover, and proactive token refresh, and cannot be diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index 12ccbf51df1..3752356b52e 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -1904,3 +1904,42 @@ original reasoning. Other model families require the preserve declaration. Tool-enabled DeepSeek requests also require reasoning on earlier assistant answers without tool calls. The configured placeholder fills these omissions; no original reasoning can be recovered from the tool-call cache for those turns. Preserve their supplied reasoning for full fidelity. + +## Auto-pause permanently refused accounts + +`providers..accountAutoPause` is opt-in and defaults to off. It applies to that +provider's OAuth accounts, Codex pool accounts (including the main account in pool mode), +and configured API-key pool entries. For example: + +```sh +ocx config set providers.anthropic.accountAutoPause.enabled true +ocx config set providers.anthropic.accountAutoPause.consecutiveFailures 2 +``` + +`consecutiveFailures` is an integer from 1 to 100; omitting it uses 2. Only exact, +account-scoped HTTP 401/403 refusals qualify: an organization forbidding OAuth, an inactive +or expired subscription, an account denied API/Claude Code access, or a downgrade to a free +plan with no credits. A model permission error, expired access token, quota window, 429, +5xx, malformed response or network failure never pauses an account. + +The counter is process-local, serialized per account, and resets on a successful HTTP +response, explicit resume or a new login. Token refresh alone does not reset an OAuth +account's streak. Transient errors leave the counter unchanged. Restarting the proxy clears +counters; completed pauses, their fixed reason codes and timestamps persist. Independent +proxy processes maintain independent counters. Changing, disabling or re-enabling the policy, disabling the provider, +or changing its endpoint discards in-flight refusal observations, so a refusal from a request +sent under the earlier policy can never pause an account afterwards. A refusal counts only for the credential that actually +reached the upstream: when a local plugin rewrites the credential or destination of a send, that +send never counts toward a pause. For Codex pool accounts this includes the workspace +(`chatgpt-account-id`) the request was sent for. + +At the threshold, OpenCodex excludes the account using its existing pause mechanism and +attempts an eligible sibling for the current request within the existing retry/send budget. +Fixed Codex account selectors and account-bound uploaded files retain their restrictions. +A pause never cancels already dispatched work or changes credentials. It emits one generic +log line without account identifiers or upstream error text. + +`ocx account list ` shows the pause reason. The dashboard's Paused badge exposes +the reason and time. Use `ocx account resume ` to clear the +reason and allow selection again. On an API-key row, clicking Resume also activates the key. +This policy never resumes an account automatically. diff --git a/gui/src/account-pause-display.ts b/gui/src/account-pause-display.ts new file mode 100644 index 00000000000..3a656bfe89d --- /dev/null +++ b/gui/src/account-pause-display.ts @@ -0,0 +1,20 @@ +import type { AccountPauseReason } from "../../src/types/account-auto-pause"; +import type { Locale, TFn, TKey } from "./i18n/shared"; + +const REASON_KEYS = { + oauth_organization_disabled: "accountPause.reason.organizationDisabled", + account_permission_denied: "accountPause.reason.permissionDenied", + subscription_inactive: "accountPause.reason.subscriptionInactive", + subscription_expired: "accountPause.reason.subscriptionExpired", + plan_downgraded: "accountPause.reason.planDowngraded", +} satisfies Record; + +/** Display fixed reason labels and timestamps in the selected dashboard language. */ +export function formatAccountPauseTooltip( + t: TFn, locale: Locale, details: { reason: string; pausedAt: number }, +): string { + const reason = Object.hasOwn(REASON_KEYS, details.reason) + ? t(REASON_KEYS[details.reason as AccountPauseReason]) : t("codexAuth.paused"); + const date = new Date(details.pausedAt); + return Number.isNaN(date.getTime()) ? reason : t("accountPause.tooltip", { reason, date: date.toLocaleString(locale) }); +} diff --git a/gui/src/components/codex-account-pool-cards.tsx b/gui/src/components/codex-account-pool-cards.tsx index e607afbf1bf..d6d015dff1b 100644 --- a/gui/src/components/codex-account-pool-cards.tsx +++ b/gui/src/components/codex-account-pool-cards.tsx @@ -1,4 +1,5 @@ import { useState } from "react"; +import { formatAccountPauseTooltip } from "../account-pause-display"; import { useT, useI18n } from "../i18n/shared"; import { useCopyFeedback } from "./use-copy-feedback"; import { IconAlert, IconPause, IconPlay, IconX } from "../icons"; @@ -118,7 +119,7 @@ export function CodexAccountPoolCards({ )} {a.paused && ( - + {t("codexAuth.paused")} )} diff --git a/gui/src/components/provider-workspace/ProviderAuthPanel.tsx b/gui/src/components/provider-workspace/ProviderAuthPanel.tsx index e21f627d5e5..d6f6b0c833c 100644 --- a/gui/src/components/provider-workspace/ProviderAuthPanel.tsx +++ b/gui/src/components/provider-workspace/ProviderAuthPanel.tsx @@ -5,7 +5,8 @@ import { anthropicInstanceRowShapeMatches, isAnthropicInstanceId } from "../../. * handlers via props-down; no internal auth machinery. */ import { useEffect, useMemo, useRef, useState } from "react"; -import { useT } from "../../i18n/shared"; +import { useT, useI18n } from "../../i18n/shared"; +import { formatAccountPauseTooltip } from "../../account-pause-display"; import { IconLock, IconRefresh, IconTrash } from "../../icons"; import type { WorkspaceItem } from "../../provider-workspace/catalog"; import { oauthAccountDisplayLabel, providerAuthSurface } from "../../provider-workspace/auth"; @@ -201,6 +202,7 @@ export default function ProviderAuthPanel({ codexController?: CodexAccountPoolController; }) { const t = useT(); + const { locale } = useI18n(); const [addingKey, setAddingKey] = useState(false); const [kiroChooser, setKiroChooser] = useState<{ addAccount: boolean } | null>(null); const kiroLoginTriggerRef = useRef(null); @@ -589,7 +591,7 @@ export default function ProviderAuthPanel({ )} {showReauth && !healthLabel && {t("pws.reauth")}} {kiroSkipReasonKey(account, item.name) && {t(kiroSkipReasonKey(account, item.name)!)}} - {account.paused && {t("codexAuth.paused")}} + {account.paused && {t("codexAuth.paused")}} {account.active && !account.paused && {t("prov.accountActive")}} {switching && {t("pws.accountSwitching")}} @@ -722,13 +724,14 @@ export default function ProviderAuthPanel({