@bitkyc08/opencodex 2.79.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 (245) 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/cursor/live-transport.ts +5 -3
  20. package/src/adapters/cursor/native-exec-fs.ts +12 -9
  21. package/src/adapters/cursor/native-exec.ts +13 -5
  22. package/src/adapters/cursor/protobuf-request.ts +13 -7
  23. package/src/adapters/cursor/request-builder.ts +23 -10
  24. package/src/adapters/cursor/tool-definitions.ts +9 -6
  25. package/src/adapters/cursor/tool-guidance.ts +29 -8
  26. package/src/adapters/cursor/tool-naming.ts +40 -7
  27. package/src/adapters/cursor/tool-wording.ts +46 -0
  28. package/src/adapters/cursor.ts +3 -0
  29. package/src/adapters/devin/live-models.ts +2 -0
  30. package/src/adapters/devin.ts +25 -11
  31. package/src/adapters/kiro-retry.ts +9 -4
  32. package/src/adapters/openai-chat/messages.ts +7 -4
  33. package/src/adapters/physical-send.ts +8 -5
  34. package/src/adapters/qoder/adapter.ts +41 -6
  35. package/src/adapters/qoder/scaffold-guard.ts +2 -3
  36. package/src/adapters/zed.ts +16 -1
  37. package/src/claude/inbound-cache-stabilize.ts +4 -9
  38. package/src/claude/inbound.ts +10 -0
  39. package/src/claude/intercept/model-bindings.ts +1 -0
  40. package/src/claude/intercept/picker-listener.ts +71 -4
  41. package/src/cli/account-anthropic-threshold.ts +14 -5
  42. package/src/cli/account-api.ts +3 -1
  43. package/src/cli/account-extended.ts +13 -9
  44. package/src/cli/account-policy-dto.ts +1 -1
  45. package/src/cli/account-policy.ts +13 -4
  46. package/src/cli/account.ts +3 -3
  47. package/src/cli/capabilities-accounts.ts +2 -2
  48. package/src/cli/capabilities-agents-routing.ts +24 -0
  49. package/src/cli/capabilities-integrations.ts +16 -0
  50. package/src/cli/codex-shim-autorestore.ts +3 -0
  51. package/src/cli/dispatch.ts +10 -1
  52. package/src/cli/help.ts +4 -1
  53. package/src/cli/index.ts +11 -5
  54. package/src/cli/integration-plan-dto.ts +1 -0
  55. package/src/cli/integration-preview.ts +8 -1
  56. package/src/cli/integrations.ts +32 -3
  57. package/src/cli/message-args.ts +34 -0
  58. package/src/cli/message-command.ts +43 -0
  59. package/src/cli/message-runtime.ts +6 -0
  60. package/src/cli/observe.ts +3 -1
  61. package/src/cli/provider.ts +29 -6
  62. package/src/cli/registry.ts +24 -2
  63. package/src/clients/config-export/commandcode.ts +131 -0
  64. package/src/clients/config-export/contracts.ts +12 -0
  65. package/src/clients/config-export.ts +64 -1
  66. package/src/codex/auth-api/account-list.ts +11 -3
  67. package/src/codex/auth-api/main-account-probe.ts +25 -8
  68. package/src/codex/auth-api/pool-mode-gate.ts +33 -9
  69. package/src/codex/auth-api/pool-quota-probe.ts +15 -1
  70. package/src/codex/auth-api/routes.ts +1 -0
  71. package/src/codex/autostart-health.ts +3 -3
  72. package/src/codex/catalog/auto-review.ts +13 -6
  73. package/src/codex/catalog/build-entries.ts +15 -7
  74. package/src/codex/catalog/control-plane.ts +45 -0
  75. package/src/codex/catalog/effort.ts +11 -1
  76. package/src/codex/catalog/gather-capture.ts +5 -3
  77. package/src/codex/catalog/parsing.ts +2 -1
  78. package/src/codex/catalog/provider-models.ts +15 -9
  79. package/src/codex/control-plane-models.ts +6 -0
  80. package/src/codex/journal.ts +36 -5
  81. package/src/codex/shim-overlay.ts +9 -1
  82. package/src/combos/jev-decision-contract.ts +25 -2
  83. package/src/combos/jev.ts +6 -3
  84. package/src/combos/types.ts +1 -1
  85. package/src/config/diagnostics.ts +27 -16
  86. package/src/config/load-degrade.ts +34 -0
  87. package/src/config/schema/anthropic-account-pool.ts +143 -0
  88. package/src/config/schema/config-schema.ts +7 -1
  89. package/src/config/schema/leaf-validators.ts +4 -0
  90. package/src/config.ts +2 -0
  91. package/src/generated/compatibility-version.json +347 -199
  92. package/src/generated/model-metadata.ts +5 -4
  93. package/src/images/loop.ts +2 -1
  94. package/src/integrations/catalog-refresh.ts +1 -1
  95. package/src/integrations/mutation-plan.ts +7 -0
  96. package/src/integrations/registry.ts +8 -0
  97. package/src/integrations/state.ts +9 -2
  98. package/src/integrations/writer.ts +8 -2
  99. package/src/lib/lab-live-route-production.ts +9 -1
  100. package/src/lib/request-execution-budget.ts +236 -58
  101. package/src/lib/spend-pool-alias-validation.ts +79 -0
  102. package/src/lib/spend-pool-continuity.ts +34 -0
  103. package/src/lib/spend-reservation-ledger.ts +412 -50
  104. package/src/lib/state-store-registrations.ts +20 -3
  105. package/src/lib/upstream-retry.ts +17 -5
  106. package/src/lib/workflow-budget.ts +53 -11
  107. package/src/messaging/budget.ts +45 -0
  108. package/src/messaging/discovery.ts +57 -0
  109. package/src/messaging/envelope.ts +49 -0
  110. package/src/messaging/input.ts +37 -0
  111. package/src/messaging/rpc.ts +196 -0
  112. package/src/messaging/send.ts +58 -0
  113. package/src/messaging/socket.ts +72 -0
  114. package/src/messaging/types.ts +29 -0
  115. package/src/oauth/anthropic-account-refusal.ts +51 -17
  116. package/src/oauth/anthropic-account-threshold.ts +8 -2
  117. package/src/oauth/anthropic-continuity.ts +15 -1
  118. package/src/oauth/anthropic-model-quota.ts +120 -48
  119. package/src/oauth/anthropic-model-routes.ts +11 -3
  120. package/src/oauth/anthropic-oauth-definitions.ts +98 -0
  121. package/src/oauth/anthropic-pool-config.ts +30 -0
  122. package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
  123. package/src/oauth/anthropic-routing.ts +1202 -866
  124. package/src/oauth/anthropic-send-ownership.ts +41 -0
  125. package/src/oauth/anthropic.ts +4 -1
  126. package/src/oauth/generic-account-failover.ts +3 -1
  127. package/src/oauth/health.ts +8 -6
  128. package/src/oauth/index.ts +90 -173
  129. package/src/oauth/kiro.ts +22 -9
  130. package/src/oauth/login-cli.ts +2 -0
  131. package/src/oauth/model-discovery-auth.ts +43 -0
  132. package/src/oauth/pool-kernel.ts +11 -5
  133. package/src/oauth/pool-settings-capability.ts +5 -3
  134. package/src/oauth/store-anthropic-instance.ts +87 -0
  135. package/src/oauth/store.ts +67 -9
  136. package/src/oauth/token-guardian.ts +2 -0
  137. package/src/oauth/types.ts +2 -0
  138. package/src/protocols/plan-snapshot.ts +5 -0
  139. package/src/protocols/settings.ts +20 -5
  140. package/src/providers/anthropic-instance-id.ts +47 -0
  141. package/src/providers/anthropic-instance.ts +48 -0
  142. package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
  143. package/src/providers/derive.ts +2 -0
  144. package/src/providers/kiro-models.ts +3 -0
  145. package/src/providers/label.ts +1 -1
  146. package/src/providers/model-rename-fields.ts +2 -0
  147. package/src/providers/quota/account-cache.ts +37 -11
  148. package/src/providers/quota/anthropic-account-quota.ts +93 -0
  149. package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
  150. package/src/providers/quota/report-cache.ts +24 -1
  151. package/src/providers/quota/vendor-probes-oauth.ts +46 -14
  152. package/src/providers/quota.ts +22 -32
  153. package/src/providers/registry/entries-core.ts +35 -30
  154. package/src/providers/registry/model-ids.ts +1 -0
  155. package/src/providers/registry/model-seeds.ts +7 -4
  156. package/src/providers/registry/types.ts +3 -0
  157. package/src/providers/registry.ts +3 -3
  158. package/src/responses/spill-store.ts +5 -1
  159. package/src/responses/state/metrics.ts +87 -0
  160. package/src/responses/state/snapshot-codec.ts +6 -0
  161. package/src/responses/state/spill-queue.ts +1 -0
  162. package/src/responses/state/unforced-store-false.ts +55 -0
  163. package/src/responses/state.ts +50 -66
  164. package/src/router.ts +28 -4
  165. package/src/routing/compatibility/assemble.ts +1 -1
  166. package/src/routing/quota.ts +7 -3
  167. package/src/server/auth-cors.ts +5 -0
  168. package/src/server/chat-completions.ts +6 -1
  169. package/src/server/chat-native.ts +45 -25
  170. package/src/server/claude-messages.ts +16 -2
  171. package/src/server/gui-static.ts +38 -4
  172. package/src/server/index/spend-ledger-lifecycle.ts +10 -4
  173. package/src/server/index.ts +16 -16
  174. package/src/server/inference/context.ts +1 -1
  175. package/src/server/lifecycle.ts +1 -1
  176. package/src/server/management/agent-settings-routes.ts +30 -6
  177. package/src/server/management/anthropic-account-threshold.ts +12 -9
  178. package/src/server/management/anthropic-pool-settings.ts +72 -0
  179. package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
  180. package/src/server/management/config-routes.ts +27 -7
  181. package/src/server/management/decision-routes.ts +2 -1
  182. package/src/server/management/integration-routes.ts +25 -2
  183. package/src/server/management/oauth-account-routes.ts +79 -98
  184. package/src/server/management/provider-instance-ownership.ts +44 -0
  185. package/src/server/management/provider-routes.ts +21 -28
  186. package/src/server/management/route-registry.ts +3 -1
  187. package/src/server/management/routing-profile-routes.ts +4 -4
  188. package/src/server/management/system-restart.ts +44 -14
  189. package/src/server/management/system-routes.ts +24 -2
  190. package/src/server/management/vision-sidecar-options.ts +9 -6
  191. package/src/server/management/web-search-sidecar-options.ts +51 -2
  192. package/src/server/management-auth.ts +6 -0
  193. package/src/server/messages-native-eligibility.ts +3 -2
  194. package/src/server/messages-native-oauth.ts +86 -41
  195. package/src/server/messages-native-scope.ts +4 -1
  196. package/src/server/messages-native-selector.ts +39 -0
  197. package/src/server/messages-native.ts +84 -25
  198. package/src/server/request-log.ts +5 -0
  199. package/src/server/responses/adapter-continuation.ts +29 -17
  200. package/src/server/responses/adapter-dispatch.ts +47 -17
  201. package/src/server/responses/antigravity-validation-refusal.ts +20 -6
  202. package/src/server/responses/combo-requested-effort.ts +14 -0
  203. package/src/server/responses/compact.ts +25 -8
  204. package/src/server/responses/compaction-recovery.ts +1 -1
  205. package/src/server/responses/core-combo-native.ts +4 -1
  206. package/src/server/responses/core-combo.ts +21 -18
  207. package/src/server/responses/core-normalize.ts +1 -0
  208. package/src/server/responses/core-options.ts +3 -1
  209. package/src/server/responses/core-replay.ts +5 -0
  210. package/src/server/responses/passthrough-delivery.ts +7 -3
  211. package/src/server/responses/passthrough-dispatch.ts +21 -8
  212. package/src/server/responses/request-prepare.ts +37 -5
  213. package/src/server/responses/request-send-budget.ts +74 -37
  214. package/src/server/responses/request-spend.ts +325 -33
  215. package/src/server/responses/request-transport.ts +132 -52
  216. package/src/server/responses/run-turn-execution.ts +11 -3
  217. package/src/server/responses/sidecar-execution.ts +96 -14
  218. package/src/server/responses-undeclared-tool-guard.ts +5 -1
  219. package/src/server/workflow-refusal.ts +30 -0
  220. package/src/service/desktop-startup.ts +100 -3
  221. package/src/service/ownership-mutation-lease.d.mts +1 -1
  222. package/src/service/ownership-mutation-lease.mjs +5 -7
  223. package/src/sidecar/anthropic-binding.ts +79 -0
  224. package/src/sidecar/auth.ts +76 -2
  225. package/src/types/anthropic-account-pool.ts +33 -0
  226. package/src/types/config.ts +12 -28
  227. package/src/types/provider.ts +5 -0
  228. package/src/types/tools.ts +7 -0
  229. package/src/types.ts +4 -0
  230. package/src/usage/cost.ts +3 -2
  231. package/src/usage/expected-prices.ts +66 -14
  232. package/src/vision/anthropic-describe.ts +16 -3
  233. package/src/vision/index.ts +3 -0
  234. package/src/vision/plan.ts +19 -6
  235. package/src/web-search/alpha-search.ts +7 -1
  236. package/src/web-search/anthropic-executor.ts +16 -4
  237. package/src/web-search/header-deadline.ts +67 -0
  238. package/src/web-search/index.ts +7 -3
  239. package/src/web-search/loop.ts +32 -10
  240. package/src/web-search/passthrough-bridge.ts +4 -1
  241. package/src/web-search/sidecar-providers.ts +4 -3
  242. package/gui/dist/assets/App-BOUa-ek3.js +0 -54
  243. package/gui/dist/assets/Tray-DUtzB4zF.js +0 -1
  244. package/gui/dist/assets/index-DwHNDF4s.js +0 -94
  245. package/gui/dist/assets/quota-summary-BuL4WIBo.js +0 -1
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Registry- and config-aware Anthropic instance checks. See `./anthropic-instance-id.ts` for the IDs and
3
+ * the pure row-shape rule; this module adds the registry declaration and the configured-row lookup.
4
+ *
5
+ * Instance membership is never inferred from `adapter === "anthropic"` (the API-key provider and
6
+ * Anthropic-compatible gateways share that adapter) or from a name prefix.
7
+ */
8
+ import type { OcxConfig, OcxProviderConfig } from "../types";
9
+ import { getProviderRegistryEntry } from "./registry";
10
+ import { anthropicInstanceRowShapeMatches, isAnthropicInstanceId, type AnthropicInstanceId } from "./anthropic-instance-id";
11
+
12
+ export * from "./anthropic-instance-id";
13
+
14
+ /** Exact registry check: one of the two IDs, declared as an Anthropic-family OAuth entry with its own OAuth ID. */
15
+ export function isAnthropicOAuthInstance(id: string | undefined): id is AnthropicInstanceId {
16
+ if (!isAnthropicInstanceId(id)) return false;
17
+ const entry = getProviderRegistryEntry(id);
18
+ return entry !== undefined
19
+ && entry.authKind === "oauth"
20
+ && entry.oauthId === id
21
+ && entry.oauthFamily === "anthropic";
22
+ }
23
+
24
+ export function isBuiltinAnthropicInstanceRow(
25
+ name: string,
26
+ row?: Partial<Pick<OcxProviderConfig, "adapter" | "authMode" | "baseUrl" | "anthropicOAuthInstance">>,
27
+ ): boolean {
28
+ return isAnthropicOAuthInstance(name) && anthropicInstanceRowShapeMatches(name, row);
29
+ }
30
+
31
+ /**
32
+ * The instance a configured provider name runs as, or undefined.
33
+ *
34
+ * For `anthropic` this is a compatibility identity result: it returns the instance whenever the name is
35
+ * `anthropic`, and callers keep every auth-mode, enabled-provider and eligibility check they already
36
+ * apply. For `anthropic2` the row must exist, must not be disabled and must pass the builtin row-shape
37
+ * rule, so an orphan `anthropic2` auth row or a custom provider of that name never activates the pool.
38
+ */
39
+ export function configuredAnthropicInstance(
40
+ config: Pick<OcxConfig, "providers">,
41
+ name: string | undefined,
42
+ ): AnthropicInstanceId | undefined {
43
+ if (!isAnthropicOAuthInstance(name)) return undefined;
44
+ if (name === "anthropic") return "anthropic";
45
+ const row = config.providers?.[name];
46
+ if (!row || row.disabled === true) return undefined;
47
+ return anthropicInstanceRowShapeMatches(name, row) ? name : undefined;
48
+ }
@@ -21,6 +21,7 @@ import { existsSync, mkdirSync, readFileSync, writeSync } from "node:fs";
21
21
  import { dirname, join } from "node:path";
22
22
  import { atomicWriteFileStreamed } from "../config/atomic-write";
23
23
  import { getConfigDir } from "../config/paths";
24
+ import type { AnthropicInstanceId } from "./anthropic-instance-id";
24
25
 
25
26
  export const ANTHROPIC_RESET_LEASE_MS = 90_000;
26
27
  export const ANTHROPIC_RESET_RETRY_WINDOW_MS = 600_000;
@@ -83,7 +84,16 @@ export function anthropicOrgDigest(organizationUuid: string): string {
83
84
  }
84
85
 
85
86
  export function anthropicResetJournalPath(customDir?: string): string {
86
- return join(customDir ?? getConfigDir(), "anthropic-reset-grant-ledger.json");
87
+ return anthropicResetJournalPathForInstance("anthropic", customDir);
88
+ }
89
+
90
+ export function anthropicResetJournalPathForInstance(instance: AnthropicInstanceId, customDir?: string): string {
91
+ return join(customDir ?? getConfigDir(), `${instance}-reset-grant-ledger.json`);
92
+ }
93
+
94
+ /** Namespace is in the path; A's version-1 journal bytes and record schema stay unchanged. */
95
+ export function anthropicResetLedgerOptionsForInstance(instance: AnthropicInstanceId, customDir?: string): AnthropicResetLedgerOptions {
96
+ return { journalPath: anthropicResetJournalPathForInstance(instance, customDir) };
87
97
  }
88
98
 
89
99
  function isRecordShape(value: unknown): value is OperationRecord {
@@ -245,6 +245,8 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
245
245
  // Preserve the registry auth kind verbatim (including "local") so fail-closed gates that
246
246
  // distinguish local runtimes from API-key providers keep working after the seed round-trip.
247
247
  authMode: entry.authKind,
248
+ ...(entry.id === "anthropic2" && entry.authKind === "oauth" && entry.oauthFamily === "anthropic"
249
+ ? { anthropicOAuthInstance: "anthropic2" as const } : {}),
248
250
  ...(entry.codexAccountMode ? { codexAccountMode: entry.codexAccountMode } : {}),
249
251
  ...(entry.keyOptional !== undefined ? { keyOptional: entry.keyOptional } : {}),
250
252
  ...(entry.freeTier !== undefined ? { freeTier: entry.freeTier } : {}),
@@ -25,6 +25,8 @@ export const KIRO_MODELS = [
25
25
  "claude-sonnet-4.6",
26
26
  "claude-sonnet-4.5",
27
27
  "claude-sonnet-4.0",
28
+ // 261008 preemptive: Haiku 5.5; calls fail upstream until Kiro ships it.
29
+ "claude-haiku-5.5",
28
30
  "claude-haiku-4.5",
29
31
  "deepseek-3.2",
30
32
  "minimax-m2.5",
@@ -55,6 +57,7 @@ export const KIRO_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
55
57
  "claude-sonnet-4.6": 1_000_000,
56
58
  "claude-sonnet-4.5": 200_000,
57
59
  "claude-sonnet-4.0": 200_000,
60
+ "claude-haiku-5.5": 1_000_000,
58
61
  "claude-haiku-4.5": 200_000,
59
62
  "deepseek-3.2": 128_000,
60
63
  "minimax-m2.5": 200_000,
@@ -89,6 +89,6 @@ export function stampOAuthAccountLabel(
89
89
  if (!accountId) return;
90
90
  if (provider.authMode !== "oauth") return;
91
91
  const base = baseProviderLabel(providerName);
92
- if (base === "openai" || base === "anthropic") return;
92
+ if (base === "openai" || (base === "anthropic" || base === "anthropic2")) return;
93
93
  logCtx.accountLogLabel = oauthAccountLogLabel(accountId, base);
94
94
  }
@@ -78,6 +78,8 @@ export const PROVIDER_MODEL_RENAME_ROLES = {
78
78
  modelVercelGatewayRouting: "record",
79
79
  authMode: "none",
80
80
  oauthAccountFailover: "none",
81
+ anthropicAccountPool: "none",
82
+ anthropicOAuthInstance: "none",
81
83
  keyOptional: "none",
82
84
  freeTier: "none",
83
85
  note: "none",
@@ -1,8 +1,9 @@
1
1
  import { parseAnthropicFamilyHeaders, mergeAnthropicFamilyWindows } from "./anthropic-family-headers";
2
- import { observeAnthropicFamilyQuota, clearAnthropicRequestedFamilyQuota, ANTHROPIC_PASSIVE_FAMILY_MAX_AGE_MS } from "../../oauth/anthropic-model-quota";
2
+ import { anthropicModelQuotaFor, ANTHROPIC_PASSIVE_FAMILY_MAX_AGE_MS } from "../../oauth/anthropic-model-quota";
3
3
  import { createHash } from "node:crypto";
4
4
  import { getValidAccessTokenForAccount } from "../../oauth";
5
5
  import { credentialGeneration, getAccountCredential, getAccountCredentialWithStatus, getAccountSet } from "../../oauth/store";
6
+ import { isAnthropicInstanceId, type AnthropicInstanceId } from "../anthropic-instance-id";
6
7
  import type { GenerationContext } from "../../lib/state-store-sweeper";
7
8
  import { ACCOUNT_QUOTA_TTL_MS, toFiniteNumber } from "../quota-wire";
8
9
  import { clearKiroAccountUsageState, hydrateKiroUsageVerdict, kiroPersistableVerdicts, reconcileKiroAccountUsageState } from "../kiro-usage";
@@ -96,8 +97,19 @@ export function normalizeAnthropicQuota(quota: ProviderQuota | null | undefined,
96
97
  return hasQuotaRows(result) ? result : null;
97
98
  }
98
99
 
100
+ function isAnthropicAccountCacheKey(key: string): boolean {
101
+ const separator = key.indexOf("\0");
102
+ return separator > 0 && isAnthropicInstanceId(key.slice(0, separator));
103
+ }
104
+
99
105
  export const accountQuotaCache = new Map<string, AccountQuotaCacheEntry>();
100
106
  export let explicitAccountEpoch = 0;
107
+ let allAccountQuotaEpoch = 0;
108
+ const providerAccountQuotaEpochs = new Map<string, number>();
109
+ /** Instance-owned cache clears do not revoke an unrelated account's probe. */
110
+ export function captureProviderAccountQuotaEpoch(provider: string): number {
111
+ return allAccountQuotaEpoch + (providerAccountQuotaEpochs.get(provider) ?? 0);
112
+ }
101
113
 
102
114
  /**
103
115
  * Seed the cache from the last run, once.
@@ -127,7 +139,7 @@ export function hydrateAccountQuotaCache(): void {
127
139
  }
128
140
  // Disk stores observation time, not the Anthropic usage probe's clock.
129
141
  if (!accountQuotaCache.has(key)) {
130
- const anthropic = key.startsWith("anthropic\u0000");
142
+ const anthropic = isAnthropicAccountCacheKey(key);
131
143
  accountQuotaCache.set(key, {
132
144
  ts: anthropic ? 0 : quota.updatedAt,
133
145
  quota: anthropic ? normalizeAnthropicQuota(quota, now)
@@ -148,7 +160,7 @@ export function persistAccountQuotaCache(): void {
148
160
  const liveKiro = new Map<string, ProviderAccount>(getAccountSet("kiro")?.accounts.map(account =>
149
161
  [accountCacheKey("kiro", account.id), account]) ?? []);
150
162
  for (const [key, entry] of accountQuotaCache) {
151
- const quota = key.startsWith("anthropic\u0000") ? normalizeAnthropicQuota(entry.quota, now) : entry.quota;
163
+ const quota = isAnthropicAccountCacheKey(key) ? normalizeAnthropicQuota(entry.quota, now) : entry.quota;
152
164
  if (quota && key.startsWith("kiro\0")) {
153
165
  const account = liveKiro.get(key);
154
166
  if (account && entry.identity === kiroEvidenceIdentity(account))
@@ -182,7 +194,7 @@ export interface ProviderAccountQuota {
182
194
 
183
195
  /** Providers whose per-account quota can be probed. Extend as other OAuth APIs are covered. */
184
196
  export function supportsPerAccountQuota(provider: string): boolean {
185
- return provider === "anthropic" || provider === "kiro" || provider === "google-antigravity"
197
+ return isAnthropicInstanceId(provider) || provider === "kiro" || provider === "google-antigravity"
186
198
  || explicitAccountReader(provider);
187
199
  }
188
200
 
@@ -209,12 +221,12 @@ export function getCachedProviderAccountQuota(provider: string, accountId: strin
209
221
  const account = getAccountSet("kiro")?.accounts.find(row => row.id === accountId);
210
222
  if (!account || entry?.identity !== kiroEvidenceIdentity(account)) return null;
211
223
  }
212
- if (provider === "anthropic" && entry?.anthropicCredentialGeneration) {
224
+ if (isAnthropicInstanceId(provider) && entry?.anthropicCredentialGeneration) {
213
225
  const credential = getAccountCredential(provider, accountId);
214
226
  if (!credential || credentialGeneration(credential) !== entry.anthropicCredentialGeneration) return null;
215
227
  }
216
228
  if (entry?.isCurrent && !entry.isCurrent()) return null;
217
- return provider === "anthropic" ? normalizeAnthropicQuota(entry?.quota, Date.now()) : entry?.quota ?? null;
229
+ return isAnthropicInstanceId(provider) ? normalizeAnthropicQuota(entry?.quota, Date.now()) : entry?.quota ?? null;
218
230
  }
219
231
 
220
232
  /** Test-only: seed or clear the per-account quota cache without probing upstream. */
@@ -270,7 +282,8 @@ function normalizeUtilizationFraction(value: string | null): number | undefined
270
282
  * erasing model-specific windows. The caller owns credential attribution; this guard
271
283
  * prevents a retired account key from being revived by an older config generation.
272
284
  */
273
- export function recordAnthropicAccountQuotaFromHeaders(
285
+ export function recordAnthropicAccountQuotaFromHeadersForInstance(
286
+ instance: AnthropicInstanceId,
274
287
  accountId: string,
275
288
  headers: Headers,
276
289
  writerGeneration: number,
@@ -278,7 +291,8 @@ export function recordAnthropicAccountQuotaFromHeaders(
278
291
  model?: string,
279
292
  ): void {
280
293
  if (!accountId) return;
281
- const key = accountCacheKey("anthropic", accountId);
294
+ const key = accountCacheKey(instance, accountId);
295
+ const { observeAnthropicFamilyQuota, clearAnthropicRequestedFamilyQuota } = anthropicModelQuotaFor(instance);
282
296
  if (!mayCommitAccountQuotaKey(key, writerGeneration)) return;
283
297
  if (status !== undefined && status >= 200 && status < 300) clearAnthropicRequestedFamilyQuota(accountId, model);
284
298
  const observed = parseAnthropicRateLimitHeaders(headers, status);
@@ -289,7 +303,7 @@ export function recordAnthropicAccountQuotaFromHeaders(
289
303
  // every other provider's saved row.
290
304
  hydrateAccountQuotaCache();
291
305
  const candidate = accountQuotaCache.get(key);
292
- const credential = getAccountCredential("anthropic", accountId);
306
+ const credential = getAccountCredential(instance, accountId);
293
307
  const generation = credential && credentialGeneration(credential);
294
308
  const previous = candidate?.isCurrent?.() === false
295
309
  || candidate?.anthropicCredentialGeneration && candidate.anthropicCredentialGeneration !== generation ? undefined : candidate;
@@ -307,6 +321,13 @@ export function recordAnthropicAccountQuotaFromHeaders(
307
321
  persistAccountQuotaCache();
308
322
  }
309
323
 
324
+ /** Legacy recorder: config/roster generation is numeric and distinct from bearer ownership. */
325
+ export function recordAnthropicAccountQuotaFromHeaders(
326
+ accountId: string, headers: Headers, writerGeneration: number, status?: number, model?: string,
327
+ ): void {
328
+ recordAnthropicAccountQuotaFromHeadersForInstance("anthropic", accountId, headers, writerGeneration, status, model);
329
+ }
330
+
310
331
  /**
311
332
  * Providers whose per-account quota is OBSERVED in-band, never probed.
312
333
  *
@@ -380,7 +401,7 @@ export function sweepExpiredProviderAccountQuotaRows(now = Date.now()): number {
380
401
  let removed = 0;
381
402
  for (const [key, entry] of accountQuotaCache) {
382
403
  // Anthropic observations extend retention, never the usage probe's eligibility clock.
383
- const retainedAt = key.startsWith("anthropic\u0000")
404
+ const retainedAt = isAnthropicAccountCacheKey(key)
384
405
  ? Math.max(entry.ts, entry.quota?.updatedAt ?? 0)
385
406
  : entry.ts;
386
407
  if (retainedAt + ACCOUNT_QUOTA_TTL_MS > now) continue;
@@ -425,8 +446,10 @@ export function resetProviderQuotaReconcileStateForTests(): void {
425
446
 
426
447
  /** Drop cached per-account rows (all, or just one provider's). */
427
448
  export function clearAccountQuotaCache(provider?: string): void {
428
- explicitAccountEpoch += 1;
449
+ // Anthropic readers use provider-owned epochs; B cannot revoke an A flight.
450
+ if (provider !== "anthropic2") explicitAccountEpoch += 1;
429
451
  if (!provider) {
452
+ allAccountQuotaEpoch += 1;
430
453
  accountQuotaCache.clear();
431
454
  accountQuotaInflight.clear();
432
455
  clearKiroAccountUsageState();
@@ -436,6 +459,7 @@ export function clearAccountQuotaCache(provider?: string): void {
436
459
  cancelPendingAccountQuotaPersist();
437
460
  return;
438
461
  }
462
+ providerAccountQuotaEpochs.set(provider, (providerAccountQuotaEpochs.get(provider) ?? 0) + 1);
439
463
  hydrateAccountQuotaCache();
440
464
  const prefix = `${provider}\u0000`;
441
465
  for (const key of [...accountQuotaCache.keys()]) {
@@ -464,7 +488,9 @@ export async function getTokenForAccountQuotaProbe(provider: string, accountId:
464
488
  const row = getAccountCredentialWithStatus(provider, accountId);
465
489
  if (!row) throw new Error("account credential missing");
466
490
  // An operator-paused account is excluded from every automatic upstream use, quota reads included.
491
+ // Anthropic instances also skip a needs-reauth row; other providers keep their probe behavior.
467
492
  if (row.paused) throw new Error("account is paused; quota probe skipped");
493
+ if (row.needsReauth && isAnthropicInstanceId(provider)) throw new Error("account needs sign-in; quota probe skipped");
468
494
  const stored = row.credential;
469
495
  if (stored.expires > Date.now() + ACCOUNT_TOKEN_SKEW_MS) return stored.access;
470
496
  const activeId = getAccountSet(provider)?.activeAccountId;
@@ -0,0 +1,93 @@
1
+ import { loadConfig } from "../../config";
2
+ import type { OcxConfig } from "../../types";
3
+ import { getAccountSet } from "../../oauth/store";
4
+ import { captureConfigGeneration, sweepExpiredOnWrite } from "../../lib/state-store-sweeper";
5
+ import { configuredAnthropicInstance, type AnthropicInstanceId } from "../anthropic-instance";
6
+ import { ACCOUNT_QUOTA_TTL_MS } from "../quota-wire";
7
+ import { fetchAnthropicUsageQuotaForInstance } from "./vendor-probes-oauth";
8
+ import { AnthropicQuotaProbeOwnershipError, anthropicCooldownRecoveryFor, type AnthropicQuotaRecoveryResult } from "./anthropic-cooldown-recovery";
9
+ import {
10
+ accountCacheKey, accountQuotaCache, accountQuotaInflight, captureProviderAccountQuotaEpoch,
11
+ getTokenForAccountQuotaProbe, hydrateAccountQuotaCache, mayCommitAccountQuotaKey,
12
+ normalizeAnthropicQuota, type AccountQuotaCacheEntry,
13
+ } from "./account-cache";
14
+
15
+ /**
16
+ * Resolve renewal first: a flight always belongs to the credential actually dispatched.
17
+ * `config` is the caller's live object. Currency re-reads that one object, never a fresh file
18
+ * load, so memory/disk drift cannot refuse every probe; commit authority stays with the
19
+ * captured writer generation.
20
+ */
21
+ export async function fetchAnthropicAccountQuota(
22
+ instance: AnthropicInstanceId, accountId: string, force: boolean, config: OcxConfig = loadConfig(),
23
+ ): Promise<AccountQuotaCacheEntry> {
24
+ const unavailable = (): AccountQuotaCacheEntry => ({ ts: Date.now(), quota: null, unavailable: true });
25
+ const target = config.providers[instance];
26
+ if (target?.disabled || target && target.authMode !== "oauth" || configuredAnthropicInstance(config, instance) !== instance) return unavailable();
27
+ const targetIdentity = JSON.stringify(target);
28
+ const epoch = captureProviderAccountQuotaEpoch(instance);
29
+ const initialRow = getAccountSet(instance)?.accounts.find(row => row.id === accountId);
30
+ if (!initialRow) return unavailable();
31
+ const recovery = anthropicCooldownRecoveryFor(instance);
32
+ const incarnation = recovery.reserveAnthropicAccountIncarnation(accountId);
33
+ const ownerCurrent = () => {
34
+ const row = getAccountSet(instance)?.accounts.find(row => row.id === accountId);
35
+ return epoch === captureProviderAccountQuotaEpoch(instance)
36
+ && configuredAnthropicInstance(config, instance) === instance
37
+ && JSON.stringify(config.providers[instance]) === targetIdentity
38
+ && recovery.anthropicAccountIncarnation(accountId) === incarnation
39
+ && !!row && row.loginId === initialRow.loginId && row.addedAt === initialRow.addedAt;
40
+ };
41
+ hydrateAccountQuotaCache();
42
+ const key = accountCacheKey(instance, accountId);
43
+ const candidate = accountQuotaCache.get(key);
44
+ const cached = candidate?.isCurrent?.() === false ? undefined : candidate;
45
+ if (!force && cached && Date.now() - cached.ts < ACCOUNT_QUOTA_TTL_MS) {
46
+ return { ...cached, quota: normalizeAnthropicQuota(cached.quota, Date.now()) };
47
+ }
48
+ let token: string;
49
+ try { token = await getTokenForAccountQuotaProbe(instance, accountId); }
50
+ catch { return unavailable(); }
51
+ if (!ownerCurrent()) return unavailable();
52
+ const flightKey = recovery.anthropicCredentialQuotaFlightKey(key, accountId);
53
+ const flightCurrent = () => ownerCurrent()
54
+ && recovery.anthropicCredentialQuotaFlightKey(key, accountId) === flightKey;
55
+ const joinable = accountQuotaInflight.get(flightKey);
56
+ if (joinable) {
57
+ const joined = await joinable;
58
+ return ownerCurrent() && (joined.isCurrent?.() ?? flightCurrent()) ? joined : unavailable();
59
+ }
60
+ const writerGeneration = captureConfigGeneration();
61
+ // One publication path for a result and for a failed dispatch. A null result (HTTP refusal)
62
+ // and a thrown read (network failure) both record the unavailable row over the retained
63
+ // in-flight observations, as the pre-instance A path did, so joined callers and later cached
64
+ // reads agree and windows that expired during the shared probe are normalized away.
65
+ const publish = (result: AnthropicQuotaRecoveryResult | null): AccountQuotaCacheEntry => {
66
+ const previous = accountQuotaCache.get(key);
67
+ const retained = previous?.isCurrent?.() === false ? undefined : previous;
68
+ const isCurrent = result?.isCurrent ?? flightCurrent;
69
+ const entry: AccountQuotaCacheEntry = {
70
+ ts: Date.now(), quota: normalizeAnthropicQuota(result?.quota ?? retained?.quota, Date.now()),
71
+ ...(!result ? { unavailable: true as const } : {}), isCurrent,
72
+ };
73
+ if (isCurrent() && mayCommitAccountQuotaKey(key, writerGeneration)) { accountQuotaCache.set(key, entry); sweepExpiredOnWrite(entry.ts); }
74
+ return entry;
75
+ };
76
+ const probe = (async (): Promise<AccountQuotaCacheEntry> => {
77
+ try {
78
+ const result = await recovery.probeAnthropicQuotaWithRecovery(accountId, token, fresh => {
79
+ if (!flightCurrent()) throw new AnthropicQuotaProbeOwnershipError("quota flight changed");
80
+ recovery.assertAnthropicQuotaSendAllowed(accountId, token);
81
+ return fetchAnthropicUsageQuotaForInstance(instance, token, fresh);
82
+ }, () => ownerCurrent() && mayCommitAccountQuotaKey(key, writerGeneration));
83
+ if (result && !result.isCurrent()) return unavailable();
84
+ if (!result && !flightCurrent()) return unavailable();
85
+ return publish(result);
86
+ } catch (error) {
87
+ if (error instanceof AnthropicQuotaProbeOwnershipError || !flightCurrent()) return unavailable();
88
+ return publish(null);
89
+ }
90
+ })().finally(() => { if (accountQuotaInflight.get(flightKey) === probe) accountQuotaInflight.delete(flightKey); });
91
+ accountQuotaInflight.set(flightKey, probe);
92
+ return probe;
93
+ }