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