@bitkyc08/opencodex 2.76.0 → 2.77.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/gui/dist/assets/App-COdEeqSj.css +1 -0
  2. package/gui/dist/assets/App-DEpCJEP_.js +54 -0
  3. package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-gloIYNaW.js} +1 -1
  4. package/gui/dist/assets/index-Cd7ittMv.css +1 -0
  5. package/gui/dist/assets/index-DwJKFJm5.js +94 -0
  6. package/gui/dist/assets/quota-summary-DGcEJU3h.js +1 -0
  7. package/gui/dist/index.html +4 -3
  8. package/package.json +1 -1
  9. package/src/adapters/antigravity-refusal.ts +28 -0
  10. package/src/adapters/cursor/protobuf-request.ts +6 -0
  11. package/src/adapters/cursor/request-builder.ts +3 -0
  12. package/src/adapters/cursor/structured-output.ts +13 -0
  13. package/src/adapters/cursor/types.ts +2 -0
  14. package/src/adapters/ollama-native.ts +29 -2
  15. package/src/adapters/openai-responses/passthrough.ts +133 -103
  16. package/src/adapters/openai-responses/request-strips.ts +10 -4
  17. package/src/adapters/zed.ts +26 -4
  18. package/src/bridge/response-json.ts +14 -14
  19. package/src/bridge/sse.ts +15 -5
  20. package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
  21. package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
  22. package/src/claude/agents-inject.ts +4 -43
  23. package/src/claude/gateway-cache.ts +16 -13
  24. package/src/claude/inbound-model-options.ts +9 -2
  25. package/src/claude/intercept/cli-catalog.ts +27 -16
  26. package/src/claude/intercept/picker-bootstrap.ts +37 -16
  27. package/src/claude/intercept/picker-budget.ts +41 -0
  28. package/src/claude/outbound.ts +2 -0
  29. package/src/claude/subagent-force-status.ts +53 -0
  30. package/src/claude/subagent-model.ts +93 -0
  31. package/src/cli/account-api.ts +3 -0
  32. package/src/cli/account.ts +3 -1
  33. package/src/cli/agent.ts +10 -0
  34. package/src/cli/capabilities.ts +9 -0
  35. package/src/cli/chatgpt-command.ts +10 -7
  36. package/src/cli/claude.ts +22 -6
  37. package/src/cli/codex-shim-readiness.ts +89 -0
  38. package/src/cli/connect.ts +19 -0
  39. package/src/cli/dispatch.ts +3 -3
  40. package/src/cli/export-command.ts +20 -9
  41. package/src/cli/gui.ts +8 -3
  42. package/src/cli/help-catalog.ts +84 -0
  43. package/src/cli/help-models-context.ts +17 -0
  44. package/src/cli/help-navigation.ts +50 -0
  45. package/src/cli/help-recovery.ts +66 -0
  46. package/src/cli/help.ts +52 -8
  47. package/src/cli/models-runtime.ts +2 -1
  48. package/src/cli/opencode.ts +54 -10
  49. package/src/cli/provider.ts +3 -29
  50. package/src/cli/registry.ts +29 -5
  51. package/src/cli/root.ts +30 -9
  52. package/src/cli/runtime-api.ts +1 -1
  53. package/src/cli/tray-proxy.ts +3 -2
  54. package/src/clients/config-export/contracts.ts +100 -0
  55. package/src/clients/config-export/droid.ts +54 -2
  56. package/src/clients/config-export/kilo.ts +12 -2
  57. package/src/clients/config-export/model-metadata.ts +10 -4
  58. package/src/clients/config-export/reasoning-metadata.ts +47 -0
  59. package/src/clients/config-export.ts +46 -66
  60. package/src/clients/omo-role-models.ts +17 -3
  61. package/src/codex/account-credit-use.ts +4 -2
  62. package/src/codex/account-runtime-state.ts +21 -0
  63. package/src/codex/account-store.ts +11 -47
  64. package/src/codex/account-usability.ts +11 -8
  65. package/src/codex/auth-api/account-list.ts +73 -63
  66. package/src/codex/auth-api/main-account-probe.ts +25 -12
  67. package/src/codex/auth-api/pool-mode-gate.ts +6 -9
  68. package/src/codex/auth-api/pool-quota-probe.ts +2 -4
  69. package/src/codex/auth-api/reset-credit-service.ts +3 -2
  70. package/src/codex/auth-context.ts +30 -11
  71. package/src/codex/catalog/aggregation.ts +3 -0
  72. package/src/codex/catalog/model-hints.ts +2 -1
  73. package/src/codex/catalog/model-visibility.ts +6 -3
  74. package/src/codex/catalog/parsing.ts +2 -0
  75. package/src/codex/catalog/provider-models.ts +13 -4
  76. package/src/codex/catalog/retained-sync.ts +2 -1
  77. package/src/codex/chatgpt-refresh-failure.ts +56 -0
  78. package/src/codex/convergence.ts +2 -1
  79. package/src/codex/main-account.ts +144 -11
  80. package/src/codex/quota-refresh-outcome.ts +17 -2
  81. package/src/codex/quota-types.ts +33 -1
  82. package/src/codex/quota.ts +56 -15
  83. package/src/codex/routing/cooldown-math.ts +10 -4
  84. package/src/codex/routing/selection.ts +11 -4
  85. package/src/codex/routing.ts +9 -9
  86. package/src/codex/shim-path-resolution.ts +133 -0
  87. package/src/codex/shim.ts +39 -39
  88. package/src/codex/subagent-model-fallback.ts +2 -1
  89. package/src/combos/failover.ts +82 -1
  90. package/src/combos/jev.ts +13 -3
  91. package/src/config/diagnostics.ts +4 -0
  92. package/src/config/load-degrade.ts +4 -0
  93. package/src/config/provider-validation.ts +3 -0
  94. package/src/config/schema/compaction-triggers.ts +8 -0
  95. package/src/config/schema/config-schema.ts +11 -0
  96. package/src/config/schema/leaf-validators.ts +5 -1
  97. package/src/config/subagent-models.ts +12 -0
  98. package/src/generated/compatibility-version.json +245 -153
  99. package/src/grok/session-identity.ts +33 -0
  100. package/src/integrations/catalog-refresh.ts +1 -1
  101. package/src/integrations/mutation-plan.ts +4 -2
  102. package/src/integrations/state.ts +68 -6
  103. package/src/integrations/writer.ts +18 -4
  104. package/src/lib/gui-pair-capability.ts +17 -0
  105. package/src/lib/provider-client-headers.ts +62 -0
  106. package/src/oauth/anthropic-continuity.ts +65 -0
  107. package/src/oauth/anthropic-identity.ts +75 -0
  108. package/src/oauth/anthropic-routing.ts +3 -3
  109. package/src/oauth/anthropic.ts +7 -1
  110. package/src/oauth/generic-account-failover.ts +21 -0
  111. package/src/oauth/health.ts +32 -5
  112. package/src/oauth/index.ts +36 -27
  113. package/src/oauth/local-token-detect.ts +7 -0
  114. package/src/oauth/refresh-policy.ts +2 -0
  115. package/src/oauth/store.ts +55 -6
  116. package/src/oauth/types.ts +9 -0
  117. package/src/protocols/encoders/messages.ts +4 -3
  118. package/src/providers/antigravity-effort-families.ts +75 -0
  119. package/src/providers/antigravity-models.ts +54 -16
  120. package/src/providers/antigravity-wire-snapshot.ts +66 -0
  121. package/src/providers/derive.ts +2 -0
  122. package/src/providers/model-rename-fields.ts +3 -0
  123. package/src/providers/new-model-policy.ts +48 -10
  124. package/src/providers/registry/entries-extended.ts +3 -0
  125. package/src/providers/registry/model-ids.ts +2 -0
  126. package/src/providers/registry/model-seeds.ts +5 -0
  127. package/src/providers/registry/types.ts +4 -0
  128. package/src/responses/compaction.ts +19 -0
  129. package/src/router.ts +6 -0
  130. package/src/server/auth-cors.ts +9 -0
  131. package/src/server/chat-completions.ts +10 -0
  132. package/src/server/claude-messages.ts +18 -11
  133. package/src/server/droid-reasoning-default.ts +50 -0
  134. package/src/server/github-copilot-responses-repair.ts +3 -6
  135. package/src/server/grok-responses-control-frame.ts +2 -2
  136. package/src/server/grok-responses-snapshot-repair.ts +4 -5
  137. package/src/server/gui-session.ts +33 -11
  138. package/src/server/index/claude-intercept-lifecycle.ts +4 -0
  139. package/src/server/index/serve-options.ts +2 -1
  140. package/src/server/management/agent-settings-routes.ts +6 -129
  141. package/src/server/management/config-routes.ts +1 -1
  142. package/src/server/management/integration-routes.ts +64 -6
  143. package/src/server/management/link-routes.ts +20 -13
  144. package/src/server/management/model-row-export-metadata.ts +150 -0
  145. package/src/server/management/model-rows.ts +148 -9
  146. package/src/server/management/route-registry.ts +4 -3
  147. package/src/server/management/shared.ts +19 -5
  148. package/src/server/management/subagent-model-routes.ts +214 -0
  149. package/src/server/relay.ts +14 -44
  150. package/src/server/request-log-generation-window.ts +38 -0
  151. package/src/server/request-log.ts +13 -0
  152. package/src/server/responses/adapter-dispatch.ts +100 -1
  153. package/src/server/responses/codex-auth-error.ts +10 -2
  154. package/src/server/responses/combo-stream-preflight.ts +29 -3
  155. package/src/server/responses/compaction-routing.ts +10 -1
  156. package/src/server/responses/core-auth.ts +57 -26
  157. package/src/server/responses/core-codex-account.ts +6 -3
  158. package/src/server/responses/core-combo-failure.ts +5 -1
  159. package/src/server/responses/core-combo-native.ts +7 -1
  160. package/src/server/responses/core-combo.ts +18 -1
  161. package/src/server/responses/core-normalize.ts +7 -0
  162. package/src/server/responses/core-options.ts +5 -0
  163. package/src/server/responses/fetch-helpers.ts +9 -0
  164. package/src/server/responses/passthrough-delivery.ts +1 -1
  165. package/src/server/responses/policy-fallback.ts +1 -1
  166. package/src/server/responses/request-prepare.ts +5 -0
  167. package/src/server/responses/request-spend.ts +30 -30
  168. package/src/server/responses-custom-tool-repair.ts +2 -2
  169. package/src/server/responses-hosted-image-display.ts +70 -9
  170. package/src/server/responses-terminal-repair.ts +15 -2
  171. package/src/server/sse-frame-buffer.ts +38 -36
  172. package/src/server/sse-payload-rewrite.ts +78 -49
  173. package/src/server/ws-bridge.ts +1 -11
  174. package/src/service/windows-taskxml.ts +1 -2
  175. package/src/service-manager-probe.ts +36 -2
  176. package/src/types/config.ts +4 -0
  177. package/src/types/provider.ts +18 -0
  178. package/src/update/npm-invocation.mjs +35 -3
  179. package/src/update/proxy-liveness-probe.mjs +38 -8
  180. package/src/usage/expected-prices.ts +9 -0
  181. package/src/usage/log.ts +7 -0
  182. package/src/usage/summary.ts +82 -1
  183. package/src/web-search/loop.ts +11 -8
  184. package/src/web-search/run-turn-loop.ts +10 -7
  185. package/gui/dist/assets/App-CddorHdA.js +0 -51
  186. package/gui/dist/assets/App-DMudtISW.css +0 -1
  187. package/gui/dist/assets/index-Btze1I3T.css +0 -1
  188. package/gui/dist/assets/index-CvnY7E6i.js +0 -86
  189. package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
@@ -16,14 +16,15 @@ import { getMainAccountPlan, isMainAccountTokenVerifiablyLive, MAIN_CODEX_ACCOUN
16
16
  import { captureConfigGeneration } from "../../lib/state-store-sweeper";
17
17
  import { captureMainAccountIdentityGeneration, clearMainAccountInfoCache, getMainAccountInfoCache, getMainQuotaCredentialGeneration, isMainAccountIdentityGenerationLive, isMainQuotaWriterLive, matchesMainQuotaCredential, observeMainQuotaCredential, setMainAccountCredentialPresence, setMainAccountInfoCache } from "../main-account-cache";
18
18
  import type { MainQuotaWriter, MainAccountInfo } from "../main-account-cache";
19
- import type { CodexQuotaRefreshOutcome } from "../quota-refresh-outcome";
19
+ import { isCodexTerminalAuthCode } from "../quota-refresh-outcome";
20
+ import type { CodexQuotaRefreshOutcome, CodexTerminalAuthCode } from "../quota-refresh-outcome";
20
21
  import { observeMainReserveRevocation } from "../reserve-availability";
21
22
  import type { AdmissionLease } from "../../lib/admission";
22
23
  import { nonEmptyPlan } from "./runtime-config";
23
24
  import { tryAcquireNativeMainProfileClaim } from "../native-main-admission";
24
25
  import { WHAM_REQUEST_TIMEOUT_MS } from "../quota-recovery-timing";
25
26
  import { withNativeMainCredentialClaim, isNativeMainClaimUnavailable } from "./http";
26
- import { MAIN_TERMINAL_AUTH_CODES, readMainAuthErrorCode, currentQuotaDispatchSequence, nextQuotaDispatchSequence, isQuotaDispatchCurrent, publishQuotaDispatch } from "./pool-quota-probe";
27
+ import { readMainAuthErrorCode, currentQuotaDispatchSequence, nextQuotaDispatchSequence, isQuotaDispatchCurrent, publishQuotaDispatch } from "./pool-quota-probe";
27
28
 
28
29
  /**
29
30
  * Last reset-credit count this process parsed for the main account, tagged with the
@@ -79,14 +80,18 @@ function sharedMainQuotaPacingEnabled(config: OcxConfig): boolean {
79
80
  * itself would make a real 401 permanently transient.
80
81
  */
81
82
  export async function isTerminalMainAuthResponse(resp: Response, accessTokenLive: boolean): Promise<boolean> {
82
- if (resp.status === 401) {
83
- if (!accessTokenLive) return true;
84
- const code = await readMainAuthErrorCode(resp);
85
- return typeof code === "string" && MAIN_TERMINAL_AUTH_CODES.has(code);
86
- }
87
- if (resp.status !== 403) return false;
83
+ return (await classifyMainAuthResponse(resp, accessTokenLive)).terminal;
84
+ }
85
+
86
+ /** As `isTerminalMainAuthResponse`, also naming the provider code that made it terminal. */
87
+ async function classifyMainAuthResponse(
88
+ resp: Response,
89
+ accessTokenLive: boolean,
90
+ ): Promise<{ terminal: boolean; code?: CodexTerminalAuthCode }> {
91
+ if (resp.status !== 401 && resp.status !== 403) return { terminal: false };
88
92
  const code = await readMainAuthErrorCode(resp);
89
- return typeof code === "string" && MAIN_TERMINAL_AUTH_CODES.has(code);
93
+ if (isCodexTerminalAuthCode(code)) return { terminal: true, code };
94
+ return { terminal: resp.status === 401 && !accessTokenLive };
90
95
  }
91
96
 
92
97
  export interface MainResetQuotaProof {
@@ -103,6 +108,8 @@ export interface MainAccountInfoFetchResult {
103
108
  quotaRefresh?: CodexQuotaRefreshOutcome;
104
109
  /** Internal dispatch fence for diagnostics only; never copied into a public DTO or cache. */
105
110
  quotaRefreshGeneration?: number;
111
+ /** This current-credential usage response supplied terminal auth evidence; never public. */
112
+ terminalAuthFailure?: true;
106
113
  /** Whether this attempt safely inspected the physical native-main credential. */
107
114
  credentialChecked: boolean;
108
115
  /** Meaningful only when credentialChecked is true. */
@@ -288,7 +295,8 @@ export async function fetchMainAccountInfoWhileOwned(
288
295
  const resp = admission.response;
289
296
  quotaPhase = "publish";
290
297
  if (!resp.ok) {
291
- const terminalAuthFailure = await isTerminalMainAuthResponse(resp, isMainAccountTokenVerifiablyLive());
298
+ const authFailure = await classifyMainAuthResponse(resp, isMainAccountTokenVerifiablyLive());
299
+ const terminalAuthFailure = authFailure.terminal;
292
300
  const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh, paced);
293
301
  if (retried) return retried;
294
302
  if (!isQuotaDispatchCurrent(dispatchSequence) || !credentialIsCurrent()) {
@@ -302,10 +310,15 @@ export async function fetchMainAccountInfoWhileOwned(
302
310
  if (diagnosticStillLive) quotaRefreshGeneration = captureMainAccountIdentityGeneration();
303
311
  markAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID, writerGeneration);
304
312
  }
313
+ // Keep the last-known plan visible: after a plan change revokes the session, the plan the
314
+ // account HAD is what tells the operator why it stopped working.
305
315
  return {
306
- info: EMPTY_MAIN_ACCOUNT_INFO, credentialChecked: true, hasCredential: true,
307
- quotaRefresh: { status: "http_error", httpStatus: resp.status },
316
+ info: { ...EMPTY_MAIN_ACCOUNT_INFO, plan: nonEmptyPlan(cached?.plan) ?? nonEmptyPlan(getMainAccountPlan()) },
317
+ credentialChecked: true, hasCredential: true,
318
+ quotaRefresh: { status: "http_error", httpStatus: resp.status,
319
+ ...(authFailure.code ? { code: authFailure.code } : {}) },
308
320
  quotaRefreshGeneration,
321
+ ...(terminalAuthFailure ? { terminalAuthFailure: true as const } : {}),
309
322
  };
310
323
  }
311
324
  quotaPhase = "body";
@@ -1,3 +1,4 @@
1
+ import { codexAccountUsesCreditsAfterLimit } from "../account-credit-use";
1
2
  import { nextCodexUsageQueryAt, nextQuotaQueryDelay, pruneRemovedCodexPoolUsageAccounts } from "../quota-query-backoff";
2
3
  import { CODEX_PRIORITY_FAILBACK_REFRESH_MS } from "../account-priority";
3
4
  import { codexQuotaHasFreshUsage } from "../quota-observation-freshness";
@@ -6,8 +7,8 @@ import { getAccountQuota, isCompleteCodexQuotaRecoverySnapshot } from "../quota"
6
7
  import { reconcileMainCodexAccountRuntimeState } from "../account-lifecycle";
7
8
  import { claimDueCodexQuotaRecoveryProbes, settleCodexQuotaRecoveryProbe } from "../routing";
8
9
  import { readCodexTokens } from "../auth-collision";
9
- import { isAccountNeedsReauth, markAccountNeedsReauth } from "../account-runtime-state";
10
- import { getValidMainAccountToken, MainAccountTokenRefreshError, MAIN_CODEX_ACCOUNT_ID, getMainAccountPlan } from "../main-account";
10
+ import { isAccountNeedsReauth } from "../account-runtime-state";
11
+ import { getValidMainAccountToken, MAIN_CODEX_ACCOUNT_ID, getMainAccountPlan } from "../main-account";
11
12
  import { captureConfigGeneration, registerStateSweepAfterTick } from "../../lib/state-store-sweeper";
12
13
  import { observeMainQuotaCredential, getMainQuotaCredentialGeneration, captureMainAccountIdentityGeneration, isMainAccountIdentityGenerationLive } from "../main-account-cache";
13
14
  import { getMainAccountHardLockStatus } from "../main-account-hard-lock";
@@ -58,7 +59,7 @@ export async function runCodexCooldownRecoveryProbes(config: OcxConfig, now = Da
58
59
  // Defence in depth: independent scopes are already excluded at the claim site.
59
60
  // Generic WHAM must never clear Reserve even if claim selection changes.
60
61
  const recovered = (claim.scope === undefined || claim.scope === "shared")
61
- && isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.freshPlan ?? account.plan);
62
+ && isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.freshPlan ?? account.plan, codexAccountUsesCreditsAfterLimit(config, claim.accountId));
62
63
  settleCodexQuotaRecoveryProbe(claim, recovered, {
63
64
  credentialGeneration: result.freshCredentialGeneration,
64
65
  }, now);
@@ -100,18 +101,14 @@ export async function runMainAccountHardLockRecovery(config: OcxConfig): Promise
100
101
  if (getMainAccountHardLockStatus(config).state !== "blocked"
101
102
  || isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID)) return;
102
103
  const identityGeneration = captureMainAccountIdentityGeneration();
103
- const writerGeneration = captureConfigGeneration();
104
104
  try {
105
105
  // Refresh can require an exclusive credential claim: never hold WHAM's shared
106
106
  // claim while obtaining a valid token. The runtime lease spans both operations.
107
107
  const prepared = await getValidMainAccountToken({ preserveReauth: true });
108
108
  if (!prepared) return;
109
109
  observeMainQuotaCredential(prepared.accessToken, prepared.chatgptAccountId);
110
- } catch (error) {
111
- if (error instanceof MainAccountTokenRefreshError && error.reason === "reauth"
112
- && isMainAccountIdentityGenerationLive(identityGeneration)) {
113
- markAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID, writerGeneration);
114
- }
110
+ } catch {
111
+ // Native refresh retains its own grant-scoped refusal; global quarantine would outlive it.
115
112
  return;
116
113
  }
117
114
  if (isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID)) return;
@@ -15,14 +15,12 @@ import { WHAM_REQUEST_TIMEOUT_MS } from "../quota-recovery-timing";
15
15
  import { claimQuotaRecovery, fencePropagatedQuotaRecovery, quotaRecoveryTerminalFor, releaseQuotaRecovery, settleQuotaRecovery, settleQuotaRecoveryTerminal } from "../quota-401-recovery";
16
16
  import { seedLoginRowsForTests } from "./login-state";
17
17
  import { nonEmptyPlan } from "./runtime-config";
18
+ import { CODEX_TERMINAL_AUTH_CODES } from "../quota-refresh-outcome";
18
19
 
19
20
  export const POOL_CACHE_TTL = 5 * 60_000;
20
21
  export const POOL_QUOTA_REFRESH_CONCURRENCY = 4;
21
22
 
22
- export const MAIN_TERMINAL_AUTH_CODES = new Set([
23
- "invalid_workspace_selected",
24
- "invalid_refresh_token",
25
- ]);
23
+ export const MAIN_TERMINAL_AUTH_CODES: ReadonlySet<string> = new Set(CODEX_TERMINAL_AUTH_CODES);
26
24
 
27
25
  export async function readMainAuthErrorCode(resp: Response): Promise<unknown> {
28
26
  try {
@@ -1,3 +1,4 @@
1
+ import { codexAccountUsesCreditsAfterLimit } from "../account-credit-use";
1
2
  import { getValidCodexToken, isCodexAccountGenerationLive, readCodexAccountRecord, CodexCredentialGenerationConflictError } from "../account-store";
2
3
  import { isCompleteCodexQuotaRecoverySnapshot } from "../quota";
3
4
  import { reconcileMainCodexAccountRuntimeState } from "../account-lifecycle";
@@ -233,7 +234,7 @@ async function refreshAfterManualReset(
233
234
  && proof.credentialGeneration === auth.mainProof.credentialGeneration
234
235
  && proof.writer.identityKey === auth.mainProof.writer.identityKey
235
236
  && proof.writer.identityGeneration === auth.mainProof.writer.identityGeneration
236
- && isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.info.plan);
237
+ && isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.info.plan, codexAccountUsesCreditsAfterLimit(getRuntimeConfig(config), accountId));
237
238
  for (const claim of claims) settleManualResetCooldown(getRuntimeConfig(config), claim, recovered);
238
239
  return manualResetAuthStillLive(accountId, auth) ? result.freshResetCredits : undefined;
239
240
  }
@@ -255,7 +256,7 @@ async function refreshAfterManualReset(
255
256
  const record = readCodexAccountRecord(accountId);
256
257
  const recovered = didReset && record?.credential?.chatgptAccountId === auth.chatgptAccountId
257
258
  && (result.quotaProbeAttempted?.dispatchSequence ?? 0) > afterDispatchSequence
258
- && isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.freshPlan ?? account.plan);
259
+ && isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.freshPlan ?? account.plan, codexAccountUsesCreditsAfterLimit(getRuntimeConfig(config), accountId));
259
260
  for (const claim of claims) settleManualResetCooldown(getRuntimeConfig(config), claim, recovered, {
260
261
  credentialGeneration: result.freshCredentialGeneration,
261
262
  refreshLineage: result.resetRefreshLineage,
@@ -1,3 +1,4 @@
1
+ import { hasSpendableCodexCredits } from "./quota-types";
1
2
  import { noteMainAccountActivity } from "./main-account-external-usage";
2
3
  import { codexAccountPriorityFailbackEnabled } from "./account-priority";
3
4
  import type { PoolQuotaWriter } from "./quota-types";
@@ -16,7 +17,7 @@ import {
16
17
  import { isAccountNeedsReauth, markAccountNeedsReauth } from "./account-runtime-state";
17
18
  import { ConfigMutationLockError } from "../config";
18
19
  import { NativeProfileError } from "./native-profile-types";
19
- import { isCodexAccountUsable } from "./account-usability";
20
+ import { codexAccountUnusableReason, isCodexAccountUsable } from "./account-usability";
20
21
  import { reconcileMainCodexAccountRuntimeState } from "./account-lifecycle";
21
22
  import {
22
23
  MAIN_CODEX_ACCOUNT_ID,
@@ -102,7 +103,7 @@ import { getEffectiveCodexAutoSwitchThreshold } from "./account-auto-switch";
102
103
  function requestOwnedMainPinHasQuotaHeadroom(config: OcxConfig): boolean {
103
104
  const threshold = getEffectiveCodexAutoSwitchThreshold(config, MAIN_CODEX_ACCOUNT_ID);
104
105
  if (threshold <= 0) return true;
105
- const usage = computeCodexUsageScore(getAccountQuota(MAIN_CODEX_ACCOUNT_ID));
106
+ const usage = computeCodexUsageScore(getAccountQuota(MAIN_CODEX_ACCOUNT_ID), undefined, Date.now(), codexAccountUsesCreditsAfterLimit(config, MAIN_CODEX_ACCOUNT_ID));
106
107
  return usage >= CODEX_UNKNOWN_USAGE_SCORE || usage < threshold;
107
108
  }
108
109
 
@@ -378,9 +379,11 @@ export class CodexAuthContextError extends Error {
378
379
  }
379
380
 
380
381
  export class CodexPoolAuthenticationError extends Error {
381
- constructor(message = "OpenAI account pool has no usable account credential") {
382
+ readonly quarantinedMain: boolean;
383
+ constructor(message = "OpenAI account pool has no usable account credential", options?: { quarantinedMain?: boolean }) {
382
384
  super(message);
383
385
  this.name = "CodexPoolAuthenticationError";
386
+ this.quarantinedMain = options?.quarantinedMain === true;
384
387
  }
385
388
  }
386
389
 
@@ -530,7 +533,7 @@ export class CodexMainAccountCreditsOffError extends CodexAccountCooldownError {
530
533
  super(MAIN_CODEX_ACCOUNT_ID, resetAt ?? 0);
531
534
  this.name = "CodexMainAccountCreditsOffError";
532
535
  this.resetAt = resetAt;
533
- this.message = "Codex main account reached its usage limit, and spending ChatGPT credits is off for it."
536
+ this.message = "Codex main account reached its usage limit, and spending ChatGPT credits is off or no fresh spendable balance is available."
534
537
  + " Choose another account, wait for the limit to reset, or allow the main account under \"Use credits\" in Codex Auth.";
535
538
  }
536
539
  }
@@ -723,7 +726,8 @@ export function unwrapUpstreamRetryEvidenceError(error: unknown): unknown {
723
726
  * file, which several callers are forbidden to open, so every long window counts instead.
724
727
  */
725
728
  function mainCreditsHoldResetAt(config: Pick<OcxConfig, "creditCodexAccountIds">): number | undefined {
726
- if (codexAccountUsesCreditsAfterLimit(config, MAIN_CODEX_ACCOUNT_ID)) return undefined;
729
+ if (codexAccountUsesCreditsAfterLimit(config, MAIN_CODEX_ACCOUNT_ID)
730
+ && hasSpendableCodexCredits(getMainPolicyQuota())) return undefined;
727
731
  return codexUsageLimitResetAt(getMainPolicyQuota(), undefined, Date.now());
728
732
  }
729
733
 
@@ -920,7 +924,7 @@ export function shouldMarkAccountNeedsReauthForCodexAuthFailure(cause: unknown):
920
924
  && !(cause instanceof CodexCredentialRefreshBusyError)
921
925
  && !(cause instanceof CodexCredentialRefreshStaleError)
922
926
  && !(cause instanceof MainAuthJsonChangedDuringRefreshError)
923
- && !(cause instanceof MainAccountTokenRefreshError && cause.reason === "transient")
927
+ && !(cause instanceof MainAccountTokenRefreshError)
924
928
  && !(cause instanceof NativeProfileError && cause.retryable)
925
929
  && !(cause instanceof DOMException && cause.name === "AbortError")
926
930
  && !(cause instanceof ConfigMutationLockError);
@@ -1314,7 +1318,12 @@ export async function resolveCodexAuthContext(
1314
1318
  "Selected Codex account does not support this model",
1315
1319
  );
1316
1320
  }
1317
- throw new CodexPoolAuthenticationError("Selected Codex account is unavailable");
1321
+ const mainReason = fixedAccountId === MAIN_CODEX_ACCOUNT_ID && !nativeMainReadsForbidden
1322
+ && !policy.pausedCodexAccountIds?.includes(fixedAccountId)
1323
+ ? codexAccountUnusableReason(config, fixedAccountId, selectionOptions) : undefined;
1324
+ throw new CodexPoolAuthenticationError("Selected Codex account is unavailable", {
1325
+ quarantinedMain: mainReason === "needs_reauth",
1326
+ });
1318
1327
  }
1319
1328
  // Recovery or a turn drain deliberately makes physical main unobservable.
1320
1329
  // If no healthy pool route is available, report the temporary fence rather
@@ -1339,7 +1348,12 @@ export async function resolveCodexAuthContext(
1339
1348
  : "Codex accounts that support this model are currently unavailable",
1340
1349
  );
1341
1350
  }
1342
- throw new CodexPoolAuthenticationError();
1351
+ throw new CodexPoolAuthenticationError(undefined, {
1352
+ quarantinedMain: !nativeMainReadsForbidden
1353
+ && options.excludeAccountId !== MAIN_CODEX_ACCOUNT_ID
1354
+ && !policy.pausedCodexAccountIds?.includes(MAIN_CODEX_ACCOUNT_ID)
1355
+ && codexAccountUnusableReason(config, MAIN_CODEX_ACCOUNT_ID, selectionOptions) === "needs_reauth",
1356
+ });
1343
1357
  }
1344
1358
  accountId = selected;
1345
1359
  if (accountId === MAIN_CODEX_ACCOUNT_ID) assertMainAccountPolicy(policy);
@@ -1370,10 +1384,15 @@ export async function resolveCodexAuthContext(
1370
1384
  throw new CodexPoolAuthenticationError("Selected Codex account is unavailable");
1371
1385
  }
1372
1386
  if (isAccountNeedsReauth(accountId)) {
1373
- throw new CodexPoolAuthenticationError("Selected Codex account needs reauthentication");
1387
+ throw new CodexPoolAuthenticationError("Selected Codex account needs reauthentication", {
1388
+ quarantinedMain: accountId === MAIN_CODEX_ACCOUNT_ID && !nativeMainReadsForbidden,
1389
+ });
1374
1390
  }
1375
- if (!isCodexAccountUsable(config, accountId, selectionOptions)) {
1376
- throw new CodexPoolAuthenticationError("Selected Codex account is unavailable");
1391
+ const unusableReason = codexAccountUnusableReason(config, accountId, selectionOptions);
1392
+ if (unusableReason !== undefined) {
1393
+ throw new CodexPoolAuthenticationError("Selected Codex account is unavailable", {
1394
+ quarantinedMain: accountId === MAIN_CODEX_ACCOUNT_ID && unusableReason === "needs_reauth",
1395
+ });
1377
1396
  }
1378
1397
  }
1379
1398
  } catch (cause) {
@@ -1,4 +1,5 @@
1
1
  import { execFileSync } from "node:child_process";
2
+ import { collapseAntigravityPublicModels } from "../../providers/antigravity-effort-families";
2
3
  import { createHash } from "node:crypto";
3
4
  import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
4
5
  import { delimiter, dirname, join, resolve } from "node:path";
@@ -504,6 +505,7 @@ export function resolveSlugAliasCollisions(goModels: CatalogModel[]): Set<Catalo
504
505
  }
505
506
 
506
507
  export function uniqueCatalogModelsForPublicList(goModels: CatalogModel[]): CatalogModel[] {
508
+ goModels = collapseAntigravityPublicModels(goModels);
507
509
  const collisionSkipped = resolveSlugAliasCollisions(goModels);
508
510
  const comboPublicSlugs = new Set(goModels
509
511
  .filter(model => model.provider === COMBO_NAMESPACE)
@@ -525,6 +527,7 @@ export function uniqueCatalogModelsForPublicList(goModels: CatalogModel[]): Cata
525
527
  }
526
528
 
527
529
  export function uniqueCatalogModelsForRawPublicList(goModels: CatalogModel[]): CatalogModel[] {
530
+ goModels = collapseAntigravityPublicModels(goModels);
528
531
  const publicId = (model: CatalogModel): string => model.alias ?? `${model.provider}/${model.id}`;
529
532
  const comboPublicIds = new Set(goModels
530
533
  .filter(model => model.provider === COMBO_NAMESPACE)
@@ -295,7 +295,8 @@ export function applyProviderConfigHints(
295
295
  inputModalities = base.includes("image") ? [...base] : [...base, "image"];
296
296
  }
297
297
  const reasoningEfforts = configuredReasoningEfforts(prov, model.id);
298
- const suppressSyntheticMax = modelRecordValue(prov.modelSuppressSyntheticMax, model.id) === true;
298
+ const suppressSyntheticMax = modelRecordValue(prov.modelSuppressSyntheticMax, model.id)
299
+ ?? (model.antigravityEffortWireModelIds !== undefined);
299
300
  const defaultReasoningEffort = staticPolicy.model.defaultReasoningEffort ?? model.defaultReasoningEffort;
300
301
  const supportsReasoningSummaries = staticPolicy.model.supportsReasoningSummaries;
301
302
  const supportsVerbosity = staticPolicy.model.supportsVerbosity;
@@ -1,4 +1,5 @@
1
1
  import { effectiveProviderAlias, effectiveProviderAliasDecision } from "../../providers/default-aliases";
2
+ import { antigravityFamilyDisabled, collapseAntigravityPublicModels, projectAntigravitySelectedModels } from "../../providers/antigravity-effort-families";
2
3
  import { XAI_OAUTH_FAST_VARIANT_IDS } from "../../providers/xai-fast-model";
3
4
  import { initialModelSelectionPending } from "../../providers/initial-model-selection";
4
5
  import { execFileSync } from "node:child_process";
@@ -267,7 +268,7 @@ export function mergeConfiguredModelsIntoLiveCatalog(opts: {
267
268
 
268
269
  export function filterCatalogVisibleModels(
269
270
  models: CatalogModel[],
270
- config: Pick<OcxConfig, "disabledModels" | "providers">,
271
+ config: Pick<OcxConfig, "disabledModels" | "providers" | "modelDiscovery">,
271
272
  ): CatalogModel[] {
272
273
  const disabled = new Set(config.disabledModels ?? []);
273
274
  const allowByProvider = new Map<string, Set<string>>();
@@ -289,12 +290,14 @@ export function filterCatalogVisibleModels(
289
290
  // Two catalog stages with different equivalence relations is the exact bug class
290
291
  // this change exists to remove.
291
292
  if (Array.isArray(sel) && sel.length > 0) {
292
- allowByProvider.set(name, new Set(sel.map(model => slugEquivalenceKey(routedSlug(name, model)))));
293
+ allowByProvider.set(name, new Set(projectAntigravitySelectedModels(name, sel, models)
294
+ .map(model => slugEquivalenceKey(routedSlug(name, model)))));
293
295
  }
294
296
  }
295
- return models.filter(m => {
297
+ return collapseAntigravityPublicModels(models).filter(m => {
296
298
  if (initialModelSelectionPending(config.providers[m.provider])) return false;
297
299
  if (config.providers[m.provider]?.disabled === true) return false;
300
+ if (antigravityFamilyDisabled(config, m, models)) return false;
298
301
  const nativeAlias = m.provider === COMBO_NAMESPACE && m.nativeAlias === true;
299
302
  // disabledModels may be stored raw (canonical) or encoded (legacy UI writes).
300
303
  for (const stored of disabled) {
@@ -113,6 +113,8 @@ export interface CatalogModel {
113
113
  displayName?: string;
114
114
  owned_by?: string;
115
115
  reasoningEfforts?: string[];
116
+ /** Exact CCA family evidence, retained through the in-memory discovery cache. */
117
+ antigravityEffortWireModelIds?: Partial<Record<"low" | "medium" | "high", string>>;
116
118
  /** Suppress only catalog synthesis of a missing max rung; provider-declared max survives. */
117
119
  suppressSyntheticMax?: boolean;
118
120
  defaultReasoningEffort?: string;
@@ -751,20 +751,29 @@ export async function fetchProviderModelsWithAuth(
751
751
  const live = antigravity.map(model => applyProviderConfigHints(name, prov, {
752
752
  id: model.id,
753
753
  provider: name,
754
- // CCA only exposes a numeric thinking budget. Until the adapter owns an exact Codex
755
- // effort-to-wire mapping for a newly discovered model, do not advertise a false ladder.
756
- reasoningEfforts: [],
754
+ // Only an exact discovered wire map proves a new model's selectable effort ladder.
755
+ reasoningEfforts: Object.keys(model.effortWireModelIds ?? {}),
756
+ ...(model.effortWireModelIds ? {
757
+ antigravityEffortWireModelIds: model.effortWireModelIds,
758
+ suppressSyntheticMax: true,
759
+ defaultReasoningEffort: model.effortWireModelIds.medium ? "medium" : "high",
760
+ } : {}),
757
761
  ...(model.contextWindow ? { contextWindow: model.contextWindow } : {}),
758
762
  ...(model.inputModalities ? { inputModalities: model.inputModalities } : {}),
759
763
  }, contextCap, metadataModelIdCaseFold, captured.effectiveAlias));
760
764
  const forCache = withConfiguredRetention(live, { retainComboTargets: false });
761
- if (!setCached(name, forCache, Date.now(), cacheGeneration)) {
765
+ if (!isCurrentCacheGeneration()) {
762
766
  return observed(withConfiguredRetention(configured), "degraded");
763
767
  }
768
+ // Save exact family routes before publishing synthetic IDs. These synchronous writes
769
+ // cannot interleave with a credential-generation change; failure keeps the prior cache.
764
770
  registerAntigravityDiscoveredWireModels(prov.baseUrl, antigravity, {
765
771
  provider: name,
766
772
  cacheGeneration,
767
773
  });
774
+ if (!setCached(name, forCache, Date.now(), cacheGeneration)) {
775
+ return observed(withConfiguredRetention(configured), "degraded");
776
+ }
768
777
  markProviderDiscoveryOk(name, live.length);
769
778
  return observed(withConfiguredRetention(forCache, { warnDrops: true }), "authoritative");
770
779
  }
@@ -1,3 +1,4 @@
1
+ import { projectAntigravitySelectedModels } from "../../providers/antigravity-effort-families";
1
2
  import { existsSync, readFileSync } from "node:fs";
2
3
  import { join } from "node:path";
3
4
  import { initializeConfigOwnership } from "../../lib/config-ownership";
@@ -465,7 +466,7 @@ function writeRetainedCatalogSync({
465
466
  provider.disabled !== true
466
467
  && Array.isArray(provider.selectedModels)
467
468
  && provider.selectedModels.length > 0
468
- ? [[name, new Set(provider.selectedModels)] as const]
469
+ ? [[name, new Set(projectAntigravitySelectedModels(name, provider.selectedModels, goModels))] as const]
469
470
  : []
470
471
  )),
471
472
  );
@@ -0,0 +1,56 @@
1
+ /** Closed diagnostic vocabulary; provider descriptions and unknown codes never escape. */
2
+ const REFRESH_DIAGNOSTIC_CODES = [
3
+ "invalid_grant", "refresh_token_invalidated", "token_invalidated", "refresh_token_reused",
4
+ "refresh_token_expired", "invalid_client", "invalid_request", "server_error",
5
+ "temporarily_unavailable", "rate_limit_exceeded", "unsupported_grant_type", "invalid_scope",
6
+ ] as const;
7
+ export type ChatgptRefreshDiagnosticCode = typeof REFRESH_DIAGNOSTIC_CODES[number];
8
+ export type ChatgptRefreshFailure = {
9
+ reason: "expired" | "revoked" | "unknown";
10
+ code?: ChatgptRefreshDiagnosticCode;
11
+ };
12
+
13
+ /** HTTP availability failures cannot establish that a refresh grant was revoked. */
14
+ export function classifyChatgptRefreshFailure(status: number, body: string): ChatgptRefreshFailure {
15
+ if (body.length > 16_384) return { reason: "unknown" };
16
+ let payload: unknown;
17
+ try { payload = JSON.parse(body); } catch { return { reason: "unknown" }; }
18
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return { reason: "unknown" };
19
+ const row = payload as Record<string, unknown>;
20
+ let exactCode: string | undefined;
21
+ let description = typeof row.error_description === "string" ? row.error_description : "";
22
+ if (typeof row.error === "string") {
23
+ exactCode = row.error.trim() || undefined;
24
+ } else if (row.error !== undefined) {
25
+ if (!row.error || typeof row.error !== "object" || Array.isArray(row.error)) return { reason: "unknown" };
26
+ const error = row.error as Record<string, unknown>;
27
+ if (error.code !== undefined && typeof error.code !== "string") return { reason: "unknown" };
28
+ exactCode = typeof error.code === "string" ? error.code.trim() || undefined : undefined;
29
+ if (typeof error.message === "string") description = `${error.message} ${description}`;
30
+ }
31
+ const code = (REFRESH_DIAGNOSTIC_CODES as readonly unknown[]).includes(exactCode)
32
+ ? exactCode as ChatgptRefreshDiagnosticCode : undefined;
33
+ let reason: ChatgptRefreshFailure["reason"] = "unknown";
34
+ if (status === 400 || status === 401 || status === 403) {
35
+ if (["invalid_grant", "refresh_token_invalidated", "token_invalidated", "refresh_token_reused"].includes(exactCode ?? "")) {
36
+ reason = "revoked";
37
+ } else if (exactCode === "refresh_token_expired") {
38
+ reason = "expired";
39
+ } else if (status === 400 && exactCode === undefined) {
40
+ // Preserve description-only OAuth 400 compatibility; a named code always wins.
41
+ if (description.includes("invalidated") || description.includes("revoked")) reason = "revoked";
42
+ else if (description.includes("expired")) reason = "expired";
43
+ }
44
+ }
45
+ return { reason, ...(code ? { code } : {}) };
46
+ }
47
+
48
+ /** Endpoint verdict only: no credential identity, body, or arbitrary upstream string. */
49
+ export function noteChatgptRefreshFailure(
50
+ kind: "pool" | "native main",
51
+ status: number,
52
+ failure: ChatgptRefreshFailure,
53
+ ): void {
54
+ console.warn(`[codex] ${kind} refresh: ${failure.reason === "unknown" ? "transient" : "reauth"}`
55
+ + ` status=${status} code=${failure.code ?? "none"}`);
56
+ }
@@ -1,3 +1,4 @@
1
+ import { projectAntigravitySelectedModels } from "../providers/antigravity-effort-families";
1
2
  import { join } from "node:path";
2
3
 
3
4
  import { getConfigDir, saveConfigPreservingClaudeCode, websocketsEnabled, withExpectedConfigGenerationSync } from "../config";
@@ -347,7 +348,7 @@ function prepareCatalog(
347
348
  const selectedModelsByProvider = new Map<string, ReadonlySet<string>>(
348
349
  enabledProviders.flatMap(([name, provider]) => (
349
350
  Array.isArray(provider.selectedModels) && provider.selectedModels.length > 0
350
- ? [[name, new Set(provider.selectedModels)] as const]
351
+ ? [[name, new Set(projectAntigravitySelectedModels(name, provider.selectedModels, routedModels))] as const]
351
352
  : []
352
353
  )),
353
354
  );