@bitkyc08/opencodex 2.80.0 → 2.82.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 (376) hide show
  1. package/README.md +3 -3
  2. package/bin/ocx.mjs +166 -73
  3. package/gui/dist/assets/App-Ds_qL7o8.js +54 -0
  4. package/gui/dist/assets/Tray-BqHF5Trd.js +1 -0
  5. package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
  6. package/gui/dist/assets/index-D3FBCNiP.js +94 -0
  7. package/gui/dist/assets/quota-summary-DTBC8n7-.js +1 -0
  8. package/gui/dist/index.html +2 -2
  9. package/gui/dist/provider-icons/claude-green.svg +1 -0
  10. package/package.json +3 -2
  11. package/src/adapters/anthropic-model-contract.ts +8 -2
  12. package/src/adapters/anthropic.ts +8 -3
  13. package/src/adapters/codebuddy/mcp-server.ts +2 -25
  14. package/src/adapters/codebuddy/tool-bridge.ts +1 -7
  15. package/src/adapters/coding-agent/mcp-server.ts +33 -0
  16. package/src/adapters/coding-agent/protocol.ts +122 -17
  17. package/src/adapters/coding-agent/tool-catalog.ts +75 -0
  18. package/src/adapters/coding-agent/turn.ts +101 -44
  19. package/src/adapters/cursor/catalog.ts +8 -0
  20. package/src/adapters/cursor/effort-map.ts +2 -0
  21. package/src/adapters/devin/live-models.ts +2 -0
  22. package/src/adapters/kiro-retry.ts +9 -4
  23. package/src/adapters/openai-chat/messages.ts +7 -6
  24. package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
  25. package/src/adapters/openai-responses/passthrough.ts +9 -4
  26. package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
  27. package/src/adapters/physical-send.ts +8 -5
  28. package/src/adapters/qoder/adapter.ts +41 -6
  29. package/src/adapters/qoder/scaffold-guard.ts +2 -3
  30. package/src/adapters/zed.ts +16 -1
  31. package/src/claude/agents-inject.ts +14 -3
  32. package/src/claude/context-windows.ts +19 -17
  33. package/src/claude/desktop-3p.ts +88 -12
  34. package/src/claude/desktop-profile.ts +2 -1
  35. package/src/claude/gateway-cache.ts +18 -1
  36. package/src/claude/inbound-cache-stabilize.ts +4 -9
  37. package/src/claude/inbound.ts +78 -3
  38. package/src/claude/intercept/cli-catalog.ts +3 -0
  39. package/src/claude/intercept/model-bindings.ts +1 -0
  40. package/src/claude/intercept/picker-ca-persistence.ts +7 -6
  41. package/src/claude/intercept/picker-listener.ts +71 -4
  42. package/src/claude/intercept/picker-models.ts +14 -6
  43. package/src/claude/intercept/settings.ts +3 -0
  44. package/src/claude/long-context.ts +66 -0
  45. package/src/claude/model-info.ts +19 -16
  46. package/src/claude/outbound.ts +94 -9
  47. package/src/claude/subagent-model.ts +40 -11
  48. package/src/cli/account-anthropic-threshold.ts +14 -5
  49. package/src/cli/account-api.ts +3 -1
  50. package/src/cli/account-extended.ts +13 -9
  51. package/src/cli/account-policy-dto.ts +1 -1
  52. package/src/cli/account-policy.ts +13 -4
  53. package/src/cli/account.ts +3 -3
  54. package/src/cli/agent.ts +9 -5
  55. package/src/cli/aside-profile-recovery.ts +33 -0
  56. package/src/cli/capabilities-accounts.ts +2 -2
  57. package/src/cli/capabilities-agents-routing.ts +24 -0
  58. package/src/cli/capabilities-base.ts +5 -5
  59. package/src/cli/capabilities-integrations.ts +18 -2
  60. package/src/cli/cli-path-diagnostics.ts +132 -0
  61. package/src/cli/codex-shim-autorestore.ts +3 -0
  62. package/src/cli/desktop-runtime-guidance.ts +65 -0
  63. package/src/cli/dispatch.ts +15 -1
  64. package/src/cli/doctor.ts +16 -3
  65. package/src/cli/help.ts +4 -1
  66. package/src/cli/index.ts +25 -14
  67. package/src/cli/integration-aside-sync.ts +2 -1
  68. package/src/cli/integration-plan-dto.ts +1 -0
  69. package/src/cli/integration-preview.ts +8 -1
  70. package/src/cli/integrations.ts +48 -6
  71. package/src/cli/message-args.ts +34 -0
  72. package/src/cli/message-command.ts +43 -0
  73. package/src/cli/message-runtime.ts +6 -0
  74. package/src/cli/observe.ts +3 -1
  75. package/src/cli/provider.ts +29 -6
  76. package/src/cli/registry.ts +24 -2
  77. package/src/cli/resolve.ts +25 -2
  78. package/src/cli/root.ts +4 -2
  79. package/src/cli/status.ts +64 -12
  80. package/src/cli/update-restart-child.ts +4 -2
  81. package/src/cli/update-restart-home.ts +28 -10
  82. package/src/cli/update-restart-service-record.ts +116 -0
  83. package/src/cli/update-restart-supervision.ts +96 -0
  84. package/src/cli/update-restart.ts +59 -13
  85. package/src/cli/v2.ts +27 -20
  86. package/src/cli/version-skew-notice.ts +77 -0
  87. package/src/clients/config-export/commandcode.ts +131 -0
  88. package/src/clients/config-export/contracts.ts +12 -0
  89. package/src/clients/config-export.ts +64 -1
  90. package/src/clients/lazycodex.ts +1 -1
  91. package/src/clients/omo-role-models.ts +32 -7
  92. package/src/codex/auth-api/account-list.ts +11 -3
  93. package/src/codex/auth-api/account-selection.ts +107 -0
  94. package/src/codex/auth-api/main-account-probe.ts +25 -8
  95. package/src/codex/auth-api/pool-mode-gate.ts +33 -9
  96. package/src/codex/auth-api/pool-quota-probe.ts +15 -1
  97. package/src/codex/auth-api/routes.ts +11 -49
  98. package/src/codex/auth-context.ts +20 -1
  99. package/src/codex/autostart-health.ts +12 -3
  100. package/src/codex/catalog/auto-review.ts +13 -6
  101. package/src/codex/catalog/build-entries.ts +33 -7
  102. package/src/codex/catalog/bundled.ts +155 -26
  103. package/src/codex/catalog/combo-member.ts +3 -2
  104. package/src/codex/catalog/control-plane.ts +45 -0
  105. package/src/codex/catalog/effort.ts +37 -6
  106. package/src/codex/catalog/gather-capture.ts +10 -3
  107. package/src/codex/catalog/model-hints.ts +15 -8
  108. package/src/codex/catalog/model-visibility.ts +3 -1
  109. package/src/codex/catalog/operator-tiers.ts +38 -0
  110. package/src/codex/catalog/parsing.ts +2 -1
  111. package/src/codex/catalog/provider-models.ts +58 -44
  112. package/src/codex/catalog/retained-sync.ts +7 -5
  113. package/src/codex/catalog/routed-gather.ts +15 -1
  114. package/src/codex/catalog-auto-refresh-sources.ts +3 -4
  115. package/src/codex/catalog-auto-refresh.ts +19 -0
  116. package/src/codex/catalog.ts +1 -1
  117. package/src/codex/codex-home-owner.ts +3 -0
  118. package/src/codex/config-write-lock.ts +333 -0
  119. package/src/codex/control-plane-models.ts +6 -0
  120. package/src/codex/convergence.ts +2 -1
  121. package/src/codex/features.ts +257 -200
  122. package/src/codex/inject/config-write-section.ts +34 -0
  123. package/src/codex/inject/multi-agent-v2.ts +14 -6
  124. package/src/codex/inject/remove.ts +36 -11
  125. package/src/codex/inject/restore.ts +85 -20
  126. package/src/codex/inject-coordination.ts +15 -6
  127. package/src/codex/inject.ts +62 -20
  128. package/src/codex/journal.ts +62 -17
  129. package/src/codex/main-account-cache.ts +43 -0
  130. package/src/codex/paths.ts +2 -0
  131. package/src/codex/prepared-home.ts +18 -0
  132. package/src/codex/prompt-journal.ts +25 -12
  133. package/src/codex/prompt-layers.ts +49 -26
  134. package/src/codex/prompt-lock-claim.ts +134 -0
  135. package/src/codex/prompt-lock-evidence.ts +26 -0
  136. package/src/codex/prompt-lock-io.ts +16 -0
  137. package/src/codex/prompt-lock-owner.ts +130 -0
  138. package/src/codex/prompt-lock.ts +84 -53
  139. package/src/codex/routing/failure-window.ts +146 -0
  140. package/src/codex/routing/selection.ts +4 -1
  141. package/src/codex/routing.ts +13 -17
  142. package/src/codex/runtime.ts +106 -18
  143. package/src/codex/shim.ts +45 -19
  144. package/src/combos/jev-decision-contract.ts +25 -2
  145. package/src/combos/jev-service-exchange.ts +281 -0
  146. package/src/combos/jev.ts +34 -219
  147. package/src/combos/types.ts +1 -1
  148. package/src/config/atomic-write.ts +1 -1
  149. package/src/config/codex-ws-reuse-setting.ts +10 -0
  150. package/src/config/diagnostics.ts +27 -16
  151. package/src/config/live-reconcile.ts +17 -0
  152. package/src/config/load-degrade.ts +42 -0
  153. package/src/config/rebase-provenance.ts +11 -0
  154. package/src/config/schema/anthropic-account-pool.ts +143 -0
  155. package/src/config/schema/config-schema.ts +11 -1
  156. package/src/config/schema/leaf-validators.ts +4 -0
  157. package/src/config.ts +2 -0
  158. package/src/generated/compatibility-version.json +566 -298
  159. package/src/generated/model-metadata.ts +7 -6
  160. package/src/grok/inject.ts +50 -4
  161. package/src/grok/sync.ts +2 -1
  162. package/src/images/loop.ts +30 -11
  163. package/src/integrations/aside-profiles.ts +10 -3
  164. package/src/integrations/catalog-refresh.ts +19 -3
  165. package/src/integrations/mutation-plan.ts +7 -0
  166. package/src/integrations/registry.ts +8 -0
  167. package/src/integrations/state.ts +9 -2
  168. package/src/integrations/writer.ts +17 -5
  169. package/src/lib/bun-path-runtime.d.mts +32 -0
  170. package/src/lib/bun-path-runtime.mjs +95 -0
  171. package/src/lib/bun-runtime-preflight.ts +140 -0
  172. package/src/lib/desktop-cli-handoff.d.mts +20 -0
  173. package/src/lib/desktop-cli-handoff.mjs +92 -0
  174. package/src/lib/desktop-cli-record.d.mts +29 -0
  175. package/src/lib/desktop-cli-record.mjs +102 -0
  176. package/src/lib/lab-live-route-production.ts +9 -1
  177. package/src/lib/request-execution-budget.ts +244 -58
  178. package/src/lib/spend-pool-alias-validation.ts +79 -0
  179. package/src/lib/spend-pool-continuity.ts +34 -0
  180. package/src/lib/spend-reservation-ledger.ts +412 -50
  181. package/src/lib/state-store-registrations.ts +20 -3
  182. package/src/lib/synced-state-location.ts +46 -6
  183. package/src/lib/test-home-guard.ts +87 -1
  184. package/src/lib/translator-budget.ts +17 -4
  185. package/src/lib/upstream-retry.ts +17 -5
  186. package/src/lib/windows-atomic-replace.ts +38 -16
  187. package/src/lib/windows-elevation.ts +6 -0
  188. package/src/lib/windows-secret-acl.ts +92 -25
  189. package/src/lib/winsw.ts +2 -3
  190. package/src/lib/workflow-budget.ts +53 -11
  191. package/src/messaging/budget.ts +45 -0
  192. package/src/messaging/discovery.ts +57 -0
  193. package/src/messaging/envelope.ts +49 -0
  194. package/src/messaging/input.ts +37 -0
  195. package/src/messaging/rpc.ts +196 -0
  196. package/src/messaging/send.ts +58 -0
  197. package/src/messaging/socket.ts +72 -0
  198. package/src/messaging/types.ts +29 -0
  199. package/src/oauth/anthropic-account-refusal.ts +90 -18
  200. package/src/oauth/anthropic-account-threshold.ts +8 -2
  201. package/src/oauth/anthropic-continuity.ts +15 -1
  202. package/src/oauth/anthropic-model-quota.ts +120 -48
  203. package/src/oauth/anthropic-model-routes.ts +11 -3
  204. package/src/oauth/anthropic-oauth-definitions.ts +98 -0
  205. package/src/oauth/anthropic-pool-config.ts +30 -0
  206. package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
  207. package/src/oauth/anthropic-routing.ts +1202 -866
  208. package/src/oauth/anthropic-send-ownership.ts +41 -0
  209. package/src/oauth/anthropic.ts +4 -1
  210. package/src/oauth/chatgpt.ts +113 -41
  211. package/src/oauth/generic-account-failover.ts +3 -1
  212. package/src/oauth/health.ts +8 -6
  213. package/src/oauth/index.ts +96 -175
  214. package/src/oauth/kiro.ts +22 -9
  215. package/src/oauth/login-cli.ts +2 -0
  216. package/src/oauth/model-discovery-auth.ts +43 -0
  217. package/src/oauth/pool-kernel.ts +11 -5
  218. package/src/oauth/pool-settings-capability.ts +5 -3
  219. package/src/oauth/store-anthropic-instance.ts +87 -0
  220. package/src/oauth/store.ts +68 -10
  221. package/src/oauth/token-guardian.ts +2 -0
  222. package/src/oauth/types.ts +2 -0
  223. package/src/protocols/plan-snapshot.ts +5 -0
  224. package/src/protocols/settings.ts +20 -5
  225. package/src/providers/anthropic-instance-id.ts +47 -0
  226. package/src/providers/anthropic-instance.ts +48 -0
  227. package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
  228. package/src/providers/azure-model-metadata.ts +115 -0
  229. package/src/providers/derive.ts +23 -0
  230. package/src/providers/kiro-models.ts +3 -0
  231. package/src/providers/label.ts +1 -1
  232. package/src/providers/model-rename-fields.ts +2 -0
  233. package/src/providers/quota/account-cache.ts +37 -11
  234. package/src/providers/quota/anthropic-account-quota.ts +93 -0
  235. package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
  236. package/src/providers/quota/report-cache.ts +24 -1
  237. package/src/providers/quota/vendor-probes-key.ts +127 -17
  238. package/src/providers/quota/vendor-probes-oauth.ts +46 -14
  239. package/src/providers/quota.ts +23 -33
  240. package/src/providers/registry/entries-core.ts +35 -30
  241. package/src/providers/registry/model-ids.ts +1 -0
  242. package/src/providers/registry/model-seeds.ts +7 -4
  243. package/src/providers/registry/types.ts +3 -0
  244. package/src/providers/registry.ts +3 -3
  245. package/src/responses/reasoning-envelope.ts +29 -1
  246. package/src/responses/reasoning-replay-cache.ts +17 -1
  247. package/src/responses/spill-store.ts +20 -3
  248. package/src/responses/state/metrics.ts +95 -0
  249. package/src/responses/state/snapshot-codec.ts +6 -0
  250. package/src/responses/state/spill-failure.ts +12 -0
  251. package/src/responses/state/spill-queue.ts +13 -5
  252. package/src/responses/state/unforced-store-false.ts +55 -0
  253. package/src/responses/state.ts +68 -76
  254. package/src/responses/task-input.ts +17 -0
  255. package/src/router.ts +28 -4
  256. package/src/routing/compatibility/assemble.ts +1 -1
  257. package/src/routing/quota.ts +7 -3
  258. package/src/server/auth-cors.ts +5 -0
  259. package/src/server/background-lifecycle.ts +27 -0
  260. package/src/server/chat-completions.ts +7 -1
  261. package/src/server/chat-native.ts +64 -28
  262. package/src/server/claude-messages.ts +59 -8
  263. package/src/server/gui-static.ts +38 -4
  264. package/src/server/inbound-body-admission.ts +1 -0
  265. package/src/server/index/claude-intercept-lifecycle.ts +3 -1
  266. package/src/server/index/serve-options.ts +10 -2
  267. package/src/server/index/spend-ledger-lifecycle.ts +12 -5
  268. package/src/server/index/startup-warnings.ts +19 -0
  269. package/src/server/index.ts +5 -2
  270. package/src/server/inference/context.ts +1 -1
  271. package/src/server/lifecycle.ts +1 -1
  272. package/src/server/management/agent-settings-routes.ts +123 -38
  273. package/src/server/management/anthropic-account-threshold.ts +12 -9
  274. package/src/server/management/anthropic-pool-settings.ts +72 -0
  275. package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
  276. package/src/server/management/codex-agent-role-routes.ts +4 -2
  277. package/src/server/management/codex-prompt-routes.ts +1 -0
  278. package/src/server/management/codex-role-auto-assign.ts +13 -2
  279. package/src/server/management/config-routes.ts +85 -14
  280. package/src/server/management/context.ts +2 -2
  281. package/src/server/management/decision-routes.ts +2 -1
  282. package/src/server/management/integration-routes.ts +25 -2
  283. package/src/server/management/oauth-account-routes.ts +79 -98
  284. package/src/server/management/provider-instance-ownership.ts +44 -0
  285. package/src/server/management/provider-routes.ts +21 -28
  286. package/src/server/management/route-registry.ts +3 -1
  287. package/src/server/management/routing-profile-routes.ts +4 -4
  288. package/src/server/management/shared.ts +3 -3
  289. package/src/server/management/system-restart.ts +44 -14
  290. package/src/server/management/system-routes.ts +24 -2
  291. package/src/server/management/vision-sidecar-options.ts +9 -6
  292. package/src/server/management/web-search-sidecar-options.ts +51 -2
  293. package/src/server/management-auth.ts +6 -0
  294. package/src/server/messages-native-eligibility.ts +3 -2
  295. package/src/server/messages-native-oauth.ts +91 -45
  296. package/src/server/messages-native-selector.ts +39 -0
  297. package/src/server/messages-native.ts +133 -38
  298. package/src/server/messages-response-headers.ts +14 -0
  299. package/src/server/request-log-conversation.ts +48 -5
  300. package/src/server/request-log.ts +36 -2
  301. package/src/server/responses/adapter-continuation.ts +31 -19
  302. package/src/server/responses/adapter-dispatch.ts +75 -24
  303. package/src/server/responses/antigravity-validation-refusal.ts +20 -6
  304. package/src/server/responses/codex-ws-exchange.ts +17 -2
  305. package/src/server/responses/codex-ws-pool.ts +100 -13
  306. package/src/server/responses/codex-ws-wire.ts +9 -0
  307. package/src/server/responses/combo-requested-effort.ts +3 -1
  308. package/src/server/responses/compact.ts +25 -8
  309. package/src/server/responses/compaction-recovery.ts +1 -1
  310. package/src/server/responses/core-codex-account.ts +22 -1
  311. package/src/server/responses/core-combo-native.ts +24 -4
  312. package/src/server/responses/core-combo.ts +38 -25
  313. package/src/server/responses/core-lifetime.ts +3 -36
  314. package/src/server/responses/core-normalize.ts +1 -0
  315. package/src/server/responses/core-options.ts +8 -1
  316. package/src/server/responses/core-replay.ts +78 -0
  317. package/src/server/responses/core.ts +3 -3
  318. package/src/server/responses/fetch-helpers.ts +26 -3
  319. package/src/server/responses/passthrough-delivery.ts +30 -4
  320. package/src/server/responses/passthrough-dispatch.ts +39 -9
  321. package/src/server/responses/request-prepare.ts +41 -5
  322. package/src/server/responses/request-send-budget.ts +150 -50
  323. package/src/server/responses/request-spend.ts +325 -33
  324. package/src/server/responses/request-transport.ts +132 -52
  325. package/src/server/responses/response-effects.ts +1 -0
  326. package/src/server/responses/run-turn-execution.ts +13 -3
  327. package/src/server/responses/sidecar-execution.ts +113 -22
  328. package/src/server/responses/sidecar-send-budget.ts +58 -0
  329. package/src/server/responses/ws-upstream.ts +19 -6
  330. package/src/server/responses-undeclared-tool-guard.ts +5 -1
  331. package/src/server/startup-health-cache.ts +2 -1
  332. package/src/server/workflow-refusal.ts +33 -0
  333. package/src/service/cli.ts +20 -4
  334. package/src/service/desktop-command-guard.ts +32 -0
  335. package/src/service/desktop-startup.ts +66 -40
  336. package/src/service/desktop-supervision.d.mts +26 -0
  337. package/src/service/desktop-supervision.mjs +140 -0
  338. package/src/service/managing-cli.ts +1 -0
  339. package/src/service/orchestration.ts +46 -20
  340. package/src/service/ownership-mutation-lease.d.mts +1 -1
  341. package/src/service/ownership-mutation-lease.mjs +5 -7
  342. package/src/service/repair.ts +25 -9
  343. package/src/service/state.ts +4 -3
  344. package/src/service/systemd.ts +17 -8
  345. package/src/service/windows-ops.ts +61 -22
  346. package/src/sidecar/anthropic-binding.ts +79 -0
  347. package/src/sidecar/auth.ts +76 -2
  348. package/src/storage/policy-job.ts +5 -1
  349. package/src/storage/policy-worker.ts +3 -1
  350. package/src/storage/policy.ts +9 -0
  351. package/src/types/anthropic-account-pool.ts +33 -0
  352. package/src/types/config.ts +33 -28
  353. package/src/types/provider.ts +5 -0
  354. package/src/types/request.ts +5 -0
  355. package/src/types/tools.ts +7 -0
  356. package/src/types/wire.ts +1 -0
  357. package/src/types.ts +4 -0
  358. package/src/update/index.ts +57 -9
  359. package/src/update/restart-ownership.ts +10 -3
  360. package/src/update/runtime-ownership.d.mts +6 -0
  361. package/src/update/runtime-ownership.mjs +23 -7
  362. package/src/usage/cost.ts +3 -2
  363. package/src/usage/expected-prices.ts +66 -14
  364. package/src/vision/anthropic-describe.ts +16 -3
  365. package/src/vision/index.ts +3 -0
  366. package/src/vision/plan.ts +19 -6
  367. package/src/web-search/alpha-search.ts +7 -1
  368. package/src/web-search/anthropic-executor.ts +16 -4
  369. package/src/web-search/index.ts +7 -3
  370. package/src/web-search/loop.ts +32 -7
  371. package/src/web-search/passthrough-bridge.ts +4 -1
  372. package/src/web-search/sidecar-providers.ts +4 -3
  373. package/gui/dist/assets/App-MbDVfUeC.js +0 -54
  374. package/gui/dist/assets/Tray-CzkxK82A.js +0 -1
  375. package/gui/dist/assets/index-UJt6npXk.js +0 -94
  376. 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]) => {
@@ -38,6 +38,7 @@ const DEEPSEEK_BASE_URL = "https://api.deepseek.com";
38
38
  const CLINE_BASE_URL = "https://api.cline.bot";
39
39
  const OLLAMA_CLOUD_BASE_URL = "https://ollama.com";
40
40
  const OLLAMA_CLOUD_USAGE_URL = `${OLLAMA_CLOUD_BASE_URL}/api/usage`;
41
+ const OLLAMA_CLOUD_BALANCE_URL = `${OLLAMA_CLOUD_BASE_URL}/api/balance`;
41
42
  const ZAI_BASE_URL = "https://api.z.ai";
42
43
  const ZAI_CN_BASE_URL = "https://open.bigmodel.cn";
43
44
  const MINIMAX_REMAINS_PATH = "/v1/api/openplatform/coding_plan/remains";
@@ -439,11 +440,80 @@ async function fetchClineQuota(provider: string, config: OcxProviderConfig): Pro
439
440
  }
440
441
 
441
442
  /**
442
- * Ollama Cloud `GET https://ollama.com/api/usage` — returns account usage.
443
- * Legacy plans report rolling 5-hour `limits.session.usage` and 7-day
444
- * `limits.weekly.usage`. Migrated monthly-credit plans report
445
- * `limits.monthly.usage`. `usage` values are normalized fractions (0..1).
443
+ * Ollama Cloud quota. `GET https://ollama.com/api/balance` is the live source: it
444
+ * returns the included (plan) balance plus purchased credits. Legacy plans report
445
+ * rolling 5-hour `included.session.remaining_percent` and 7-day
446
+ * `included.weekly.remaining_percent` (remaining shares, so used = 100 - remaining);
447
+ * credit plans report `included.balance_usd`/`included.allowance_usd` over a
448
+ * `period`.
449
+ *
450
+ * `GET https://ollama.com/api/usage` used to carry `limits.{session,weekly,monthly}.usage`
451
+ * as normalized fractions but now returns only a request-count timeseries, so it is
452
+ * kept as a fallback for older deployments.
446
453
  */
454
+ function parseOllamaUsedPercent(row: Record<string, unknown>): number | undefined {
455
+ if (row.remaining_percent !== undefined) {
456
+ const remaining = toFiniteNumber(row.remaining_percent);
457
+ if (remaining === undefined) return undefined;
458
+ return normalizePercent(Math.round((100 - remaining) * 100) / 100);
459
+ }
460
+ return parseOllamaPercent(row.usage);
461
+ }
462
+
463
+ export function parseOllamaCloudBalance(body: Record<string, unknown> | null): ProviderQuota | null {
464
+ if (!body) return null;
465
+ const included = asRecord(body.included);
466
+ if (!included) return null;
467
+
468
+ const quota: ProviderQuota = { updatedAt: Date.now() };
469
+ let windows = 0;
470
+
471
+ const rows: Array<[string, "fiveHour" | "weekly" | "monthly"]> = [
472
+ ["session", "fiveHour"],
473
+ ["weekly", "weekly"],
474
+ ["monthly", "monthly"],
475
+ ];
476
+ for (const [key, window] of rows) {
477
+ const row = asRecord(included[key]);
478
+ if (!row) continue;
479
+ const percent = parseOllamaUsedPercent(row);
480
+ if (percent === undefined) continue;
481
+ const resetAt = normalizeResetAt(row.resets_at);
482
+ if (window === "fiveHour") {
483
+ quota.fiveHourPercent = percent;
484
+ if (resetAt !== undefined) quota.fiveHourResetAt = resetAt;
485
+ } else if (window === "weekly") {
486
+ quota.weeklyPercent = percent;
487
+ if (resetAt !== undefined) quota.weeklyResetAt = resetAt;
488
+ } else {
489
+ quota.monthlyPercent = percent;
490
+ if (resetAt !== undefined) quota.monthlyResetAt = resetAt;
491
+ }
492
+ windows += 1;
493
+ }
494
+
495
+ const allowance = toFiniteNumber(included.allowance_usd);
496
+ const balance = toFiniteNumber(included.balance_usd);
497
+ if (allowance !== undefined && allowance > 0 && balance !== undefined) {
498
+ const used = Math.max(0, allowance - balance);
499
+ const percent = normalizePercent(Math.round((used / allowance) * 10000) / 100);
500
+ if (percent !== undefined) {
501
+ const expiresAt = normalizeResetAt(asRecord(included.period)?.until);
502
+ // creditsUsd is the included allowance only; purchased credits are ignored.
503
+ quota.creditsUsd = {
504
+ used,
505
+ limit: allowance,
506
+ remaining: balance,
507
+ percent,
508
+ ...(expiresAt !== undefined ? { expiresAt } : {}),
509
+ };
510
+ windows += 1;
511
+ }
512
+ }
513
+
514
+ return windows > 0 ? quota : null;
515
+ }
516
+
447
517
  function parseOllamaPercent(usageValue: unknown): number | undefined {
448
518
  const usage = toFiniteNumber(usageValue);
449
519
  if (usage === undefined || usage < 0) return undefined;
@@ -489,25 +559,65 @@ export function parseOllamaCloudQuota(body: Record<string, unknown> | null): Pro
489
559
  return windows > 0 ? quota : null;
490
560
  }
491
561
 
562
+ /**
563
+ * Routing projection for an `/api/balance` report. `creditsUsd` covers the included
564
+ * allowance only, so it may veto routing only when purchased credit is known to be empty;
565
+ * otherwise an exhausted allowance would block an account that still has paid balance.
566
+ */
567
+ function ollamaBalanceInferenceQuota(quota: ProviderQuota, body: Record<string, unknown> | null): ProviderQuota {
568
+ if (!quota.creditsUsd) return quota;
569
+ const purchased = toFiniteNumber(asRecord(body?.purchased)?.balance_usd);
570
+ if (purchased !== undefined && purchased <= 0) return quota;
571
+ const { creditsUsd: _includedOnly, ...inference } = quota;
572
+ return inference;
573
+ }
574
+
492
575
  async function fetchOllamaCloudQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
493
576
  const effectiveBaseUrl = config.baseUrl ?? getProviderRegistryEntry(provider)?.baseUrl ?? "";
494
577
  if (!isCanonicalOllamaCloudBaseUrl(effectiveBaseUrl)) return null;
495
578
  const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
496
579
  if (!apiKey) return null;
497
- const response = await quotaFetch(provider, config, OLLAMA_CLOUD_USAGE_URL, {
498
- headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
499
- redirect: "error",
500
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
501
- });
502
- if (!response.ok) {
503
- if (response.status === 404) return null;
504
- return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
505
- ? TERMINAL_QUOTA_FAILURE
506
- : null;
580
+ const attempts: Array<{
581
+ url: string;
582
+ source: string;
583
+ parse: (body: Record<string, unknown> | null) => ProviderQuota | null;
584
+ }> = [
585
+ { url: OLLAMA_CLOUD_BALANCE_URL, source: "ollama-cloud:balance", parse: parseOllamaCloudBalance },
586
+ { url: OLLAMA_CLOUD_USAGE_URL, source: "ollama-cloud:usage", parse: parseOllamaCloudQuota },
587
+ ];
588
+ let terminalFailure = false;
589
+ for (const attempt of attempts) {
590
+ let response: Response;
591
+ try {
592
+ response = await quotaFetch(provider, config, attempt.url, {
593
+ headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
594
+ redirect: "error",
595
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
596
+ });
597
+ } catch {
598
+ continue;
599
+ }
600
+ if (!response.ok) {
601
+ if (response.status !== 404 && response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429) {
602
+ terminalFailure = true;
603
+ }
604
+ try {
605
+ void response.body?.cancel().catch(() => undefined);
606
+ } catch {
607
+ // Best-effort cancellation only.
608
+ }
609
+ continue;
610
+ }
611
+ const raw = await readQuotaJson(response);
612
+ if (raw === QUOTA_JSON_READ_FAILURE) return terminalFailure ? TERMINAL_QUOTA_FAILURE : null;
613
+ const body = asRecord(raw);
614
+ const quota = attempt.parse(body);
615
+ if (quota) {
616
+ const inference = attempt.url === OLLAMA_CLOUD_BALANCE_URL ? ollamaBalanceInferenceQuota(quota, body) : quota;
617
+ return keyReport(provider, attempt.source, quota, config, apiKey, inference);
618
+ }
507
619
  }
508
- const body = asRecord(await readQuotaJson(response));
509
- const quota = parseOllamaCloudQuota(body);
510
- return quota ? keyReport(provider, "ollama-cloud:usage", quota, config, apiKey, quota) : null;
620
+ return terminalFailure ? TERMINAL_QUOTA_FAILURE : null;
511
621
  }
512
622
 
513
623
  /**