@bitkyc08/opencodex 2.80.0 → 2.81.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 (227) hide show
  1. package/README.md +3 -3
  2. package/gui/dist/assets/App-7yhoys8a.js +54 -0
  3. package/gui/dist/assets/Tray-EwAOsP0S.js +1 -0
  4. package/gui/dist/assets/index-RsoaAxXY.js +94 -0
  5. package/gui/dist/assets/quota-summary-D2lN8tDm.js +1 -0
  6. package/gui/dist/index.html +1 -1
  7. package/gui/dist/provider-icons/claude-green.svg +1 -0
  8. package/package.json +3 -2
  9. package/src/adapters/anthropic-model-contract.ts +8 -2
  10. package/src/adapters/anthropic.ts +7 -2
  11. package/src/adapters/codebuddy/mcp-server.ts +2 -25
  12. package/src/adapters/codebuddy/tool-bridge.ts +1 -7
  13. package/src/adapters/coding-agent/mcp-server.ts +33 -0
  14. package/src/adapters/coding-agent/protocol.ts +122 -17
  15. package/src/adapters/coding-agent/tool-catalog.ts +75 -0
  16. package/src/adapters/coding-agent/turn.ts +101 -44
  17. package/src/adapters/cursor/catalog.ts +8 -0
  18. package/src/adapters/cursor/effort-map.ts +2 -0
  19. package/src/adapters/devin/live-models.ts +2 -0
  20. package/src/adapters/kiro-retry.ts +9 -4
  21. package/src/adapters/openai-chat/messages.ts +7 -6
  22. package/src/adapters/physical-send.ts +8 -5
  23. package/src/adapters/qoder/adapter.ts +41 -6
  24. package/src/adapters/qoder/scaffold-guard.ts +2 -3
  25. package/src/adapters/zed.ts +16 -1
  26. package/src/claude/inbound-cache-stabilize.ts +4 -9
  27. package/src/claude/inbound.ts +1 -0
  28. package/src/claude/intercept/model-bindings.ts +1 -0
  29. package/src/claude/intercept/picker-listener.ts +71 -4
  30. package/src/cli/account-anthropic-threshold.ts +14 -5
  31. package/src/cli/account-api.ts +3 -1
  32. package/src/cli/account-extended.ts +13 -9
  33. package/src/cli/account-policy-dto.ts +1 -1
  34. package/src/cli/account-policy.ts +13 -4
  35. package/src/cli/account.ts +3 -3
  36. package/src/cli/capabilities-accounts.ts +2 -2
  37. package/src/cli/capabilities-agents-routing.ts +24 -0
  38. package/src/cli/capabilities-integrations.ts +16 -0
  39. package/src/cli/codex-shim-autorestore.ts +3 -0
  40. package/src/cli/dispatch.ts +10 -1
  41. package/src/cli/help.ts +4 -1
  42. package/src/cli/index.ts +11 -5
  43. package/src/cli/integration-plan-dto.ts +1 -0
  44. package/src/cli/integration-preview.ts +8 -1
  45. package/src/cli/integrations.ts +32 -3
  46. package/src/cli/message-args.ts +34 -0
  47. package/src/cli/message-command.ts +43 -0
  48. package/src/cli/message-runtime.ts +6 -0
  49. package/src/cli/observe.ts +3 -1
  50. package/src/cli/provider.ts +29 -6
  51. package/src/cli/registry.ts +24 -2
  52. package/src/clients/config-export/commandcode.ts +131 -0
  53. package/src/clients/config-export/contracts.ts +12 -0
  54. package/src/clients/config-export.ts +64 -1
  55. package/src/codex/auth-api/account-list.ts +11 -3
  56. package/src/codex/auth-api/main-account-probe.ts +25 -8
  57. package/src/codex/auth-api/pool-mode-gate.ts +33 -9
  58. package/src/codex/auth-api/pool-quota-probe.ts +15 -1
  59. package/src/codex/auth-api/routes.ts +1 -0
  60. package/src/codex/catalog/auto-review.ts +13 -6
  61. package/src/codex/catalog/build-entries.ts +15 -7
  62. package/src/codex/catalog/control-plane.ts +45 -0
  63. package/src/codex/catalog/effort.ts +11 -1
  64. package/src/codex/catalog/gather-capture.ts +5 -3
  65. package/src/codex/catalog/parsing.ts +2 -1
  66. package/src/codex/catalog/provider-models.ts +15 -9
  67. package/src/codex/control-plane-models.ts +6 -0
  68. package/src/combos/jev-decision-contract.ts +25 -2
  69. package/src/combos/jev.ts +3 -2
  70. package/src/combos/types.ts +1 -1
  71. package/src/config/diagnostics.ts +27 -16
  72. package/src/config/load-degrade.ts +34 -0
  73. package/src/config/schema/anthropic-account-pool.ts +143 -0
  74. package/src/config/schema/config-schema.ts +7 -1
  75. package/src/config/schema/leaf-validators.ts +4 -0
  76. package/src/config.ts +2 -0
  77. package/src/generated/compatibility-version.json +320 -184
  78. package/src/generated/model-metadata.ts +5 -4
  79. package/src/images/loop.ts +2 -1
  80. package/src/integrations/catalog-refresh.ts +1 -1
  81. package/src/integrations/mutation-plan.ts +7 -0
  82. package/src/integrations/registry.ts +8 -0
  83. package/src/integrations/state.ts +9 -2
  84. package/src/integrations/writer.ts +8 -2
  85. package/src/lib/lab-live-route-production.ts +9 -1
  86. package/src/lib/request-execution-budget.ts +236 -58
  87. package/src/lib/spend-pool-alias-validation.ts +79 -0
  88. package/src/lib/spend-pool-continuity.ts +34 -0
  89. package/src/lib/spend-reservation-ledger.ts +412 -50
  90. package/src/lib/state-store-registrations.ts +20 -3
  91. package/src/lib/upstream-retry.ts +17 -5
  92. package/src/lib/workflow-budget.ts +53 -11
  93. package/src/messaging/budget.ts +45 -0
  94. package/src/messaging/discovery.ts +57 -0
  95. package/src/messaging/envelope.ts +49 -0
  96. package/src/messaging/input.ts +37 -0
  97. package/src/messaging/rpc.ts +196 -0
  98. package/src/messaging/send.ts +58 -0
  99. package/src/messaging/socket.ts +72 -0
  100. package/src/messaging/types.ts +29 -0
  101. package/src/oauth/anthropic-account-refusal.ts +51 -17
  102. package/src/oauth/anthropic-account-threshold.ts +8 -2
  103. package/src/oauth/anthropic-continuity.ts +15 -1
  104. package/src/oauth/anthropic-model-quota.ts +120 -48
  105. package/src/oauth/anthropic-model-routes.ts +11 -3
  106. package/src/oauth/anthropic-oauth-definitions.ts +98 -0
  107. package/src/oauth/anthropic-pool-config.ts +30 -0
  108. package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
  109. package/src/oauth/anthropic-routing.ts +1202 -866
  110. package/src/oauth/anthropic-send-ownership.ts +41 -0
  111. package/src/oauth/anthropic.ts +4 -1
  112. package/src/oauth/generic-account-failover.ts +3 -1
  113. package/src/oauth/health.ts +8 -6
  114. package/src/oauth/index.ts +90 -173
  115. package/src/oauth/kiro.ts +22 -9
  116. package/src/oauth/login-cli.ts +2 -0
  117. package/src/oauth/model-discovery-auth.ts +43 -0
  118. package/src/oauth/pool-kernel.ts +11 -5
  119. package/src/oauth/pool-settings-capability.ts +5 -3
  120. package/src/oauth/store-anthropic-instance.ts +87 -0
  121. package/src/oauth/store.ts +67 -9
  122. package/src/oauth/token-guardian.ts +2 -0
  123. package/src/oauth/types.ts +2 -0
  124. package/src/protocols/plan-snapshot.ts +5 -0
  125. package/src/protocols/settings.ts +20 -5
  126. package/src/providers/anthropic-instance-id.ts +47 -0
  127. package/src/providers/anthropic-instance.ts +48 -0
  128. package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
  129. package/src/providers/derive.ts +2 -0
  130. package/src/providers/kiro-models.ts +3 -0
  131. package/src/providers/label.ts +1 -1
  132. package/src/providers/model-rename-fields.ts +2 -0
  133. package/src/providers/quota/account-cache.ts +37 -11
  134. package/src/providers/quota/anthropic-account-quota.ts +93 -0
  135. package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
  136. package/src/providers/quota/report-cache.ts +24 -1
  137. package/src/providers/quota/vendor-probes-oauth.ts +46 -14
  138. package/src/providers/quota.ts +22 -32
  139. package/src/providers/registry/entries-core.ts +35 -30
  140. package/src/providers/registry/model-ids.ts +1 -0
  141. package/src/providers/registry/model-seeds.ts +7 -4
  142. package/src/providers/registry/types.ts +3 -0
  143. package/src/providers/registry.ts +3 -3
  144. package/src/responses/spill-store.ts +5 -1
  145. package/src/responses/state/metrics.ts +87 -0
  146. package/src/responses/state/snapshot-codec.ts +6 -0
  147. package/src/responses/state/spill-queue.ts +1 -0
  148. package/src/responses/state/unforced-store-false.ts +55 -0
  149. package/src/responses/state.ts +50 -66
  150. package/src/router.ts +28 -4
  151. package/src/routing/compatibility/assemble.ts +1 -1
  152. package/src/routing/quota.ts +7 -3
  153. package/src/server/auth-cors.ts +5 -0
  154. package/src/server/chat-completions.ts +6 -1
  155. package/src/server/chat-native.ts +45 -25
  156. package/src/server/claude-messages.ts +16 -2
  157. package/src/server/gui-static.ts +38 -4
  158. package/src/server/index/spend-ledger-lifecycle.ts +10 -4
  159. package/src/server/index.ts +4 -2
  160. package/src/server/inference/context.ts +1 -1
  161. package/src/server/lifecycle.ts +1 -1
  162. package/src/server/management/agent-settings-routes.ts +30 -6
  163. package/src/server/management/anthropic-account-threshold.ts +12 -9
  164. package/src/server/management/anthropic-pool-settings.ts +72 -0
  165. package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
  166. package/src/server/management/config-routes.ts +27 -7
  167. package/src/server/management/decision-routes.ts +2 -1
  168. package/src/server/management/integration-routes.ts +25 -2
  169. package/src/server/management/oauth-account-routes.ts +79 -98
  170. package/src/server/management/provider-instance-ownership.ts +44 -0
  171. package/src/server/management/provider-routes.ts +21 -28
  172. package/src/server/management/route-registry.ts +3 -1
  173. package/src/server/management/routing-profile-routes.ts +4 -4
  174. package/src/server/management/system-restart.ts +44 -14
  175. package/src/server/management/system-routes.ts +24 -2
  176. package/src/server/management/vision-sidecar-options.ts +9 -6
  177. package/src/server/management/web-search-sidecar-options.ts +51 -2
  178. package/src/server/management-auth.ts +6 -0
  179. package/src/server/messages-native-eligibility.ts +3 -2
  180. package/src/server/messages-native-oauth.ts +86 -41
  181. package/src/server/messages-native-selector.ts +39 -0
  182. package/src/server/messages-native.ts +84 -25
  183. package/src/server/request-log.ts +5 -0
  184. package/src/server/responses/adapter-continuation.ts +29 -17
  185. package/src/server/responses/adapter-dispatch.ts +47 -17
  186. package/src/server/responses/antigravity-validation-refusal.ts +20 -6
  187. package/src/server/responses/compact.ts +25 -8
  188. package/src/server/responses/compaction-recovery.ts +1 -1
  189. package/src/server/responses/core-combo-native.ts +4 -1
  190. package/src/server/responses/core-combo.ts +16 -11
  191. package/src/server/responses/core-normalize.ts +1 -0
  192. package/src/server/responses/core-options.ts +3 -1
  193. package/src/server/responses/core-replay.ts +5 -0
  194. package/src/server/responses/passthrough-delivery.ts +7 -3
  195. package/src/server/responses/passthrough-dispatch.ts +21 -8
  196. package/src/server/responses/request-prepare.ts +37 -5
  197. package/src/server/responses/request-send-budget.ts +74 -37
  198. package/src/server/responses/request-spend.ts +325 -33
  199. package/src/server/responses/request-transport.ts +132 -52
  200. package/src/server/responses/run-turn-execution.ts +11 -3
  201. package/src/server/responses/sidecar-execution.ts +96 -14
  202. package/src/server/responses-undeclared-tool-guard.ts +5 -1
  203. package/src/server/workflow-refusal.ts +30 -0
  204. package/src/service/ownership-mutation-lease.d.mts +1 -1
  205. package/src/service/ownership-mutation-lease.mjs +5 -7
  206. package/src/sidecar/anthropic-binding.ts +79 -0
  207. package/src/sidecar/auth.ts +76 -2
  208. package/src/types/anthropic-account-pool.ts +33 -0
  209. package/src/types/config.ts +12 -28
  210. package/src/types/provider.ts +5 -0
  211. package/src/types/tools.ts +7 -0
  212. package/src/types.ts +4 -0
  213. package/src/usage/cost.ts +3 -2
  214. package/src/usage/expected-prices.ts +66 -14
  215. package/src/vision/anthropic-describe.ts +16 -3
  216. package/src/vision/index.ts +3 -0
  217. package/src/vision/plan.ts +19 -6
  218. package/src/web-search/alpha-search.ts +7 -1
  219. package/src/web-search/anthropic-executor.ts +16 -4
  220. package/src/web-search/index.ts +7 -3
  221. package/src/web-search/loop.ts +27 -5
  222. package/src/web-search/passthrough-bridge.ts +4 -1
  223. package/src/web-search/sidecar-providers.ts +4 -3
  224. package/gui/dist/assets/App-MbDVfUeC.js +0 -54
  225. package/gui/dist/assets/Tray-CzkxK82A.js +0 -1
  226. package/gui/dist/assets/index-UJt6npXk.js +0 -94
  227. package/gui/dist/assets/quota-summary-DU3sD6Z-.js +0 -1
@@ -1,48 +1,15 @@
1
1
  import { mergeAnthropicFamilyWindows, hasAnthropicFamilyEnumeration } from "./anthropic-family-headers";
2
- import { getCachedProviderAccountQuota } from "./account-cache";
3
- import { observeAnthropicFamilyQuota, anthropicFamilyQuotaGeneration } from "../../oauth/anthropic-model-quota";
4
- import { credentialGeneration, getAccountCredential, getAccountCredentialWithStatus } from "../../oauth/store";
2
+ import { getCachedProviderAccountQuota, captureProviderAccountQuotaEpoch } from "./account-cache";
3
+ import { anthropicModelQuotaFor } from "../../oauth/anthropic-model-quota";
4
+ import type { AnthropicInstanceId } from "../anthropic-instance-id";
5
+ import type { GenerationContext } from "../../lib/state-store-sweeper";
6
+ import { credentialGeneration, getAccountCredential, getAccountCredentialWithStatus, getAccountSet } from "../../oauth/store";
5
7
  import type { ProviderQuota } from "../quota-types";
6
8
 
7
9
  export class AnthropicQuotaProbeOwnershipError extends Error {}
8
10
 
9
- /** Check live pause and bearer ownership at the last synchronous step before usage dispatch. */
10
- export function assertAnthropicQuotaSendAllowed(accountId: string, token: string): void {
11
- const row = getAccountCredentialWithStatus("anthropic", accountId);
12
- if (!row || row.paused || row.needsReauth || row.credential.access !== token) {
13
- throw new AnthropicQuotaProbeOwnershipError("anthropic quota account is no longer eligible");
14
- }
15
- }
16
-
17
- const cooldownGenerationByAccount = new Map<string, number>();
18
- let nextCooldownGeneration = 1;
19
- let afterSettlementForTests: (() => void) | undefined;
20
-
21
- export function setAnthropicQuotaAfterSettlementForTests(hook: (() => void) | undefined): void {
22
- afterSettlementForTests = hook;
23
- }
24
-
25
- /** Monotonic per-account fence shared by routing mutations and quota publication. */
26
- export function anthropicCooldownGeneration(accountId: string): number {
27
- return cooldownGenerationByAccount.get(accountId) ?? 0;
28
- }
29
-
30
- /** Keep same-state quota calls joinable while moving post-429 calls to a fresh flight. */
31
- export function anthropicCooldownFlightKey(baseKey: string, accountId: string): string {
32
- return `${baseKey}\0anthropic-cooldown:${anthropicCooldownGeneration(accountId)}`;
33
- }
34
-
35
- export function noteAnthropicCooldownMutation(accountId: string): number {
36
- const generation = nextCooldownGeneration++;
37
- cooldownGenerationByAccount.set(accountId, generation);
38
- return generation;
39
- }
40
-
41
- export function clearAnthropicCooldownGenerations(): void {
42
- cooldownGenerationByAccount.clear();
43
- }
44
-
45
11
  export type AnthropicCooldownRecoveryProbe = Readonly<{
12
+ instance: AnthropicInstanceId;
46
13
  requiresFreshDispatch: boolean;
47
14
  isCurrentCredential(): boolean;
48
15
  isCurrentFamily(): boolean;
@@ -51,86 +18,230 @@ export type AnthropicCooldownRecoveryProbe = Readonly<{
51
18
  }>;
52
19
 
53
20
  export type AnthropicQuotaRecoveryResult = Readonly<{
21
+ instance: AnthropicInstanceId;
54
22
  quota: ProviderQuota;
55
23
  /** Must be checked synchronously at each publication boundary. */
56
24
  isCurrent(): boolean;
57
25
  }>;
58
26
 
59
- /**
60
- * Bind a quota probe to both the credential and cooldown generations it observed before
61
- * dispatch. A successful response may otherwise arrive after either credential replacement
62
- * or a newer 429 and incorrectly make unrelated state eligible.
63
- */
64
- export async function captureAnthropicCooldownRecoveryProbe(
65
- accountId: string,
66
- accessToken: string,
67
- ): Promise<AnthropicCooldownRecoveryProbe | null> {
68
- const credential = getAccountCredential("anthropic", accountId);
69
- if (!credential || credential.access !== accessToken) return null;
70
- const generation = credentialGeneration(credential);
71
- // Lazy because anthropic-routing reads the quota cache on normal request routing.
72
- const routing = await import("../../oauth/anthropic-routing");
73
- const cooldownGeneration = anthropicCooldownGeneration(accountId);
74
- const familyGeneration = anthropicFamilyQuotaGeneration(accountId);
75
- const claim = routing.captureAnthropicCooldownRecovery(accountId);
76
- const isCurrentCredential = () => {
77
- const current = getAccountCredential("anthropic", accountId);
78
- return !!current && credentialGeneration(current) === generation;
79
- };
80
- return {
81
- requiresFreshDispatch: claim !== null,
82
- isCurrentCredential,
83
- isCurrentFamily: () => anthropicFamilyQuotaGeneration(accountId) === familyGeneration,
84
- isCurrent: () => isCurrentCredential()
85
- && anthropicCooldownGeneration(accountId) === cooldownGeneration,
86
- settle: quota => claim === null ? "retained"
87
- : routing.settleAnthropicCooldownRecovery(claim, quota),
88
- };
89
- }
27
+ function createAnthropicCooldownRecovery(instance: AnthropicInstanceId) {
28
+ const familyGeneration = (accountId: string) => anthropicModelQuotaFor(instance).anthropicFamilyQuotaGeneration(accountId);
90
29
 
91
- /** Run one authoritative usage read and publish its recovery effect only while still owned. */
92
- export async function probeAnthropicQuotaWithRecovery(
93
- accountId: string,
94
- accessToken: string,
95
- read: (requireFreshDispatch: boolean) => Promise<ProviderQuota | null>,
96
- mayPublish: () => boolean,
97
- ): Promise<AnthropicQuotaRecoveryResult | null> {
98
- const probe = await captureAnthropicCooldownRecoveryProbe(accountId, accessToken);
99
- if (!probe) throw new AnthropicQuotaProbeOwnershipError("anthropic quota probe lost credential ownership");
100
- let quota: ProviderQuota | null;
101
- try { quota = await read(probe.requiresFreshDispatch); }
102
- catch (error) {
103
- if (!probe.isCurrent() || !mayPublish()) throw new AnthropicQuotaProbeOwnershipError("anthropic quota probe failure is stale");
104
- throw error;
30
+ /** Check live pause and bearer ownership at the last synchronous step before usage dispatch. */
31
+ function assertAnthropicQuotaSendAllowed(accountId: string, token: string): void {
32
+ const row = getAccountCredentialWithStatus(instance, accountId);
33
+ if (!row || row.paused || row.needsReauth || row.credential.access !== token) {
34
+ throw new AnthropicQuotaProbeOwnershipError(`${instance} quota account is no longer eligible`);
35
+ }
36
+ }
37
+
38
+ let cooldownGenerationByAccount: Map<string, number> | undefined;
39
+ let emptyGeneration = 0;
40
+ let nextCooldownGeneration = 1;
41
+ // Account lifetime is distinct from refusal/probe mutations; a valid refusal must not revoke itself.
42
+ let accountIncarnations: Map<string, number> | undefined;
43
+ let emptyAccountIncarnation = 0;
44
+ let nextAccountIncarnation = 1;
45
+ let afterSettlementForTests: (() => void) | undefined;
46
+
47
+ function setAnthropicQuotaAfterSettlementForTests(hook: (() => void) | undefined): void {
48
+ afterSettlementForTests = hook;
49
+ }
50
+
51
+ /** Monotonic per-account fence shared by routing mutations and quota publication. */
52
+ function anthropicCooldownGeneration(accountId: string): number {
53
+ return cooldownGenerationByAccount?.get(accountId) ?? emptyGeneration;
54
+ }
55
+
56
+ /** Keep same-state quota calls joinable while moving post-429 calls to a fresh flight. */
57
+ function anthropicCooldownFlightKey(baseKey: string, accountId: string): string {
58
+ // Flight reservation establishes an incarnation even without prior refusal evidence.
59
+ const generation = reserveCooldownGeneration(accountId);
60
+ return `${baseKey}\0${instance}-cooldown:${generation}`;
61
+ }
62
+
63
+ /** Active usage readers bind this key after token refresh; renewal keeps the legacy flight valid. */
64
+ function anthropicCredentialQuotaFlightKey(baseKey: string, accountId: string): string {
65
+ const row = getAccountSet(instance)?.accounts.find(row => row.id === accountId);
66
+ return `${anthropicCooldownFlightKey(baseKey, accountId)}\0${row ? JSON.stringify([credentialGeneration(row.credential), row.loginId, row.addedAt]) : "missing"}`;
105
67
  }
106
- if (!probe.isCurrent() || !mayPublish()) {
107
- throw new AnthropicQuotaProbeOwnershipError("anthropic quota probe result is stale");
68
+
69
+ function reserveCooldownGeneration(accountId: string): number {
70
+ return cooldownGenerationByAccount?.get(accountId) ?? noteAnthropicCooldownMutation(accountId);
71
+ }
72
+
73
+ function noteAnthropicCooldownMutation(accountId: string): number {
74
+ const generation = nextCooldownGeneration++;
75
+ (cooldownGenerationByAccount ??= new Map()).set(accountId, generation);
76
+ return generation;
77
+ }
78
+
79
+ function anthropicAccountIncarnation(accountId: string): number {
80
+ return accountIncarnations?.get(accountId) ?? emptyAccountIncarnation;
81
+ }
82
+
83
+ function reserveAnthropicAccountIncarnation(accountId: string): number {
84
+ const existing = accountIncarnations?.get(accountId);
85
+ if (existing !== undefined) return existing;
86
+ const incarnation = nextAccountIncarnation++;
87
+ (accountIncarnations ??= new Map()).set(accountId, incarnation);
88
+ return incarnation;
89
+ }
90
+
91
+ function clearAnthropicCooldownGenerations(): void {
92
+ emptyGeneration = nextCooldownGeneration++;
93
+ cooldownGenerationByAccount = undefined;
94
+ emptyAccountIncarnation = nextAccountIncarnation++;
95
+ accountIncarnations = undefined;
96
+ }
97
+
98
+ /**
99
+ * Bind a quota probe to both the credential and cooldown generations it observed before
100
+ * dispatch. A successful response may otherwise arrive after either credential replacement
101
+ * or a newer 429 and incorrectly make unrelated state eligible.
102
+ */
103
+ async function captureAnthropicCooldownRecoveryProbe(
104
+ accountId: string,
105
+ accessToken: string,
106
+ ): Promise<AnthropicCooldownRecoveryProbe | null> {
107
+ const credential = getAccountCredential(instance, accountId);
108
+ if (!credential || credential.access !== accessToken) return null;
109
+ const generation = credentialGeneration(credential);
110
+ const incarnation = reserveAnthropicAccountIncarnation(accountId);
111
+ const quotaEpoch = captureProviderAccountQuotaEpoch(instance);
112
+ const initialRow = getAccountSet(instance)?.accounts.find(row => row.id === accountId);
113
+ if (!initialRow || initialRow.paused || initialRow.needsReauth || credentialGeneration(initialRow.credential) !== generation) return null;
114
+ // Lazy because anthropic-routing reads the quota cache on normal request routing.
115
+ const { anthropicRoutingFor } = await import("../../oauth/anthropic-routing");
116
+ const routing = anthropicRoutingFor(instance);
117
+ const live = getAccountCredential(instance, accountId);
118
+ if (!live || credentialGeneration(live) !== generation) return null;
119
+ const cooldownGeneration = reserveCooldownGeneration(accountId);
120
+ const capturedFamilyGeneration = anthropicModelQuotaFor(instance).captureAnthropicFamilyQuotaGeneration(accountId);
121
+ const claim = routing.captureAnthropicCooldownRecovery(accountId);
122
+ const isCurrentCredential = () => {
123
+ const currentRow = getAccountSet(instance)?.accounts.find(row => row.id === accountId);
124
+ const current = currentRow?.credential;
125
+ return quotaEpoch === captureProviderAccountQuotaEpoch(instance)
126
+ && anthropicAccountIncarnation(accountId) === incarnation
127
+ && currentRow?.loginId === initialRow?.loginId && currentRow?.addedAt === initialRow?.addedAt
128
+ && !!current && credentialGeneration(current) === generation;
129
+ };
130
+ return {
131
+ instance,
132
+ requiresFreshDispatch: claim !== null,
133
+ isCurrentCredential,
134
+ isCurrentFamily: () => familyGeneration(accountId) === capturedFamilyGeneration,
135
+ isCurrent: () => isCurrentCredential()
136
+ && anthropicCooldownGeneration(accountId) === cooldownGeneration,
137
+ settle: quota => !isCurrentCredential() || anthropicCooldownGeneration(accountId) !== cooldownGeneration ? "superseded"
138
+ : claim === null ? "retained" : routing.settleAnthropicCooldownRecovery(claim, quota),
139
+ };
108
140
  }
109
- if (!quota) return null;
110
- const settlement = probe.settle(quota);
111
- if (settlement === "superseded") {
112
- throw new AnthropicQuotaProbeOwnershipError("anthropic quota probe lost cooldown ownership");
141
+
142
+ /** Run one authoritative usage read and publish its recovery effect only while still owned. */
143
+ async function probeAnthropicQuotaWithRecovery(
144
+ accountId: string,
145
+ accessToken: string,
146
+ read: (requireFreshDispatch: boolean) => Promise<ProviderQuota | null>,
147
+ mayPublish: () => boolean,
148
+ ): Promise<AnthropicQuotaRecoveryResult | null> {
149
+ const probe = await captureAnthropicCooldownRecoveryProbe(accountId, accessToken);
150
+ if (!probe) throw new AnthropicQuotaProbeOwnershipError(`${instance} quota probe lost credential ownership`);
151
+ if (!probe.isCurrent() || !mayPublish()) throw new AnthropicQuotaProbeOwnershipError(`${instance} quota probe lost dispatch ownership`);
152
+ let quota: ProviderQuota | null;
153
+ try { quota = await read(probe.requiresFreshDispatch); }
154
+ catch (error) {
155
+ if (!probe.isCurrent() || !mayPublish()) throw new AnthropicQuotaProbeOwnershipError(`${instance} quota probe failure is stale`);
156
+ throw error;
157
+ }
158
+ if (!probe.isCurrent() || !mayPublish()) {
159
+ throw new AnthropicQuotaProbeOwnershipError(`${instance} quota probe result is stale`);
160
+ }
161
+ if (!quota) return null;
162
+ const settlement = probe.settle(quota);
163
+ if (settlement === "superseded") {
164
+ throw new AnthropicQuotaProbeOwnershipError(`${instance} quota probe lost cooldown ownership`);
165
+ }
166
+ // Clearing the claimed cooldown intentionally advances the fence. Adopt that exact new
167
+ // generation; any later observation/429 then invalidates publication before a cache write.
168
+ const ownsFamily = probe.isCurrentFamily();
169
+ const authoritative = ownsFamily && hasAnthropicFamilyEnumeration(quota);
170
+ if (ownsFamily) anthropicModelQuotaFor(instance).observeAnthropicFamilyQuota(accountId, quota.customWindows ?? [], quota.updatedAt, authoritative);
171
+ if (!authoritative) {
172
+ const cached = getCachedProviderAccountQuota(instance, accountId)?.customWindows;
173
+ const windows = ownsFamily ? mergeAnthropicFamilyWindows(cached, quota.customWindows)
174
+ : cached ?? [];
175
+ // A superseded family enumeration has no authority, including over absent families.
176
+ if (!ownsFamily || windows.length) quota = { ...quota, customWindows: windows };
177
+ }
178
+ const publicationGeneration = anthropicCooldownGeneration(accountId);
179
+ const publicationFamilyGeneration = familyGeneration(accountId);
180
+ afterSettlementForTests?.();
181
+ return {
182
+ instance,
183
+ quota,
184
+ isCurrent: () => probe.isCurrentCredential()
185
+ && anthropicCooldownGeneration(accountId) === publicationGeneration
186
+ && familyGeneration(accountId) === publicationFamilyGeneration
187
+ && mayPublish(),
188
+ };
113
189
  }
114
- // Clearing the claimed cooldown intentionally advances the fence. Adopt that exact new
115
- // generation; any later observation/429 then invalidates publication before a cache write.
116
- const ownsFamily = probe.isCurrentFamily();
117
- const authoritative = ownsFamily && hasAnthropicFamilyEnumeration(quota);
118
- if (ownsFamily) observeAnthropicFamilyQuota(accountId, quota.customWindows ?? [], quota.updatedAt, authoritative);
119
- if (!authoritative) {
120
- const cached = getCachedProviderAccountQuota("anthropic", accountId)?.customWindows;
121
- const windows = ownsFamily ? mergeAnthropicFamilyWindows(cached, quota.customWindows)
122
- : cached ?? [];
123
- // A superseded family enumeration has no authority, including over absent families.
124
- if (!ownsFamily || windows.length) quota = { ...quota, customWindows: windows };
190
+
191
+ function reconcileAnthropicCooldownGenerations(context: GenerationContext): number {
192
+ // Probe reservations include accounts with no cooldown, so every claim retires here.
193
+ for (const id of cooldownGenerationByAccount?.keys() ?? []) {
194
+ if (!context.oauthAccountKeys.has(`${instance}\0${id}`)) noteAnthropicCooldownMutation(id);
195
+ }
196
+ for (const id of accountIncarnations?.keys() ?? []) {
197
+ if (!context.oauthAccountKeys.has(`${instance}\0${id}`)) accountIncarnations!.set(id, nextAccountIncarnation++);
198
+ }
199
+ return 0; // Tombstones are retained, never reclaimed into an older generation.
125
200
  }
126
- const publicationGeneration = anthropicCooldownGeneration(accountId);
127
- const publicationFamilyGeneration = anthropicFamilyQuotaGeneration(accountId);
128
- afterSettlementForTests?.();
129
- return {
130
- quota,
131
- isCurrent: () => probe.isCurrentCredential()
132
- && anthropicCooldownGeneration(accountId) === publicationGeneration
133
- && anthropicFamilyQuotaGeneration(accountId) === publicationFamilyGeneration
134
- && mayPublish(),
135
- };
201
+ return Object.freeze({ instance, assertAnthropicQuotaSendAllowed, setAnthropicQuotaAfterSettlementForTests,
202
+ anthropicCooldownGeneration, anthropicCooldownFlightKey, anthropicCredentialQuotaFlightKey, noteAnthropicCooldownMutation,
203
+ anthropicAccountIncarnation, reserveAnthropicAccountIncarnation,
204
+ clearAnthropicCooldownGenerations, captureAnthropicCooldownRecoveryProbe,
205
+ probeAnthropicQuotaWithRecovery, reconcileAnthropicCooldownGenerations });
206
+ }
207
+ export type AnthropicCooldownRecovery = ReturnType<typeof createAnthropicCooldownRecovery>;
208
+ const instances = new Map<AnthropicInstanceId, AnthropicCooldownRecovery>();
209
+ export function anthropicCooldownRecoveryFor(instance: AnthropicInstanceId): AnthropicCooldownRecovery {
210
+ let facade = instances.get(instance);
211
+ if (!facade) { facade = createAnthropicCooldownRecovery(instance); instances.set(instance, facade); }
212
+ return facade;
213
+ }
214
+ export function reconcileAllAnthropicCooldownGenerations(context: GenerationContext): number {
215
+ for (const facade of instances.values()) facade.reconcileAnthropicCooldownGenerations(context);
216
+ return 0;
217
+ }
218
+ export function clearAllAnthropicCooldownGenerations(): void {
219
+ for (const facade of instances.values()) facade.clearAnthropicCooldownGenerations();
220
+ }
221
+ export function assertAnthropicQuotaSendAllowed(accountId: string, token: string): void {
222
+ anthropicCooldownRecoveryFor("anthropic").assertAnthropicQuotaSendAllowed(accountId, token);
223
+ }
224
+ export function setAnthropicQuotaAfterSettlementForTests(hook: (() => void) | undefined): void {
225
+ anthropicCooldownRecoveryFor("anthropic").setAnthropicQuotaAfterSettlementForTests(hook);
226
+ }
227
+ export function anthropicCooldownGeneration(accountId: string): number {
228
+ return anthropicCooldownRecoveryFor("anthropic").anthropicCooldownGeneration(accountId);
229
+ }
230
+ export function anthropicCooldownFlightKey(baseKey: string, accountId: string): string {
231
+ return anthropicCooldownRecoveryFor("anthropic").anthropicCooldownFlightKey(baseKey, accountId);
232
+ }
233
+ export function noteAnthropicCooldownMutation(accountId: string): number {
234
+ return anthropicCooldownRecoveryFor("anthropic").noteAnthropicCooldownMutation(accountId);
235
+ }
236
+ export function clearAnthropicCooldownGenerations(): void {
237
+ anthropicCooldownRecoveryFor("anthropic").clearAnthropicCooldownGenerations();
238
+ }
239
+ export function captureAnthropicCooldownRecoveryProbe(accountId: string, accessToken: string): Promise<AnthropicCooldownRecoveryProbe | null> {
240
+ return anthropicCooldownRecoveryFor("anthropic").captureAnthropicCooldownRecoveryProbe(accountId, accessToken);
241
+ }
242
+ export function probeAnthropicQuotaWithRecovery(
243
+ accountId: string, accessToken: string,
244
+ read: (requireFreshDispatch: boolean) => Promise<ProviderQuota | null>, mayPublish: () => boolean,
245
+ ): Promise<AnthropicQuotaRecoveryResult | null> {
246
+ return anthropicCooldownRecoveryFor("anthropic").probeAnthropicQuotaWithRecovery(accountId, accessToken, read, mayPublish);
136
247
  }
@@ -8,7 +8,7 @@ import { apiKeyPoolEntryId } from "../api-keys";
8
8
  import { getProviderRegistryEntry, providerCodexAccountMode } from "../registry";
9
9
  import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../openai-tiers";
10
10
  import { CODEX_CAPACITY_MAX_QUOTA_AGE_MS, type CodexCapacityAggregation, type CodexCapacityQuota } from "../codex-capacity";
11
- import { clearCachedProviderQuotas, providerQuotaRoutingBinding, type ProviderQuotaRoutingEvidence } from "../quota-routing-cache";
11
+ import { clearCachedProviderQuotas, providerQuotaRoutingBinding, replaceCachedProviderQuotas, type ProviderQuotaRoutingEvidence } from "../quota-routing-cache";
12
12
  import { clearProviderApiKeyQuotaCache } from "../quota-key-accounts";
13
13
  import { QUOTA_JSON_READ_FAILURE, readQuotaJson } from "../quota-wire";
14
14
  import type { OcxConfig, OcxProviderConfig } from "../../types";
@@ -101,6 +101,29 @@ export function clearProviderQuotaCache(): void {
101
101
  invalidationEpoch += 1;
102
102
  }
103
103
 
104
+ const providerReportEpochs = new Map<string, number>();
105
+
106
+ export function captureProviderReportEpochs(): ReadonlyMap<string, number> {
107
+ return new Map(providerReportEpochs);
108
+ }
109
+
110
+ export function providerReportEpochChanged(captured: ReadonlyMap<string, number>, provider: string): boolean {
111
+ return (providerReportEpochs.get(provider) ?? 0) !== (captured.get(provider) ?? 0);
112
+ }
113
+
114
+ /**
115
+ * Provider-scoped invalidation: drop one provider's report and routing row. The global epoch
116
+ * is untouched, so another provider's cached row and in-flight commit authority survive; an
117
+ * aggregate already in flight drops only this provider's row when it publishes.
118
+ */
119
+ export function clearProviderQuotaCacheFor(provider: string): void {
120
+ providerReportEpochs.set(provider, (providerReportEpochs.get(provider) ?? 0) + 1);
121
+ if (!cache) return;
122
+ const reports = cache.response.reports.filter(report => report.provider !== provider);
123
+ cache = { ...cache, response: { ...cache.response, reports } };
124
+ replaceCachedProviderQuotas(reports, routingEvidence);
125
+ }
126
+
104
127
  function cacheKey(config: OcxConfig): string {
105
128
  const providers = Object.entries(config.providers)
106
129
  .map(([name, provider]) => {
@@ -2,7 +2,9 @@ import { markAnthropicFamilyEnumeration } from "./anthropic-family-headers";
2
2
  import { effectiveCodexAuthAccountId, fetchMainAccountInfoSnapshot, listCodexAuthAccountsSnapshot } from "../../codex/auth-api";
3
3
  import { MAIN_CODEX_ACCOUNT_ID } from "../../codex/main-account";
4
4
  import { getValidAccessToken } from "../../oauth";
5
- import { captureOAuthAccountSelection, getAccountCredential, getAccountSet } from "../../oauth/store";
5
+ import { loadConfig } from "../../config";
6
+ import { configuredAnthropicInstance, type AnthropicInstanceId } from "../anthropic-instance";
7
+ import { captureOAuthAccountSelection, credentialGeneration, getAccountCredential, getAccountSet } from "../../oauth/store";
6
8
  import { hydrateKiroAccountState, persistKiroAccountState } from "../kiro-account-state-disk";
7
9
  import { kiroProbeCurrent, kiroProbeIdentity } from "./kiro-account-probe";
8
10
  import { fetchMuseKeyQuotaSnapshot } from "../muse-key-quota";
@@ -33,6 +35,7 @@ import {
33
35
  } from "./report-cache";
34
36
  import {
35
37
  accountCacheKey,
38
+ captureProviderAccountQuotaEpoch,
36
39
  accountQuotaCache,
37
40
  hydrateAccountQuotaCache,
38
41
  mayCommitAccountQuotaKey,
@@ -40,7 +43,7 @@ import {
40
43
  } from "./account-cache";
41
44
  import type { OcxConfig, OcxProviderConfig } from "../../types";
42
45
  import type { ProviderQuota, ProviderQuotaWindow } from "../quota-types";
43
- import { AnthropicQuotaProbeOwnershipError, assertAnthropicQuotaSendAllowed, probeAnthropicQuotaWithRecovery } from "./anthropic-cooldown-recovery";
46
+ import { AnthropicQuotaProbeOwnershipError, anthropicCooldownRecoveryFor } from "./anthropic-cooldown-recovery";
44
47
 
45
48
  const XAI_BILLING_URL = "https://cli-chat-proxy.grok.com/v1/billing";
46
49
  const XAI_CREDITS_URL = `${XAI_BILLING_URL}?format=credits`;
@@ -336,39 +339,68 @@ async function readAnthropicUsageQuota(accessToken: string): Promise<ProviderQuo
336
339
  export async function fetchAnthropicUsageQuota(
337
340
  accessToken: string,
338
341
  requireFreshDispatch = false,
342
+ ): Promise<ProviderQuota | null> {
343
+ return fetchAnthropicUsageQuotaForInstance("anthropic", accessToken, requireFreshDispatch);
344
+ }
345
+
346
+ export async function fetchAnthropicUsageQuotaForInstance(
347
+ instance: AnthropicInstanceId,
348
+ accessToken: string,
349
+ requireFreshDispatch = false,
339
350
  ): Promise<ProviderQuota | null> {
340
351
  // Recovery evidence must be requested after the claimed cooldown. Joining an older request
341
352
  // can return after the 429 while still describing provider state from before that refusal.
342
- const joinable = requireFreshDispatch ? undefined : anthropicUsageInflight.get(accessToken);
353
+ const credentialIdentity = getAccountSet(instance)?.accounts.filter(row => row.credential.access === accessToken)
354
+ .map(row => [row.id, row.loginId, row.addedAt, credentialGeneration(row.credential)]);
355
+ const key = `${instance}\0${captureProviderAccountQuotaEpoch(instance)}\0${JSON.stringify(credentialIdentity)}\0${accessToken}`;
356
+ const joinable = requireFreshDispatch ? undefined : anthropicUsageInflight.get(key);
343
357
  if (joinable) return joinable;
344
358
 
345
359
  const probe = readAnthropicUsageQuota(accessToken).finally(() => {
346
- if (anthropicUsageInflight.get(accessToken) === probe) anthropicUsageInflight.delete(accessToken);
360
+ if (anthropicUsageInflight.get(key) === probe) anthropicUsageInflight.delete(key);
347
361
  });
348
- anthropicUsageInflight.set(accessToken, probe);
362
+ anthropicUsageInflight.set(key, probe);
349
363
  return probe;
350
364
  }
351
365
 
352
- export async function fetchAnthropicQuota(provider: string): Promise<ProviderQuotaReport | null> {
366
+ /** `config` is the caller's live object; currency re-reads it instead of reloading the file. */
367
+ export async function fetchAnthropicQuota(provider: string, config: OcxConfig = loadConfig()): Promise<ProviderQuotaReport | null> {
368
+ const instance = configuredAnthropicInstance(config, provider);
369
+ const target = config.providers[provider];
370
+ if (!instance || target?.disabled || target && target.authMode !== "oauth") return null;
371
+ const targetIdentity = JSON.stringify(target);
372
+ const targetCurrent = () => configuredAnthropicInstance(config, provider) === instance
373
+ && JSON.stringify(config.providers[provider]) === targetIdentity;
374
+ const recovery = anthropicCooldownRecoveryFor(instance);
353
375
  // Capture the account we intend to probe before awaiting — a mid-flight active
354
376
  // switch must not seed the wrong account's cache with this response.
355
- const selection = captureOAuthAccountSelection("anthropic");
377
+ const selection = captureOAuthAccountSelection(instance);
356
378
  const probedAccountId = selection?.accountId;
357
- const probedAccountKey = probedAccountId ? accountCacheKey("anthropic", probedAccountId) : null;
379
+ const initialRow = getAccountSet(instance)?.accounts.find(row => row.id === probedAccountId);
380
+ const epoch = captureProviderAccountQuotaEpoch(instance);
381
+ const incarnation = probedAccountId ? recovery.reserveAnthropicAccountIncarnation(probedAccountId) : undefined;
382
+ const probedAccountKey = probedAccountId ? accountCacheKey(instance, probedAccountId) : null;
358
383
  const writerGeneration = captureConfigGeneration();
359
384
  let accessToken: string;
360
385
  try {
361
- accessToken = await getValidAccessToken("anthropic");
386
+ accessToken = await getValidAccessToken(instance);
362
387
  } catch {
363
388
  return null;
364
389
  }
365
390
  if (!probedAccountId || !probedAccountKey) return null;
391
+ const currentRow = getAccountSet(instance)?.accounts.find(row => row.id === probedAccountId);
392
+ if (!initialRow || !currentRow || initialRow.loginId !== currentRow.loginId || initialRow.addedAt !== currentRow.addedAt
393
+ || epoch !== captureProviderAccountQuotaEpoch(instance) || recovery.anthropicAccountIncarnation(probedAccountId) !== incarnation) return null;
366
394
  let quota: ProviderQuota | null;
367
395
  let anthropicCurrent: (() => boolean) | undefined;
368
396
  try {
369
- const result = await probeAnthropicQuotaWithRecovery(probedAccountId, accessToken,
370
- fresh => { assertAnthropicQuotaSendAllowed(probedAccountId, accessToken); return fetchAnthropicUsageQuota(accessToken, fresh); },
371
- () => mayCommitAccountQuotaKey(probedAccountKey, writerGeneration));
397
+ const result = await recovery.probeAnthropicQuotaWithRecovery(probedAccountId, accessToken,
398
+ fresh => {
399
+ if (!targetCurrent()) throw new AnthropicQuotaProbeOwnershipError("quota target changed");
400
+ recovery.assertAnthropicQuotaSendAllowed(probedAccountId, accessToken);
401
+ return fetchAnthropicUsageQuotaForInstance(instance, accessToken, fresh);
402
+ },
403
+ () => targetCurrent() && mayCommitAccountQuotaKey(probedAccountKey, writerGeneration));
372
404
  if (result && !result.isCurrent()) return null;
373
405
  quota = result?.quota ?? null;
374
406
  anthropicCurrent = result?.isCurrent;
@@ -380,14 +412,14 @@ export async function fetchAnthropicQuota(provider: string): Promise<ProviderQuo
380
412
  // Share the active-account probe with the per-account cache so Providers-page
381
413
  // loads do not double-hit Anthropic's rate-limited usage endpoint.
382
414
  if (probedAccountId && probedAccountKey) {
383
- const stillOwnsToken = getAccountCredential("anthropic", probedAccountId)?.access === accessToken;
415
+ const stillOwnsToken = getAccountCredential(instance, probedAccountId)?.access === accessToken;
384
416
  if (stillOwnsToken && anthropicCurrent?.() && mayCommitAccountQuotaKey(probedAccountKey, writerGeneration)) {
385
417
  accountQuotaCache.set(probedAccountKey, { ts: Date.now(), quota, isCurrent: anthropicCurrent });
386
418
  }
387
419
  }
388
420
  const quotaReport = report(provider, "anthropic:oauth-usage", quota);
389
421
  if (quotaReport && anthropicCurrent) {
390
- accountReportCurrent.set(quotaReport, () => anthropicCurrent() && getAccountSet("anthropic")?.activeAccountId === probedAccountId);
422
+ accountReportCurrent.set(quotaReport, () => anthropicCurrent() && getAccountSet(instance)?.activeAccountId === probedAccountId);
391
423
  }
392
424
  return quotaReport;
393
425
  }