@bitkyc08/opencodex 2.72.0 → 2.73.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 (79) hide show
  1. package/gui/dist/assets/{App-Cf-WYstE.js → App-mHGa7dE1.js} +15 -15
  2. package/gui/dist/assets/{Tray-DkM0mULy.js → Tray-BxY_RYZg.js} +1 -1
  3. package/gui/dist/assets/{index-PWrwOfio.js → index-ur_BPgK1.js} +12 -12
  4. package/gui/dist/assets/{quota-summary-D-FKtrgr.js → quota-summary-D1Vq7N9H.js} +1 -1
  5. package/gui/dist/index.html +1 -1
  6. package/package.json +1 -1
  7. package/src/adapters/cursor/native-exec.ts +28 -10
  8. package/src/adapters/devin/live-models.ts +5 -0
  9. package/src/adapters/kiro/reasoning.ts +2 -0
  10. package/src/adapters/openai-chat/messages.ts +4 -1
  11. package/src/adapters/openai-responses/passthrough.ts +12 -1
  12. package/src/adapters/openai-responses/reasoning.ts +15 -0
  13. package/src/bridge/internal.ts +12 -0
  14. package/src/cli/account-anthropic-threshold.ts +41 -0
  15. package/src/cli/account-api.ts +2 -0
  16. package/src/cli/account-extended.ts +32 -21
  17. package/src/cli/account.ts +1 -0
  18. package/src/cli/capabilities.ts +8 -4
  19. package/src/cli/index.ts +1 -1
  20. package/src/codex/catalog/build-entries.ts +6 -5
  21. package/src/codex/catalog/derive-entry.ts +3 -4
  22. package/src/codex/catalog/gather-capture.ts +2 -0
  23. package/src/codex/catalog/metadata.ts +6 -0
  24. package/src/codex/catalog/native-models.ts +17 -3
  25. package/src/codex/catalog/parsing.ts +4 -1
  26. package/src/codex/catalog/pinned-models.ts +3 -1
  27. package/src/codex/catalog/provider-models.ts +22 -2
  28. package/src/codex/data/roster-pinned-models.json +178 -1
  29. package/src/codex/model-entitlements.ts +1 -1
  30. package/src/codex/native-profile-store.ts +2 -1
  31. package/src/combos/resolve.ts +16 -1
  32. package/src/config/subagent-models.ts +30 -5
  33. package/src/generated/compatibility-version.json +95 -67
  34. package/src/generated/model-metadata.ts +4 -4
  35. package/src/lib/account-selection-events.ts +28 -0
  36. package/src/lib/errors.ts +7 -5
  37. package/src/lib/keyring-native.ts +132 -0
  38. package/src/lib/provider-outbound.ts +13 -0
  39. package/src/lib/retry-delay.ts +10 -0
  40. package/src/oauth/anthropic-account-threshold.ts +13 -0
  41. package/src/oauth/anthropic-routing.ts +223 -52
  42. package/src/oauth/index.ts +4 -2
  43. package/src/oauth/store.ts +45 -3
  44. package/src/oauth/types.ts +2 -0
  45. package/src/providers/api-key-resolve.ts +2 -4
  46. package/src/providers/codebuddy-models.ts +6 -0
  47. package/src/providers/kiro-models.ts +4 -0
  48. package/src/providers/label.ts +20 -1
  49. package/src/providers/quota/account-cache.ts +2 -1
  50. package/src/providers/quota/anthropic-cooldown-recovery.ts +118 -0
  51. package/src/providers/quota/vendor-probes-oauth.ts +84 -57
  52. package/src/providers/quota.ts +45 -46
  53. package/src/providers/registry/entries-core.ts +3 -1
  54. package/src/providers/registry/entries-extended.ts +14 -1
  55. package/src/providers/registry/model-seeds.ts +7 -1
  56. package/src/server/claude-messages.ts +84 -27
  57. package/src/server/management/anthropic-account-threshold.ts +27 -0
  58. package/src/server/management/oauth-account-routes.ts +17 -4
  59. package/src/server/management/route-registry.ts +1 -0
  60. package/src/server/messages-native-oauth.ts +20 -21
  61. package/src/server/messages-native.ts +39 -14
  62. package/src/server/relay-eager.ts +5 -2
  63. package/src/server/relay.ts +268 -82
  64. package/src/server/responses/adapter-continuation.ts +6 -0
  65. package/src/server/responses/adapter-delivery.ts +25 -7
  66. package/src/server/responses/adapter-dispatch.ts +31 -0
  67. package/src/server/responses/buffered-sse-json.ts +345 -0
  68. package/src/server/responses/core-codex-account.ts +4 -1
  69. package/src/server/responses/core-combo.ts +28 -0
  70. package/src/server/responses/core-lifetime.ts +4 -0
  71. package/src/server/responses/passthrough-delivery.ts +305 -10
  72. package/src/server/responses/passthrough-dispatch.ts +60 -25
  73. package/src/server/responses/passthrough-error.ts +3 -1
  74. package/src/server/responses/request-transport.ts +22 -5
  75. package/src/server/responses/terminal-error-redaction.ts +53 -0
  76. package/src/service/windows-taskxml.ts +20 -6
  77. package/src/service-manager-probe.ts +115 -6
  78. package/src/types/wire.ts +7 -0
  79. package/src/usage/expected-prices.ts +11 -0
@@ -16,8 +16,8 @@
16
16
  * Policy: a reference that cannot be resolved fails closed (no key) and is warned once per
17
17
  * account; nothing ever rewrites plaintext into config or its backups.
18
18
  */
19
- import { createRequire } from "node:module";
20
19
  import { resolveEnvValue } from "../config/proxy-env";
20
+ import { loadKeyringBinding } from "../lib/keyring-native";
21
21
  import type { OcxProviderConfig } from "../types";
22
22
 
23
23
  export const KEYCHAIN_REFERENCE_PREFIX = "keychain:";
@@ -31,10 +31,8 @@ export interface ProviderKeychainEntry {
31
31
 
32
32
  export type ProviderKeychainEntryFactory = (service: string, account: string) => ProviderKeychainEntry;
33
33
 
34
- const nodeRequire = createRequire(import.meta.url);
35
-
36
34
  function defaultEntryFactory(service: string, account: string): ProviderKeychainEntry {
37
- const { Entry } = nodeRequire("@napi-rs/keyring") as { Entry: new (s: string, a: string) => ProviderKeychainEntry };
35
+ const { Entry } = loadKeyringBinding() as { Entry: new (s: string, a: string) => ProviderKeychainEntry };
38
36
  return new Entry(service, account);
39
37
  }
40
38
 
@@ -28,6 +28,8 @@ export const CODEBUDDY_GLOBAL_MODELS = [
28
28
  // 260923 preemptive: GPT-6 Sol and Luna (OpenAI announced 2026-09-22) added ahead of this provider's own catalog; mirrors the GPT-5.6 Sol/Luna rows.
29
29
  "gpt-6-sol",
30
30
  "gpt-6-luna",
31
+ // 260930 preemptive: GPT-6.1 Sol (OpenAI announced 2026-09-29); mirrors the GPT-6 Sol row.
32
+ "gpt-6.1-sol",
31
33
  "gpt-5.5",
32
34
  "gpt-5.4",
33
35
  "gpt-5.3-codex",
@@ -79,6 +81,7 @@ export const CODEBUDDY_GLOBAL_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
79
81
  "gpt-5.6-luna": 1_000_000,
80
82
  "gpt-6-sol": 1_000_000,
81
83
  "gpt-6-luna": 1_000_000,
84
+ "gpt-6.1-sol": 1_000_000,
82
85
  "gpt-5.5": 1_000_000,
83
86
  "gpt-5.4": 272_000,
84
87
  "gpt-5.3-codex": 272_000,
@@ -101,6 +104,7 @@ export const CODEBUDDY_GLOBAL_MODEL_MAX_OUTPUT_TOKENS: Record<string, number> =
101
104
  "gpt-5.6-luna": 128_000,
102
105
  "gpt-6-sol": 128_000,
103
106
  "gpt-6-luna": 128_000,
107
+ "gpt-6.1-sol": 128_000,
104
108
  "gpt-5.5": 72_000,
105
109
  "gpt-5.4": 128_000,
106
110
  "gpt-5.3-codex": 128_000,
@@ -119,6 +123,7 @@ export const CODEBUDDY_GLOBAL_MODEL_REASONING_EFFORTS: Record<string, string[]>
119
123
  "gpt-5.6-luna": ["low", "medium", "high", "xhigh"],
120
124
  "gpt-6-sol": ["low", "medium", "high", "xhigh"],
121
125
  "gpt-6-luna": ["low", "medium", "high", "xhigh"],
126
+ "gpt-6.1-sol": ["low", "medium", "high", "xhigh"],
122
127
  "glm-5.3": ["low", "high", "max"],
123
128
  "glm-5.2": ["high", "xhigh"],
124
129
  };
@@ -129,6 +134,7 @@ export const CODEBUDDY_GLOBAL_MODEL_DEFAULT_REASONING_EFFORTS: Record<string, st
129
134
  "gpt-5.6-luna": "high",
130
135
  "gpt-6-sol": "high",
131
136
  "gpt-6-luna": "high",
137
+ "gpt-6.1-sol": "high",
132
138
  "glm-5.3": "high",
133
139
  "glm-5.2": "high",
134
140
  };
@@ -7,6 +7,9 @@ export const KIRO_MODELS = [
7
7
  // 260923 preemptive: GPT-6 Sol and Luna (OpenAI announced 2026-09-22) added ahead of this provider's own catalog; mirrors the GPT-5.6 Sol/Luna rows. Calls fail upstream until Kiro ships the models.
8
8
  "gpt-6-sol",
9
9
  "gpt-6-luna",
10
+ // 260930 preemptive: GPT-6.1 Sol (OpenAI announced 2026-09-29; kiro.dev did not list it on 2026-09-30).
11
+ // Mirrors gpt-6-sol; calls fail upstream until Kiro ships the model.
12
+ "gpt-6.1-sol",
10
13
  // 260929 preemptive: Claude Sonnet 5.5 added ahead of Kiro's catalog (kiro.dev did not list it on
11
14
  // 2026-09-29). Mirrors claude-sonnet-5; calls fail upstream until Kiro ships the model.
12
15
  "claude-sonnet-5.5",
@@ -40,6 +43,7 @@ export const KIRO_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
40
43
  "gpt-5.6-luna": 1_000_000,
41
44
  "gpt-6-sol": 272_000,
42
45
  "gpt-6-luna": 272_000,
46
+ "gpt-6.1-sol": 272_000,
43
47
  "claude-sonnet-5.5": 1_000_000,
44
48
  "claude-sonnet-5": 1_000_000,
45
49
  "claude-opus-5.5": 1_000_000,
@@ -1,4 +1,4 @@
1
- import { CODEX_ACCOUNT_LOG_LABEL_RE, KEY_ACCOUNT_LOG_LABEL_RE, apiKeyAccountLogLabel, oauthAccountLogLabel } from "../codex/account-label";
1
+ import { ACCOUNT_LOG_LABEL_RE, CODEX_ACCOUNT_LOG_LABEL_RE, KEY_ACCOUNT_LOG_LABEL_RE, apiKeyAccountLogLabel, oauthAccountLogLabel } from "../codex/account-label";
2
2
  import type { OcxProviderConfig } from "../types";
3
3
 
4
4
  export function canonicalUsageProviderLabel(provider: string): string {
@@ -25,6 +25,25 @@ export function stampApiKeyAccountLabel(
25
25
  }
26
26
  }
27
27
 
28
+ /**
29
+ * The account-qualified provider label a pool dispatch stamped for this request, or undefined.
30
+ *
31
+ * An account pool rewrites the logged provider to name the account that served the turn
32
+ * (`anthropic-p2e22d0`, `openai-main`); the configured provider name never does, so a label that
33
+ * differs from the configured one AND ends in an account label identifies exactly one account.
34
+ *
35
+ * Deliberately NOT `accountLogLabel`: that is stamped for a lone configured API key too, so it
36
+ * cannot tell a pooled account apart from a provider that has no pool to rotate within.
37
+ */
38
+ export function poolAccountProviderLabel(
39
+ loggedProvider: string | undefined,
40
+ configuredProvider: string,
41
+ ): string | undefined {
42
+ if (!loggedProvider?.startsWith(`${configuredProvider}-`)) return undefined;
43
+ return ACCOUNT_LOG_LABEL_RE.test(loggedProvider.slice(configuredProvider.length + 1))
44
+ ? loggedProvider : undefined;
45
+ }
46
+
28
47
  export function baseProviderLabel(provider: string): string {
29
48
  const canonical = canonicalUsageProviderLabel(provider);
30
49
  if (canonical !== provider) return canonical;
@@ -272,7 +272,8 @@ export function recordAnthropicAccountQuotaFromHeaders(
272
272
  // whole map. Landing before any reader has hydrated would persist this single row and erase
273
273
  // every other provider's saved row.
274
274
  hydrateAccountQuotaCache();
275
- const previous = accountQuotaCache.get(key);
275
+ const candidate = accountQuotaCache.get(key);
276
+ const previous = candidate?.isCurrent?.() === false ? undefined : candidate;
276
277
  accountQuotaCache.set(key, {
277
278
  ...previous,
278
279
  // Headers do not prove that the last usage probe succeeded.
@@ -0,0 +1,118 @@
1
+ import { credentialGeneration, getAccountCredential, getAccountCredentialWithStatus } from "../../oauth/store";
2
+ import type { ProviderQuota } from "../quota-types";
3
+
4
+ export class AnthropicQuotaProbeOwnershipError extends Error {}
5
+
6
+ /** Check live pause and bearer ownership at the last synchronous step before usage dispatch. */
7
+ export function assertAnthropicQuotaSendAllowed(accountId: string, token: string): void {
8
+ const row = getAccountCredentialWithStatus("anthropic", accountId);
9
+ if (!row || row.paused || row.needsReauth || row.credential.access !== token) {
10
+ throw new AnthropicQuotaProbeOwnershipError("anthropic quota account is no longer eligible");
11
+ }
12
+ }
13
+
14
+ const cooldownGenerationByAccount = new Map<string, number>();
15
+ let nextCooldownGeneration = 1;
16
+ let afterSettlementForTests: (() => void) | undefined;
17
+
18
+ export function setAnthropicQuotaAfterSettlementForTests(hook: (() => void) | undefined): void {
19
+ afterSettlementForTests = hook;
20
+ }
21
+
22
+ /** Monotonic per-account fence shared by routing mutations and quota publication. */
23
+ export function anthropicCooldownGeneration(accountId: string): number {
24
+ return cooldownGenerationByAccount.get(accountId) ?? 0;
25
+ }
26
+
27
+ /** Keep same-state quota calls joinable while moving post-429 calls to a fresh flight. */
28
+ export function anthropicCooldownFlightKey(baseKey: string, accountId: string): string {
29
+ return `${baseKey}\0anthropic-cooldown:${anthropicCooldownGeneration(accountId)}`;
30
+ }
31
+
32
+ export function noteAnthropicCooldownMutation(accountId: string): number {
33
+ const generation = nextCooldownGeneration++;
34
+ cooldownGenerationByAccount.set(accountId, generation);
35
+ return generation;
36
+ }
37
+
38
+ export function clearAnthropicCooldownGenerations(): void {
39
+ cooldownGenerationByAccount.clear();
40
+ }
41
+
42
+ export type AnthropicCooldownRecoveryProbe = Readonly<{
43
+ requiresFreshDispatch: boolean;
44
+ isCurrentCredential(): boolean;
45
+ isCurrent(): boolean;
46
+ settle(quota: ProviderQuota): "cleared" | "retained" | "superseded";
47
+ }>;
48
+
49
+ export type AnthropicQuotaRecoveryResult = Readonly<{
50
+ quota: ProviderQuota;
51
+ /** Must be checked synchronously at each publication boundary. */
52
+ isCurrent(): boolean;
53
+ }>;
54
+
55
+ /**
56
+ * Bind a quota probe to both the credential and cooldown generations it observed before
57
+ * dispatch. A successful response may otherwise arrive after either credential replacement
58
+ * or a newer 429 and incorrectly make unrelated state eligible.
59
+ */
60
+ export async function captureAnthropicCooldownRecoveryProbe(
61
+ accountId: string,
62
+ accessToken: string,
63
+ ): Promise<AnthropicCooldownRecoveryProbe | null> {
64
+ const credential = getAccountCredential("anthropic", accountId);
65
+ if (!credential || credential.access !== accessToken) return null;
66
+ const generation = credentialGeneration(credential);
67
+ // Lazy because anthropic-routing reads the quota cache on normal request routing.
68
+ const routing = await import("../../oauth/anthropic-routing");
69
+ const cooldownGeneration = anthropicCooldownGeneration(accountId);
70
+ const claim = routing.captureAnthropicCooldownRecovery(accountId);
71
+ const isCurrentCredential = () => {
72
+ const current = getAccountCredential("anthropic", accountId);
73
+ return !!current && credentialGeneration(current) === generation;
74
+ };
75
+ return {
76
+ requiresFreshDispatch: claim !== null,
77
+ isCurrentCredential,
78
+ isCurrent: () => isCurrentCredential()
79
+ && anthropicCooldownGeneration(accountId) === cooldownGeneration,
80
+ settle: quota => claim === null ? "retained"
81
+ : routing.settleAnthropicCooldownRecovery(claim, quota),
82
+ };
83
+ }
84
+
85
+ /** Run one authoritative usage read and publish its recovery effect only while still owned. */
86
+ export async function probeAnthropicQuotaWithRecovery(
87
+ accountId: string,
88
+ accessToken: string,
89
+ read: (requireFreshDispatch: boolean) => Promise<ProviderQuota | null>,
90
+ mayPublish: () => boolean,
91
+ ): Promise<AnthropicQuotaRecoveryResult | null> {
92
+ const probe = await captureAnthropicCooldownRecoveryProbe(accountId, accessToken);
93
+ if (!probe) throw new AnthropicQuotaProbeOwnershipError("anthropic quota probe lost credential ownership");
94
+ let quota: ProviderQuota | null;
95
+ try { quota = await read(probe.requiresFreshDispatch); }
96
+ catch (error) {
97
+ if (!probe.isCurrent() || !mayPublish()) throw new AnthropicQuotaProbeOwnershipError("anthropic quota probe failure is stale");
98
+ throw error;
99
+ }
100
+ if (!probe.isCurrent() || !mayPublish()) {
101
+ throw new AnthropicQuotaProbeOwnershipError("anthropic quota probe result is stale");
102
+ }
103
+ if (!quota) return null;
104
+ const settlement = probe.settle(quota);
105
+ if (settlement === "superseded") {
106
+ throw new AnthropicQuotaProbeOwnershipError("anthropic quota probe lost cooldown ownership");
107
+ }
108
+ // Clearing the claimed cooldown intentionally advances the fence. Adopt that exact new
109
+ // generation; any later observation/429 then invalidates publication before a cache write.
110
+ const publicationGeneration = anthropicCooldownGeneration(accountId);
111
+ afterSettlementForTests?.();
112
+ return {
113
+ quota,
114
+ isCurrent: () => probe.isCurrentCredential()
115
+ && anthropicCooldownGeneration(accountId) === publicationGeneration
116
+ && mayPublish(),
117
+ };
118
+ }
@@ -1,7 +1,7 @@
1
1
  import { effectiveCodexAuthAccountId, fetchMainAccountInfoSnapshot, listCodexAuthAccountsSnapshot } from "../../codex/auth-api";
2
2
  import { MAIN_CODEX_ACCOUNT_ID } from "../../codex/main-account";
3
3
  import { getValidAccessToken } from "../../oauth";
4
- import { getAccountCredential, getAccountSet } from "../../oauth/store";
4
+ import { captureOAuthAccountSelection, getAccountCredential, getAccountSet } from "../../oauth/store";
5
5
  import { hydrateKiroAccountState, persistKiroAccountState } from "../kiro-account-state-disk";
6
6
  import { kiroProbeCurrent, kiroProbeIdentity } from "./kiro-account-probe";
7
7
  import { fetchMuseKeyQuotaSnapshot } from "../muse-key-quota";
@@ -18,6 +18,7 @@ import { aggregateCodexPoolCapacity, CODEX_CAPACITY_MAX_QUOTA_AGE_MS, type Codex
18
18
  import { asRecord, normalizePercent, normalizeResetAt, readQuotaJson, REQUEST_TIMEOUT_MS, toFiniteNumber } from "../quota-wire";
19
19
  import { providerCodexAccountMode } from "../registry";
20
20
  import {
21
+ accountReportCurrent,
21
22
  TERMINAL_QUOTA_FAILURE,
22
23
  hasQuotaRows,
23
24
  providerLabel,
@@ -38,6 +39,7 @@ import {
38
39
  } from "./account-cache";
39
40
  import type { OcxConfig, OcxProviderConfig } from "../../types";
40
41
  import type { ProviderQuota, ProviderQuotaWindow } from "../quota-types";
42
+ import { AnthropicQuotaProbeOwnershipError, assertAnthropicQuotaSendAllowed, probeAnthropicQuotaWithRecovery } from "./anthropic-cooldown-recovery";
41
43
 
42
44
  const XAI_BILLING_URL = "https://cli-chat-proxy.grok.com/v1/billing";
43
45
  const XAI_CREDITS_URL = `${XAI_BILLING_URL}?format=credits`;
@@ -262,6 +264,56 @@ function parseClaudeLimit(value: unknown): ProviderQuotaWindow | null {
262
264
  /** Claude's OAuth usage endpoint, probed with ONE account's own bearer token. */
263
265
  const anthropicUsageInflight = new Map<string, Promise<ProviderQuota | null>>();
264
266
 
267
+ async function readAnthropicUsageQuota(accessToken: string): Promise<ProviderQuota | null> {
268
+ const response = await fetch("https://api.anthropic.com/api/oauth/usage", {
269
+ headers: {
270
+ Accept: "application/json, text/plain, */*",
271
+ "Content-Type": "application/json",
272
+ "User-Agent": CLAUDE_CLI_USER_AGENT,
273
+ "anthropic-beta": "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05",
274
+ Authorization: `Bearer ${accessToken}`,
275
+ },
276
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
277
+ });
278
+ if (!response.ok) return null;
279
+ const body = asRecord(await readQuotaJson(response));
280
+ if (!body) return null;
281
+ const fiveHour = parseClaudeBucket(body.five_hour);
282
+ const sevenDay = parseClaudeBucket(body.seven_day);
283
+ const fable = parseClaudeBucket(body.seven_day_fable);
284
+ const opus = parseClaudeBucket(body.seven_day_opus);
285
+ const sonnet = parseClaudeBucket(body.seven_day_sonnet);
286
+ const customWindows: ProviderQuotaWindow[] = [];
287
+ if (fable?.percent !== undefined) customWindows.push({ label: "Fable", scope: "model", percent: fable.percent, ...(fable.resetAt !== undefined ? { resetAt: fable.resetAt } : {}) });
288
+ if (opus?.percent !== undefined) customWindows.push({ label: "Opus", scope: "model", percent: opus.percent, ...(opus.resetAt !== undefined ? { resetAt: opus.resetAt } : {}) });
289
+ if (sonnet?.percent !== undefined) customWindows.push({ label: "Sonnet", scope: "model", percent: sonnet.percent, ...(sonnet.resetAt !== undefined ? { resetAt: sonnet.resetAt } : {}) });
290
+ const knownLabels = new Set(customWindows.map(window => window.label.toLowerCase()));
291
+ const limits = Array.isArray(body.limits) ? body.limits : [];
292
+ for (const rawLimit of limits) {
293
+ const limitRecord = asRecord(rawLimit);
294
+ // `session` and `weekly_all` mirror the canonical five-hour and weekly
295
+ // buckets above; only model-scoped weekly limits add a third window.
296
+ if (String(limitRecord?.kind ?? "").trim().toLowerCase() !== "weekly_scoped") continue;
297
+ const limit = parseClaudeLimit(rawLimit);
298
+ if (!limit || knownLabels.has(limit.label.toLowerCase())) continue;
299
+ knownLabels.add(limit.label.toLowerCase());
300
+ customWindows.push(limit);
301
+ }
302
+ const quota: ProviderQuota = {
303
+ // Claude's 5-hour window is a first-class rate limit, same as the Codex login 5h/weekly
304
+ // rows: report it in the canonical fields so the dashboard renders it with the standard
305
+ // "5-hour limit" label and ordering instead of as a generic extra window.
306
+ ...(fiveHour?.percent !== undefined ? { fiveHourPercent: fiveHour.percent } : {}),
307
+ ...(fiveHour?.resetAt !== undefined ? { fiveHourResetAt: fiveHour.resetAt } : {}),
308
+ ...(sevenDay?.percent !== undefined ? { weeklyPercent: sevenDay.percent } : {}),
309
+ ...(sevenDay?.resetAt !== undefined ? { weeklyResetAt: sevenDay.resetAt } : {}),
310
+ ...(customWindows.length > 0 ? { customWindows } : {}),
311
+ updatedAt: Date.now(),
312
+ };
313
+ // Empty / schema-changed payloads must not cache as "success with no bars".
314
+ return hasQuotaRows(quota) ? quota : null;
315
+ }
316
+
265
317
  /**
266
318
  * Anthropic per-credential usage.
267
319
  *
@@ -277,59 +329,16 @@ const anthropicUsageInflight = new Map<string, Promise<ProviderQuota | null>>();
277
329
  * model-scoped window tracks entitlement rather than seat size. Populate `plan` only when
278
330
  * upstream returns the tier itself.
279
331
  */
280
- export async function fetchAnthropicUsageQuota(accessToken: string): Promise<ProviderQuota | null> {
281
- const joinable = anthropicUsageInflight.get(accessToken);
332
+ export async function fetchAnthropicUsageQuota(
333
+ accessToken: string,
334
+ requireFreshDispatch = false,
335
+ ): Promise<ProviderQuota | null> {
336
+ // Recovery evidence must be requested after the claimed cooldown. Joining an older request
337
+ // can return after the 429 while still describing provider state from before that refusal.
338
+ const joinable = requireFreshDispatch ? undefined : anthropicUsageInflight.get(accessToken);
282
339
  if (joinable) return joinable;
283
340
 
284
- const probe = (async (): Promise<ProviderQuota | null> => {
285
- const response = await fetch("https://api.anthropic.com/api/oauth/usage", {
286
- headers: {
287
- Accept: "application/json, text/plain, */*",
288
- "Content-Type": "application/json",
289
- "User-Agent": CLAUDE_CLI_USER_AGENT,
290
- "anthropic-beta": "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05",
291
- Authorization: `Bearer ${accessToken}`,
292
- },
293
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
294
- });
295
- if (!response.ok) return null;
296
- const body = asRecord(await readQuotaJson(response));
297
- if (!body) return null;
298
- const fiveHour = parseClaudeBucket(body.five_hour);
299
- const sevenDay = parseClaudeBucket(body.seven_day);
300
- const fable = parseClaudeBucket(body.seven_day_fable);
301
- const opus = parseClaudeBucket(body.seven_day_opus);
302
- const sonnet = parseClaudeBucket(body.seven_day_sonnet);
303
- const customWindows: ProviderQuotaWindow[] = [];
304
- if (fable?.percent !== undefined) customWindows.push({ label: "Fable", scope: "model", percent: fable.percent, ...(fable.resetAt !== undefined ? { resetAt: fable.resetAt } : {}) });
305
- if (opus?.percent !== undefined) customWindows.push({ label: "Opus", scope: "model", percent: opus.percent, ...(opus.resetAt !== undefined ? { resetAt: opus.resetAt } : {}) });
306
- if (sonnet?.percent !== undefined) customWindows.push({ label: "Sonnet", scope: "model", percent: sonnet.percent, ...(sonnet.resetAt !== undefined ? { resetAt: sonnet.resetAt } : {}) });
307
- const knownLabels = new Set(customWindows.map(window => window.label.toLowerCase()));
308
- const limits = Array.isArray(body.limits) ? body.limits : [];
309
- for (const rawLimit of limits) {
310
- const limitRecord = asRecord(rawLimit);
311
- // `session` and `weekly_all` mirror the canonical five-hour and weekly
312
- // buckets above; only model-scoped weekly limits add a third window.
313
- if (String(limitRecord?.kind ?? "").trim().toLowerCase() !== "weekly_scoped") continue;
314
- const limit = parseClaudeLimit(rawLimit);
315
- if (!limit || knownLabels.has(limit.label.toLowerCase())) continue;
316
- knownLabels.add(limit.label.toLowerCase());
317
- customWindows.push(limit);
318
- }
319
- const quota: ProviderQuota = {
320
- // Claude's 5-hour window is a first-class rate limit, same as the Codex login 5h/weekly
321
- // rows: report it in the canonical fields so the dashboard renders it with the standard
322
- // "5-hour limit" label and ordering instead of as a generic extra window.
323
- ...(fiveHour?.percent !== undefined ? { fiveHourPercent: fiveHour.percent } : {}),
324
- ...(fiveHour?.resetAt !== undefined ? { fiveHourResetAt: fiveHour.resetAt } : {}),
325
- ...(sevenDay?.percent !== undefined ? { weeklyPercent: sevenDay.percent } : {}),
326
- ...(sevenDay?.resetAt !== undefined ? { weeklyResetAt: sevenDay.resetAt } : {}),
327
- ...(customWindows.length > 0 ? { customWindows } : {}),
328
- updatedAt: Date.now(),
329
- };
330
- // Empty / schema-changed payloads must not cache as "success with no bars".
331
- return hasQuotaRows(quota) ? quota : null;
332
- })().finally(() => {
341
+ const probe = readAnthropicUsageQuota(accessToken).finally(() => {
333
342
  if (anthropicUsageInflight.get(accessToken) === probe) anthropicUsageInflight.delete(accessToken);
334
343
  });
335
344
  anthropicUsageInflight.set(accessToken, probe);
@@ -339,7 +348,8 @@ export async function fetchAnthropicUsageQuota(accessToken: string): Promise<Pro
339
348
  export async function fetchAnthropicQuota(provider: string): Promise<ProviderQuotaReport | null> {
340
349
  // Capture the account we intend to probe before awaiting — a mid-flight active
341
350
  // switch must not seed the wrong account's cache with this response.
342
- const probedAccountId = getAccountSet("anthropic")?.activeAccountId;
351
+ const selection = captureOAuthAccountSelection("anthropic");
352
+ const probedAccountId = selection?.accountId;
343
353
  const probedAccountKey = probedAccountId ? accountCacheKey("anthropic", probedAccountId) : null;
344
354
  const writerGeneration = captureConfigGeneration();
345
355
  let accessToken: string;
@@ -348,17 +358,34 @@ export async function fetchAnthropicQuota(provider: string): Promise<ProviderQuo
348
358
  } catch {
349
359
  return null;
350
360
  }
351
- const quota = await fetchAnthropicUsageQuota(accessToken);
361
+ if (!probedAccountId || !probedAccountKey) return null;
362
+ let quota: ProviderQuota | null;
363
+ let anthropicCurrent: (() => boolean) | undefined;
364
+ try {
365
+ const result = await probeAnthropicQuotaWithRecovery(probedAccountId, accessToken,
366
+ fresh => { assertAnthropicQuotaSendAllowed(probedAccountId, accessToken); return fetchAnthropicUsageQuota(accessToken, fresh); },
367
+ () => mayCommitAccountQuotaKey(probedAccountKey, writerGeneration));
368
+ if (result && !result.isCurrent()) return null;
369
+ quota = result?.quota ?? null;
370
+ anthropicCurrent = result?.isCurrent;
371
+ } catch (error) {
372
+ if (error instanceof AnthropicQuotaProbeOwnershipError) return null;
373
+ throw error;
374
+ }
352
375
  if (!quota) return null;
353
376
  // Share the active-account probe with the per-account cache so Providers-page
354
377
  // loads do not double-hit Anthropic's rate-limited usage endpoint.
355
378
  if (probedAccountId && probedAccountKey) {
356
379
  const stillOwnsToken = getAccountCredential("anthropic", probedAccountId)?.access === accessToken;
357
- if (stillOwnsToken && mayCommitAccountQuotaKey(probedAccountKey, writerGeneration)) {
358
- accountQuotaCache.set(probedAccountKey, { ts: Date.now(), quota });
380
+ if (stillOwnsToken && anthropicCurrent?.() && mayCommitAccountQuotaKey(probedAccountKey, writerGeneration)) {
381
+ accountQuotaCache.set(probedAccountKey, { ts: Date.now(), quota, isCurrent: anthropicCurrent });
359
382
  }
360
383
  }
361
- return report(provider, "anthropic:oauth-usage", quota);
384
+ const quotaReport = report(provider, "anthropic:oauth-usage", quota);
385
+ if (quotaReport && anthropicCurrent) {
386
+ accountReportCurrent.set(quotaReport, () => anthropicCurrent() && getAccountSet("anthropic")?.activeAccountId === probedAccountId);
387
+ }
388
+ return quotaReport;
362
389
  }
363
390
 
364
391
  /**
@@ -68,7 +68,7 @@ import { fetchCommandCodeQuota, fetchKimiQuota, keyQuotaReaderForProvider } from
68
68
  import { antigravityQuotaDiagnosticIdentity, fetchAntigravityQuota, probeAntigravityUsageQuota } from "./quota/antigravity";
69
69
  import { persistKiroAccountState } from "./kiro-account-state-disk";
70
70
  import { kiroProbeCurrent, kiroProbeIdentity } from "./quota/kiro-account-probe";
71
-
71
+ import { AnthropicQuotaProbeOwnershipError, anthropicCooldownFlightKey, assertAnthropicQuotaSendAllowed, probeAnthropicQuotaWithRecovery } from "./quota/anthropic-cooldown-recovery";
72
72
  export type { ProviderQuota, ProviderQuotaCreditsUsd, ProviderQuotaWindow } from "./quota-types";
73
73
  export { QUOTA_RESPONSE_MAX_BYTES } from "./quota-wire";
74
74
  export {
@@ -98,7 +98,6 @@ export {
98
98
  export { fetchAntigravityUsageQuota, isCanonicalAntigravityQuotaUrl, setAntigravityAccountQuotaTransportForTests } from "./quota/antigravity";
99
99
  export { parseOllamaCloudQuota, parseZaiQuotaLimits, providerApiKeyQuotaMode } from "./quota/vendor-probes-key";
100
100
  export { parseXaiCreditsResponse } from "./quota/vendor-probes-oauth";
101
-
102
101
  export async function fetchProviderApiKeyQuotas(config: OcxConfig, name: string, forceRefresh = false): Promise<ProviderApiKeyQuota[]> {
103
102
  const provider = config.providers[name];
104
103
  if (!provider || !keyQuotaReaderForProvider(name, provider)) return [];
@@ -109,7 +108,6 @@ export async function fetchProviderApiKeyQuotas(config: OcxConfig, name: string,
109
108
  return result ? { kind: "quota", quota: result.quota } : { kind: "unavailable" };
110
109
  });
111
110
  }
112
-
113
111
  async function maybeFetchProviderQuota(
114
112
  name: string,
115
113
  provider: OcxProviderConfig,
@@ -338,7 +336,6 @@ export async function fetchProviderQuotaReports(config: OcxConfig, forceRefresh
338
336
  if (inflight.get(key) === entry) inflight.delete(key);
339
337
  }
340
338
  }
341
-
342
339
  async function readExplicitAccountQuota(provider: string, accountId: string, configured?: OcxProviderConfig): Promise<{
343
340
  result: ProviderQuotaProbeResult;
344
341
  identity: string | undefined;
@@ -367,7 +364,6 @@ async function readExplicitAccountQuota(provider: string, accountId: string, con
367
364
  }
368
365
  return { result, identity, isCurrent };
369
366
  }
370
-
371
367
  async function fetchExplicitAccountQuota(provider: string, accountId: string, force: boolean, configured?: OcxProviderConfig): Promise<AccountQuotaCacheEntry> {
372
368
  const key = accountCacheKey(provider, accountId);
373
369
  const identity = explicitQuotaIdentity(provider, accountId, configured);
@@ -403,7 +399,6 @@ async function fetchExplicitAccountQuota(provider: string, accountId: string, fo
403
399
  accountQuotaInflight.set(flightKey, flight);
404
400
  return flight;
405
401
  }
406
-
407
402
  async function fetchExplicitCurrentQuota(provider: string, config: OcxProviderConfig, liveConfig: OcxConfig): Promise<ProviderQuotaProbeResult> {
408
403
  const id = getAccountSet(provider)?.activeAccountId;
409
404
  if (!id) return null;
@@ -428,18 +423,22 @@ async function fetchAccountQuota(
428
423
  const writerGeneration = captureConfigGeneration();
429
424
  const kiroIdentity = provider === "kiro" ? kiroProbeIdentity(accountId) : undefined;
430
425
  const cachedCandidate = accountQuotaCache.get(key);
431
- const cached = provider !== "kiro" || cachedCandidate?.identity === kiroIdentity ? cachedCandidate : undefined;
426
+ const cached = (provider !== "kiro" || cachedCandidate?.identity === kiroIdentity)
427
+ && (provider !== "anthropic" || cachedCandidate?.isCurrent?.() !== false) ? cachedCandidate : undefined;
432
428
  if (!forceRefresh && cached && Date.now() - cached.ts < ACCOUNT_QUOTA_TTL_MS) {
433
429
  if (provider === "google-antigravity" && cached.quotaFailure && cached.quotaFailureIsCurrent?.() !== true) return { ...cached, quotaFailure: undefined };
434
430
  return provider === "anthropic" ? { ...cached, quota: normalizeAnthropicQuota(cached.quota, Date.now()) } : cached;
435
431
  }
436
- const joinable = accountQuotaInflight.get(key);
432
+ const flightKey = provider === "anthropic" ? anthropicCooldownFlightKey(key, accountId) : key;
433
+ const flightCurrent = () => provider !== "anthropic" || anthropicCooldownFlightKey(key, accountId) === flightKey;
434
+ const joinable = accountQuotaInflight.get(flightKey);
437
435
  if (joinable) {
438
436
  const joined = await joinable;
437
+ const joinedCurrent = joined.isCurrent?.();
438
+ if (joinedCurrent === false || (!flightCurrent() && joinedCurrent !== true)) return { ts: Date.now(), quota: null, unavailable: true };
439
439
  return provider !== "kiro" || joined.identity === kiroIdentity
440
440
  ? joined : fetchAccountQuota(provider, accountId, true, providerConfig);
441
441
  }
442
-
443
442
  const epoch = explicitAccountEpoch;
444
443
  const probe = (async (): Promise<AccountQuotaCacheEntry> => {
445
444
  let diagnosticIdentity: string | undefined;
@@ -449,15 +448,36 @@ async function fetchAccountQuota(
449
448
  catch { return false; }
450
449
  };
451
450
  const diagnosticFields = () => quotaFailure && quotaFailureIsCurrent() ? { quotaFailure, quotaFailureIsCurrent } : {};
451
+ const unavailable = (): AccountQuotaCacheEntry => {
452
+ if (!flightCurrent()) return { ts: Date.now(), quota: null, unavailable: true };
453
+ const previous = accountQuotaCache.get(key);
454
+ const retained = provider === "anthropic" && previous?.isCurrent?.() === false ? undefined : previous;
455
+ const entry: AccountQuotaCacheEntry = {
456
+ ts: Date.now(),
457
+ quota: provider === "anthropic" ? normalizeAnthropicQuota(retained?.quota, Date.now()) : cached?.quota ?? null,
458
+ unavailable: true,
459
+ ...(provider === "anthropic" && retained?.isCurrent ? { isCurrent: retained.isCurrent } : {}),
460
+ ...(provider === "kiro" ? { identity: kiroIdentity } : {}),
461
+ ...diagnosticFields(),
462
+ };
463
+ if (mayCommitAccountQuotaKey(key, writerGeneration) && (provider !== "kiro" || kiroProbeCurrent(accountId, kiroIdentity))) {
464
+ accountQuotaCache.set(key, entry);
465
+ if (provider === "kiro") persistKiroAccountState();
466
+ sweepExpiredOnWrite(entry.ts);
467
+ }
468
+ return entry;
469
+ };
452
470
  try {
453
471
  if (provider === "google-antigravity") diagnosticIdentity = antigravityQuotaDiagnosticIdentity(accountId);
454
472
  let quota: ProviderQuota | null;
473
+ let anthropicCurrent: (() => boolean) | undefined;
455
474
  let kiroSnapshot: KiroUsageSnapshot | null = null;
456
475
  if (provider === "kiro") {
457
476
  kiroSnapshot = await fetchKiroUsageSnapshot(await kiroUsageContextForAccount(accountId));
458
477
  quota = kiroSnapshot?.quota ?? null;
459
478
  } else {
460
479
  const token = await getTokenForAccountQuotaProbe(provider, accountId);
480
+ if (!flightCurrent()) throw new AnthropicQuotaProbeOwnershipError("anthropic quota flight is stale");
461
481
  if (provider === "google-antigravity") {
462
482
  // Per-account Gem/Cla windows (#1082). The project id is part of the stored
463
483
  // credential; without it the probe cannot be made, and that is "unavailable",
@@ -469,62 +489,40 @@ async function fetchAccountQuota(
469
489
  quota = result.kind === "available" ? result.quota : null;
470
490
  if (result.kind === "unavailable") quotaFailure = result.failure;
471
491
  } else if (provider === "anthropic") {
472
- quota = await fetchAnthropicUsageQuota(token);
492
+ const result = await probeAnthropicQuotaWithRecovery(accountId, token,
493
+ fresh => { assertAnthropicQuotaSendAllowed(accountId, token); return fetchAnthropicUsageQuota(token, fresh); },
494
+ () => mayCommitAccountQuotaKey(key, writerGeneration));
495
+ if (result && !result.isCurrent()) throw new AnthropicQuotaProbeOwnershipError("anthropic quota probe lost publication ownership");
496
+ quota = result?.quota ?? null;
497
+ anthropicCurrent = result?.isCurrent;
473
498
  } else {
474
499
  return { ts: Date.now(), quota: null, unavailable: true };
475
500
  }
476
501
  }
477
- if (!quota) {
478
- // Preserve last-good bars while negative-caching the failed probe.
479
- const entry: AccountQuotaCacheEntry = {
480
- ts: Date.now(),
481
- quota: provider === "anthropic"
482
- ? normalizeAnthropicQuota(accountQuotaCache.get(key)?.quota, Date.now()) : cached?.quota ?? null,
483
- unavailable: true,
484
- ...(provider === "kiro" ? { identity: kiroIdentity } : {}),
485
- ...diagnosticFields(),
486
- };
487
- if (mayCommitAccountQuotaKey(key, writerGeneration) && (provider !== "kiro" || kiroProbeCurrent(accountId, kiroIdentity))) {
488
- accountQuotaCache.set(key, entry);
489
- if (provider === "kiro") persistKiroAccountState();
490
- sweepExpiredOnWrite(entry.ts);
491
- }
492
- return entry;
493
- }
502
+ if (!quota) return unavailable();
494
503
  const entry: AccountQuotaCacheEntry = {
495
504
  ts: Date.now(), quota: provider === "anthropic" ? normalizeAnthropicQuota(quota, Date.now()) : quota,
496
505
  ...(provider === "kiro" ? { identity: kiroIdentity } : {}),
506
+ ...(anthropicCurrent ? { isCurrent: anthropicCurrent } : {}),
497
507
  };
498
- if (mayCommitAccountQuotaKey(key, writerGeneration) && (provider !== "kiro" || kiroProbeCurrent(accountId, kiroIdentity))) {
508
+ if (mayCommitAccountQuotaKey(key, writerGeneration)
509
+ && (provider !== "kiro" || kiroProbeCurrent(accountId, kiroIdentity))) {
499
510
  accountQuotaCache.set(key, entry);
500
511
  if (provider === "kiro") { commitKiroAccountUsageState(key, kiroSnapshot, kiroIdentity); persistKiroAccountState(); }
501
512
  sweepExpiredOnWrite(entry.ts);
502
513
  }
503
514
  return entry;
504
- } catch {
515
+ } catch (error) {
516
+ if (provider === "anthropic" && error instanceof AnthropicQuotaProbeOwnershipError) return { ts: Date.now(), quota: null, unavailable: true };
505
517
  if (provider === "google-antigravity") quotaFailure = "account_unavailable";
506
- const entry: AccountQuotaCacheEntry = {
507
- ts: Date.now(),
508
- quota: provider === "anthropic"
509
- ? normalizeAnthropicQuota(accountQuotaCache.get(key)?.quota, Date.now()) : cached?.quota ?? null,
510
- unavailable: true,
511
- ...(provider === "kiro" ? { identity: kiroIdentity } : {}),
512
- ...diagnosticFields(),
513
- };
514
- if (mayCommitAccountQuotaKey(key, writerGeneration) && (provider !== "kiro" || kiroProbeCurrent(accountId, kiroIdentity))) {
515
- accountQuotaCache.set(key, entry);
516
- if (provider === "kiro") persistKiroAccountState();
517
- sweepExpiredOnWrite(entry.ts);
518
- }
519
- return entry;
518
+ return unavailable();
520
519
  }
521
520
  })().finally(() => {
522
- if (accountQuotaInflight.get(key) === probe) accountQuotaInflight.delete(key);
521
+ if (accountQuotaInflight.get(flightKey) === probe) accountQuotaInflight.delete(flightKey);
523
522
  });
524
- accountQuotaInflight.set(key, probe);
523
+ accountQuotaInflight.set(flightKey, probe);
525
524
  return probe;
526
525
  }
527
-
528
526
  /**
529
527
  * Per-account quota rows for a provider's logged-in accounts. Probes run in parallel; a
530
528
  * single failing account never blocks the others.
@@ -546,6 +544,7 @@ export async function fetchProviderAccountQuotas(
546
544
  ...(entry.unavailable && entry.quotaFailure && entry.quotaFailureIsCurrent?.() === true ? { quotaFailure: entry.quotaFailure } : {}),
547
545
  };
548
546
  if (entry.quotaFailureIsCurrent) Object.defineProperty(result, "quotaFailureIsCurrent", { value: entry.quotaFailureIsCurrent });
547
+ if (provider === "anthropic" && entry.isCurrent) Object.defineProperty(result, "isCurrent", { value: entry.isCurrent });
549
548
  if (!explicitAccountReader(provider)) return result;
550
549
  const identity = entry.identity;
551
550
  Object.defineProperty(result, "isCurrent", { value: () => {
@@ -190,7 +190,7 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
190
190
  // (it is the current catalog, so its default ordering wins), then the ids
191
191
  // only the old devin entry carried. Degraded-mode seed only either way —
192
192
  // `liveModels` discovers the account's real roster.
193
- models: ["swe-2", "swe-1-7", "gpt-5-6-sol", "gpt-6-astra", "gpt-6-sol", "gpt-6-luna", "claude-opus-5-5", "claude-opus-5", "claude-fable-5-1", "claude-sonnet-5-5", "claude-sonnet-5", "glm-5-3", "kimi-k3", "gemini-3-8-flash", "grok-4-6", "grok-4-7", "swe-1-7-lightning", "gpt-5-6-luna", "gpt-5-6-terra", "claude-opus-4-8", "glm-5-2", "kimi-k2-7", "grok-4-5"],
193
+ models: ["swe-2", "swe-1-7", "gpt-5-6-sol", "gpt-6-astra", "gpt-6-1-sol", "gpt-6-sol", "gpt-6-luna", "claude-opus-5-5", "claude-opus-5", "claude-fable-5-1", "claude-sonnet-5-5", "claude-sonnet-5", "glm-5-3", "kimi-k3", "gemini-3-8-flash", "grok-4-6", "grok-4-7", "swe-1-7-lightning", "gpt-5-6-luna", "gpt-5-6-terra", "claude-opus-4-8", "glm-5-2", "kimi-k2-7", "grok-4-5"],
194
194
  liveModels: true,
195
195
  defaultModel: "swe-2",
196
196
  modelContextWindows: DEVIN_MODEL_CONTEXT_WINDOWS,
@@ -1025,6 +1025,8 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
1025
1025
  // 260923 preemptive: GPT-6 Sol/Luna are OpenAI-backed routes like the GPT-5.6 rows above.
1026
1026
  "openai/gpt-6-sol": true,
1027
1027
  "openai/gpt-6-luna": true,
1028
+ // Listed 2026-09-29 as an OpenAI-backed route like GPT-6 Sol.
1029
+ "openai/gpt-6.1-sol": true,
1028
1030
  },
1029
1031
  // Deliberately no OpenRouter route pin: it bills the endpoint actually used and reports the
1030
1032
  // actual service_tier. B0 confirmation therefore owns downgrade safety. Forcing `only` plus