@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
@@ -2,6 +2,8 @@
2
2
  import { credentialGeneration, getAccountCredential } from "./store";
3
3
  import { getCachedProviderAccountQuota } from "../providers/quota/account-cache";
4
4
  import type { ProviderQuota, ProviderQuotaWindow } from "../providers/quota-types";
5
+ import type { AnthropicInstanceId } from "../providers/anthropic-instance-id";
6
+ import type { GenerationContext } from "../lib/state-store-sweeper";
5
7
 
6
8
  export const ANTHROPIC_PASSIVE_FAMILY_MAX_AGE_MS = 30 * 60_000;
7
9
  export type AnthropicModelFamily = "Fable" | "Opus" | "Sonnet";
@@ -17,67 +19,137 @@ export function anthropicModelPercents(quota: ProviderQuota | null, model?: stri
17
19
  ...(quota?.customWindows ?? []).filter(window => !model || window.scope !== "model" || (family && window.label === family)).map(window => window.percent)]
18
20
  .filter((value): value is number => typeof value === "number" && Number.isFinite(value));
19
21
  }
20
- export function anthropicModelExhausted(accountId: string, model?: string): boolean {
21
- return anthropicModelPercents(getCachedProviderAccountQuota("anthropic", accountId), model).some(percent => percent >= 100);
22
- }
23
-
24
22
  interface FamilyObservation { generation: string; seenAt: number; resetAt?: number; busy: boolean; }
25
- const spentFamilies = new Map<string, FamilyObservation>();
26
- const familyGenerations = new Map<string, number>();
27
- let nextFamilyGeneration = 1;
28
- export function anthropicFamilyQuotaGeneration(id: string): number { return familyGenerations.get(id) ?? 0; }
29
23
  const keyFor = (id: string, family: string) => `${id}\0${family}`;
30
- function ownedObservation(id: string, family: string): FamilyObservation | undefined {
31
- const key = keyFor(id, family);
32
- const entry = spentFamilies.get(key);
33
- if (!entry) return undefined;
34
- const credential = getAccountCredential("anthropic", id);
35
- if (!credential || credentialGeneration(credential) !== entry.generation) { spentFamilies.delete(key); return undefined; }
36
- return entry;
24
+ function createAnthropicModelQuota(instance: AnthropicInstanceId) {
25
+ let spentFamilies: Map<string, FamilyObservation> | undefined;
26
+ let familyGenerations: Map<string, number> | undefined;
27
+ let nextFamilyGeneration = 1;
28
+ let emptyGeneration = 0;
29
+ function bump(id: string): void { (familyGenerations ??= new Map()).set(id, nextFamilyGeneration++); }
30
+ function anthropicFamilyQuotaGeneration(id: string): number { return familyGenerations?.get(id) ?? emptyGeneration; }
31
+ /** Reserving an asynchronous probe is a write; ordinary generation reads stay allocation-free. */
32
+ function captureAnthropicFamilyQuotaGeneration(id: string): number {
33
+ if (!familyGenerations?.has(id)) bump(id);
34
+ return anthropicFamilyQuotaGeneration(id);
35
+ }
36
+ function anthropicModelExhausted(accountId: string, model?: string): boolean {
37
+ return anthropicModelPercents(getCachedProviderAccountQuota(instance, accountId), model).some(percent => percent >= 100);
38
+ }
39
+ function ownedObservation(id: string, family: string): FamilyObservation | undefined {
40
+ const key = keyFor(id, family);
41
+ const entry = spentFamilies?.get(key);
42
+ if (!entry) return undefined;
43
+ const credential = getAccountCredential(instance, id);
44
+ if (!credential || credentialGeneration(credential) !== entry.generation) { spentFamilies!.delete(key); bump(id); return undefined; }
45
+ return entry;
46
+ }
47
+ function observeAnthropicFamilyQuota(id: string, windows: ProviderQuotaWindow[], now: number, authoritative = false): void {
48
+ const credential = getAccountCredential(instance, id);
49
+ if (!credential) return;
50
+ if (!authoritative && !windows.some(window => window.scope === "model")) return;
51
+ bump(id);
52
+ const observations = spentFamilies ??= new Map();
53
+ if (authoritative) for (const key of observations.keys()) if (key.startsWith(`${id}\0`)) observations.delete(key);
54
+ for (const window of windows) {
55
+ if (window.scope !== "model") continue;
56
+ const key = keyFor(id, window.label);
57
+ if (!window.rejected) { observations.delete(key); continue; }
58
+ observations.set(key, { generation: credentialGeneration(credential), seenAt: now, resetAt: window.resetAt, busy: false });
59
+ }
60
+ }
61
+ function anthropicFamilyRejected(id: string, model?: string, now = Date.now()): boolean {
62
+ const family = anthropicModelFamily(model);
63
+ const entry = family && ownedObservation(id, family);
64
+ if (!entry) return false;
65
+ if (entry.resetAt !== undefined && entry.resetAt <= now) return entry.busy;
66
+ return entry.busy || now - entry.seenAt < ANTHROPIC_PASSIVE_FAMILY_MAX_AGE_MS;
67
+ }
68
+ /** Only stale passive family exclusions require a request-driven single-flight physical send. */
69
+ function claimAnthropicFamilyRevalidation(id: string, model?: string, now = Date.now()): (() => void) | null {
70
+ const family = anthropicModelFamily(model);
71
+ const entry = family && ownedObservation(id, family);
72
+ if (!entry) return () => {};
73
+ if (anthropicFamilyRejected(id, model, now)) return null;
74
+ entry.busy = true;
75
+ return () => { if (spentFamilies?.get(keyFor(id, family!)) === entry) entry.busy = false; };
76
+ }
77
+ /** An owned successful send proves admission for its requested family even without headers. */
78
+ function clearAnthropicRequestedFamilyQuota(id: string, model?: string): void {
79
+ const family = anthropicModelFamily(model);
80
+ if (family && spentFamilies?.delete(keyFor(id, family))) bump(id);
81
+ }
82
+ function clearAnthropicFamilyQuota(): void {
83
+ // A prior zero-generation claim must stay stale even after an identical account is re-added.
84
+ emptyGeneration = nextFamilyGeneration++;
85
+ spentFamilies = undefined;
86
+ familyGenerations = undefined;
87
+ }
88
+ function reconcileAnthropicFamilyQuota(context: GenerationContext): number {
89
+ let removed = 0;
90
+ const retired = new Set<string>();
91
+ for (const [key] of spentFamilies ?? []) {
92
+ const id = key.slice(0, key.indexOf("\0"));
93
+ if (context.oauthAccountKeys.has(`${instance}\0${id}`)) continue;
94
+ spentFamilies!.delete(key); retired.add(id); removed++;
95
+ }
96
+ for (const id of familyGenerations?.keys() ?? []) {
97
+ if (!context.oauthAccountKeys.has(`${instance}\0${id}`)) retired.add(id);
98
+ }
99
+ for (const id of retired) bump(id);
100
+ return removed;
101
+ }
102
+ function anthropicFamilyRetryAt(id: string, model?: string, now = Date.now()): number | undefined {
103
+ const family = anthropicModelFamily(model);
104
+ const entry = family && ownedObservation(id, family);
105
+ if (!entry || !anthropicFamilyRejected(id, model, now)) return undefined;
106
+ return entry.busy ? now + 1000 : Math.min(entry.resetAt ?? Infinity, entry.seenAt + ANTHROPIC_PASSIVE_FAMILY_MAX_AGE_MS);
107
+ }
108
+ return Object.freeze({ instance, anthropicModelExhausted, anthropicFamilyQuotaGeneration,
109
+ captureAnthropicFamilyQuotaGeneration,
110
+ observeAnthropicFamilyQuota, anthropicFamilyRejected, claimAnthropicFamilyRevalidation,
111
+ clearAnthropicRequestedFamilyQuota, clearAnthropicFamilyQuota, anthropicFamilyRetryAt, reconcileAnthropicFamilyQuota });
112
+ }
113
+
114
+ export type AnthropicModelQuota = ReturnType<typeof createAnthropicModelQuota>;
115
+ const instances = new Map<AnthropicInstanceId, AnthropicModelQuota>();
116
+ export function anthropicModelQuotaFor(instance: AnthropicInstanceId): AnthropicModelQuota {
117
+ let facade = instances.get(instance);
118
+ if (!facade) { facade = createAnthropicModelQuota(instance); instances.set(instance, facade); }
119
+ return facade;
120
+ }
121
+ export function reconcileAllAnthropicFamilyQuota(context: GenerationContext): number {
122
+ let removed = 0;
123
+ for (const facade of instances.values()) removed += facade.reconcileAnthropicFamilyQuota(context);
124
+ return removed;
125
+ }
126
+ export function clearAllAnthropicFamilyQuota(): void {
127
+ for (const facade of instances.values()) facade.clearAnthropicFamilyQuota();
128
+ }
129
+ export function anthropicModelExhausted(accountId: string, model?: string): boolean {
130
+ return anthropicModelQuotaFor("anthropic").anthropicModelExhausted(accountId, model);
131
+ }
132
+ export function anthropicFamilyQuotaGeneration(id: string): number {
133
+ return anthropicModelQuotaFor("anthropic").anthropicFamilyQuotaGeneration(id);
37
134
  }
38
135
  export function observeAnthropicFamilyQuota(id: string, windows: ProviderQuotaWindow[], now: number, authoritative = false): void {
39
- const credential = getAccountCredential("anthropic", id);
40
- if (!credential) return;
41
- if (authoritative || windows.some(window => window.scope === "model")) familyGenerations.set(id, nextFamilyGeneration++);
42
- if (authoritative) for (const key of spentFamilies.keys()) if (key.startsWith(`${id}\0`)) spentFamilies.delete(key);
43
- for (const window of windows) {
44
- if (window.scope !== "model") continue;
45
- const key = keyFor(id, window.label);
46
- if (!window.rejected) { spentFamilies.delete(key); continue; }
47
- spentFamilies.set(key, { generation: credentialGeneration(credential), seenAt: now, resetAt: window.resetAt, busy: false });
48
- }
136
+ anthropicModelQuotaFor("anthropic").observeAnthropicFamilyQuota(id, windows, now, authoritative);
49
137
  }
50
138
  export function anthropicFamilyRejected(id: string, model?: string, now = Date.now()): boolean {
51
- const family = anthropicModelFamily(model);
52
- const entry = family && ownedObservation(id, family);
53
- if (!entry) return false;
54
- if (entry.resetAt !== undefined && entry.resetAt <= now) return entry.busy;
55
- return entry.busy || now - entry.seenAt < ANTHROPIC_PASSIVE_FAMILY_MAX_AGE_MS;
139
+ return anthropicModelQuotaFor("anthropic").anthropicFamilyRejected(id, model, now);
56
140
  }
57
- /** Only stale passive family exclusions require a request-driven single-flight physical send. */
58
141
  export function claimAnthropicFamilyRevalidation(id: string, model?: string, now = Date.now()): (() => void) | null {
59
- const family = anthropicModelFamily(model);
60
- const entry = family && ownedObservation(id, family);
61
- if (!entry) return () => {};
62
- if (anthropicFamilyRejected(id, model, now)) return null;
63
- entry.busy = true;
64
- return () => { entry.busy = false; };
142
+ return anthropicModelQuotaFor("anthropic").claimAnthropicFamilyRevalidation(id, model, now);
65
143
  }
66
- /** An owned successful send proves admission for its requested family even without headers. */
67
144
  export function clearAnthropicRequestedFamilyQuota(id: string, model?: string): void {
68
- const family = anthropicModelFamily(model);
69
- if (family && spentFamilies.delete(keyFor(id, family))) familyGenerations.set(id, nextFamilyGeneration++);
145
+ anthropicModelQuotaFor("anthropic").clearAnthropicRequestedFamilyQuota(id, model);
146
+ }
147
+ export function clearAnthropicFamilyQuota(): void { anthropicModelQuotaFor("anthropic").clearAnthropicFamilyQuota(); }
148
+ export function anthropicFamilyRetryAt(id: string, model?: string, now = Date.now()): number | undefined {
149
+ return anthropicModelQuotaFor("anthropic").anthropicFamilyRetryAt(id, model, now);
70
150
  }
71
- export function clearAnthropicFamilyQuota(): void { spentFamilies.clear(); familyGenerations.clear(); }
72
151
 
73
152
  export function anthropicModelWeeklyPercent(quota: ProviderQuota | null, model?: string): number | undefined {
74
153
  const family = anthropicModelFamily(model);
75
154
  return family ? quota?.customWindows?.find(window => window.scope === "model" && window.label === family)?.percent : undefined;
76
155
  }
77
-
78
- export function anthropicFamilyRetryAt(id: string, model?: string, now = Date.now()): number | undefined {
79
- const family = anthropicModelFamily(model);
80
- const entry = family && ownedObservation(id, family);
81
- if (!entry || !anthropicFamilyRejected(id, model, now)) return undefined;
82
- return entry.busy ? now + 1000 : Math.min(entry.resetAt ?? Infinity, entry.seenAt + ANTHROPIC_PASSIVE_FAMILY_MAX_AGE_MS);
83
- }
@@ -1,3 +1,5 @@
1
+ import type { AnthropicInstanceId } from "../providers/anthropic-instance-id";
2
+ import { resolveAnthropicAccountPoolConfig } from "./anthropic-pool-config";
1
3
  import type { AnthropicModelRoute, OcxConfig } from "../types/config";
2
4
 
3
5
  export interface AnthropicRouteDecision {
@@ -79,12 +81,14 @@ function matches(pattern: string, modelId: string): boolean {
79
81
  return p === pattern.length;
80
82
  }
81
83
 
82
- export function resolveAnthropicModelRoute(
84
+ export function resolveAnthropicModelRouteForInstance(
85
+ instance: AnthropicInstanceId,
83
86
  config: OcxConfig,
84
87
  modelId: string,
85
88
  ): { decision: AnthropicRouteDecision | null; error?: string } {
86
- if (config.anthropicAccountPool?.enabled !== true) return { decision: null };
87
- const raw = (config.anthropicAccountPool as { routes?: unknown } | undefined)?.routes;
89
+ const pool = resolveAnthropicAccountPoolConfig(config, instance);
90
+ if (pool.enabled !== true) return { decision: null };
91
+ const raw = pool.routes;
88
92
  if (raw === undefined) return { decision: null };
89
93
  const parsed = parseAnthropicModelRoutes(raw);
90
94
  if (!parsed.ok) return { decision: null, error: parsed.error };
@@ -103,3 +107,7 @@ export function routeCandidates(eligible: readonly string[], decision: Anthropic
103
107
  const routed = decision.accounts.filter(id => available.has(id));
104
108
  return routed.length > 0 || !decision.fallback ? routed : [...eligible];
105
109
  }
110
+
111
+ export function resolveAnthropicModelRoute(config: OcxConfig, modelId: string): { decision: AnthropicRouteDecision | null; error?: string } {
112
+ return resolveAnthropicModelRouteForInstance("anthropic", config, modelId);
113
+ }
@@ -0,0 +1,98 @@
1
+ import { ConfigMutationLockError } from "../config";
2
+ import { deriveOAuthDefaultModel, deriveOAuthProviderConfig } from "../providers/derive";
3
+ import type { AnthropicInstanceId } from "../providers/anthropic-instance-id";
4
+ import { loginAnthropic, refreshAnthropicToken } from "./anthropic";
5
+ import type { OAuthProviderDef } from "./index";
6
+ import {
7
+ clearOAuthRefreshIntent, clearOAuthRefreshIntentIfMatch, markOAuthRefreshIntentCleanupPending,
8
+ OAuthRefreshIntentIOError, type OAuthRefreshIntent, type OAuthRefreshIntentCleanupPending,
9
+ } from "./store";
10
+
11
+ /** Both pools use the same flow; only A may import the local Claude credential. */
12
+ export function anthropicOAuthDefinition(instance: AnthropicInstanceId): OAuthProviderDef {
13
+ const providerConfig = deriveOAuthProviderConfig(instance);
14
+ const defaultModel = deriveOAuthDefaultModel(instance);
15
+ if (!providerConfig || !defaultModel) throw new Error(`OAuth provider missing from registry: ${instance}`);
16
+ return {
17
+ login: (ctrl, opts) => loginAnthropic(ctrl, {
18
+ instance, importLocal: instance === "anthropic2" || opts?.forceLogin ? "off" : "fallback",
19
+ }),
20
+ refresh: refreshAnthropicToken,
21
+ providerConfig,
22
+ defaultModel,
23
+ // Subscription OAuth must never generate unattended traffic by default.
24
+ defaultRefreshPolicy: "disabled",
25
+ };
26
+ }
27
+
28
+ export { AnthropicInstanceCollisionError, assertAnthropicInstanceLoginConfig } from "./store-anthropic-instance";
29
+
30
+ /** Cleanup is secondary once a rotated credential has become durable. */
31
+ export function clearAnthropicRefreshIntentBestEffort(provider: string, accountId: string, expected: OAuthRefreshIntent): boolean {
32
+ try {
33
+ return expected.attemptId
34
+ ? clearOAuthRefreshIntentIfMatch(provider, accountId, expected)
35
+ : clearOAuthRefreshIntent(provider, accountId, expected.generation);
36
+ } catch {
37
+ console.warn("[opencodex] Anthropic refresh intent cleanup failed; preserving the durable replay guard.");
38
+ return false;
39
+ }
40
+ }
41
+
42
+ const ANTHROPIC_INTENT_MARK_RETRY_DELAYS_MS = [10, 25, 50] as const;
43
+ function isConfigMutationLockContention(error: unknown): boolean {
44
+ if (!(error instanceof ConfigMutationLockError)) return false;
45
+ const cause = error.cause;
46
+ const code = cause && typeof cause === "object" && "code" in cause
47
+ ? String((cause as { code?: unknown }).code) : "";
48
+ return code === "SQLITE_BUSY" || code === "SQLITE_LOCKED";
49
+ }
50
+
51
+ export async function clearAnthropicRefreshIntentForKnownFailure(
52
+ provider: string, accountId: string, expected: OAuthRefreshIntent,
53
+ cleanupPending: OAuthRefreshIntentCleanupPending, refreshError: unknown,
54
+ ): Promise<boolean> {
55
+ let marked: OAuthRefreshIntent | undefined;
56
+ for (let attempt = 0; attempt <= ANTHROPIC_INTENT_MARK_RETRY_DELAYS_MS.length; attempt += 1) {
57
+ try {
58
+ marked = markOAuthRefreshIntentCleanupPending(provider, accountId, expected, cleanupPending);
59
+ break;
60
+ } catch (cause) {
61
+ const retryDelay = ANTHROPIC_INTENT_MARK_RETRY_DELAYS_MS[attempt];
62
+ if (!isConfigMutationLockContention(cause) || retryDelay === undefined) {
63
+ throw new OAuthRefreshIntentIOError("mark-cleanup-pending", cause, refreshError);
64
+ }
65
+ // Settlement retains the account lock after a definitive provider answer.
66
+ await Bun.sleep(retryDelay);
67
+ }
68
+ }
69
+ if (!marked) throw new OAuthRefreshIntentIOError("mark-cleanup-pending",
70
+ new Error("Anthropic refresh intent changed before safe cleanup"), refreshError);
71
+ let cleared: boolean;
72
+ try {
73
+ cleared = clearOAuthRefreshIntentIfMatch(provider, accountId, marked);
74
+ } catch {
75
+ console.warn("[opencodex] Anthropic refresh intent cleanup failed; retry-safe cleanup remains pending.");
76
+ return false;
77
+ }
78
+ if (!cleared) throw new OAuthRefreshIntentIOError("clear-cleanup-pending",
79
+ new Error("Anthropic refresh intent changed during safe cleanup"), refreshError);
80
+ return true;
81
+ }
82
+
83
+ export function resumeAnthropicRefreshIntentCleanup(provider: string, accountId: string, pendingIntent: OAuthRefreshIntent): void {
84
+ let cleared: boolean;
85
+ try {
86
+ cleared = clearOAuthRefreshIntentIfMatch(provider, accountId, pendingIntent);
87
+ } catch (cause) {
88
+ throw new OAuthRefreshIntentIOError("resume-cleanup", cause);
89
+ }
90
+ if (!cleared) throw new OAuthRefreshIntentIOError("resume-cleanup",
91
+ new Error("Pending Anthropic refresh intent changed before cleanup"));
92
+ }
93
+
94
+ export function clearObservedAnthropicRefreshIntent(provider: string, accountId: string, pendingIntent: OAuthRefreshIntent): boolean {
95
+ return pendingIntent.attemptId
96
+ ? clearOAuthRefreshIntentIfMatch(provider, accountId, pendingIntent)
97
+ : clearOAuthRefreshIntent(provider, accountId, pendingIntent.generation);
98
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Per-instance Anthropic pool settings. A reads only `config.anthropicAccountPool`; B reads only
3
+ * `config.providers.anthropic2.anthropicAccountPool`. A missing B block means product defaults, never
4
+ * A's settings. Pure: no store, network or state access.
5
+ */
6
+ import type { OcxConfig } from "../types";
7
+ import type { AnthropicAccountPoolConfig } from "../types/anthropic-account-pool";
8
+ import type { AnthropicInstanceId } from "../providers/anthropic-instance-id";
9
+
10
+ /** The raw value at the instance's own location, preserving absent/invalid distinctions for callers that need them. */
11
+ export function rawAnthropicAccountPool(config: Pick<OcxConfig, "anthropicAccountPool" | "providers">, instance: AnthropicInstanceId): unknown {
12
+ if (instance === "anthropic") return config.anthropicAccountPool;
13
+ const row: unknown = config.providers?.[instance];
14
+ return row && typeof row === "object" ? (row as { anthropicAccountPool?: unknown }).anthropicAccountPool : undefined;
15
+ }
16
+
17
+ export function resolveAnthropicAccountPoolConfig(
18
+ config: Pick<OcxConfig, "anthropicAccountPool" | "providers">,
19
+ instance: AnthropicInstanceId,
20
+ ): AnthropicAccountPoolConfig {
21
+ const raw = rawAnthropicAccountPool(config, instance);
22
+ return raw && typeof raw === "object" && !Array.isArray(raw) ? raw as AnthropicAccountPoolConfig : {};
23
+ }
24
+
25
+ export function isAnthropicPoolEnabledFor(
26
+ config: Pick<OcxConfig, "anthropicAccountPool" | "providers">,
27
+ instance: AnthropicInstanceId,
28
+ ): boolean {
29
+ return resolveAnthropicAccountPoolConfig(config, instance).enabled === true;
30
+ }
@@ -1,4 +1,7 @@
1
1
  /** Response-header policy for managed Anthropic sends; no account or request payload data. */
2
+ import type { AnthropicInstanceId } from "../providers/anthropic-instance-id";
3
+ import type { GenerationContext } from "../lib/state-store-sweeper";
4
+ import { credentialGeneration, getAccountCredential } from "./store";
2
5
  export type AnthropicRateLimitKind = "shared-quota" | "family-quota" | "transient-rate" | "request-scoped-unknown";
3
6
  export type RateLimitHeaders = Pick<Headers, "get">;
4
7
  export const ANTHROPIC_SHORT_RETRY_MS = 100;
@@ -22,16 +25,68 @@ export function classifyAnthropic429(headers: RateLimitHeaders, now = Date.now()
22
25
  return "request-scoped-unknown";
23
26
  }
24
27
 
25
- const pauses = new Map<string, number>();
28
+ function createAnthropicRatePolicy(instance: AnthropicInstanceId) {
29
+ let pauses: Map<string, { until: number; generation: string }> | undefined;
30
+ function pauseAnthropicRateAdmission(accountId: string, until: number): void {
31
+ const credential = getAccountCredential(instance, accountId);
32
+ if (!credential) return;
33
+ const now = Date.now();
34
+ const generation = credentialGeneration(credential);
35
+ const rows = pauses ??= new Map();
36
+ for (const [id, pause] of rows) if (pause.until <= now) rows.delete(id);
37
+ const previous = rows.get(accountId);
38
+ rows.set(accountId, { until: Math.max(previous?.generation === generation ? previous.until : 0, until), generation });
39
+ }
40
+ function anthropicRatePauseUntil(accountId: string, now = Date.now()): number | undefined {
41
+ const pause = pauses?.get(accountId);
42
+ if (!pause) return undefined;
43
+ const credential = getAccountCredential(instance, accountId);
44
+ if (pause.until <= now || !credential || credentialGeneration(credential) !== pause.generation) {
45
+ pauses!.delete(accountId); return undefined;
46
+ }
47
+ return pause.until;
48
+ }
49
+ function clearAnthropicRatePauses(): void { pauses = undefined; }
50
+ function reconcileAnthropicRatePauses(context: GenerationContext): number {
51
+ let removed = 0;
52
+ for (const id of pauses?.keys() ?? []) {
53
+ if (context.oauthAccountKeys.has(`${instance}\0${id}`)) continue;
54
+ pauses!.delete(id); removed++;
55
+ }
56
+ return removed;
57
+ }
58
+ function sweepExpiredAnthropicRatePauses(now = Date.now()): number {
59
+ let removed = 0;
60
+ for (const [id, pause] of pauses ?? []) if (pause.until <= now) { pauses!.delete(id); removed++; }
61
+ return removed;
62
+ }
63
+ return Object.freeze({ instance, pauseAnthropicRateAdmission, anthropicRatePauseUntil,
64
+ clearAnthropicRatePauses, reconcileAnthropicRatePauses, sweepExpiredAnthropicRatePauses });
65
+ }
66
+ export type AnthropicRatePolicy = ReturnType<typeof createAnthropicRatePolicy>;
67
+ const instances = new Map<AnthropicInstanceId, AnthropicRatePolicy>();
68
+ export function anthropicRatePolicyFor(instance: AnthropicInstanceId): AnthropicRatePolicy {
69
+ let facade = instances.get(instance);
70
+ if (!facade) { facade = createAnthropicRatePolicy(instance); instances.set(instance, facade); }
71
+ return facade;
72
+ }
73
+ export function reconcileAllAnthropicRatePauses(context: GenerationContext): number {
74
+ let removed = 0;
75
+ for (const facade of instances.values()) removed += facade.reconcileAnthropicRatePauses(context);
76
+ return removed;
77
+ }
78
+ export function sweepExpiredAllAnthropicRatePauses(now = Date.now()): number {
79
+ let removed = 0;
80
+ for (const facade of instances.values()) removed += facade.sweepExpiredAnthropicRatePauses(now);
81
+ return removed;
82
+ }
83
+ export function clearAllAnthropicRatePauses(): void {
84
+ for (const facade of instances.values()) facade.clearAnthropicRatePauses();
85
+ }
26
86
  export function pauseAnthropicRateAdmission(accountId: string, until: number): void {
27
- const now = Date.now();
28
- for (const [id, deadline] of pauses) if (deadline <= now) pauses.delete(id);
29
- pauses.set(accountId, Math.max(pauses.get(accountId) ?? 0, until));
87
+ anthropicRatePolicyFor("anthropic").pauseAnthropicRateAdmission(accountId, until);
30
88
  }
31
89
  export function anthropicRatePauseUntil(accountId: string, now = Date.now()): number | undefined {
32
- const until = pauses.get(accountId);
33
- if (until === undefined) return undefined;
34
- if (until <= now) { pauses.delete(accountId); return undefined; }
35
- return until;
90
+ return anthropicRatePolicyFor("anthropic").anthropicRatePauseUntil(accountId, now);
36
91
  }
37
- export function clearAnthropicRatePauses(): void { pauses.clear(); }
92
+ export function clearAnthropicRatePauses(): void { anthropicRatePolicyFor("anthropic").clearAnthropicRatePauses(); }