@iislee/opencodex 2.11.0 → 2.31.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 (675) hide show
  1. package/AGENTS_INSTALL.md +109 -0
  2. package/README.md +114 -19
  3. package/bin/ocx.mjs +164 -36
  4. package/bin/package-main.mjs +1 -1
  5. package/gui/dist/assets/index-BF38heuV.js +104 -0
  6. package/gui/dist/assets/index-DMiI18Kv.css +1 -0
  7. package/gui/dist/index.html +2 -2
  8. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  9. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  10. package/gui/dist/provider-icons/claude-color.svg +1 -1
  11. package/gui/dist/provider-icons/cline-color.svg +16 -0
  12. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  13. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  14. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  15. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  16. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  17. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  18. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  19. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  20. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  21. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  22. package/gui/dist/provider-icons/grok.svg +1 -1
  23. package/gui/dist/provider-icons/groq-color.svg +1 -1
  24. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  25. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  26. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  27. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  28. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  29. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  30. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  31. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  32. package/gui/dist/provider-icons/openai.svg +1 -1
  33. package/gui/dist/provider-icons/opencode.svg +2 -1
  34. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  35. package/gui/dist/provider-icons/pi.svg +2 -2
  36. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  37. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  38. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  39. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  40. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  41. package/package.json +19 -10
  42. package/src/adapters/anthropic-output-schema.ts +137 -0
  43. package/src/adapters/anthropic.ts +376 -52
  44. package/src/adapters/base.ts +54 -7
  45. package/src/adapters/client-fingerprint.ts +18 -12
  46. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
  47. package/src/adapters/command-code.ts +601 -0
  48. package/src/adapters/cursor/checkpoint-store.ts +303 -0
  49. package/src/adapters/cursor/cursor-errors.ts +129 -6
  50. package/src/adapters/cursor/discovery.ts +61 -4
  51. package/src/adapters/cursor/effort-map.ts +27 -3
  52. package/src/adapters/cursor/framing.ts +39 -0
  53. package/src/adapters/cursor/h2-pool.ts +123 -0
  54. package/src/adapters/cursor/http1-bidi.ts +361 -0
  55. package/src/adapters/cursor/images.ts +704 -0
  56. package/src/adapters/cursor/live-models.ts +174 -58
  57. package/src/adapters/cursor/live-transport.ts +609 -170
  58. package/src/adapters/cursor/native-exec-common.ts +23 -2
  59. package/src/adapters/cursor/native-exec-fs.ts +10 -7
  60. package/src/adapters/cursor/native-exec-network.ts +1 -1
  61. package/src/adapters/cursor/native-exec-shell.ts +0 -1
  62. package/src/adapters/cursor/native-exec.ts +101 -14
  63. package/src/adapters/cursor/protobuf-events.ts +829 -11
  64. package/src/adapters/cursor/protobuf-request.ts +383 -65
  65. package/src/adapters/cursor/request-builder.ts +237 -31
  66. package/src/adapters/cursor/tool-definitions.ts +242 -12
  67. package/src/adapters/cursor/tool-result-normalize.ts +92 -0
  68. package/src/adapters/cursor/transport.ts +22 -0
  69. package/src/adapters/cursor/types.ts +28 -1
  70. package/src/adapters/cursor.ts +133 -7
  71. package/src/adapters/google-antigravity-replay.ts +558 -34
  72. package/src/adapters/google-antigravity-wire.ts +43 -10
  73. package/src/adapters/google-http.ts +38 -10
  74. package/src/adapters/google-tool-schema.ts +80 -15
  75. package/src/adapters/google-truncation.ts +11 -0
  76. package/src/adapters/google.ts +618 -74
  77. package/src/adapters/identity.ts +39 -6
  78. package/src/adapters/kiro-errors.ts +11 -0
  79. package/src/adapters/kiro-events.ts +19 -1
  80. package/src/adapters/kiro-thinking.ts +18 -2
  81. package/src/adapters/kiro-tools.ts +10 -1
  82. package/src/adapters/kiro.ts +92 -54
  83. package/src/adapters/mimo-free.ts +17 -0
  84. package/src/adapters/openai-chat-url.ts +11 -0
  85. package/src/adapters/openai-chat.ts +1311 -336
  86. package/src/adapters/openai-responses-url.ts +16 -0
  87. package/src/adapters/openai-responses.ts +830 -56
  88. package/src/adapters/registry.ts +175 -0
  89. package/src/adapters/responses-tool-schema.ts +67 -0
  90. package/src/adapters/tool-call-id.ts +119 -0
  91. package/src/adapters/tool-catalog-nudge.ts +104 -21
  92. package/src/adapters/xai-web-search.ts +185 -0
  93. package/src/bridge.ts +428 -61
  94. package/src/chat/inbound.ts +43 -19
  95. package/src/chat/outbound.ts +82 -26
  96. package/src/claude/agents-inject.ts +32 -9
  97. package/src/claude/context-windows.ts +21 -5
  98. package/src/claude/desktop-3p.ts +243 -9
  99. package/src/claude/gateway-cache.ts +41 -4
  100. package/src/claude/inbound.ts +72 -3
  101. package/src/claude/model-info.ts +38 -15
  102. package/src/claude/outbound.ts +70 -16
  103. package/src/cli/account-api.ts +35 -1
  104. package/src/cli/account-auth.ts +33 -6
  105. package/src/cli/account-catalog-refresh.ts +14 -0
  106. package/src/cli/account-extended.ts +389 -2
  107. package/src/cli/account-main.ts +317 -0
  108. package/src/cli/account.ts +30 -6
  109. package/src/cli/agent.ts +45 -1
  110. package/src/cli/claude-agent-startup-sync.ts +73 -0
  111. package/src/cli/claude-desktop.ts +29 -4
  112. package/src/cli/claude.ts +103 -34
  113. package/src/cli/codex-log-guard-doctor.ts +103 -0
  114. package/src/cli/codex-shim-autorestore.ts +2 -0
  115. package/src/cli/codex-shim-readiness.ts +76 -0
  116. package/src/cli/combo.ts +8 -0
  117. package/src/cli/config-command.ts +74 -10
  118. package/src/cli/dispatch.ts +593 -0
  119. package/src/cli/doctor.ts +315 -43
  120. package/src/cli/ensure-desired-integrations.ts +152 -0
  121. package/src/cli/export-command.ts +46 -20
  122. package/src/cli/help.ts +20 -271
  123. package/src/cli/index.ts +378 -519
  124. package/src/cli/init.ts +4 -17
  125. package/src/cli/integrations.ts +120 -2
  126. package/src/cli/lab.ts +607 -0
  127. package/src/cli/launcher-context.ts +77 -0
  128. package/src/cli/minimax.ts +497 -0
  129. package/src/cli/models-runtime.ts +35 -2
  130. package/src/cli/models.ts +100 -14
  131. package/src/cli/observe.ts +92 -3
  132. package/src/cli/opencode.ts +4 -2
  133. package/src/cli/provider-runtime.ts +18 -1
  134. package/src/cli/provider.ts +24 -3
  135. package/src/cli/ready.ts +301 -0
  136. package/src/cli/registry.ts +437 -0
  137. package/src/cli/root.ts +86 -0
  138. package/src/cli/route-policy.ts +92 -0
  139. package/src/cli/runtime-api.ts +6 -3
  140. package/src/cli/star-prompt.ts +71 -15
  141. package/src/cli/status.ts +10 -3
  142. package/src/cli/system-restart-client.ts +146 -0
  143. package/src/cli/tray-proxy.ts +153 -6
  144. package/src/cli/v2.ts +105 -10
  145. package/src/cli.ts +1 -1
  146. package/src/clients/config-export.ts +1358 -21
  147. package/src/codex/account-label.ts +14 -1
  148. package/src/codex/account-lifecycle.ts +130 -13
  149. package/src/codex/account-namespaces.ts +49 -3
  150. package/src/codex/account-priority.ts +83 -0
  151. package/src/codex/account-store.ts +29 -2
  152. package/src/codex/account-usability.ts +25 -2
  153. package/src/codex/admission.ts +256 -0
  154. package/src/codex/affinity-debug.ts +162 -0
  155. package/src/codex/app-server-processes.ts +493 -106
  156. package/src/codex/app-server-restart-service.ts +232 -0
  157. package/src/codex/auth-api.ts +849 -242
  158. package/src/codex/auth-collision.ts +5 -3
  159. package/src/codex/auth-context.ts +345 -32
  160. package/src/codex/autostart-health.ts +8 -1
  161. package/src/codex/catalog/account-models.ts +67 -0
  162. package/src/codex/catalog/aggregation.ts +68 -10
  163. package/src/codex/catalog/bundled.ts +331 -33
  164. package/src/codex/catalog/effort.ts +121 -30
  165. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  166. package/src/codex/catalog/kinds.ts +2 -0
  167. package/src/codex/catalog/metadata.ts +529 -45
  168. package/src/codex/catalog/native-models.ts +72 -0
  169. package/src/codex/catalog/parsing.ts +224 -30
  170. package/src/codex/catalog/provider-fetch.ts +1460 -134
  171. package/src/codex/catalog/sync.ts +1449 -186
  172. package/src/codex/catalog-admission.ts +199 -0
  173. package/src/codex/catalog-refresh-status.ts +105 -0
  174. package/src/codex/catalog-write-serialization.ts +242 -0
  175. package/src/codex/catalog.ts +6 -3
  176. package/src/codex/codex-write-lock.ts +384 -0
  177. package/src/codex/convergence-types.ts +614 -0
  178. package/src/codex/convergence.ts +651 -0
  179. package/src/codex/coordinator-doctor.ts +332 -0
  180. package/src/codex/custom-model-catalog-migration.ts +176 -0
  181. package/src/codex/desired-state.ts +230 -0
  182. package/src/codex/features.ts +636 -39
  183. package/src/codex/generation.ts +202 -0
  184. package/src/codex/history-job.ts +407 -0
  185. package/src/codex/history-lock.ts +242 -0
  186. package/src/codex/history-migration-guardian.ts +26 -20
  187. package/src/codex/history-provider.ts +231 -28
  188. package/src/codex/history-transition.ts +105 -0
  189. package/src/codex/history-worker.ts +220 -0
  190. package/src/codex/inject-coordination.ts +290 -0
  191. package/src/codex/inject.ts +1073 -152
  192. package/src/codex/injected-marker.ts +37 -3
  193. package/src/codex/integration-record.ts +266 -0
  194. package/src/codex/internal/catalog-writer.ts +203 -0
  195. package/src/codex/internal/history-writer.ts +80 -0
  196. package/src/codex/journal.ts +66 -4
  197. package/src/codex/log-guard/inspect.ts +506 -0
  198. package/src/codex/log-guard/lock.ts +150 -0
  199. package/src/codex/log-guard/maintenance.ts +403 -0
  200. package/src/codex/log-guard/path-safety.ts +88 -0
  201. package/src/codex/log-guard/policy.ts +44 -0
  202. package/src/codex/log-guard/processes.ts +205 -0
  203. package/src/codex/log-guard/protection.ts +489 -0
  204. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  205. package/src/codex/main-account-cache.ts +24 -0
  206. package/src/codex/main-account.ts +29 -1
  207. package/src/codex/management-convergence.ts +167 -0
  208. package/src/codex/model-cache.ts +56 -10
  209. package/src/codex/model-entitlements.ts +353 -0
  210. package/src/codex/native-main-admission.ts +47 -0
  211. package/src/codex/native-main-auth-temp.ts +187 -0
  212. package/src/codex/native-main-claim.ts +178 -0
  213. package/src/codex/native-main-lock-file.ts +162 -0
  214. package/src/codex/native-main-owner.ts +329 -0
  215. package/src/codex/native-profile-api.ts +247 -0
  216. package/src/codex/native-profile-manager.ts +1531 -0
  217. package/src/codex/native-profile-processes.ts +121 -0
  218. package/src/codex/native-profile-recovery.ts +99 -0
  219. package/src/codex/native-profile-stage-store.ts +387 -0
  220. package/src/codex/native-profile-startup.ts +492 -0
  221. package/src/codex/native-profile-store.ts +855 -0
  222. package/src/codex/native-profile-types.ts +120 -0
  223. package/src/codex/native-residue.ts +682 -0
  224. package/src/codex/paths.ts +80 -1
  225. package/src/codex/plan-from-token.ts +140 -0
  226. package/src/codex/plan.ts +40 -0
  227. package/src/codex/plugins-doctor.ts +1 -1
  228. package/src/codex/pool-rotation.ts +74 -4
  229. package/src/codex/project-config-warnings.ts +20 -6
  230. package/src/codex/prompt-journal.ts +352 -0
  231. package/src/codex/prompt-layers.ts +967 -0
  232. package/src/codex/prompt-lock.ts +143 -0
  233. package/src/codex/quota-rejection.ts +298 -0
  234. package/src/codex/quota.ts +175 -13
  235. package/src/codex/refresh.ts +11 -2
  236. package/src/codex/reset-credit-recovery.ts +1044 -0
  237. package/src/codex/routing.ts +505 -94
  238. package/src/codex/runtime.ts +159 -38
  239. package/src/codex/shim.ts +1009 -28
  240. package/src/codex/subagent-model-fallback.ts +350 -35
  241. package/src/codex/sync.ts +191 -2
  242. package/src/codex/transition-state.ts +612 -0
  243. package/src/codex/upstream-host-health.ts +368 -0
  244. package/src/codex/user-identity.ts +557 -0
  245. package/src/codex/warmup.ts +187 -81
  246. package/src/codex/write-coordination.ts +114 -0
  247. package/src/combos/failover.ts +20 -0
  248. package/src/combos/index.ts +4 -0
  249. package/src/combos/request.ts +32 -0
  250. package/src/combos/types.ts +81 -9
  251. package/src/config/provider-name.ts +24 -0
  252. package/src/config.ts +1762 -140
  253. package/src/generated/compatibility-version.json +3164 -0
  254. package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
  255. package/src/grok/inject.ts +16 -5
  256. package/src/grok/inspect.ts +45 -0
  257. package/src/grok/sync.ts +2 -2
  258. package/src/images/loop.ts +152 -29
  259. package/src/images/plan.ts +23 -13
  260. package/src/integrations/config-io.ts +269 -0
  261. package/src/integrations/journal.ts +315 -0
  262. package/src/integrations/merge.ts +135 -0
  263. package/src/integrations/mutation-flight.ts +71 -0
  264. package/src/integrations/native/ownership-preflight.ts +202 -0
  265. package/src/integrations/omp-yaml-source.ts +358 -0
  266. package/src/integrations/owned-refresh.ts +74 -0
  267. package/src/integrations/ownership.ts +111 -0
  268. package/src/integrations/registry.ts +159 -0
  269. package/src/integrations/serialize.ts +314 -0
  270. package/src/integrations/state.ts +361 -0
  271. package/src/integrations/store.ts +103 -0
  272. package/src/integrations/writer-lock.ts +98 -0
  273. package/src/integrations/writer.ts +691 -0
  274. package/src/lab/artifacts/sanitize.ts +586 -0
  275. package/src/lab/artifacts/secure-fs.ts +475 -0
  276. package/src/lab/artifacts/store.ts +310 -0
  277. package/src/lab/automation/budgets.ts +78 -0
  278. package/src/lab/automation/config-persistence.ts +256 -0
  279. package/src/lab/automation/constants.ts +39 -0
  280. package/src/lab/automation/cooldown.ts +103 -0
  281. package/src/lab/automation/dispatch.ts +211 -0
  282. package/src/lab/automation/index.ts +13 -0
  283. package/src/lab/automation/orchestrator.ts +499 -0
  284. package/src/lab/automation/persistence.ts +512 -0
  285. package/src/lab/automation/planner.ts +371 -0
  286. package/src/lab/automation/policy.ts +136 -0
  287. package/src/lab/automation/queue.ts +191 -0
  288. package/src/lab/automation/recovery.ts +24 -0
  289. package/src/lab/automation/route-context.ts +21 -0
  290. package/src/lab/automation/run-key.ts +44 -0
  291. package/src/lab/automation/runs-query.ts +34 -0
  292. package/src/lab/automation/types.ts +160 -0
  293. package/src/lab/conformance/assertion.ts +325 -0
  294. package/src/lab/conformance/digest.ts +22 -0
  295. package/src/lab/conformance/executor.ts +741 -0
  296. package/src/lab/conformance/fixture-provider.ts +27 -0
  297. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  298. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  299. package/src/lab/conformance/harness-budget.ts +47 -0
  300. package/src/lab/conformance/index.ts +5 -0
  301. package/src/lab/conformance/jcs.ts +64 -0
  302. package/src/lab/conformance/json-pointer.ts +39 -0
  303. package/src/lab/conformance/manifest.ts +180 -0
  304. package/src/lab/conformance/mcp-stub.ts +179 -0
  305. package/src/lab/conformance/negative-controls.ts +164 -0
  306. package/src/lab/conformance/observation.ts +355 -0
  307. package/src/lab/conformance/runner.ts +68 -0
  308. package/src/lab/conformance/sse-normalize.ts +59 -0
  309. package/src/lab/conformance/suite-manifest.ts +78 -0
  310. package/src/lab/conformance/types.ts +214 -0
  311. package/src/lab/constants.ts +126 -0
  312. package/src/lab/digest.ts +64 -0
  313. package/src/lab/events/errors.ts +9 -0
  314. package/src/lab/events/limits.ts +117 -0
  315. package/src/lab/events/types.ts +229 -0
  316. package/src/lab/events/validate.ts +781 -0
  317. package/src/lab/fabric/constants.ts +40 -0
  318. package/src/lab/fabric/executor.ts +492 -0
  319. package/src/lab/fabric/index.ts +80 -0
  320. package/src/lab/fabric/manifest.ts +222 -0
  321. package/src/lab/fabric/observe.ts +489 -0
  322. package/src/lab/fabric/patch.ts +79 -0
  323. package/src/lab/fabric/producer-child.ts +139 -0
  324. package/src/lab/fabric/producer-isolate.ts +276 -0
  325. package/src/lab/fabric/producer-protocol.ts +61 -0
  326. package/src/lab/fabric/scratch.ts +439 -0
  327. package/src/lab/fabric/subject.ts +106 -0
  328. package/src/lab/fabric/types.ts +134 -0
  329. package/src/lab/fabric/verifier.ts +98 -0
  330. package/src/lab/index.ts +54 -0
  331. package/src/lab/ledger/artifact-refs.ts +127 -0
  332. package/src/lab/ledger/invalidation.ts +136 -0
  333. package/src/lab/ledger/purge.ts +310 -0
  334. package/src/lab/ledger/store.ts +532 -0
  335. package/src/lab/live/credential-lease.ts +53 -0
  336. package/src/lab/live/destination.ts +155 -0
  337. package/src/lab/live/executor.ts +336 -0
  338. package/src/lab/live/inert-tools.ts +56 -0
  339. package/src/lab/live/manifest.ts +85 -0
  340. package/src/lab/live/mcp-loopback.ts +57 -0
  341. package/src/lab/live/runner.ts +19 -0
  342. package/src/lab/live/sandbox.ts +61 -0
  343. package/src/lab/live/suite-manifest.ts +41 -0
  344. package/src/lab/live/transport.ts +118 -0
  345. package/src/lab/live/types.ts +197 -0
  346. package/src/lab/observe/from-conformance.ts +301 -0
  347. package/src/lab/observe/from-live.ts +117 -0
  348. package/src/lab/paths.ts +153 -0
  349. package/src/lab/projection/rebuild.ts +495 -0
  350. package/src/lab/projection/schema.ts +135 -0
  351. package/src/lab/projection/verdicts.ts +474 -0
  352. package/src/lab/projection/verification.ts +412 -0
  353. package/src/lab/public/bundle.ts +217 -0
  354. package/src/lab/public/community-authority.ts +175 -0
  355. package/src/lab/public/community-files.ts +29 -0
  356. package/src/lab/public/community.ts +479 -0
  357. package/src/lab/public/file-safety.ts +155 -0
  358. package/src/lab/public/ids.ts +26 -0
  359. package/src/lab/public/index.ts +16 -0
  360. package/src/lab/public/mutation-lock.ts +424 -0
  361. package/src/lab/public/operator.ts +353 -0
  362. package/src/lab/public/origin-purge.ts +79 -0
  363. package/src/lab/public/origin.ts +203 -0
  364. package/src/lab/public/privacy.ts +143 -0
  365. package/src/lab/public/private-file.ts +261 -0
  366. package/src/lab/public/project.ts +124 -0
  367. package/src/lab/public/purge-test-fault.ts +21 -0
  368. package/src/lab/public/purge.ts +223 -0
  369. package/src/lab/public/registry.ts +44 -0
  370. package/src/lab/public/revocation.ts +252 -0
  371. package/src/lab/public/signature.ts +243 -0
  372. package/src/lab/public/storage.ts +105 -0
  373. package/src/lab/public/strict-json.ts +206 -0
  374. package/src/lab/public/time.ts +26 -0
  375. package/src/lab/public/types.ts +172 -0
  376. package/src/lab/public/validate.ts +391 -0
  377. package/src/lab/query/catalog.ts +101 -0
  378. package/src/lab/query/connection.ts +107 -0
  379. package/src/lab/query/constants.ts +4 -0
  380. package/src/lab/query/cursor.ts +132 -0
  381. package/src/lab/query/dto-map.ts +277 -0
  382. package/src/lab/query/errors.ts +22 -0
  383. package/src/lab/query/freshness.ts +53 -0
  384. package/src/lab/query/index.ts +45 -0
  385. package/src/lab/query/latest-observation.ts +59 -0
  386. package/src/lab/query/passive-production.ts +159 -0
  387. package/src/lab/query/queries.ts +444 -0
  388. package/src/lab/query/types.ts +266 -0
  389. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  390. package/src/lab/subject/installation-salt.ts +112 -0
  391. package/src/lab/subject/protocol-subject.ts +80 -0
  392. package/src/lab/subject/route-subject.ts +74 -0
  393. package/src/lib/app-owned-memory-stores.ts +22 -0
  394. package/src/lib/bounded-body.ts +153 -9
  395. package/src/lib/bun-runtime.ts +125 -12
  396. package/src/lib/bun-stream-caps.ts +13 -9
  397. package/src/lib/codex-restart-contract.ts +120 -0
  398. package/src/lib/config-ownership.ts +6 -2
  399. package/src/lib/destination-policy.ts +65 -1
  400. package/src/lib/errors.ts +44 -2
  401. package/src/lib/fabric-task-execution-authority.ts +7 -0
  402. package/src/lib/fabric-task-host.ts +29 -0
  403. package/src/lib/lab-activation.ts +223 -0
  404. package/src/lib/lab-live-execution-authority.ts +13 -0
  405. package/src/lib/lab-live-host.ts +30 -0
  406. package/src/lib/lab-live-pinned-sender.ts +56 -0
  407. package/src/lib/lab-live-route-production.ts +130 -0
  408. package/src/lib/lab-passive-linker-registration.ts +26 -0
  409. package/src/lib/local-management-attestation.ts +51 -0
  410. package/src/lib/local-management-capability.ts +100 -0
  411. package/src/lib/local-provider-reload-contract.ts +100 -0
  412. package/src/lib/optional-shutdown-hooks.ts +57 -0
  413. package/src/lib/pinned-http.ts +145 -26
  414. package/src/lib/process-control.ts +4 -1
  415. package/src/lib/provider-outbound.ts +49 -9
  416. package/src/lib/redact.ts +419 -3
  417. package/src/lib/self-launch-argv.ts +15 -0
  418. package/src/lib/server-resource-ownership.ts +71 -0
  419. package/src/lib/shadow-call.ts +35 -4
  420. package/src/lib/sse-decoder.ts +41 -0
  421. package/src/lib/state-store-registrations.ts +10 -2
  422. package/src/lib/system-restart-contract.ts +73 -0
  423. package/src/lib/token-estimate.ts +19 -2
  424. package/src/lib/tool-argument-integers.ts +202 -0
  425. package/src/lib/translator-budget.ts +44 -0
  426. package/src/lib/upstream-http-version.ts +57 -0
  427. package/src/lib/upstream-reachability.ts +95 -0
  428. package/src/lib/upstream-retry.ts +156 -3
  429. package/src/lib/windows-atomic-replace.ts +155 -0
  430. package/src/lib/windows-elevation.ts +70 -2
  431. package/src/lib/windows-secret-acl.ts +409 -69
  432. package/src/lib/windows-service-wrappers.ts +72 -0
  433. package/src/lib/windows-text.ts +106 -0
  434. package/src/lib/windows-user-principal.ts +341 -0
  435. package/src/lib/winsw.ts +33 -5
  436. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  437. package/src/oauth/account-import/index.ts +15 -0
  438. package/src/oauth/account-import/parser.ts +83 -0
  439. package/src/oauth/account-import/registry.ts +18 -0
  440. package/src/oauth/account-import/service.ts +75 -0
  441. package/src/oauth/account-import/types.ts +91 -0
  442. package/src/oauth/anthropic.ts +12 -1
  443. package/src/oauth/callback-server.ts +8 -2
  444. package/src/oauth/chatgpt.ts +12 -1
  445. package/src/oauth/command-code.ts +239 -0
  446. package/src/oauth/cursor.ts +46 -5
  447. package/src/oauth/google-antigravity.ts +35 -3
  448. package/src/oauth/health.ts +20 -12
  449. package/src/oauth/index.ts +398 -66
  450. package/src/oauth/key-providers.ts +16 -0
  451. package/src/oauth/kimi.ts +16 -2
  452. package/src/oauth/kiro.ts +50 -6
  453. package/src/oauth/local-token-detect.ts +11 -2
  454. package/src/oauth/log.ts +3 -1
  455. package/src/oauth/login-cli.ts +88 -28
  456. package/src/oauth/nous.ts +798 -0
  457. package/src/oauth/store.ts +119 -21
  458. package/src/oauth/token-guardian.ts +9 -3
  459. package/src/pi/models.ts +2 -2
  460. package/src/providers/alibaba-region-migration.ts +1 -1
  461. package/src/providers/antigravity-models.ts +521 -31
  462. package/src/providers/base-url-choices.ts +10 -0
  463. package/src/providers/codex-capacity.ts +292 -0
  464. package/src/providers/command-code-efforts.ts +144 -0
  465. package/src/providers/context-cap.ts +22 -5
  466. package/src/providers/cursor-pool.ts +72 -0
  467. package/src/providers/derive.ts +253 -6
  468. package/src/providers/fastwire.ts +501 -0
  469. package/src/providers/free-directory.ts +10 -7
  470. package/src/providers/google-vertex-location.ts +14 -0
  471. package/src/providers/key-failover.ts +71 -3
  472. package/src/providers/label.ts +1 -1
  473. package/src/providers/model-discovery-limits.ts +16 -0
  474. package/src/providers/model-discovery.ts +115 -22
  475. package/src/providers/model-rename-migration.ts +255 -0
  476. package/src/providers/model-rename-startup.ts +28 -0
  477. package/src/providers/openai-sidecar.ts +72 -4
  478. package/src/providers/openai-tier-startup.ts +31 -2
  479. package/src/providers/openai-tiers.ts +119 -4
  480. package/src/providers/openai-virtual-models.ts +1 -0
  481. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  482. package/src/providers/provider-id-rewrite.ts +29 -0
  483. package/src/providers/quota.ts +1319 -38
  484. package/src/providers/registry.ts +1429 -111
  485. package/src/providers/request-pacing.ts +310 -0
  486. package/src/providers/service-tier.ts +277 -0
  487. package/src/providers/slug-codec.ts +42 -6
  488. package/src/providers/static-model-discovery.ts +86 -0
  489. package/src/providers/xai-responses-opt-in.ts +15 -0
  490. package/src/providers/xai-transport.ts +11 -4
  491. package/src/reasoning-effort.ts +49 -1
  492. package/src/responses/compaction.ts +26 -1
  493. package/src/responses/custom-tool-compat.ts +266 -0
  494. package/src/responses/hosted-tool-policy.ts +9 -0
  495. package/src/responses/namespace-tool-compat.ts +355 -0
  496. package/src/responses/parser.ts +220 -38
  497. package/src/responses/provider-continuation.ts +98 -0
  498. package/src/responses/provider-opaque-metadata.ts +73 -0
  499. package/src/responses/reasoning-envelope.ts +9 -1
  500. package/src/responses/reasoning-replay-cache.ts +426 -0
  501. package/src/responses/schema.ts +7 -1
  502. package/src/responses/spill-store.ts +75 -10
  503. package/src/responses/state.ts +565 -27
  504. package/src/responses/thought-signature-replay.ts +347 -0
  505. package/src/responses/tool-search-compat.ts +301 -0
  506. package/src/responses/truncated-stop-reason.ts +60 -0
  507. package/src/router.ts +366 -30
  508. package/src/routing/analytics.ts +378 -0
  509. package/src/routing/capability.ts +244 -0
  510. package/src/routing/compatibility/assemble.ts +73 -0
  511. package/src/routing/compatibility/behavior.ts +278 -0
  512. package/src/routing/compatibility/catalog.ts +99 -0
  513. package/src/routing/compatibility/endpoint.ts +52 -0
  514. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  515. package/src/routing/compatibility/policy.ts +181 -0
  516. package/src/routing/compatibility/provider-slot.ts +56 -0
  517. package/src/routing/compatibility/reader.ts +110 -0
  518. package/src/routing/compatibility/subject.ts +191 -0
  519. package/src/routing/compatibility/types.ts +64 -0
  520. package/src/routing/compatibility/version.ts +104 -0
  521. package/src/routing/cost.ts +77 -0
  522. package/src/routing/evaluator.ts +495 -0
  523. package/src/routing/health.ts +412 -0
  524. package/src/routing/history/cursor.ts +43 -0
  525. package/src/routing/history/indexer.ts +605 -0
  526. package/src/routing/history/schema.ts +72 -0
  527. package/src/routing/profile-namespace.ts +15 -0
  528. package/src/routing/profile.ts +547 -0
  529. package/src/routing/quota.ts +145 -0
  530. package/src/routing/request-evidence.ts +45 -0
  531. package/src/routing/trace.ts +776 -0
  532. package/src/server/adapter-resolve.ts +2 -29
  533. package/src/server/auth-cors.ts +267 -46
  534. package/src/server/background-lifecycle.ts +182 -0
  535. package/src/server/chat-completions.ts +130 -56
  536. package/src/server/chat-native-sse.ts +331 -0
  537. package/src/server/chat-native.ts +426 -0
  538. package/src/server/claude-messages.ts +159 -43
  539. package/src/server/direct-local-http.ts +347 -0
  540. package/src/server/effort-policy.ts +18 -0
  541. package/src/server/github-copilot-responses-repair.ts +338 -0
  542. package/src/server/gui-static.ts +39 -10
  543. package/src/server/images.ts +94 -12
  544. package/src/server/index.ts +865 -181
  545. package/src/server/lifecycle.ts +284 -13
  546. package/src/server/live.ts +136 -17
  547. package/src/server/local-management-read-client.ts +90 -0
  548. package/src/server/local-provider-reload-client.ts +137 -0
  549. package/src/server/management/agent-settings-routes.ts +398 -103
  550. package/src/server/management/api-key-usage.ts +31 -5
  551. package/src/server/management/body.ts +6 -0
  552. package/src/server/management/combo-routes.ts +62 -24
  553. package/src/server/management/config-routes.ts +464 -51
  554. package/src/server/management/context.ts +80 -2
  555. package/src/server/management/integration-routes.ts +498 -0
  556. package/src/server/management/lab-automation-routes.ts +206 -0
  557. package/src/server/management/lab-routes.ts +563 -0
  558. package/src/server/management/logs-usage-routes.ts +101 -32
  559. package/src/server/management/model-routes.ts +189 -131
  560. package/src/server/management/model-rows.ts +163 -0
  561. package/src/server/management/native-integration-routes.ts +769 -0
  562. package/src/server/management/oauth-account-routes.ts +80 -4
  563. package/src/server/management/provider-capability-config.ts +48 -0
  564. package/src/server/management/provider-routes.ts +764 -157
  565. package/src/server/management/request-history-routes.ts +191 -0
  566. package/src/server/management/routing-analytics-routes.ts +74 -0
  567. package/src/server/management/routing-profile-routes.ts +380 -0
  568. package/src/server/management/shared.ts +27 -11
  569. package/src/server/management/sidebar-routes.ts +47 -31
  570. package/src/server/management/storage-log-guard-routes.ts +186 -0
  571. package/src/server/management/sync-response.ts +69 -0
  572. package/src/server/management/system-restart.ts +288 -32
  573. package/src/server/management/system-routes.ts +77 -0
  574. package/src/server/management/usage-summary-cache.ts +9 -1
  575. package/src/server/management/vision-sidecar-options.ts +167 -0
  576. package/src/server/management/web-search-sidecar-options.ts +120 -0
  577. package/src/server/management-api.ts +115 -14
  578. package/src/server/management-auth.ts +220 -5
  579. package/src/server/passive-route-linker.ts +66 -0
  580. package/src/server/ports.ts +41 -1
  581. package/src/server/proxy-liveness.ts +132 -5
  582. package/src/server/readiness.ts +99 -0
  583. package/src/server/relay-eager.ts +82 -42
  584. package/src/server/relay.ts +236 -76
  585. package/src/server/request-decompress.ts +113 -6
  586. package/src/server/request-log.ts +235 -22
  587. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  588. package/src/server/responses/agent-task-recovery.ts +465 -0
  589. package/src/server/responses/collaboration.ts +204 -35
  590. package/src/server/responses/compact.ts +442 -55
  591. package/src/server/responses/core.ts +2872 -331
  592. package/src/server/responses/empty-completion-guard.ts +276 -0
  593. package/src/server/responses/encrypted-payload.ts +62 -39
  594. package/src/server/responses/fetch-helpers.ts +79 -4
  595. package/src/server/responses/input-admission.ts +185 -0
  596. package/src/server/responses/pacing-overload.ts +13 -0
  597. package/src/server/responses/policy-fallback.ts +178 -0
  598. package/src/server/responses/responses-field-backfill.ts +251 -0
  599. package/src/server/responses/terminal-guard.ts +26 -5
  600. package/src/server/responses/upstream-error.ts +5 -0
  601. package/src/server/responses/ws-upstream.ts +308 -0
  602. package/src/server/responses-custom-tool-repair.ts +282 -0
  603. package/src/server/responses-item-id-repair.ts +54 -6
  604. package/src/server/responses-json-events.ts +90 -0
  605. package/src/server/responses-model-rewrite.ts +29 -0
  606. package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
  607. package/src/server/responses-snapshot-repair.ts +621 -0
  608. package/src/server/responses-terminal-repair.ts +342 -0
  609. package/src/server/responses-tool-search-repair.ts +267 -0
  610. package/src/server/responses-undeclared-tool-guard.ts +153 -0
  611. package/src/server/responses.ts +18 -2
  612. package/src/server/search.ts +78 -13
  613. package/src/server/sse-frame-buffer.ts +292 -0
  614. package/src/server/sse-payload-rewrite.ts +110 -22
  615. package/src/server/startup-action-control.ts +8 -1
  616. package/src/server/startup-health-cache.ts +19 -1
  617. package/src/server/system-env.ts +80 -9
  618. package/src/server/ws-bridge.ts +39 -38
  619. package/src/service-manager-probe.ts +892 -0
  620. package/src/service.ts +1111 -90
  621. package/src/sidecar/auth.ts +92 -0
  622. package/src/sidecar/candidates.ts +83 -0
  623. package/src/storage/cleanup.ts +2 -2
  624. package/src/storage/scanner.ts +1 -1
  625. package/src/storage/worker-lifecycle.ts +14 -14
  626. package/src/tray/windows-tray.ps1 +83 -9
  627. package/src/tray/windows.ts +43 -16
  628. package/src/types/accounts.ts +37 -0
  629. package/src/types/config.ts +845 -0
  630. package/src/types/provider.ts +545 -0
  631. package/src/types/request.ts +384 -0
  632. package/src/types/tools.ts +131 -0
  633. package/src/types/wire.ts +80 -0
  634. package/src/types.ts +104 -1236
  635. package/src/update/index.ts +32 -19
  636. package/src/update/job.ts +442 -67
  637. package/src/update/notify.ts +12 -6
  638. package/src/update/npm-cache-preflight.d.mts +47 -0
  639. package/src/update/npm-cache-preflight.mjs +201 -0
  640. package/src/update/transactional-install.d.mts +22 -0
  641. package/src/update/transactional-install.mjs +259 -0
  642. package/src/usage/cost.ts +0 -0
  643. package/src/usage/expected-prices.ts +268 -16
  644. package/src/usage/log.ts +606 -41
  645. package/src/usage/summary.ts +177 -9
  646. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  647. package/src/usage/user-cost-overlays.ts +314 -0
  648. package/src/vision/anthropic-describe.ts +10 -6
  649. package/src/vision/backends.ts +97 -0
  650. package/src/vision/describe.ts +9 -3
  651. package/src/vision/eligibility.ts +250 -0
  652. package/src/vision/index.ts +238 -24
  653. package/src/vision/reasoning.ts +55 -0
  654. package/src/vision/routed-describe.ts +175 -0
  655. package/src/vision/timeout-bounds.ts +9 -0
  656. package/src/web-search/anthropic-executor.ts +13 -7
  657. package/src/web-search/backends.ts +108 -0
  658. package/src/web-search/exa-executor.ts +88 -0
  659. package/src/web-search/executor.ts +11 -3
  660. package/src/web-search/gemini-executor.ts +141 -0
  661. package/src/web-search/index.ts +150 -15
  662. package/src/web-search/loop.ts +279 -50
  663. package/src/web-search/parse.ts +125 -30
  664. package/src/web-search/sources.ts +60 -0
  665. package/src/web-search/xai-executor.ts +219 -0
  666. package/gui/dist/assets/index-DTpMHS4F.js +0 -67
  667. package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
  668. package/gui/dist/provider-icons/antigravity.svg +0 -1
  669. package/gui/dist/provider-icons/claude.svg +0 -1
  670. package/gui/dist/provider-icons/copilot.svg +0 -1
  671. package/gui/dist/provider-icons/cursor.svg +0 -2
  672. package/gui/dist/provider-icons/gemini.svg +0 -1
  673. package/gui/dist/provider-icons/grok-color.svg +0 -1
  674. package/gui/dist/provider-icons/kiro.svg +0 -14
  675. package/src/cli/internal-dispatch.ts +0 -20
@@ -1,10 +1,19 @@
1
- import { fetchMainAccountInfo, listCodexAuthAccounts } from "../codex/auth-api";
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ effectiveCodexAuthAccountId,
4
+ fetchMainAccountInfoSnapshot,
5
+ listCodexAuthAccountsSnapshot,
6
+ } from "../codex/auth-api";
7
+ import { isMainAccountIdentityGenerationLive } from "../codex/main-account-cache";
2
8
  import { MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
9
+ import { codexPlanKey } from "../codex/plan";
3
10
  import { resolveEnvValue } from "../config";
4
11
  import { getValidAccessToken, getValidAccessTokenForAccount } from "../oauth";
5
12
  import { getAccountCredential, getAccountSet, getCredential } from "../oauth/store";
6
13
  import { antigravityUserAgent } from "../adapters/client-fingerprint";
7
- import { getProviderRegistryEntry, providerCodexAccountMode } from "./registry";
14
+ import { apiKeyPoolEntryId } from "./api-keys";
15
+ import { XAI_GROK_CLIENT_VERSION, XAI_GROK_COMPATIBILITY } from "./xai-transport";
16
+ import { getProviderRegistryEntry, providerCodexAccountMode, registryEntryForProviderDestination } from "./registry";
8
17
  import type { OcxConfig, OcxProviderConfig } from "../types";
9
18
  import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "./openai-tiers";
10
19
  import {
@@ -12,16 +21,57 @@ import {
12
21
  sweepExpiredOnWrite,
13
22
  type GenerationContext,
14
23
  } from "../lib/state-store-sweeper";
24
+ import { readBoundedResponseBody } from "../lib/bounded-body";
25
+ import {
26
+ aggregateCodexPoolCapacity,
27
+ CODEX_CAPACITY_MAX_QUOTA_AGE_MS,
28
+ type CodexCapacityAggregation,
29
+ type CodexCapacityQuota,
30
+ } from "./codex-capacity";
15
31
 
16
32
  /** Match oauth/index REFRESH_SKEW_MS — use stored access without refresh when still fresh. */
17
33
  const ACCOUNT_TOKEN_SKEW_MS = 60_000;
18
34
 
19
35
  const CACHE_TTL_MS = 5 * 60_000;
20
36
  const REQUEST_TIMEOUT_MS = 8_000;
37
+ /** Successful provider quota payloads are small; reject oversized or stalled JSON before parsing. */
38
+ export const QUOTA_RESPONSE_MAX_BYTES = 512 * 1024;
21
39
  const KIMI_CODE_BASE_URL = "https://api.kimi.com/coding/v1";
22
40
  const KIMI_CODE_USAGE_URL = `${KIMI_CODE_BASE_URL}/usages`;
41
+ const COMMAND_CODE_BASE_URL = "https://api.commandcode.ai";
42
+ const COMMAND_CODE_WHOAMI_URL = `${COMMAND_CODE_BASE_URL}/alpha/whoami`;
43
+ const COMMAND_CODE_CREDITS_URL = `${COMMAND_CODE_BASE_URL}/alpha/billing/credits`;
44
+ const COMMAND_CODE_SUBSCRIPTIONS_URL = `${COMMAND_CODE_BASE_URL}/alpha/billing/subscriptions`;
45
+ const COMMAND_CODE_USAGE_URL = `${COMMAND_CODE_BASE_URL}/alpha/usage/summary`;
46
+ const A6API_BASE_URL = "https://api.a6api.com";
47
+ const OPENCODE_GO_BASE_URL = "https://opencode.ai/zen/go/v1";
48
+ const OPENCODE_GO_USAGE_URL = `${OPENCODE_GO_BASE_URL}/usage`;
49
+ const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
50
+ const DEEPSEEK_BASE_URL = "https://api.deepseek.com";
51
+ const CLINE_BASE_URL = "https://api.cline.bot";
52
+ const ZAI_BASE_URL = "https://api.z.ai";
53
+ const ZAI_CN_BASE_URL = "https://open.bigmodel.cn";
54
+ const MINIMAX_REMAINS_URL = "https://www.minimax.io/v1/token_plan/remains";
55
+ const MOONSHOT_BASE_URL = "https://api.moonshot.ai/v1";
56
+ const VENICE_BASE_URL = "https://api.venice.ai/api/v1";
57
+ const SYNTHETIC_BASE_URL = "https://api.synthetic.new/v2";
58
+ const DEEPINFRA_BASE_URL = "https://api.deepinfra.com";
59
+ const NEURALWATT_BASE_URL = "https://api.neuralwatt.com/v1";
60
+ const XAI_BILLING_URL = "https://cli-chat-proxy.grok.com/v1/billing";
61
+ const XAI_CREDITS_URL = `${XAI_BILLING_URL}?format=credits`;
23
62
  /** Keep a failed probe's previous row at most this long before dropping it. */
24
- const LAST_GOOD_MAX_AGE_MS = 30 * 60_000;
63
+ const LAST_GOOD_MAX_AGE_MS = CODEX_CAPACITY_MAX_QUOTA_AGE_MS;
64
+ const nativeMainReportGenerations = new WeakMap<ProviderQuotaReport, number>();
65
+ let providerQuotaBeforePublishForTests: (() => void | Promise<void>) | null = null;
66
+
67
+ /** Test-only seam for identity/config invalidation after probes but before publication. */
68
+ export function setProviderQuotaBeforePublishForTests(
69
+ hook: (() => void | Promise<void>) | null,
70
+ ): void {
71
+ providerQuotaBeforePublishForTests = hook;
72
+ }
73
+ const TERMINAL_QUOTA_FAILURE = Symbol("terminal-quota-failure");
74
+ type ProviderQuotaProbeResult = ProviderQuotaReport | null | typeof TERMINAL_QUOTA_FAILURE;
25
75
 
26
76
  export interface ProviderQuotaWindow {
27
77
  label: string;
@@ -29,6 +79,15 @@ export interface ProviderQuotaWindow {
29
79
  resetAt?: number;
30
80
  }
31
81
 
82
+ export interface ProviderQuotaCreditsUsd {
83
+ used: number;
84
+ limit: number;
85
+ remaining: number;
86
+ percent: number;
87
+ expiresAt?: number;
88
+ unlimited?: boolean;
89
+ }
90
+
32
91
  export interface ProviderQuota {
33
92
  fiveHourPercent?: number;
34
93
  fiveHourResetAt?: number;
@@ -37,6 +96,7 @@ export interface ProviderQuota {
37
96
  monthlyPercent?: number;
38
97
  monthlyResetAt?: number;
39
98
  customWindows?: ProviderQuotaWindow[];
99
+ creditsUsd?: ProviderQuotaCreditsUsd;
40
100
  updatedAt: number;
41
101
  }
42
102
 
@@ -47,6 +107,7 @@ export interface ProviderQuotaReport {
47
107
  quota: ProviderQuota;
48
108
  updatedAt: number;
49
109
  reverseEngineered?: boolean;
110
+ aggregation?: CodexCapacityAggregation;
50
111
  }
51
112
 
52
113
  export interface ProviderQuotaResponse {
@@ -67,10 +128,99 @@ export function clearProviderQuotaCache(): void {
67
128
 
68
129
  function cacheKey(config: OcxConfig): string {
69
130
  const providers = Object.entries(config.providers)
70
- .map(([name, provider]) => `${name}:${provider.adapter}:${provider.authMode ?? "key"}:${providerCodexAccountMode(name, provider) ?? "none"}:${provider.disabled === true ? "off" : "on"}:${provider.baseUrl}`)
131
+ .map(([name, provider]) => {
132
+ const resolvedKey = typeof provider.apiKey === "string"
133
+ ? resolveEnvValue(provider.apiKey)?.trim()
134
+ : undefined;
135
+ const activeKeyId = resolvedKey ? apiKeyPoolEntryId(resolvedKey) : "none";
136
+ return `${name}:${provider.adapter}:${provider.authMode ?? "key"}:${providerCodexAccountMode(name, provider) ?? "none"}:${provider.disabled === true ? "off" : "on"}:${provider.baseUrl}:${activeKeyId}`;
137
+ })
71
138
  .sort()
72
139
  .join("|");
73
- return `${config.defaultProvider}|${config.activeCodexAccountId ?? ""}|${providers}`;
140
+ return `${config.defaultProvider}|${providers}`;
141
+ }
142
+
143
+ type CodexAuthAccountsSnapshotPromise = ReturnType<typeof listCodexAuthAccountsSnapshot>;
144
+
145
+ function hasCodexPoolProvider(config: OcxConfig): boolean {
146
+ return Object.entries(config.providers).some(([name, provider]) => (
147
+ provider.disabled !== true
148
+ && isBuiltInChatGptForwardProvider(name, provider)
149
+ && providerCodexAccountMode(name, provider) !== "direct"
150
+ ));
151
+ }
152
+
153
+ function quotaSignatureValue(quota: CodexCapacityQuota | null): unknown {
154
+ if (!quota) return null;
155
+ return {
156
+ fiveHourPercent: quota.fiveHourPercent,
157
+ fiveHourResetAt: quota.fiveHourResetAt,
158
+ weeklyPercent: quota.weeklyPercent,
159
+ weeklyResetAt: quota.weeklyResetAt,
160
+ monthlyPercent: quota.monthlyPercent,
161
+ monthlyResetAt: quota.monthlyResetAt,
162
+ updatedAt: quota.updatedAt,
163
+ customWindows: [...(quota.customWindows ?? [])]
164
+ .map(window => ({ label: window.label, percent: window.percent, resetAt: window.resetAt }))
165
+ .sort((a, b) => a.label.localeCompare(b.label)),
166
+ };
167
+ }
168
+
169
+ /** Hash only presentation-relevant state; account ids and email addresses never enter the key. */
170
+ function cacheKeyWithAggregationState(
171
+ config: OcxConfig,
172
+ prefetchedSnapshot?: CodexAuthAccountsSnapshotPromise,
173
+ ): string | Promise<string> {
174
+ const base = cacheKey(config);
175
+ if (!hasCodexPoolProvider(config)) return base;
176
+ return (async () => {
177
+ try {
178
+ const activeId = effectiveCodexAuthAccountId(config);
179
+ const snapshot = await (prefetchedSnapshot ?? listCodexAuthAccountsSnapshot(config, false));
180
+ const rows = snapshot.accounts.map(account => ({
181
+ isMain: account.isMain,
182
+ active: account.id === activeId,
183
+ plan: codexPlanKey(account.plan) ?? null,
184
+ paused: account.paused,
185
+ needsReauth: account.needsReauth === true,
186
+ quota: quotaSignatureValue(account.quota as CodexCapacityQuota | null),
187
+ }));
188
+ const canonicalRows = rows.map(row => JSON.stringify(row)).sort();
189
+ const digest = createHash("sha256").update(JSON.stringify(canonicalRows)).digest("hex").slice(0, 24);
190
+ return `${base}|codex-pool:${digest}`;
191
+ } catch {
192
+ return `${base}|codex-pool:unavailable`;
193
+ }
194
+ })();
195
+ }
196
+
197
+ function publicCapacityWindow(window: import("./codex-capacity").CodexCapacityWindowAggregation) {
198
+ const { totalWeight: _totalWeight, consumedWeight: _consumedWeight, remainingWeight: _remainingWeight, ...safe } = window;
199
+ return safe;
200
+ }
201
+
202
+ /** Management API metadata intentionally omits configured/weighted unit counts. */
203
+ function publicCapacityAggregation(
204
+ aggregation: CodexCapacityAggregation,
205
+ presentation: NonNullable<CodexCapacityAggregation["presentation"]>,
206
+ ): CodexCapacityAggregation {
207
+ const safeCurrentAccount = presentation === "coverage-only" && aggregation.currentAccount
208
+ ? { ...aggregation.currentAccount, quota: null }
209
+ : aggregation.currentAccount;
210
+ return {
211
+ ...aggregation,
212
+ presentation,
213
+ ...(safeCurrentAccount ? { currentAccount: safeCurrentAccount } : {}),
214
+ ...(aggregation.fiveHour ? { fiveHour: publicCapacityWindow(aggregation.fiveHour) } : {}),
215
+ ...(aggregation.weekly ? { weekly: publicCapacityWindow(aggregation.weekly) } : {}),
216
+ ...(aggregation.monthly ? { monthly: publicCapacityWindow(aggregation.monthly) } : {}),
217
+ ...(aggregation.customWindows ? {
218
+ customWindows: aggregation.customWindows.map(window => ({
219
+ label: window.label,
220
+ ...publicCapacityWindow(window),
221
+ })),
222
+ } : {}),
223
+ };
74
224
  }
75
225
 
76
226
  function hasQuotaRows(quota: ProviderQuota | null | undefined): quota is ProviderQuota {
@@ -78,6 +228,8 @@ function hasQuotaRows(quota: ProviderQuota | null | undefined): quota is Provide
78
228
  return typeof quota.fiveHourPercent === "number"
79
229
  || typeof quota.weeklyPercent === "number"
80
230
  || typeof quota.monthlyPercent === "number"
231
+ || quota.creditsUsd?.unlimited === true
232
+ || typeof quota.creditsUsd?.percent === "number"
81
233
  || !!quota.customWindows?.some(window => typeof window.percent === "number");
82
234
  }
83
235
 
@@ -86,21 +238,27 @@ function providerLabel(providerId: string): string {
86
238
  }
87
239
 
88
240
  function normalizeResetAt(value: unknown): number | undefined {
89
- if (typeof value === "number" && Number.isFinite(value)) return value > 10_000_000_000 ? value : value * 1000;
241
+ if (typeof value === "number" && Number.isFinite(value)) return epochMillis(value);
90
242
  if (typeof value === "string" && value.trim()) {
91
243
  const trimmed = value.trim();
92
244
  // Cursor Connect RPC returns billingCycleEnd as a unix-ms decimal string ("1771077734000").
93
245
  // Date.parse treats that as invalid; numeric epoch strings must be handled explicitly.
94
- if (/^\d+(\.\d+)?$/.test(trimmed)) {
246
+ if (/^[+-]?\d+(\.\d+)?$/.test(trimmed)) {
95
247
  const numeric = Number(trimmed);
96
- if (Number.isFinite(numeric)) return numeric > 10_000_000_000 ? numeric : numeric * 1000;
248
+ return epochMillis(numeric);
97
249
  }
98
250
  const parsed = Date.parse(trimmed);
99
- return Number.isFinite(parsed) ? parsed : undefined;
251
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
100
252
  }
101
253
  return undefined;
102
254
  }
103
255
 
256
+ /** Unix 0 / negative values are sentinels, not reset clocks (Command Code fiveHour.resetAt: 0). */
257
+ function epochMillis(value: number): number | undefined {
258
+ if (!Number.isFinite(value) || value <= 0) return undefined;
259
+ return value > 10_000_000_000 ? value : value * 1000;
260
+ }
261
+
104
262
  function toFiniteNumber(value: unknown): number | undefined {
105
263
  if (typeof value === "number" && Number.isFinite(value)) return value;
106
264
  if (typeof value === "string" && value.trim()) {
@@ -119,11 +277,799 @@ function asRecord(value: unknown): Record<string, unknown> | null {
119
277
  return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null;
120
278
  }
121
279
 
280
+ const QUOTA_JSON_READ_FAILURE = Symbol("quota-json-read-failure");
281
+
282
+ async function readQuotaJson(
283
+ response: Response,
284
+ timeoutMs = REQUEST_TIMEOUT_MS,
285
+ ): Promise<unknown | typeof QUOTA_JSON_READ_FAILURE> {
286
+ const declaredLength = Number(response.headers.get("content-length"));
287
+ if (Number.isFinite(declaredLength) && declaredLength > QUOTA_RESPONSE_MAX_BYTES) {
288
+ try {
289
+ void response.body?.cancel(
290
+ new DOMException("Provider quota response is too large", "QuotaExceededError"),
291
+ ).catch(() => undefined);
292
+ } catch {
293
+ // Best-effort cancellation only.
294
+ }
295
+ return QUOTA_JSON_READ_FAILURE;
296
+ }
297
+
298
+ try {
299
+ const bounded = await readBoundedResponseBody(response, {
300
+ maxBytes: QUOTA_RESPONSE_MAX_BYTES,
301
+ totalTimeoutMs: timeoutMs,
302
+ inactivityTimeoutMs: timeoutMs,
303
+ });
304
+ if (bounded.oversized || bounded.truncated || !bounded.displaySafe) return QUOTA_JSON_READ_FAILURE;
305
+ return JSON.parse(bounded.text) as unknown;
306
+ } catch {
307
+ return QUOTA_JSON_READ_FAILURE;
308
+ }
309
+ }
310
+
311
+ /** Test-only access to the quota reader's deadline and cancellation contract. */
312
+ export async function readProviderQuotaJsonForTests(response: Response, timeoutMs: number): Promise<unknown> {
313
+ const result = await readQuotaJson(response, timeoutMs);
314
+ return result === QUOTA_JSON_READ_FAILURE ? null : result;
315
+ }
316
+
122
317
  function isBuiltInChatGptForwardProvider(name: string, provider: OcxProviderConfig): boolean {
123
318
  return name === OPENAI_CODEX_PROVIDER_ID && isCanonicalOpenAiForwardProvider(provider);
124
319
  }
125
320
 
126
- function report(provider: string, source: string, quota: ProviderQuota): ProviderQuotaReport | null {
321
+ function isCanonicalA6apiBaseUrl(baseUrl: string): boolean {
322
+ const normalized = normalizedBaseUrl(baseUrl);
323
+ return normalized === A6API_BASE_URL || normalized === `${A6API_BASE_URL}/v1`;
324
+ }
325
+
326
+ function isCanonicalOpenCodeGoBaseUrl(baseUrl: string): boolean {
327
+ return normalizedBaseUrl(baseUrl) === OPENCODE_GO_BASE_URL;
328
+ }
329
+
330
+ function isCanonicalOpenRouterBaseUrl(baseUrl: string): boolean {
331
+ const normalized = normalizedBaseUrl(baseUrl);
332
+ return normalized === OPENROUTER_BASE_URL;
333
+ }
334
+
335
+ function isCanonicalDeepSeekBaseUrl(baseUrl: string): boolean {
336
+ const normalized = normalizedBaseUrl(baseUrl);
337
+ return normalized === DEEPSEEK_BASE_URL || normalized === `${DEEPSEEK_BASE_URL}/v1`;
338
+ }
339
+
340
+ function isCanonicalClineBaseUrl(baseUrl: string): boolean {
341
+ const normalized = normalizedBaseUrl(baseUrl);
342
+ return normalized === CLINE_BASE_URL || normalized === `${CLINE_BASE_URL}/api/v1`;
343
+ }
344
+
345
+ function isCanonicalZaiBaseUrl(baseUrl: string): boolean {
346
+ const normalized = normalizedBaseUrl(baseUrl);
347
+ return normalized === ZAI_BASE_URL
348
+ || normalized === `${ZAI_BASE_URL}/api/coding/paas/v4`
349
+ || normalized === ZAI_CN_BASE_URL
350
+ || normalized === `${ZAI_CN_BASE_URL}/api/coding/paas/v4`
351
+ // BigModel serves the same GLM Coding Plan on the OpenAI Responses wire at /api/v1.
352
+ || normalized === `${ZAI_CN_BASE_URL}/api/v1`;
353
+ }
354
+
355
+ function isCanonicalMinimaxBaseUrl(baseUrl: string): boolean {
356
+ const normalized = normalizedBaseUrl(baseUrl);
357
+ return normalized === "https://api.minimax.io/v1" || normalized === "https://api.minimaxi.com/v1";
358
+ }
359
+
360
+ function isCanonicalMoonshotBaseUrl(baseUrl: string): boolean {
361
+ const normalized = normalizedBaseUrl(baseUrl);
362
+ return normalized === MOONSHOT_BASE_URL || normalized === "https://api.moonshot.cn/v1";
363
+ }
364
+
365
+ function isCanonicalVeniceBaseUrl(baseUrl: string): boolean {
366
+ return normalizedBaseUrl(baseUrl) === VENICE_BASE_URL;
367
+ }
368
+
369
+ function isCanonicalSyntheticBaseUrl(baseUrl: string): boolean {
370
+ const normalized = normalizedBaseUrl(baseUrl);
371
+ return normalized === SYNTHETIC_BASE_URL || normalized === "https://api.synthetic.new/openai/v1";
372
+ }
373
+
374
+ function isCanonicalDeepInfraBaseUrl(baseUrl: string): boolean {
375
+ const normalized = normalizedBaseUrl(baseUrl);
376
+ return normalized === DEEPINFRA_BASE_URL || normalized === `${DEEPINFRA_BASE_URL}/v1/openai`;
377
+ }
378
+
379
+ function isCanonicalNeuralwattBaseUrl(baseUrl: string): boolean {
380
+ return normalizedBaseUrl(baseUrl) === NEURALWATT_BASE_URL;
381
+ }
382
+
383
+ function a6apiPayload(value: unknown): Record<string, unknown> | null {
384
+ const body = asRecord(value);
385
+ return asRecord(body?.data) ?? body;
386
+ }
387
+
388
+ function firstFinite(record: Record<string, unknown> | null, names: string[]): number | undefined {
389
+ if (!record) return undefined;
390
+ for (const name of names) {
391
+ const value = toFiniteNumber(record[name]);
392
+ if (value !== undefined) return value;
393
+ }
394
+ return undefined;
395
+ }
396
+
397
+ async function fetchA6apiQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
398
+ // Never send a configured API key to a lookalike host or through a redirect.
399
+ if (!isCanonicalA6apiBaseUrl(config.baseUrl)) return null;
400
+ const apiKey = resolveEnvValue(config.apiKey)?.trim();
401
+ if (!apiKey) return null;
402
+ const headers = { Accept: "application/json", Authorization: `Bearer ${apiKey}` } as const;
403
+ const [subscriptionResponse, tokenResponse] = await Promise.all([
404
+ fetch(`${A6API_BASE_URL}/dashboard/billing/subscription`, {
405
+ headers, redirect: "error", signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
406
+ }),
407
+ fetch(`${A6API_BASE_URL}/api/usage/token/`, {
408
+ headers, redirect: "error", signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
409
+ }),
410
+ ]);
411
+ if (!subscriptionResponse.ok || !tokenResponse.ok) {
412
+ const statuses = [subscriptionResponse.status, tokenResponse.status];
413
+ // 408/429 are transient (timeout/throttle), not invalid-account signals: keep the
414
+ // last-good row like 5xx/network failures. 401/403 (bad key) and 404 (contract change)
415
+ // stay terminal.
416
+ return statuses.some(status => status >= 400 && status < 500 && status !== 429 && status !== 408)
417
+ ? TERMINAL_QUOTA_FAILURE
418
+ : null;
419
+ }
420
+ const [subscriptionBody, tokenBody] = await Promise.all([
421
+ readQuotaJson(subscriptionResponse),
422
+ readQuotaJson(tokenResponse),
423
+ ]);
424
+ if (subscriptionBody === QUOTA_JSON_READ_FAILURE || tokenBody === QUOTA_JSON_READ_FAILURE) return null;
425
+ const subscription = a6apiPayload(subscriptionBody);
426
+ const token = a6apiPayload(tokenBody);
427
+ const unlimited = token?.unlimited_quota === true
428
+ || token?.unlimited_quota === 1
429
+ || token?.unlimited_quota === "true";
430
+ const normalizedExpiry = normalizeResetAt(token?.expires_at);
431
+ const expiry = normalizedExpiry && normalizedExpiry > 0
432
+ ? { expiresAt: normalizedExpiry }
433
+ : {};
434
+ if (unlimited) {
435
+ return report(provider, "a6api:billing", {
436
+ creditsUsd: {
437
+ used: 0,
438
+ limit: 0,
439
+ remaining: 0,
440
+ percent: 0,
441
+ unlimited: true,
442
+ ...expiry,
443
+ },
444
+ customWindows: [{ label: "Unlimited API credits", percent: 0 }],
445
+ updatedAt: Date.now(),
446
+ });
447
+ }
448
+ const limitUsd = firstFinite(subscription, ["hard_limit_usd"]);
449
+ const grantedUnits = firstFinite(token, ["total_granted"]);
450
+ const usedUnits = firstFinite(token, ["total_used"]);
451
+ const availableUnits = firstFinite(token, ["total_available"]);
452
+ const reconciledUnits = usedUnits !== undefined && availableUnits !== undefined
453
+ ? usedUnits + availableUnits
454
+ : undefined;
455
+ const reconciliationTolerance = grantedUnits !== undefined
456
+ ? Math.abs(grantedUnits) * 1e-9
457
+ : 0;
458
+ if (limitUsd === undefined || grantedUnits === undefined || usedUnits === undefined
459
+ || availableUnits === undefined || limitUsd <= 0 || grantedUnits <= 0
460
+ || usedUnits < 0 || availableUnits < 0
461
+ || reconciledUnits === undefined
462
+ || Math.abs(reconciledUnits - grantedUnits) > reconciliationTolerance) return TERMINAL_QUOTA_FAILURE;
463
+ const usdPerUnit = limitUsd / grantedUnits;
464
+ const usedUsd = usedUnits * usdPerUnit;
465
+ const remainingUsd = Math.max(0, availableUnits * usdPerUnit);
466
+ const percent = normalizePercent((usedUsd / limitUsd) * 100);
467
+ if (percent === undefined) return TERMINAL_QUOTA_FAILURE;
468
+ const label = `API credits ($${remainingUsd.toFixed(2)} of $${limitUsd.toFixed(2)} remaining)`;
469
+ return report(provider, "a6api:billing", {
470
+ creditsUsd: {
471
+ used: usedUsd,
472
+ limit: limitUsd,
473
+ remaining: remainingUsd,
474
+ percent,
475
+ ...expiry,
476
+ },
477
+ customWindows: [{ label, percent }],
478
+ updatedAt: Date.now(),
479
+ });
480
+ }
481
+
482
+ function parseOpenCodeGoUsageWindow(value: unknown): { percent: number; resetAt?: number } | null {
483
+ const row = asRecord(value);
484
+ if (!row) return null;
485
+ const percent = normalizePercent(row.percent);
486
+ if (percent === undefined) return null;
487
+ const resetAt = normalizeResetAt(row.resetsAt);
488
+ return { percent, ...(resetAt !== undefined ? { resetAt } : {}) };
489
+ }
490
+
491
+ async function fetchOpenCodeGoQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
492
+ // Never send a configured API key when the provider destination is not the built-in Go endpoint.
493
+ if (!isCanonicalOpenCodeGoBaseUrl(config.baseUrl)) return null;
494
+ const apiKey = resolveEnvValue(config.apiKey)?.trim();
495
+ if (!apiKey) return null;
496
+ const response = await fetch(OPENCODE_GO_USAGE_URL, {
497
+ headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
498
+ redirect: "error",
499
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
500
+ });
501
+ if (!response.ok) {
502
+ return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
503
+ ? TERMINAL_QUOTA_FAILURE
504
+ : null;
505
+ }
506
+ const body = asRecord(await readQuotaJson(response));
507
+ const usage = asRecord(body?.usage);
508
+ if (!usage) return null;
509
+ const rolling = parseOpenCodeGoUsageWindow(usage.rolling);
510
+ const weekly = parseOpenCodeGoUsageWindow(usage.weekly);
511
+ const monthly = parseOpenCodeGoUsageWindow(usage.monthly);
512
+ const quota: ProviderQuota = {
513
+ ...(rolling ? {
514
+ fiveHourPercent: rolling.percent,
515
+ ...(rolling.resetAt !== undefined ? { fiveHourResetAt: rolling.resetAt } : {}),
516
+ } : {}),
517
+ ...(weekly ? {
518
+ weeklyPercent: weekly.percent,
519
+ ...(weekly.resetAt !== undefined ? { weeklyResetAt: weekly.resetAt } : {}),
520
+ } : {}),
521
+ ...(monthly ? {
522
+ monthlyPercent: monthly.percent,
523
+ ...(monthly.resetAt !== undefined ? { monthlyResetAt: monthly.resetAt } : {}),
524
+ } : {}),
525
+ updatedAt: Date.now(),
526
+ };
527
+ return report(provider, "opencode-go:usage", quota);
528
+ }
529
+
530
+ /**
531
+ * OpenRouter `GET /api/v1/key` — the key's own credit balance and optional
532
+ * per-key spending cap. `limit` is the configured cap (absent = uncapped);
533
+ * `usage` is lifetime spend; `limit_remaining` is what is left of the cap.
534
+ * When no cap is set there is no hard limit to meter against, so no bar is
535
+ * produced — the provider falls back to its documented reference.
536
+ */
537
+ async function fetchOpenRouterQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
538
+ // Never send a configured API key to a lookalike host or through a redirect.
539
+ if (!isCanonicalOpenRouterBaseUrl(config.baseUrl)) return null;
540
+ const apiKey = resolveEnvValue(config.apiKey)?.trim();
541
+ if (!apiKey) return null;
542
+ const response = await fetch(`${OPENROUTER_BASE_URL}/key`, {
543
+ headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
544
+ redirect: "error",
545
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
546
+ });
547
+ if (!response.ok) {
548
+ return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
549
+ ? TERMINAL_QUOTA_FAILURE
550
+ : null;
551
+ }
552
+ const body = asRecord(await readQuotaJson(response));
553
+ const data = asRecord(body?.data) ?? body;
554
+ if (!data) return null;
555
+ const limit = toFiniteNumber(data.limit);
556
+ const limitRemaining = toFiniteNumber(data.limit_remaining);
557
+ const usage = toFiniteNumber(data.usage);
558
+ // A successful no-cap response is a DELIBERATE change, not a transient
559
+ // failure: the old capped row must be dropped, not preserved as last-good.
560
+ if (limit === undefined || limit <= 0) return TERMINAL_QUOTA_FAILURE;
561
+ // Prefer the authoritative remaining-cap value when present: `usage` is
562
+ // lifetime accumulated spend and overstates a reset or re-capped key.
563
+ const used = limitRemaining !== undefined
564
+ ? Math.max(0, limit - limitRemaining)
565
+ : usage !== undefined && usage >= 0 ? usage : undefined;
566
+ if (used === undefined) return null;
567
+ const percent = normalizePercent((used / limit) * 100);
568
+ if (percent === undefined) return null;
569
+ const remaining = Math.max(0, limit - used);
570
+ const label = `API credits ($${remaining.toFixed(2)} of $${limit.toFixed(2)} remaining)`;
571
+ return report(provider, "openrouter:key-info", {
572
+ customWindows: [{ label, percent }],
573
+ updatedAt: Date.now(),
574
+ });
575
+ }
576
+
577
+ /**
578
+ * DeepSeek `GET /user/balance` — the account's granted + topped-up credit
579
+ * balance. The payload places `total_balance` / `granted_balance` inside
580
+ * entries of `balance_infos` (one row per currency); the row for the account's
581
+ * currency is selected by preference. `granted_balance` is a CURRENT balance
582
+ * component, not the original grant ceiling, so no consumed percentage is
583
+ * fabricated — the balance is reported as a balance-only window.
584
+ */
585
+ async function fetchDeepSeekQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
586
+ if (!isCanonicalDeepSeekBaseUrl(config.baseUrl)) return null;
587
+ const apiKey = resolveEnvValue(config.apiKey)?.trim();
588
+ if (!apiKey) return null;
589
+ const response = await fetch(`${DEEPSEEK_BASE_URL}/user/balance`, {
590
+ headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
591
+ redirect: "error",
592
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
593
+ });
594
+ if (!response.ok) {
595
+ return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
596
+ ? TERMINAL_QUOTA_FAILURE
597
+ : null;
598
+ }
599
+ const body = asRecord(await readQuotaJson(response));
600
+ // The payload nests balances under `balance_infos` rows keyed by currency;
601
+ // prefer a USD row, then CNY, then the first row that parses.
602
+ const infos = Array.isArray(body?.balance_infos) ? body.balance_infos as unknown[] : null;
603
+ const rows = infos
604
+ ? infos.map((raw): Record<string, unknown> | null => asRecord(raw)).filter((r): r is Record<string, unknown> => r !== null)
605
+ : [];
606
+ const pick = (currency: string): Record<string, unknown> | null =>
607
+ rows.find(row => String(row.currency ?? "").toUpperCase() === currency) ?? null;
608
+ const preferred = pick("USD") ?? pick("CNY") ?? rows[0] ?? null;
609
+ if (!preferred) return null;
610
+ const totalBalance = toFiniteNumber(preferred.total_balance);
611
+ const grantedBalance = toFiniteNumber(preferred.granted_balance);
612
+ const toppedUp = toFiniteNumber(preferred.topped_up_balance);
613
+ const balance = totalBalance ?? grantedBalance ?? toppedUp;
614
+ if (balance === undefined || balance < 0) return null;
615
+ const label = grantedBalance !== undefined && grantedBalance > 0
616
+ ? `API balance ($${balance.toFixed(2)} total, $${grantedBalance.toFixed(2)} granted)`
617
+ : `API balance ($${balance.toFixed(2)})`;
618
+ return report(provider, "deepseek:balance", {
619
+ customWindows: [{ label, percent: 0 }],
620
+ updatedAt: Date.now(),
621
+ });
622
+ }
623
+
624
+ /**
625
+ * ClinePass `GET /api/v1/users/me/plan/usage-limits` — the subscription's
626
+ * rolling five-hour, weekly, and monthly utilization, matching the existing
627
+ * ProviderQuota windows directly. The endpoint 404s (or returns a null plan)
628
+ * for accounts without an active ClinePass, which is a no-report, not an error.
629
+ */
630
+ async function fetchClineQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
631
+ if (!isCanonicalClineBaseUrl(config.baseUrl)) return null;
632
+ const apiKey = resolveEnvValue(config.apiKey)?.trim();
633
+ if (!apiKey) return null;
634
+ const response = await fetch(`${CLINE_BASE_URL}/api/v1/users/me/plan/usage-limits`, {
635
+ headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
636
+ redirect: "error",
637
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
638
+ });
639
+ if (!response.ok) {
640
+ // 404 = no active plan; a plain "no plan" is a no-report, everything else
641
+ // 4xx (except 408/429) is a credential/contract problem.
642
+ if (response.status === 404) return null;
643
+ return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
644
+ ? TERMINAL_QUOTA_FAILURE
645
+ : null;
646
+ }
647
+ const body = asRecord(await readQuotaJson(response));
648
+ const data = asRecord(body?.data) ?? body;
649
+ const limits = Array.isArray(data?.limits) ? data.limits : null;
650
+ if (!limits) return null;
651
+ const quota: ProviderQuota = { updatedAt: Date.now() };
652
+ let windows = 0;
653
+ for (const raw of limits) {
654
+ const row = asRecord(raw);
655
+ if (!row) continue;
656
+ const percent = normalizePercent(row.percentUsed);
657
+ if (percent === undefined) continue;
658
+ const resetAt = normalizeResetAt(row.resetsAt);
659
+ if (row.type === "five_hour") {
660
+ quota.fiveHourPercent = percent;
661
+ if (resetAt !== undefined) quota.fiveHourResetAt = resetAt;
662
+ windows += 1;
663
+ } else if (row.type === "weekly") {
664
+ quota.weeklyPercent = percent;
665
+ if (resetAt !== undefined) quota.weeklyResetAt = resetAt;
666
+ windows += 1;
667
+ } else if (row.type === "monthly") {
668
+ quota.monthlyPercent = percent;
669
+ if (resetAt !== undefined) quota.monthlyResetAt = resetAt;
670
+ windows += 1;
671
+ }
672
+ }
673
+ return windows > 0 ? report(provider, "cline:plan-usage-limits", quota) : null;
674
+ }
675
+
676
+ /**
677
+ * Z.AI GLM Coding Plan `GET /api/monitor/usage/quota/limit` — the coding-plan
678
+ * limits arrive as a `limits` array of `TOKENS_LIMIT` (newer plans call the
679
+ * same rows `CREDIT_LIMIT`) and `TIME_LIMIT` rows. `TOKENS_LIMIT`/`CREDIT_LIMIT`
680
+ * rows carry the window length as `unit`/`number`: unit 3 is hours (number 5 →
681
+ * the rolling five-hour window), unit 6 is weeks (number 1 → the weekly
682
+ * window). `TIME_LIMIT` rows are the monthly MCP tool budget (Web Search / Web
683
+ * Reader / Zread). Every row's `percentage` is the consumed share (falling
684
+ * back to `currentValue`/`usage` when absent) and `nextResetTime` (unix ms)
685
+ * the window reset.
686
+ */
687
+ export function parseZaiQuotaLimits(data: Record<string, unknown> | null): ProviderQuota | null {
688
+ const limits = Array.isArray(data?.limits) ? data.limits as unknown[] : null;
689
+ if (!limits) return null;
690
+ const quota: ProviderQuota = { updatedAt: Date.now() };
691
+ let windows = 0;
692
+ for (const raw of limits) {
693
+ const row = asRecord(raw);
694
+ if (!row) continue;
695
+ const resetAt = normalizeResetAt(row.nextResetTime);
696
+ let percent = normalizePercent(row.percentage);
697
+ if (percent === undefined) {
698
+ const used = toFiniteNumber(row.currentValue);
699
+ const total = toFiniteNumber(row.usage);
700
+ if (used !== undefined && total !== undefined && total > 0) {
701
+ percent = normalizePercent((used / total) * 100);
702
+ }
703
+ }
704
+ if (percent === undefined) continue;
705
+ if (row.type === "TOKENS_LIMIT" || row.type === "CREDIT_LIMIT") {
706
+ const unit = toFiniteNumber(row.unit);
707
+ const number = toFiniteNumber(row.number);
708
+ if (unit === 3 && number === 5) {
709
+ quota.fiveHourPercent = percent;
710
+ if (resetAt !== undefined) quota.fiveHourResetAt = resetAt;
711
+ windows += 1;
712
+ } else if (unit === 6 && number === 1) {
713
+ quota.weeklyPercent = percent;
714
+ if (resetAt !== undefined) quota.weeklyResetAt = resetAt;
715
+ windows += 1;
716
+ }
717
+ } else if (row.type === "TIME_LIMIT") {
718
+ quota.monthlyPercent = percent;
719
+ if (resetAt !== undefined) quota.monthlyResetAt = resetAt;
720
+ windows += 1;
721
+ }
722
+ }
723
+ return windows > 0 ? quota : null;
724
+ }
725
+
726
+ /**
727
+ * Legacy Z.AI payload shape: percent fields with window identifiers directly on
728
+ * the data object (optionally nested under `quota`). Kept as a fallback so
729
+ * older responses keep rendering when the `limits` array is absent.
730
+ */
731
+ function parseZaiQuotaLegacyFields(data: Record<string, unknown> | null): ProviderQuota | null {
732
+ if (!data) return null;
733
+ const quota: ProviderQuota = { updatedAt: Date.now() };
734
+ let windows = 0;
735
+ const percentAt = (key: string): number | undefined => {
736
+ const value = normalizePercent(data[key]);
737
+ if (value !== undefined) return value;
738
+ const nested = asRecord(data.quota);
739
+ return nested ? normalizePercent(nested[key]) : undefined;
740
+ };
741
+ const fiveHour = percentAt("fiveHourPercent") ?? percentAt("fiveHourUsage") ?? percentAt("fiveHourUsed");
742
+ const weekly = percentAt("weeklyPercent") ?? percentAt("weeklyUsage") ?? percentAt("weeklyUsed");
743
+ const monthly = percentAt("monthlyPercent") ?? percentAt("mcpPercent") ?? percentAt("monthlyMCPUsage");
744
+ if (fiveHour !== undefined) {
745
+ quota.fiveHourPercent = fiveHour;
746
+ windows += 1;
747
+ }
748
+ if (weekly !== undefined) {
749
+ quota.weeklyPercent = weekly;
750
+ windows += 1;
751
+ }
752
+ if (monthly !== undefined) {
753
+ quota.monthlyPercent = monthly;
754
+ windows += 1;
755
+ }
756
+ return windows > 0 ? quota : null;
757
+ }
758
+
759
+ /**
760
+ * Fetches the Z.AI GLM Coding Plan quota — on whichever region the provider
761
+ * points at (api.z.ai or open.bigmodel.cn). Authenticates with the API key as
762
+ * a Bearer token per Z.AI's API reference. The `limits` array shape is
763
+ * preferred; older field-name payloads fall back to the legacy parser.
764
+ */
765
+ async function fetchZaiQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
766
+ if (!isCanonicalZaiBaseUrl(config.baseUrl)) return null;
767
+ const apiKey = resolveEnvValue(config.apiKey)?.trim();
768
+ if (!apiKey) return null;
769
+ const normalized = normalizedBaseUrl(config.baseUrl);
770
+ const monitorHost = normalized === ZAI_BASE_URL || normalized === `${ZAI_BASE_URL}/api/coding/paas/v4`
771
+ ? ZAI_BASE_URL
772
+ : ZAI_CN_BASE_URL;
773
+ const response = await fetch(`${monitorHost}/api/monitor/usage/quota/limit`, {
774
+ headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
775
+ redirect: "error",
776
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
777
+ });
778
+ if (!response.ok) {
779
+ return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
780
+ ? TERMINAL_QUOTA_FAILURE
781
+ : null;
782
+ }
783
+ const body = asRecord(await readQuotaJson(response));
784
+ if (!body || body.success === false) return null;
785
+ const data = asRecord(body.data) ?? body;
786
+ const quota = Array.isArray(data?.limits)
787
+ ? parseZaiQuotaLimits(data)
788
+ : parseZaiQuotaLegacyFields(data);
789
+ return quota ? report(provider, "zai:quota-limit", quota) : null;
790
+ }
791
+
792
+ /**
793
+ * MiniMax Token Plan `GET /v1/token_plan/remains` — the subscription's
794
+ * remaining quota as a countdown-time value (ms). The endpoint does not expose
795
+ * the plan's total duration, so no percentage is fabricated from a presumed
796
+ * window: the remaining time is reported as a duration-only window. When the
797
+ * API supplies a total (`total_time` / `plan_duration_ms`), a consumed share
798
+ * is derived from it. Region selects the host: `minimax` → www.minimax.io,
799
+ * `minimax-cn` → api.minimaxi.com.
800
+ */
801
+ async function fetchMinimaxQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
802
+ if (!isCanonicalMinimaxBaseUrl(config.baseUrl)) return null;
803
+ const apiKey = resolveEnvValue(config.apiKey)?.trim();
804
+ if (!apiKey) return null;
805
+ const cnHost = normalizedBaseUrl(config.baseUrl)?.startsWith("https://api.minimaxi.com");
806
+ const remainsUrl = cnHost ? "https://api.minimaxi.com/v1/token_plan/remains" : MINIMAX_REMAINS_URL;
807
+ const response = await fetch(remainsUrl, {
808
+ headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
809
+ redirect: "error",
810
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
811
+ });
812
+ if (!response.ok) {
813
+ return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
814
+ ? TERMINAL_QUOTA_FAILURE
815
+ : null;
816
+ }
817
+ const body = asRecord(await readQuotaJson(response));
818
+ if (!body || body.success === false) return null;
819
+ const data = asRecord(body.data) ?? body;
820
+ const remainsMs = toFiniteNumber(data.remains_time ?? data.remainsTime);
821
+ if (remainsMs === undefined || remainsMs < 0) return null;
822
+ const hours = Math.floor(remainsMs / 3_600_000);
823
+ const label = `Token Plan remaining (${hours}h)`;
824
+ // Only derive a consumed share when the API actually reports the plan total;
825
+ // a presumed window (e.g. 30 days) would fabricate utilization. A valid
826
+ // response that omits the total after a prior refresh had it is a DELIBERATE
827
+ // contract change — the old row must be dropped (terminal), not preserved as
828
+ // a transient last-good.
829
+ const totalMs = toFiniteNumber(data.total_time ?? data.plan_duration_ms ?? data.total_duration_ms);
830
+ if (totalMs === undefined || totalMs <= 0) return TERMINAL_QUOTA_FAILURE;
831
+ const consumed = Math.max(0, totalMs - remainsMs);
832
+ const percent = normalizePercent((consumed / totalMs) * 100);
833
+ if (percent === undefined) return null;
834
+ return report(provider, "minimax:token-plan-remains", {
835
+ customWindows: [{ label, percent }],
836
+ updatedAt: Date.now(),
837
+ });
838
+ }
839
+
840
+ /**
841
+ * Moonshot/Kimi `GET /v1/users/me/balance` — the account's available balance
842
+ * (voucher + cash). Renders a single balance window against the sum of
843
+ * voucher + cash when positive (there is no per-window rate limit to meter).
844
+ */
845
+ async function fetchMoonshotQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
846
+ if (!isCanonicalMoonshotBaseUrl(config.baseUrl)) return null;
847
+ const apiKey = resolveEnvValue(config.apiKey)?.trim();
848
+ if (!apiKey) return null;
849
+ const host = normalizedBaseUrl(config.baseUrl)?.startsWith("https://api.moonshot.cn") ? "https://api.moonshot.cn/v1" : MOONSHOT_BASE_URL;
850
+ const response = await fetch(`${host}/users/me/balance`, {
851
+ headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
852
+ redirect: "error",
853
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
854
+ });
855
+ if (!response.ok) {
856
+ return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
857
+ ? TERMINAL_QUOTA_FAILURE
858
+ : null;
859
+ }
860
+ const body = asRecord(await readQuotaJson(response));
861
+ const data = asRecord(body?.data) ?? body;
862
+ if (!data) return null;
863
+ const available = toFiniteNumber(data.available_balance);
864
+ const voucher = toFiniteNumber(data.voucher_balance);
865
+ const cash = toFiniteNumber(data.cash_balance);
866
+ if (available === undefined || available < 0) return null;
867
+ // Moonshot exposes no per-window quota ceiling, only a balance — report it
868
+ // as a balance-only window (percent 0) rather than a fabricated utilization.
869
+ // Currency is host-scoped: China platform (api.moonshot.cn) bills in CNY;
870
+ // the international platform (api.moonshot.ai) bills in USD. Do not force
871
+ // either side into the other unit — the number is correct, only the unit
872
+ // must match the host.
873
+ const isChinaHost = host.startsWith("https://api.moonshot.cn");
874
+ const money = (n: number) => isChinaHost ? `¥${n.toFixed(2)}` : `$${n.toFixed(2)}`;
875
+ const unit = isChinaHost ? "CNY" : "USD";
876
+ const label = voucher !== undefined && cash !== undefined
877
+ ? `Balance (${money(available)} ${unit} available, ${money(voucher)} voucher)`
878
+ : `Balance (${money(available)} ${unit} available)`;
879
+ return report(provider, "moonshot:balance", {
880
+ customWindows: [{ label, percent: 0 }],
881
+ updatedAt: Date.now(),
882
+ });
883
+ }
884
+
885
+ /**
886
+ * Venice `GET /api/v1/billing/balance` — DIEM (native credits) or USD balance.
887
+ * Shows the remaining balance; epoch allocation progress when present.
888
+ */
889
+ async function fetchVeniceQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
890
+ if (!isCanonicalVeniceBaseUrl(config.baseUrl)) return null;
891
+ const apiKey = resolveEnvValue(config.apiKey)?.trim();
892
+ if (!apiKey) return null;
893
+ const response = await fetch(`${VENICE_BASE_URL}/billing/balance`, {
894
+ headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
895
+ redirect: "error",
896
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
897
+ });
898
+ if (!response.ok) {
899
+ return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
900
+ ? TERMINAL_QUOTA_FAILURE
901
+ : null;
902
+ }
903
+ const body = asRecord(await readQuotaJson(response));
904
+ const data = asRecord(body?.data) ?? body;
905
+ if (!data) return null;
906
+ const diemBalance = toFiniteNumber(data.balance);
907
+ const usdBalance = toFiniteNumber(data.balance_usd);
908
+ const epochUsed = toFiniteNumber(data.diem_epoch_used);
909
+ const epochAllocated = toFiniteNumber(data.diem_epoch_allocated);
910
+ if (diemBalance === undefined && usdBalance === undefined) return null;
911
+ const label = diemBalance !== undefined
912
+ ? `DIEM balance (${Math.round(diemBalance)})`
913
+ : `USD balance ($${usdBalance?.toFixed(2) ?? "?"})`;
914
+ if (epochAllocated !== undefined && epochAllocated > 0 && epochUsed !== undefined) {
915
+ const percent = normalizePercent((epochUsed / epochAllocated) * 100);
916
+ if (percent === undefined) return null;
917
+ return report(provider, "venice:billing-balance", {
918
+ customWindows: [{ label, percent }],
919
+ updatedAt: Date.now(),
920
+ });
921
+ }
922
+ return report(provider, "venice:billing-balance", {
923
+ customWindows: [{ label, percent: 0 }],
924
+ updatedAt: Date.now(),
925
+ });
926
+ }
927
+
928
+ /**
929
+ * Synthetic `GET /v2/quotas` — the known quota lanes (rolling 5-hour,
930
+ * weekly token, search-hourly) mapped onto the quota windows.
931
+ */
932
+ async function fetchSyntheticQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
933
+ if (!isCanonicalSyntheticBaseUrl(config.baseUrl)) return null;
934
+ const apiKey = resolveEnvValue(config.apiKey)?.trim();
935
+ if (!apiKey) return null;
936
+ const response = await fetch(`${SYNTHETIC_BASE_URL}/quotas`, {
937
+ headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
938
+ redirect: "error",
939
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
940
+ });
941
+ if (!response.ok) {
942
+ return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
943
+ ? TERMINAL_QUOTA_FAILURE
944
+ : null;
945
+ }
946
+ const body = asRecord(await readQuotaJson(response));
947
+ const data = asRecord(body?.data) ?? body;
948
+ const quota: ProviderQuota = { updatedAt: Date.now() };
949
+ let windows = 0;
950
+ const percentAt = (key: string): number | undefined => {
951
+ const value = normalizePercent(data?.[key]);
952
+ if (value !== undefined) return value;
953
+ const nested = asRecord(data?.quota) ?? asRecord(data?.quotas);
954
+ return nested ? normalizePercent(nested[key]) : undefined;
955
+ };
956
+ const fiveHour = percentAt("rollingFiveHourLimit");
957
+ const weekly = percentAt("weeklyTokenLimit");
958
+ if (fiveHour !== undefined) {
959
+ quota.fiveHourPercent = fiveHour;
960
+ windows += 1;
961
+ }
962
+ if (weekly !== undefined) {
963
+ quota.weeklyPercent = weekly;
964
+ windows += 1;
965
+ }
966
+ const search = asRecord(data?.search);
967
+ const searchHourly = search ? normalizePercent(search.hourly) : undefined;
968
+ if (searchHourly !== undefined) {
969
+ quota.customWindows = [...(quota.customWindows ?? []), { label: "Search hourly", percent: searchHourly }];
970
+ windows += 1;
971
+ }
972
+ return windows > 0 ? report(provider, "synthetic:quotas", quota) : null;
973
+ }
974
+
975
+ /**
976
+ * DeepInfra `GET /payment/checklist?compute_owed=true` — prepaid balance,
977
+ * recent spend, spending limit, and suspension state. Renders a balance
978
+ * window (prepaid funds are a negative `stripe_balance` → positive available).
979
+ */
980
+ async function fetchDeepInfraQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
981
+ if (!isCanonicalDeepInfraBaseUrl(config.baseUrl)) return null;
982
+ const apiKey = resolveEnvValue(config.apiKey)?.trim();
983
+ if (!apiKey) return null;
984
+ const response = await fetch(`${DEEPINFRA_BASE_URL}/payment/checklist?compute_owed=true`, {
985
+ headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
986
+ redirect: "error",
987
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
988
+ });
989
+ if (!response.ok) {
990
+ return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
991
+ ? TERMINAL_QUOTA_FAILURE
992
+ : null;
993
+ }
994
+ const body = asRecord(await readQuotaJson(response));
995
+ const data = asRecord(body?.data) ?? body;
996
+ if (!data) return null;
997
+ const stripeBalance = toFiniteNumber(data.stripe_balance);
998
+ const spendLimit = toFiniteNumber(data.spending_limit);
999
+ const total = toFiniteNumber(data.total_amount_due);
1000
+ if (stripeBalance === undefined) return null;
1001
+ // Prepaid funds are negative; a positive value is money owed.
1002
+ const available = stripeBalance < 0 ? -stripeBalance : 0;
1003
+ if (spendLimit !== undefined && spendLimit > 0) {
1004
+ const spent = total !== undefined && total > 0 ? total : Math.max(0, spendLimit - available);
1005
+ const percent = normalizePercent((spent / spendLimit) * 100);
1006
+ if (percent === undefined) return null;
1007
+ return report(provider, "deepinfra:billing-checklist", {
1008
+ customWindows: [{ label: `Billing cycle spend ($${spent.toFixed(2)} of $${spendLimit.toFixed(2)})`, percent }],
1009
+ updatedAt: Date.now(),
1010
+ });
1011
+ }
1012
+ return report(provider, "deepinfra:billing-checklist", {
1013
+ customWindows: [{ label: `Prepaid balance ($${available.toFixed(2)})`, percent: 0 }],
1014
+ updatedAt: Date.now(),
1015
+ });
1016
+ }
1017
+
1018
+ /**
1019
+ * Neuralwatt `GET /v1/quota` — subscription kWh usage (primary window) and
1020
+ * prepaid USD credit balance (secondary).
1021
+ */
1022
+ async function fetchNeuralwattQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
1023
+ if (!isCanonicalNeuralwattBaseUrl(config.baseUrl)) return null;
1024
+ const apiKey = resolveEnvValue(config.apiKey)?.trim();
1025
+ if (!apiKey) return null;
1026
+ const response = await fetch(`${NEURALWATT_BASE_URL}/quota`, {
1027
+ headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
1028
+ redirect: "error",
1029
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
1030
+ });
1031
+ if (!response.ok) {
1032
+ return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
1033
+ ? TERMINAL_QUOTA_FAILURE
1034
+ : null;
1035
+ }
1036
+ const body = asRecord(await readQuotaJson(response));
1037
+ const data = asRecord(body?.data) ?? body;
1038
+ const quota: ProviderQuota = { updatedAt: Date.now() };
1039
+ let windows = 0;
1040
+ const subscription = asRecord(data?.subscription);
1041
+ const kwhUsed = subscription ? toFiniteNumber(subscription.kwh_used) : undefined;
1042
+ const kwhIncluded = subscription ? toFiniteNumber(subscription.kwh_included) : undefined;
1043
+ if (kwhUsed !== undefined && kwhIncluded !== undefined && kwhIncluded > 0) {
1044
+ const percent = normalizePercent((kwhUsed / kwhIncluded) * 100);
1045
+ if (percent !== undefined) {
1046
+ quota.fiveHourPercent = percent;
1047
+ const periodEnd = subscription ? normalizeResetAt(subscription.current_period_end) : undefined;
1048
+ if (periodEnd !== undefined) quota.fiveHourResetAt = periodEnd;
1049
+ windows += 1;
1050
+ }
1051
+ }
1052
+ const balance = asRecord(data?.balance);
1053
+ const totalCredits = balance ? toFiniteNumber(balance.total_credits_usd) : undefined;
1054
+ const remainingCredits = balance ? toFiniteNumber(balance.credits_remaining_usd) : undefined;
1055
+ if (totalCredits !== undefined && totalCredits > 0 && remainingCredits !== undefined) {
1056
+ // Utilization is CONSUMED credits, not the remaining share.
1057
+ const used = Math.max(0, totalCredits - remainingCredits);
1058
+ const percent = normalizePercent((used / totalCredits) * 100);
1059
+ if (percent !== undefined) {
1060
+ quota.customWindows = [...(quota.customWindows ?? []), { label: "Prepaid credits", percent }];
1061
+ windows += 1;
1062
+ }
1063
+ }
1064
+ return windows > 0 ? report(provider, "neuralwatt:quota", quota) : null;
1065
+ }
1066
+
1067
+ function report(
1068
+ provider: string,
1069
+ source: string,
1070
+ quota: ProviderQuota,
1071
+ aggregation?: CodexCapacityAggregation,
1072
+ ): ProviderQuotaReport | null {
127
1073
  if (!hasQuotaRows(quota)) return null;
128
1074
  return {
129
1075
  provider,
@@ -131,27 +1077,92 @@ function report(provider: string, source: string, quota: ProviderQuota): Provide
131
1077
  source,
132
1078
  quota,
133
1079
  updatedAt: quota.updatedAt,
1080
+ ...(aggregation ? { aggregation } : {}),
134
1081
  };
135
1082
  }
136
1083
 
1084
+ function tagNativeMainReport(
1085
+ value: ProviderQuotaReport | null,
1086
+ generation: number,
1087
+ ): ProviderQuotaReport | null {
1088
+ if (value) nativeMainReportGenerations.set(value, generation);
1089
+ return value;
1090
+ }
1091
+
1092
+ function isProviderQuotaReportCurrent(value: ProviderQuotaReport): boolean {
1093
+ const generation = nativeMainReportGenerations.get(value);
1094
+ return generation === undefined || isMainAccountIdentityGenerationLive(generation);
1095
+ }
1096
+
137
1097
  async function fetchChatGptForwardQuota(
138
1098
  config: OcxConfig,
139
1099
  provider: string,
140
1100
  providerConfig: OcxProviderConfig,
141
1101
  forceRefresh: boolean,
1102
+ prefetchedSnapshot?: CodexAuthAccountsSnapshotPromise,
142
1103
  ): Promise<ProviderQuotaReport | null> {
143
1104
  if (providerCodexAccountMode(provider, providerConfig) === "direct") {
144
- const main = await fetchMainAccountInfo(forceRefresh);
145
- const quota = main.quota ? { ...main.quota, updatedAt: Date.now() } as ProviderQuota : null;
146
- return quota ? report(provider, "chatgpt:wham", quota) : null;
1105
+ const snapshot = await fetchMainAccountInfoSnapshot(forceRefresh);
1106
+ const quota = snapshot.info.quota
1107
+ ? { ...snapshot.info.quota, updatedAt: Date.now() } as ProviderQuota
1108
+ : null;
1109
+ return quota
1110
+ ? tagNativeMainReport(report(provider, "chatgpt:wham", quota), snapshot.mainIdentityGeneration)
1111
+ : null;
147
1112
  }
148
- const accounts = await listCodexAuthAccounts(config, forceRefresh);
149
- const activeId = config.activeCodexAccountId || MAIN_CODEX_ACCOUNT_ID;
150
- const active = accounts.find(account => account.id === activeId)
1113
+ const snapshot = await (prefetchedSnapshot ?? listCodexAuthAccountsSnapshot(config, forceRefresh));
1114
+ const accounts = snapshot.accounts;
1115
+ const activeId = effectiveCodexAuthAccountId(config);
1116
+ const capacityAccounts = accounts.map(account => ({ ...account, active: account.id === activeId }));
1117
+ const active = capacityAccounts.find(account => account.active)
151
1118
  ?? accounts.find(account => account.id === MAIN_CODEX_ACCOUNT_ID)
152
1119
  ?? accounts[0];
153
- const quota = active?.quota ? { ...active.quota, updatedAt: active.quota.updatedAt ?? Date.now() } as ProviderQuota : null;
154
- return quota ? report(provider, "chatgpt:wham", quota) : null;
1120
+ const now = Date.now();
1121
+ const capacity = aggregateCodexPoolCapacity(capacityAccounts, now);
1122
+ if (capacity.aggregation && capacity.quota) {
1123
+ return tagNativeMainReport(
1124
+ report(
1125
+ provider,
1126
+ "chatgpt:wham",
1127
+ capacity.quota as ProviderQuota,
1128
+ publicCapacityAggregation(capacity.aggregation, "aggregate"),
1129
+ ),
1130
+ snapshot.mainIdentityGeneration,
1131
+ );
1132
+ }
1133
+ const activeUsable = !!active && !active.paused && active.needsReauth !== true;
1134
+ const quota = activeUsable && active?.quota
1135
+ ? { ...active.quota, updatedAt: active.quota.updatedAt ?? Date.now() } as CodexCapacityQuota
1136
+ : null;
1137
+ const quotaFresh = !!quota
1138
+ && Number.isFinite(quota.updatedAt)
1139
+ && now - quota.updatedAt < CODEX_CAPACITY_MAX_QUOTA_AGE_MS;
1140
+ if (quota && quotaFresh) {
1141
+ const fallback = report(
1142
+ provider,
1143
+ "chatgpt:wham",
1144
+ quota as ProviderQuota,
1145
+ capacity.aggregation
1146
+ ? publicCapacityAggregation(capacity.aggregation, "effective-account-fallback")
1147
+ : undefined,
1148
+ );
1149
+ return tagNativeMainReport(fallback, snapshot.mainIdentityGeneration);
1150
+ }
1151
+ if (capacity.aggregation) {
1152
+ const updatedAt = Date.now();
1153
+ return tagNativeMainReport(
1154
+ {
1155
+ provider,
1156
+ label: providerLabel(provider),
1157
+ source: "chatgpt:wham",
1158
+ quota: { updatedAt },
1159
+ updatedAt,
1160
+ aggregation: publicCapacityAggregation(capacity.aggregation, "coverage-only"),
1161
+ },
1162
+ snapshot.mainIdentityGeneration,
1163
+ );
1164
+ }
1165
+ return null;
155
1166
  }
156
1167
 
157
1168
  function centsValue(value: unknown): number | undefined {
@@ -475,7 +1486,7 @@ async function fetchAnthropicUsageQuota(accessToken: string): Promise<ProviderQu
475
1486
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
476
1487
  });
477
1488
  if (!response.ok) return null;
478
- const body = asRecord(await response.json().catch(() => null));
1489
+ const body = asRecord(await readQuotaJson(response));
479
1490
  if (!body) return null;
480
1491
  const fiveHour = parseClaudeBucket(body.five_hour);
481
1492
  const sevenDay = parseClaudeBucket(body.seven_day);
@@ -640,6 +1651,13 @@ export function reconcileProviderAccountQuotaRows(context: GenerationContext): n
640
1651
  return removed;
641
1652
  }
642
1653
 
1654
+ /** Test-only reset so a direct reconcile call in one file cannot leak across files. */
1655
+ export function resetProviderQuotaReconcileStateForTests(): void {
1656
+ lastReconciledGeneration = 0;
1657
+ liveAccountQuotaKeys = new Set();
1658
+ liveProviderQuotaKeys = new Set();
1659
+ }
1660
+
643
1661
  /** Drop cached per-account rows (all, or just one provider's). */
644
1662
  export function clearAccountQuotaCache(provider?: string): void {
645
1663
  if (!provider) {
@@ -781,7 +1799,7 @@ export async function fetchProviderAccountQuotas(
781
1799
  function normalizedBaseUrl(value: string): string | null {
782
1800
  try {
783
1801
  const url = new URL(value);
784
- if (url.search || url.hash) return null;
1802
+ if (url.username || url.password || url.search || url.hash) return null;
785
1803
  return `${url.origin.toLowerCase()}${url.pathname.replace(/\/+$/, "")}`;
786
1804
  } catch {
787
1805
  return null;
@@ -796,6 +1814,12 @@ function isCanonicalKimiCodeBaseUrl(baseUrl: string): boolean {
796
1814
  return normalizedBaseUrl(baseUrl) === KIMI_CODE_BASE_URL;
797
1815
  }
798
1816
 
1817
+ function isCanonicalCommandCodeBaseUrl(baseUrl: string): boolean {
1818
+ const normalized = normalizedBaseUrl(baseUrl);
1819
+ // OAuth preset points at the API root; the Provider-API preset at /provider/v1.
1820
+ return normalized === COMMAND_CODE_BASE_URL || normalized === `${COMMAND_CODE_BASE_URL}/provider/v1`;
1821
+ }
1822
+
799
1823
  /** Prefer the nested `data` shell when the outer object is only an envelope. */
800
1824
  function unwrapKimiQuotaPayload(value: unknown): Record<string, unknown> | null {
801
1825
  const body = asRecord(value);
@@ -913,10 +1937,149 @@ async function fetchKimiQuota(provider: string, config: OcxProviderConfig): Prom
913
1937
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
914
1938
  });
915
1939
  if (!response.ok) return null;
916
- const quota = parseKimiQuotaPayload(await response.json().catch(() => null));
1940
+ const quota = parseKimiQuotaPayload(await readQuotaJson(response));
917
1941
  return quota ? report(provider, "kimi:usages", quota) : null;
918
1942
  }
919
1943
 
1944
+ /**
1945
+ * Command Code rolling window: `{ cap, used, resetAt }` off /alpha/billing/credits,
1946
+ * normalized to a percent with an optional reset timestamp.
1947
+ */
1948
+ function parseCommandCodeWindow(value: unknown): { percent: number; resetAt?: number } | null {
1949
+ const row = asRecord(value);
1950
+ if (!row) return null;
1951
+ const cap = toFiniteNumber(row.cap);
1952
+ const used = toFiniteNumber(row.used);
1953
+ if (cap === undefined || used === undefined || cap <= 0 || used < 0) return null;
1954
+ const percent = normalizePercent((used / cap) * 100);
1955
+ if (percent === undefined) return null;
1956
+ const resetAt = quotaResetAt(row);
1957
+ return { percent, ...(resetAt !== undefined ? { resetAt } : {}) };
1958
+ }
1959
+
1960
+ /** Soft-fail GET returning a parsed record, or null when unavailable. */
1961
+ async function fetchCommandCodeJson(url: string, bearer: string): Promise<Record<string, unknown> | null> {
1962
+ try {
1963
+ const response = await fetch(url, {
1964
+ headers: { Accept: "application/json", Authorization: `Bearer ${bearer}` },
1965
+ redirect: "error",
1966
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
1967
+ });
1968
+ if (!response.ok) return null;
1969
+ return asRecord(await readQuotaJson(response));
1970
+ } catch {
1971
+ return null;
1972
+ }
1973
+ }
1974
+
1975
+ /**
1976
+ * Soft-fail period spend (used) against the remaining credit pools → creditsUsd.
1977
+ * Period scoping: `since=<currentPeriodStart>` keeps spend aligned with the
1978
+ * pools' billing cycle, and `currentPeriodEnd` becomes expiresAt.
1979
+ */
1980
+ async function fetchCommandCodeSpend(
1981
+ bearer: string,
1982
+ credits: Record<string, unknown> | null,
1983
+ orgQuery: string,
1984
+ ): Promise<ProviderQuotaCreditsUsd | undefined> {
1985
+ if (!credits) return undefined;
1986
+ const subscriptionBody = await fetchCommandCodeJson(`${COMMAND_CODE_SUBSCRIPTIONS_URL}${orgQuery}`, bearer);
1987
+ const subscription = asRecord(subscriptionBody?.data) ?? subscriptionBody;
1988
+ const periodStart = typeof subscription?.currentPeriodStart === "string" ? subscription.currentPeriodStart.trim() : "";
1989
+ // Unscoped /usage/summary is lifetime spend; mixing it with current-cycle
1990
+ // remaining pools produces a wrong percent. Omit creditsUsd until a period exists.
1991
+ if (!periodStart) return undefined;
1992
+ const sinceQuery = `${orgQuery ? "&" : "?"}since=${encodeURIComponent(periodStart)}`;
1993
+ const expiresAt = normalizeResetAt(subscription?.currentPeriodEnd);
1994
+ const summaryBody = await fetchCommandCodeJson(`${COMMAND_CODE_USAGE_URL}${orgQuery}${sinceQuery}`, bearer);
1995
+ const summary = asRecord(summaryBody?.data) ?? summaryBody;
1996
+ const used = toFiniteNumber(summary?.totalCost) ?? toFiniteNumber(summary?.totalMonthlyCredits);
1997
+ if (used === undefined || used < 0) return undefined;
1998
+ const pools = [credits.monthlyCredits, credits.purchasedCredits, credits.freeCredits]
1999
+ .map(value => toFiniteNumber(value))
2000
+ .filter((value): value is number => value !== undefined);
2001
+ // Field presence is what separates a real balance from absent data: an exhausted
2002
+ // all-zero account still reports remaining=0, while no remaining-credit field at
2003
+ // all means there is nothing to meter.
2004
+ if (pools.length === 0) return undefined;
2005
+ const remaining = pools.reduce((sum, value) => sum + Math.max(0, value ?? 0), 0);
2006
+ const limit = used + remaining;
2007
+ const percent = normalizePercent(limit > 0 ? (used / limit) * 100 : 0);
2008
+ // Purchased credits roll over past the subscription period end, so an expiry is
2009
+ // only truthful when the aggregate contains no non-expiring purchased pool.
2010
+ const purchased = toFiniteNumber(credits.purchasedCredits) ?? 0;
2011
+ return percent === undefined
2012
+ ? undefined
2013
+ : {
2014
+ used,
2015
+ limit,
2016
+ remaining,
2017
+ percent,
2018
+ ...(expiresAt !== undefined && purchased <= 0 ? { expiresAt } : {}),
2019
+ };
2020
+ }
2021
+
2022
+ /** OAuth access token or ACTIVE Provider-API key for the Command Code quota probe. */
2023
+ async function resolveCommandCodeQuotaBearer(config: OcxProviderConfig): Promise<string | null> {
2024
+ if (config.authMode === "oauth") {
2025
+ try {
2026
+ return await getValidAccessToken("command-code");
2027
+ } catch {
2028
+ return null;
2029
+ }
2030
+ }
2031
+ // ACTIVE key only: a quota bar for a different account than the one routing
2032
+ // requests is a wrong meter, not a helpful one.
2033
+ return resolveEnvValue(config.apiKey)?.trim() || null;
2034
+ }
2035
+
2036
+ /**
2037
+ * Command Code `GET /alpha/billing/credits` — the same Bearer surface the CLI's
2038
+ * usage view uses (windowLimits.fiveHour / windowLimits.weekly), plus soft
2039
+ * whoami (team orgId scoping) and subscription-scoped spend for creditsUsd.
2040
+ */
2041
+ async function fetchCommandCodeQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
2042
+ // Never release credentials to a user-edited or lookalike provider host.
2043
+ if (!isCanonicalCommandCodeBaseUrl(config.baseUrl)) return null;
2044
+ const bearer = await resolveCommandCodeQuotaBearer(config);
2045
+ if (!bearer) return null;
2046
+ const whoamiBody = await fetchCommandCodeJson(COMMAND_CODE_WHOAMI_URL, bearer);
2047
+ const whoami = asRecord(whoamiBody?.data) ?? whoamiBody;
2048
+ const org = asRecord(whoami?.org);
2049
+ const orgId = typeof org?.id === "string" && org.id.trim() ? org.id.trim() : null;
2050
+ const orgQuery = orgId ? `?orgId=${encodeURIComponent(orgId)}` : "";
2051
+ const response = await fetch(`${COMMAND_CODE_CREDITS_URL}${orgQuery}`, {
2052
+ headers: { Accept: "application/json", Authorization: `Bearer ${bearer}` },
2053
+ redirect: "error",
2054
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
2055
+ });
2056
+ if (!response.ok) {
2057
+ return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
2058
+ ? TERMINAL_QUOTA_FAILURE
2059
+ : null;
2060
+ }
2061
+ const raw = asRecord(await readQuotaJson(response));
2062
+ const body = asRecord(raw?.data) ?? raw;
2063
+ const credits = asRecord(body?.credits);
2064
+ const limits = asRecord(body?.windowLimits);
2065
+ if (!credits && !limits) return null;
2066
+ const fiveHour = parseCommandCodeWindow(limits?.fiveHour);
2067
+ const weekly = parseCommandCodeWindow(limits?.weekly);
2068
+ const creditsUsd = await fetchCommandCodeSpend(bearer, credits, orgQuery);
2069
+ return report(provider, "command-code:credits", {
2070
+ ...(fiveHour ? {
2071
+ fiveHourPercent: fiveHour.percent,
2072
+ ...(fiveHour.resetAt !== undefined ? { fiveHourResetAt: fiveHour.resetAt } : {}),
2073
+ } : {}),
2074
+ ...(weekly ? {
2075
+ weeklyPercent: weekly.percent,
2076
+ ...(weekly.resetAt !== undefined ? { weeklyResetAt: weekly.resetAt } : {}),
2077
+ } : {}),
2078
+ ...(creditsUsd ? { creditsUsd } : {}),
2079
+ updatedAt: Date.now(),
2080
+ });
2081
+ }
2082
+
920
2083
  /** Cursor included usage via api2.cursor.sh (Bearer from OAuth) — unofficial, may change. */
921
2084
  async function fetchCursorQuota(provider: string): Promise<ProviderQuotaReport | null> {
922
2085
  let accessToken: string;
@@ -946,7 +2109,7 @@ async function fetchCursorQuota(provider: string): Promise<ProviderQuotaReport |
946
2109
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
947
2110
  });
948
2111
  if (periodRes.ok) {
949
- const body = asRecord(await periodRes.json().catch(() => null));
2112
+ const body = asRecord(await readQuotaJson(periodRes));
950
2113
  const planUsage = asRecord(body?.planUsage);
951
2114
  if (planUsage) {
952
2115
  const resetAt = normalizeResetAt(body?.billingCycleEnd ?? planUsage.billingCycleEnd ?? body?.periodEnd);
@@ -1008,7 +2171,7 @@ async function fetchCursorQuota(provider: string): Promise<ProviderQuotaReport |
1008
2171
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
1009
2172
  });
1010
2173
  if (summaryRes.ok) {
1011
- const body = asRecord(await summaryRes.json().catch(() => null));
2174
+ const body = asRecord(await readQuotaJson(summaryRes));
1012
2175
  const individual = asRecord(body?.individualUsage);
1013
2176
  const plan = asRecord(individual?.plan);
1014
2177
  if (plan) {
@@ -1037,7 +2200,7 @@ async function fetchCursorQuota(provider: string): Promise<ProviderQuotaReport |
1037
2200
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
1038
2201
  });
1039
2202
  if (!response.ok) return null;
1040
- const body = asRecord(await response.json().catch(() => null));
2203
+ const body = asRecord(await readQuotaJson(response));
1041
2204
  if (!body) return null;
1042
2205
 
1043
2206
  // Prefer the gpt-4 bucket (historical "fast requests"); else first model with used+limit.
@@ -1151,7 +2314,7 @@ async function fetchAntigravityQuota(provider: string, config: OcxProviderConfig
1151
2314
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
1152
2315
  });
1153
2316
  if (!response.ok) return null;
1154
- const body = asRecord(await response.json().catch(() => null));
2317
+ const body = asRecord(await readQuotaJson(response));
1155
2318
  const models = asRecord(body?.models);
1156
2319
  if (!models) return null;
1157
2320
 
@@ -1188,10 +2351,13 @@ async function maybeFetchProviderQuota(
1188
2351
  provider: OcxProviderConfig,
1189
2352
  config: OcxConfig,
1190
2353
  forceRefresh: boolean,
1191
- ): Promise<ProviderQuotaReport | null> {
2354
+ prefetchedCodexSnapshot?: CodexAuthAccountsSnapshotPromise,
2355
+ ): Promise<ProviderQuotaProbeResult> {
1192
2356
  if (provider.disabled === true) return null;
1193
2357
  try {
1194
- if (isBuiltInChatGptForwardProvider(name, provider)) return fetchChatGptForwardQuota(config, name, provider, forceRefresh);
2358
+ if (isBuiltInChatGptForwardProvider(name, provider)) {
2359
+ return fetchChatGptForwardQuota(config, name, provider, forceRefresh, prefetchedCodexSnapshot);
2360
+ }
1195
2361
  if (provider.authMode === "oauth" && name === "xai") return fetchXaiQuota(name);
1196
2362
  if (provider.authMode === "oauth" && name === "anthropic") return fetchAnthropicQuota(name);
1197
2363
  if (provider.authMode === "oauth" && name === "cursor") return fetchCursorQuota(name);
@@ -1202,6 +2368,59 @@ async function maybeFetchProviderQuota(
1202
2368
  if (provider.authMode === "key" && isCanonicalKimiCodeBaseUrl(provider.baseUrl)) {
1203
2369
  return fetchKimiQuota(name, provider);
1204
2370
  }
2371
+ // OAuth account login or Provider-API key only; forward/local modes carry no
2372
+ // credential of ours on the canonical host.
2373
+ if (provider.authMode === "oauth" && name === "command-code") {
2374
+ return fetchCommandCodeQuota(name, provider);
2375
+ }
2376
+ if ((provider.authMode ?? "key") === "key" && name === "commandcode"
2377
+ && isCanonicalCommandCodeBaseUrl(provider.baseUrl)) {
2378
+ return fetchCommandCodeQuota(name, provider);
2379
+ }
2380
+ // Identify OpenCode Go by where it routes, not by what the row is called. Multi-account
2381
+ // setups keep the same destination under names like `opencode-go-2` (#1924), and those rows
2382
+ // silently had no quota panel and no `ocx provider quota --json` report while the literal
2383
+ // name was the gate. `registryEntryForProviderDestination` is the existing predicate for
2384
+ // exactly this question: normalized endpoint + adapter + key auth, so a canonical URL behind
2385
+ // a different adapter is still not OpenCode Go. The defensive URL check inside
2386
+ // `fetchOpenCodeGoQuota` stays — sending a key anywhere must not depend on this gate.
2387
+ if ((provider.authMode ?? "key") === "key" && registryEntryForProviderDestination(provider)?.id === "opencode-go") {
2388
+ return fetchOpenCodeGoQuota(name, provider);
2389
+ }
2390
+ if ((provider.authMode ?? "key") === "key" && isCanonicalA6apiBaseUrl(provider.baseUrl)) {
2391
+ return fetchA6apiQuota(name, provider);
2392
+ }
2393
+ if ((provider.authMode ?? "key") === "key" && name === "openrouter") {
2394
+ return fetchOpenRouterQuota(name, provider);
2395
+ }
2396
+ if ((provider.authMode ?? "key") === "key" && name === "deepseek") {
2397
+ return fetchDeepSeekQuota(name, provider);
2398
+ }
2399
+ if ((provider.authMode ?? "key") === "key" && name === "cline-pass") {
2400
+ return fetchClineQuota(name, provider);
2401
+ }
2402
+ if ((provider.authMode ?? "key") === "key"
2403
+ && (name === "zai" || name === "glm" || name === "glm-cn" || name === "zhipu-bigmodel-coding")) {
2404
+ return fetchZaiQuota(name, provider);
2405
+ }
2406
+ if ((provider.authMode ?? "key") === "key" && (name === "minimax" || name === "minimax-cn")) {
2407
+ return fetchMinimaxQuota(name, provider);
2408
+ }
2409
+ if ((provider.authMode ?? "key") === "key" && name === "moonshot") {
2410
+ return fetchMoonshotQuota(name, provider);
2411
+ }
2412
+ if ((provider.authMode ?? "key") === "key" && name === "venice") {
2413
+ return fetchVeniceQuota(name, provider);
2414
+ }
2415
+ if ((provider.authMode ?? "key") === "key" && name === "synthetic") {
2416
+ return fetchSyntheticQuota(name, provider);
2417
+ }
2418
+ if ((provider.authMode ?? "key") === "key" && name === "deepinfra") {
2419
+ return fetchDeepInfraQuota(name, provider);
2420
+ }
2421
+ if ((provider.authMode ?? "key") === "key" && name === "neuralwatt") {
2422
+ return fetchNeuralwattQuota(name, provider);
2423
+ }
1205
2424
  return null;
1206
2425
  } catch {
1207
2426
  return null;
@@ -1209,41 +2428,96 @@ async function maybeFetchProviderQuota(
1209
2428
  }
1210
2429
 
1211
2430
  export async function fetchProviderQuotaReports(config: OcxConfig, forceRefresh = false): Promise<ProviderQuotaResponse> {
1212
- const key = cacheKey(config);
2431
+ // A Pool report's cache signature and provider fetch must share one account snapshot.
2432
+ // Preserve force semantics when deciding whether that snapshot refreshes upstream data.
2433
+ const prefetchedCodexSnapshot = hasCodexPoolProvider(config)
2434
+ ? listCodexAuthAccountsSnapshot(config, forceRefresh)
2435
+ : undefined;
2436
+ const keyCandidate = cacheKeyWithAggregationState(config, prefetchedCodexSnapshot);
2437
+ const key = typeof keyCandidate === "string" ? keyCandidate : await keyCandidate;
1213
2438
  const writerGeneration = captureConfigGeneration();
1214
2439
  const now = Date.now();
1215
2440
  // The cache fast path must not extend a preserved last-good row past its 30-minute bound:
1216
2441
  // a row preserved at age 29:59 plus a full 5-minute TTL would otherwise serve until ~35min.
1217
2442
  const cacheFresh = cache && cache.key === key && now - cache.ts < CACHE_TTL_MS
1218
- && cache.response.reports.every(item => now - item.updatedAt < LAST_GOOD_MAX_AGE_MS);
2443
+ && cache.response.reports.every(item =>
2444
+ now - item.updatedAt < LAST_GOOD_MAX_AGE_MS && isProviderQuotaReportCurrent(item));
1219
2445
  if (!forceRefresh && cacheFresh) return cache!.response;
2446
+
2447
+ // Stale-while-revalidate for the Providers overview: when the TTL has expired but last-good
2448
+ // rows are still within LAST_GOOD_MAX_AGE_MS, paint them immediately and refresh in the
2449
+ // background. A cold load with no cache still waits on the probe (nothing to show yet).
1220
2450
  const joinable = inflight.get(key);
1221
- if (!forceRefresh && joinable && joinable.epoch === invalidationEpoch) return joinable.promise;
2451
+ if (!forceRefresh && joinable && joinable.epoch === invalidationEpoch) {
2452
+ if (cache && cache.key === key
2453
+ && cache.response.reports.every(item => now - item.updatedAt < LAST_GOOD_MAX_AGE_MS)) {
2454
+ return cache.response;
2455
+ }
2456
+ return joinable.promise;
2457
+ }
2458
+
2459
+ const staleUsable = !forceRefresh
2460
+ && cache
2461
+ && cache.key === key
2462
+ && cache.response.reports.every(item => now - item.updatedAt < LAST_GOOD_MAX_AGE_MS);
2463
+
1222
2464
  // A forced probe takes commit authority: older in-flight probes must not overwrite its result.
1223
2465
  if (forceRefresh) invalidationEpoch += 1;
1224
2466
  const epoch = invalidationEpoch;
1225
2467
 
1226
2468
  const promise = (async (): Promise<ProviderQuotaResponse> => {
1227
2469
  const previous = cache && cache.key === key ? cache.response.reports : [];
1228
- const fresh = (await Promise.all(
1229
- Object.entries(config.providers).map(([name, provider]) => maybeFetchProviderQuota(name, provider, config, forceRefresh)),
1230
- )).filter((item): item is ProviderQuotaReport => item !== null);
2470
+ const probeResults = await Promise.all(
2471
+ Object.entries(config.providers).map(([name, provider]) => (
2472
+ maybeFetchProviderQuota(name, provider, config, forceRefresh, prefetchedCodexSnapshot)
2473
+ )),
2474
+ );
2475
+ const fresh = probeResults.filter((item): item is ProviderQuotaReport => item !== null && item !== TERMINAL_QUOTA_FAILURE);
2476
+ const terminalFailures = new Set(
2477
+ Object.keys(config.providers).filter((_, index) => probeResults[index] === TERMINAL_QUOTA_FAILURE),
2478
+ );
2479
+ await providerQuotaBeforePublishForTests?.();
2480
+ let commitKey: string | null = null;
2481
+ if (epoch === invalidationEpoch) {
2482
+ const commitKeyCandidate = cacheKeyWithAggregationState(config);
2483
+ commitKey = typeof commitKeyCandidate === "string" ? commitKeyCandidate : await commitKeyCandidate;
2484
+ }
1231
2485
 
1232
- // Keep bounded last-good rows when a probe fails (e.g. transient upstream flake); never
1233
- // re-stamp their timestamps, and drop rows older than LAST_GOOD_MAX_AGE_MS.
2486
+ // Keep bounded last-good rows when a probe fails transiently; terminal-invalid provider
2487
+ // responses explicitly suppress their old row. Never re-stamp preserved timestamps.
1234
2488
  // Note: the cache key encodes the provider set (name/adapter/authMode/disabled/baseUrl),
1235
2489
  // so previous rows always correspond to currently configured, enabled providers — a
1236
2490
  // disabled or removed provider changes the key and starts from an empty previous set.
1237
2491
  const cutoff = Date.now() - LAST_GOOD_MAX_AGE_MS;
1238
2492
  const byProvider = new Map<string, ProviderQuotaReport>();
2493
+ const generationMismatchedProviders = new Set<string>();
1239
2494
  for (const item of previous) {
1240
- if (item.updatedAt >= cutoff) byProvider.set(item.provider, item);
2495
+ if (item.updatedAt < cutoff) continue;
2496
+ if (isProviderQuotaReportCurrent(item)) byProvider.set(item.provider, item);
2497
+ else generationMismatchedProviders.add(item.provider);
2498
+ }
2499
+ for (const item of fresh) {
2500
+ if (isProviderQuotaReportCurrent(item)) {
2501
+ byProvider.set(item.provider, item);
2502
+ generationMismatchedProviders.delete(item.provider);
2503
+ } else {
2504
+ byProvider.delete(item.provider);
2505
+ generationMismatchedProviders.add(item.provider);
2506
+ }
2507
+ }
2508
+ // Terminal-invalid probes suppress their previous row (transient failures keep it).
2509
+ for (const provider of terminalFailures) {
2510
+ byProvider.delete(provider);
2511
+ generationMismatchedProviders.delete(provider);
1241
2512
  }
1242
- for (const item of fresh) byProvider.set(item.provider, item);
1243
2513
 
1244
2514
  const response = { generatedAt: Date.now(), reports: [...byProvider.values()] };
1245
2515
  // Commit only when this probe still holds authority (no clear/force superseded it).
1246
- if (epoch === invalidationEpoch) {
2516
+ if (
2517
+ epoch === invalidationEpoch
2518
+ && commitKey === key
2519
+ && generationMismatchedProviders.size === 0
2520
+ ) {
1247
2521
  const reports = response.reports.filter(item => mayCommitProviderQuotaKey(item.provider, writerGeneration));
1248
2522
  cache = { key, ts: Date.now(), response: { ...response, reports } };
1249
2523
  }
@@ -1252,6 +2526,13 @@ export async function fetchProviderQuotaReports(config: OcxConfig, forceRefresh
1252
2526
 
1253
2527
  const entry = { epoch, promise };
1254
2528
  inflight.set(key, entry);
2529
+ if (staleUsable) {
2530
+ // Detach: do not await. Callers get last-good now; the flight still fills the cache.
2531
+ void promise.finally(() => {
2532
+ if (inflight.get(key) === entry) inflight.delete(key);
2533
+ });
2534
+ return cache!.response;
2535
+ }
1255
2536
  try {
1256
2537
  return await promise;
1257
2538
  } finally {