@yansigit/opencodex 2.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (826) hide show
  1. package/AGENTS_INSTALL.md +109 -0
  2. package/LICENSE +21 -0
  3. package/README.md +303 -0
  4. package/assets/architecture.png +0 -0
  5. package/assets/banner.png +0 -0
  6. package/assets/claude-code-models.gif +0 -0
  7. package/assets/codex-app-picker.png +0 -0
  8. package/bin/ocx.mjs +587 -0
  9. package/bin/package-main.mjs +9 -0
  10. package/gui/dist/assets/index-BNJ7r4Gd.js +102 -0
  11. package/gui/dist/assets/index-CGoDO3uO.css +1 -0
  12. package/gui/dist/favicon.png +0 -0
  13. package/gui/dist/icons.svg +24 -0
  14. package/gui/dist/index.html +25 -0
  15. package/gui/dist/logo.png +0 -0
  16. package/gui/dist/provider-icons/alibaba-color.svg +1 -0
  17. package/gui/dist/provider-icons/antigravity-color.svg +1 -0
  18. package/gui/dist/provider-icons/claude-color.svg +1 -0
  19. package/gui/dist/provider-icons/cline-color.svg +16 -0
  20. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
  21. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  22. package/gui/dist/provider-icons/copilot-color.svg +1 -0
  23. package/gui/dist/provider-icons/cursor-color.svg +2 -0
  24. package/gui/dist/provider-icons/deepseek-color.svg +1 -0
  25. package/gui/dist/provider-icons/discord.svg +1 -0
  26. package/gui/dist/provider-icons/firepass-color.svg +1 -0
  27. package/gui/dist/provider-icons/fireworks-color.svg +1 -0
  28. package/gui/dist/provider-icons/gemini-color.svg +1 -0
  29. package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
  30. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
  31. package/gui/dist/provider-icons/grok.svg +1 -0
  32. package/gui/dist/provider-icons/groq-color.svg +1 -0
  33. package/gui/dist/provider-icons/huggingface-color.svg +1 -0
  34. package/gui/dist/provider-icons/kimi-color.svg +1 -0
  35. package/gui/dist/provider-icons/kiro-color.svg +15 -0
  36. package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
  37. package/gui/dist/provider-icons/mistral-color.svg +1 -0
  38. package/gui/dist/provider-icons/moonshot-color.svg +1 -0
  39. package/gui/dist/provider-icons/nvidia-color.svg +1 -0
  40. package/gui/dist/provider-icons/ollama-color.svg +1 -0
  41. package/gui/dist/provider-icons/openai.svg +1 -0
  42. package/gui/dist/provider-icons/opencode.svg +2 -0
  43. package/gui/dist/provider-icons/openrouter-color.svg +1 -0
  44. package/gui/dist/provider-icons/pi.svg +21 -0
  45. package/gui/dist/provider-icons/qianfan-color.svg +1 -0
  46. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
  47. package/gui/dist/provider-icons/telegram.svg +1 -0
  48. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
  49. package/gui/dist/provider-icons/vllm-color.svg +1 -0
  50. package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
  51. package/package.json +108 -0
  52. package/src/AGENTS.md +28 -0
  53. package/src/adapters/anthropic-image-guard.ts +251 -0
  54. package/src/adapters/anthropic-image-normalize.ts +518 -0
  55. package/src/adapters/anthropic-output-schema.ts +137 -0
  56. package/src/adapters/anthropic.ts +1327 -0
  57. package/src/adapters/azure.ts +36 -0
  58. package/src/adapters/base.ts +121 -0
  59. package/src/adapters/client-fingerprint.ts +65 -0
  60. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
  61. package/src/adapters/command-code.ts +601 -0
  62. package/src/adapters/cursor/arg-codec.ts +38 -0
  63. package/src/adapters/cursor/arg-normalize.ts +104 -0
  64. package/src/adapters/cursor/checkpoint-store.ts +303 -0
  65. package/src/adapters/cursor/cursor-errors.ts +288 -0
  66. package/src/adapters/cursor/discovery.ts +333 -0
  67. package/src/adapters/cursor/effort-map.ts +151 -0
  68. package/src/adapters/cursor/exec-policy.ts +88 -0
  69. package/src/adapters/cursor/framing.ts +250 -0
  70. package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
  71. package/src/adapters/cursor/h2-pool.ts +123 -0
  72. package/src/adapters/cursor/http1-bidi.ts +361 -0
  73. package/src/adapters/cursor/images.ts +704 -0
  74. package/src/adapters/cursor/kv-store.ts +52 -0
  75. package/src/adapters/cursor/live-models.ts +269 -0
  76. package/src/adapters/cursor/live-smoke-gate.ts +41 -0
  77. package/src/adapters/cursor/live-transport.ts +1653 -0
  78. package/src/adapters/cursor/mcp-config.ts +42 -0
  79. package/src/adapters/cursor/mcp-manager.ts +333 -0
  80. package/src/adapters/cursor/message-mapper.ts +49 -0
  81. package/src/adapters/cursor/native-exec-common.ts +76 -0
  82. package/src/adapters/cursor/native-exec-desktop.ts +184 -0
  83. package/src/adapters/cursor/native-exec-fs.ts +332 -0
  84. package/src/adapters/cursor/native-exec-mcp.ts +153 -0
  85. package/src/adapters/cursor/native-exec-network.ts +43 -0
  86. package/src/adapters/cursor/native-exec-shell.ts +547 -0
  87. package/src/adapters/cursor/native-exec-tools.ts +118 -0
  88. package/src/adapters/cursor/native-exec.ts +663 -0
  89. package/src/adapters/cursor/protobuf-events.ts +1381 -0
  90. package/src/adapters/cursor/protobuf-request.ts +1032 -0
  91. package/src/adapters/cursor/request-builder.ts +461 -0
  92. package/src/adapters/cursor/thread-continuity.ts +67 -0
  93. package/src/adapters/cursor/tool-definitions.ts +735 -0
  94. package/src/adapters/cursor/tool-result-normalize.ts +92 -0
  95. package/src/adapters/cursor/transport-retry.ts +132 -0
  96. package/src/adapters/cursor/transport.ts +79 -0
  97. package/src/adapters/cursor/types.ts +79 -0
  98. package/src/adapters/cursor.ts +322 -0
  99. package/src/adapters/google-antigravity-hosts.ts +48 -0
  100. package/src/adapters/google-antigravity-replay.ts +827 -0
  101. package/src/adapters/google-antigravity-tools.ts +105 -0
  102. package/src/adapters/google-antigravity-wire.ts +141 -0
  103. package/src/adapters/google-errors.ts +92 -0
  104. package/src/adapters/google-http.ts +460 -0
  105. package/src/adapters/google-tool-schema.ts +238 -0
  106. package/src/adapters/google-truncation.ts +24 -0
  107. package/src/adapters/google-wire-compiler.ts +232 -0
  108. package/src/adapters/google.ts +1377 -0
  109. package/src/adapters/identity.ts +77 -0
  110. package/src/adapters/image.ts +23 -0
  111. package/src/adapters/kiro-constants.ts +16 -0
  112. package/src/adapters/kiro-errors.ts +208 -0
  113. package/src/adapters/kiro-events.ts +197 -0
  114. package/src/adapters/kiro-images.ts +129 -0
  115. package/src/adapters/kiro-retry.ts +312 -0
  116. package/src/adapters/kiro-thinking.ts +112 -0
  117. package/src/adapters/kiro-tool-fallback.ts +36 -0
  118. package/src/adapters/kiro-tools.ts +224 -0
  119. package/src/adapters/kiro-truncation.ts +33 -0
  120. package/src/adapters/kiro-wire.ts +129 -0
  121. package/src/adapters/kiro.ts +1936 -0
  122. package/src/adapters/mimo-free.ts +280 -0
  123. package/src/adapters/openai-chat-url.ts +11 -0
  124. package/src/adapters/openai-chat.ts +1980 -0
  125. package/src/adapters/openai-responses-url.ts +16 -0
  126. package/src/adapters/openai-responses.ts +1911 -0
  127. package/src/adapters/registry.ts +175 -0
  128. package/src/adapters/responses-tool-schema.ts +67 -0
  129. package/src/adapters/run-turn-queue.ts +114 -0
  130. package/src/adapters/tool-call-id.ts +119 -0
  131. package/src/adapters/tool-catalog-nudge.ts +154 -0
  132. package/src/adapters/upstream-http-error.ts +48 -0
  133. package/src/adapters/xai-web-search.ts +185 -0
  134. package/src/bridge.ts +1986 -0
  135. package/src/chat/inbound.ts +319 -0
  136. package/src/chat/outbound.ts +821 -0
  137. package/src/claude/agents-inject.ts +266 -0
  138. package/src/claude/alias.ts +149 -0
  139. package/src/claude/auth-detect.ts +229 -0
  140. package/src/claude/auth-mode-migration.ts +32 -0
  141. package/src/claude/auth-mode.ts +62 -0
  142. package/src/claude/context-windows.ts +205 -0
  143. package/src/claude/desktop-3p-guard.ts +35 -0
  144. package/src/claude/desktop-3p-paths.ts +84 -0
  145. package/src/claude/desktop-3p.ts +615 -0
  146. package/src/claude/desktop-health.ts +26 -0
  147. package/src/claude/desktop-profile.ts +263 -0
  148. package/src/claude/gateway-cache.ts +107 -0
  149. package/src/claude/inbound-debug.ts +163 -0
  150. package/src/claude/inbound.ts +578 -0
  151. package/src/claude/model-info.ts +174 -0
  152. package/src/claude/outbound.ts +926 -0
  153. package/src/cli/access.ts +108 -0
  154. package/src/cli/account-api.ts +302 -0
  155. package/src/cli/account-auth.ts +250 -0
  156. package/src/cli/account-catalog-refresh.ts +14 -0
  157. package/src/cli/account-extended.ts +737 -0
  158. package/src/cli/account-main.ts +317 -0
  159. package/src/cli/account.ts +299 -0
  160. package/src/cli/agent-driven.ts +70 -0
  161. package/src/cli/agent.ts +290 -0
  162. package/src/cli/catalog-prewarm.ts +27 -0
  163. package/src/cli/claude-agent-startup-sync.ts +73 -0
  164. package/src/cli/claude-desktop.ts +213 -0
  165. package/src/cli/claude.ts +355 -0
  166. package/src/cli/codex-log-guard-doctor.ts +103 -0
  167. package/src/cli/codex-shim-autorestore.ts +47 -0
  168. package/src/cli/codex-shim-readiness.ts +76 -0
  169. package/src/cli/combo.ts +127 -0
  170. package/src/cli/config-command.ts +209 -0
  171. package/src/cli/debug.ts +228 -0
  172. package/src/cli/dispatch.ts +585 -0
  173. package/src/cli/doctor.ts +1202 -0
  174. package/src/cli/ensure-desired-integrations.ts +152 -0
  175. package/src/cli/export-command.ts +213 -0
  176. package/src/cli/help.ts +101 -0
  177. package/src/cli/index.ts +973 -0
  178. package/src/cli/init.ts +211 -0
  179. package/src/cli/integrations.ts +260 -0
  180. package/src/cli/interactive-confirm.ts +133 -0
  181. package/src/cli/lab.ts +607 -0
  182. package/src/cli/launcher-context.ts +77 -0
  183. package/src/cli/minimax.ts +497 -0
  184. package/src/cli/models-runtime.ts +245 -0
  185. package/src/cli/models.ts +422 -0
  186. package/src/cli/observe.ts +206 -0
  187. package/src/cli/opencode.ts +588 -0
  188. package/src/cli/provider-replit.ts +232 -0
  189. package/src/cli/provider-runtime.ts +179 -0
  190. package/src/cli/provider.ts +492 -0
  191. package/src/cli/ready.ts +301 -0
  192. package/src/cli/registry.ts +422 -0
  193. package/src/cli/replit-gateway-key-input.ts +138 -0
  194. package/src/cli/root.ts +86 -0
  195. package/src/cli/route-policy.ts +92 -0
  196. package/src/cli/runtime-api.ts +328 -0
  197. package/src/cli/star-prompt.ts +211 -0
  198. package/src/cli/status-oauth.ts +78 -0
  199. package/src/cli/status.ts +328 -0
  200. package/src/cli/system-command.ts +112 -0
  201. package/src/cli/system-restart-client.ts +146 -0
  202. package/src/cli/tray-proxy.ts +199 -0
  203. package/src/cli/v2.ts +268 -0
  204. package/src/cli.ts +10 -0
  205. package/src/clients/config-export.ts +1704 -0
  206. package/src/codex/account-id.ts +34 -0
  207. package/src/codex/account-label.ts +47 -0
  208. package/src/codex/account-lifecycle.ts +172 -0
  209. package/src/codex/account-namespace-match.ts +63 -0
  210. package/src/codex/account-namespaces.ts +195 -0
  211. package/src/codex/account-pause.ts +20 -0
  212. package/src/codex/account-priority.ts +83 -0
  213. package/src/codex/account-runtime-state.ts +31 -0
  214. package/src/codex/account-store.ts +544 -0
  215. package/src/codex/account-usability.ts +43 -0
  216. package/src/codex/admission.ts +256 -0
  217. package/src/codex/affinity-debug.ts +162 -0
  218. package/src/codex/agent-roles-sync.ts +225 -0
  219. package/src/codex/agent-roles.ts +238 -0
  220. package/src/codex/app-server-processes.ts +1143 -0
  221. package/src/codex/app-server-restart-service.ts +232 -0
  222. package/src/codex/auth-api.ts +2147 -0
  223. package/src/codex/auth-collision.ts +109 -0
  224. package/src/codex/auth-context.ts +665 -0
  225. package/src/codex/autostart-health.ts +156 -0
  226. package/src/codex/catalog/account-models.ts +67 -0
  227. package/src/codex/catalog/aggregation.ts +436 -0
  228. package/src/codex/catalog/bundled.ts +549 -0
  229. package/src/codex/catalog/effort.ts +446 -0
  230. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  231. package/src/codex/catalog/kinds.ts +2 -0
  232. package/src/codex/catalog/metadata.ts +664 -0
  233. package/src/codex/catalog/native-models.ts +72 -0
  234. package/src/codex/catalog/parsing.ts +650 -0
  235. package/src/codex/catalog/provider-fetch.ts +2064 -0
  236. package/src/codex/catalog/sync.ts +1883 -0
  237. package/src/codex/catalog-admission.ts +199 -0
  238. package/src/codex/catalog-refresh-status.ts +105 -0
  239. package/src/codex/catalog-write-serialization.ts +242 -0
  240. package/src/codex/catalog.ts +14 -0
  241. package/src/codex/codex-write-lock.ts +384 -0
  242. package/src/codex/convergence-types.ts +614 -0
  243. package/src/codex/convergence.ts +651 -0
  244. package/src/codex/coordinator-doctor.ts +332 -0
  245. package/src/codex/custom-model-catalog-migration.ts +176 -0
  246. package/src/codex/data/upstream-models.json +830 -0
  247. package/src/codex/desired-state.ts +230 -0
  248. package/src/codex/exec-invocation.ts +22 -0
  249. package/src/codex/features.ts +1566 -0
  250. package/src/codex/generation.ts +202 -0
  251. package/src/codex/history-job.ts +407 -0
  252. package/src/codex/history-lock.ts +242 -0
  253. package/src/codex/history-migration-guardian.ts +108 -0
  254. package/src/codex/history-provider.ts +979 -0
  255. package/src/codex/history-transition.ts +105 -0
  256. package/src/codex/history-worker.ts +220 -0
  257. package/src/codex/home.ts +206 -0
  258. package/src/codex/inject-coordination.ts +290 -0
  259. package/src/codex/inject.ts +1733 -0
  260. package/src/codex/injected-marker.ts +106 -0
  261. package/src/codex/integration-record.ts +266 -0
  262. package/src/codex/internal/catalog-writer.ts +203 -0
  263. package/src/codex/internal/history-writer.ts +80 -0
  264. package/src/codex/journal.ts +225 -0
  265. package/src/codex/log-guard/inspect.ts +506 -0
  266. package/src/codex/log-guard/lock.ts +150 -0
  267. package/src/codex/log-guard/maintenance.ts +403 -0
  268. package/src/codex/log-guard/path-safety.ts +88 -0
  269. package/src/codex/log-guard/policy.ts +44 -0
  270. package/src/codex/log-guard/processes.ts +205 -0
  271. package/src/codex/log-guard/protection.ts +489 -0
  272. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  273. package/src/codex/main-account-cache.ts +56 -0
  274. package/src/codex/main-account.ts +68 -0
  275. package/src/codex/management-convergence.ts +167 -0
  276. package/src/codex/model-cache.ts +273 -0
  277. package/src/codex/model-entitlements.ts +353 -0
  278. package/src/codex/native-main-admission.ts +47 -0
  279. package/src/codex/native-main-auth-temp.ts +187 -0
  280. package/src/codex/native-main-claim.ts +178 -0
  281. package/src/codex/native-main-lock-file.ts +162 -0
  282. package/src/codex/native-main-owner.ts +329 -0
  283. package/src/codex/native-profile-api.ts +247 -0
  284. package/src/codex/native-profile-manager.ts +1531 -0
  285. package/src/codex/native-profile-processes.ts +121 -0
  286. package/src/codex/native-profile-recovery.ts +99 -0
  287. package/src/codex/native-profile-stage-store.ts +387 -0
  288. package/src/codex/native-profile-startup.ts +492 -0
  289. package/src/codex/native-profile-store.ts +855 -0
  290. package/src/codex/native-profile-types.ts +120 -0
  291. package/src/codex/native-residue.ts +682 -0
  292. package/src/codex/paths.ts +144 -0
  293. package/src/codex/plan-from-token.ts +140 -0
  294. package/src/codex/plan.ts +40 -0
  295. package/src/codex/plugins-doctor.ts +242 -0
  296. package/src/codex/pool-rotation.ts +295 -0
  297. package/src/codex/project-config-warnings.ts +425 -0
  298. package/src/codex/prompt-journal.ts +352 -0
  299. package/src/codex/prompt-layers.ts +967 -0
  300. package/src/codex/prompt-lock.ts +143 -0
  301. package/src/codex/quota-rejection.ts +298 -0
  302. package/src/codex/quota.ts +573 -0
  303. package/src/codex/refresh.ts +62 -0
  304. package/src/codex/reset-credit-recovery.ts +1044 -0
  305. package/src/codex/routing.ts +1888 -0
  306. package/src/codex/runtime.ts +659 -0
  307. package/src/codex/shim.ts +2170 -0
  308. package/src/codex/subagent-defaults.ts +550 -0
  309. package/src/codex/subagent-model-fallback.ts +784 -0
  310. package/src/codex/sync.ts +319 -0
  311. package/src/codex/transition-state.ts +612 -0
  312. package/src/codex/upstream-host-health.ts +368 -0
  313. package/src/codex/user-identity.ts +557 -0
  314. package/src/codex/warmup.ts +298 -0
  315. package/src/codex/websocket-registry.ts +100 -0
  316. package/src/codex/write-coordination.ts +114 -0
  317. package/src/combos/failover.ts +160 -0
  318. package/src/combos/index.ts +45 -0
  319. package/src/combos/request.ts +94 -0
  320. package/src/combos/resolve.ts +232 -0
  321. package/src/combos/types.ts +398 -0
  322. package/src/config/provider-name.ts +24 -0
  323. package/src/config.ts +4041 -0
  324. package/src/fork/register.ts +3 -0
  325. package/src/generated/compatibility-version.json +3116 -0
  326. package/src/generated/model-metadata.ts +106 -0
  327. package/src/github/star-state.ts +203 -0
  328. package/src/grok/inject.ts +530 -0
  329. package/src/grok/inspect.ts +45 -0
  330. package/src/grok/status.ts +121 -0
  331. package/src/grok/sync.ts +66 -0
  332. package/src/images/artifacts.ts +516 -0
  333. package/src/images/fulfill-video.ts +163 -0
  334. package/src/images/fulfill.ts +149 -0
  335. package/src/images/index.ts +4 -0
  336. package/src/images/loop.ts +955 -0
  337. package/src/images/plan.ts +143 -0
  338. package/src/images/synthetic-tool.ts +133 -0
  339. package/src/images/types.ts +41 -0
  340. package/src/images/xai-client.ts +141 -0
  341. package/src/images/xai-video-client.ts +163 -0
  342. package/src/index.ts +22 -0
  343. package/src/integrations/config-io.ts +269 -0
  344. package/src/integrations/journal.ts +315 -0
  345. package/src/integrations/merge.ts +135 -0
  346. package/src/integrations/mutation-flight.ts +71 -0
  347. package/src/integrations/native/ownership-preflight.ts +202 -0
  348. package/src/integrations/omp-yaml-source.ts +358 -0
  349. package/src/integrations/owned-refresh.ts +74 -0
  350. package/src/integrations/ownership.ts +111 -0
  351. package/src/integrations/registry.ts +159 -0
  352. package/src/integrations/serialize.ts +314 -0
  353. package/src/integrations/state.ts +361 -0
  354. package/src/integrations/store.ts +103 -0
  355. package/src/integrations/writer-lock.ts +98 -0
  356. package/src/integrations/writer.ts +691 -0
  357. package/src/lab/artifacts/sanitize.ts +586 -0
  358. package/src/lab/artifacts/secure-fs.ts +475 -0
  359. package/src/lab/artifacts/store.ts +310 -0
  360. package/src/lab/automation/budgets.ts +78 -0
  361. package/src/lab/automation/config-persistence.ts +256 -0
  362. package/src/lab/automation/constants.ts +39 -0
  363. package/src/lab/automation/cooldown.ts +103 -0
  364. package/src/lab/automation/dispatch.ts +211 -0
  365. package/src/lab/automation/index.ts +13 -0
  366. package/src/lab/automation/orchestrator.ts +499 -0
  367. package/src/lab/automation/persistence.ts +512 -0
  368. package/src/lab/automation/planner.ts +371 -0
  369. package/src/lab/automation/policy.ts +136 -0
  370. package/src/lab/automation/queue.ts +191 -0
  371. package/src/lab/automation/recovery.ts +24 -0
  372. package/src/lab/automation/route-context.ts +21 -0
  373. package/src/lab/automation/run-key.ts +44 -0
  374. package/src/lab/automation/runs-query.ts +34 -0
  375. package/src/lab/automation/types.ts +160 -0
  376. package/src/lab/conformance/assertion.ts +325 -0
  377. package/src/lab/conformance/digest.ts +22 -0
  378. package/src/lab/conformance/executor.ts +741 -0
  379. package/src/lab/conformance/fixture-provider.ts +27 -0
  380. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  381. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  382. package/src/lab/conformance/harness-budget.ts +47 -0
  383. package/src/lab/conformance/index.ts +5 -0
  384. package/src/lab/conformance/jcs.ts +64 -0
  385. package/src/lab/conformance/json-pointer.ts +39 -0
  386. package/src/lab/conformance/manifest.ts +180 -0
  387. package/src/lab/conformance/mcp-stub.ts +179 -0
  388. package/src/lab/conformance/negative-controls.ts +164 -0
  389. package/src/lab/conformance/observation.ts +355 -0
  390. package/src/lab/conformance/runner.ts +68 -0
  391. package/src/lab/conformance/sse-normalize.ts +59 -0
  392. package/src/lab/conformance/suite-manifest.ts +78 -0
  393. package/src/lab/conformance/types.ts +214 -0
  394. package/src/lab/constants.ts +126 -0
  395. package/src/lab/digest.ts +64 -0
  396. package/src/lab/events/errors.ts +9 -0
  397. package/src/lab/events/limits.ts +117 -0
  398. package/src/lab/events/types.ts +229 -0
  399. package/src/lab/events/validate.ts +781 -0
  400. package/src/lab/fabric/constants.ts +40 -0
  401. package/src/lab/fabric/executor.ts +492 -0
  402. package/src/lab/fabric/index.ts +80 -0
  403. package/src/lab/fabric/manifest.ts +222 -0
  404. package/src/lab/fabric/observe.ts +489 -0
  405. package/src/lab/fabric/patch.ts +79 -0
  406. package/src/lab/fabric/producer-child.ts +139 -0
  407. package/src/lab/fabric/producer-isolate.ts +276 -0
  408. package/src/lab/fabric/producer-protocol.ts +61 -0
  409. package/src/lab/fabric/scratch.ts +439 -0
  410. package/src/lab/fabric/subject.ts +106 -0
  411. package/src/lab/fabric/types.ts +134 -0
  412. package/src/lab/fabric/verifier.ts +98 -0
  413. package/src/lab/index.ts +54 -0
  414. package/src/lab/ledger/artifact-refs.ts +127 -0
  415. package/src/lab/ledger/invalidation.ts +136 -0
  416. package/src/lab/ledger/purge.ts +310 -0
  417. package/src/lab/ledger/store.ts +532 -0
  418. package/src/lab/live/credential-lease.ts +53 -0
  419. package/src/lab/live/destination.ts +155 -0
  420. package/src/lab/live/executor.ts +336 -0
  421. package/src/lab/live/inert-tools.ts +56 -0
  422. package/src/lab/live/manifest.ts +85 -0
  423. package/src/lab/live/mcp-loopback.ts +57 -0
  424. package/src/lab/live/runner.ts +19 -0
  425. package/src/lab/live/sandbox.ts +61 -0
  426. package/src/lab/live/suite-manifest.ts +41 -0
  427. package/src/lab/live/transport.ts +118 -0
  428. package/src/lab/live/types.ts +197 -0
  429. package/src/lab/observe/from-conformance.ts +301 -0
  430. package/src/lab/observe/from-live.ts +117 -0
  431. package/src/lab/paths.ts +153 -0
  432. package/src/lab/projection/rebuild.ts +495 -0
  433. package/src/lab/projection/schema.ts +135 -0
  434. package/src/lab/projection/verdicts.ts +474 -0
  435. package/src/lab/projection/verification.ts +412 -0
  436. package/src/lab/public/bundle.ts +217 -0
  437. package/src/lab/public/community-authority.ts +175 -0
  438. package/src/lab/public/community-files.ts +29 -0
  439. package/src/lab/public/community.ts +479 -0
  440. package/src/lab/public/file-safety.ts +155 -0
  441. package/src/lab/public/ids.ts +26 -0
  442. package/src/lab/public/index.ts +16 -0
  443. package/src/lab/public/mutation-lock.ts +424 -0
  444. package/src/lab/public/operator.ts +353 -0
  445. package/src/lab/public/origin-purge.ts +79 -0
  446. package/src/lab/public/origin.ts +203 -0
  447. package/src/lab/public/privacy.ts +143 -0
  448. package/src/lab/public/private-file.ts +261 -0
  449. package/src/lab/public/project.ts +124 -0
  450. package/src/lab/public/purge-test-fault.ts +21 -0
  451. package/src/lab/public/purge.ts +223 -0
  452. package/src/lab/public/registry.ts +44 -0
  453. package/src/lab/public/revocation.ts +252 -0
  454. package/src/lab/public/signature.ts +243 -0
  455. package/src/lab/public/storage.ts +105 -0
  456. package/src/lab/public/strict-json.ts +206 -0
  457. package/src/lab/public/time.ts +26 -0
  458. package/src/lab/public/types.ts +172 -0
  459. package/src/lab/public/validate.ts +391 -0
  460. package/src/lab/query/catalog.ts +101 -0
  461. package/src/lab/query/connection.ts +107 -0
  462. package/src/lab/query/constants.ts +4 -0
  463. package/src/lab/query/cursor.ts +132 -0
  464. package/src/lab/query/dto-map.ts +277 -0
  465. package/src/lab/query/errors.ts +22 -0
  466. package/src/lab/query/freshness.ts +53 -0
  467. package/src/lab/query/index.ts +45 -0
  468. package/src/lab/query/latest-observation.ts +59 -0
  469. package/src/lab/query/passive-production.ts +159 -0
  470. package/src/lab/query/queries.ts +444 -0
  471. package/src/lab/query/types.ts +266 -0
  472. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  473. package/src/lab/subject/installation-salt.ts +112 -0
  474. package/src/lab/subject/protocol-subject.ts +80 -0
  475. package/src/lab/subject/route-subject.ts +74 -0
  476. package/src/lib/abort.ts +146 -0
  477. package/src/lib/admin-secrets.ts +25 -0
  478. package/src/lib/admission.ts +83 -0
  479. package/src/lib/app-owned-memory-stores.ts +195 -0
  480. package/src/lib/app-owned-memory.ts +265 -0
  481. package/src/lib/bounded-body.ts +346 -0
  482. package/src/lib/bun-binary-validator.d.mts +3 -0
  483. package/src/lib/bun-binary-validator.mjs +18 -0
  484. package/src/lib/bun-runtime.ts +184 -0
  485. package/src/lib/bun-stream-caps.ts +130 -0
  486. package/src/lib/codex-restart-contract.ts +120 -0
  487. package/src/lib/config-ownership.ts +364 -0
  488. package/src/lib/crash-guard.ts +344 -0
  489. package/src/lib/debug-log-buffer.ts +83 -0
  490. package/src/lib/debug-settings.ts +108 -0
  491. package/src/lib/debug.ts +31 -0
  492. package/src/lib/destination-policy.ts +380 -0
  493. package/src/lib/errors.ts +406 -0
  494. package/src/lib/eventstream-decoder.ts +253 -0
  495. package/src/lib/fabric-task-execution-authority.ts +7 -0
  496. package/src/lib/fabric-task-host.ts +29 -0
  497. package/src/lib/gcp-adc.ts +341 -0
  498. package/src/lib/injection-debug-log.ts +58 -0
  499. package/src/lib/lab-activation.ts +223 -0
  500. package/src/lib/lab-live-execution-authority.ts +13 -0
  501. package/src/lib/lab-live-host.ts +30 -0
  502. package/src/lib/lab-live-pinned-sender.ts +56 -0
  503. package/src/lib/lab-live-route-production.ts +130 -0
  504. package/src/lib/lab-passive-linker-registration.ts +26 -0
  505. package/src/lib/local-management-attestation.ts +51 -0
  506. package/src/lib/local-management-capability.ts +100 -0
  507. package/src/lib/local-provider-reload-contract.ts +100 -0
  508. package/src/lib/open-url.ts +25 -0
  509. package/src/lib/optional-shutdown-hooks.ts +57 -0
  510. package/src/lib/pinned-http.ts +270 -0
  511. package/src/lib/privacy.ts +20 -0
  512. package/src/lib/process-control.ts +168 -0
  513. package/src/lib/provider-outbound.ts +210 -0
  514. package/src/lib/provider-url.ts +14 -0
  515. package/src/lib/proxy-env.ts +18 -0
  516. package/src/lib/redact.ts +521 -0
  517. package/src/lib/retry-after.ts +55 -0
  518. package/src/lib/self-launch-argv.ts +15 -0
  519. package/src/lib/server-resource-ownership.ts +71 -0
  520. package/src/lib/service-secrets.ts +25 -0
  521. package/src/lib/shadow-call.ts +61 -0
  522. package/src/lib/sidecar-tracker.ts +52 -0
  523. package/src/lib/sse-decoder.ts +364 -0
  524. package/src/lib/state-store-registrations.ts +119 -0
  525. package/src/lib/state-store-sweeper.ts +184 -0
  526. package/src/lib/system-restart-contract.ts +73 -0
  527. package/src/lib/test-home-guard.ts +90 -0
  528. package/src/lib/token-estimate.ts +86 -0
  529. package/src/lib/tool-argument-integers.ts +202 -0
  530. package/src/lib/translator-budget.ts +400 -0
  531. package/src/lib/upstream-http-version.ts +57 -0
  532. package/src/lib/upstream-reachability.ts +95 -0
  533. package/src/lib/upstream-retry.ts +392 -0
  534. package/src/lib/win-exec.ts +115 -0
  535. package/src/lib/win-paths.ts +68 -0
  536. package/src/lib/windows-atomic-replace.ts +156 -0
  537. package/src/lib/windows-elevation.ts +773 -0
  538. package/src/lib/windows-secret-acl.ts +854 -0
  539. package/src/lib/windows-service-wrappers.ts +72 -0
  540. package/src/lib/windows-text.ts +106 -0
  541. package/src/lib/windows-user-principal.ts +341 -0
  542. package/src/lib/winsw.ts +403 -0
  543. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  544. package/src/oauth/account-import/index.ts +15 -0
  545. package/src/oauth/account-import/parser.ts +83 -0
  546. package/src/oauth/account-import/registry.ts +18 -0
  547. package/src/oauth/account-import/service.ts +75 -0
  548. package/src/oauth/account-import/types.ts +91 -0
  549. package/src/oauth/anthropic-routing.ts +594 -0
  550. package/src/oauth/anthropic.ts +188 -0
  551. package/src/oauth/antigravity-routing.ts +151 -0
  552. package/src/oauth/callback-server.ts +300 -0
  553. package/src/oauth/chatgpt.ts +161 -0
  554. package/src/oauth/command-code.ts +239 -0
  555. package/src/oauth/cursor.ts +252 -0
  556. package/src/oauth/github-copilot.ts +428 -0
  557. package/src/oauth/google-antigravity.ts +262 -0
  558. package/src/oauth/health.ts +407 -0
  559. package/src/oauth/index.ts +1504 -0
  560. package/src/oauth/key-providers.ts +124 -0
  561. package/src/oauth/kimi.ts +227 -0
  562. package/src/oauth/kiro-credentials.ts +726 -0
  563. package/src/oauth/kiro.ts +621 -0
  564. package/src/oauth/local-token-detect.ts +130 -0
  565. package/src/oauth/log.ts +50 -0
  566. package/src/oauth/login-cli.ts +223 -0
  567. package/src/oauth/nous.ts +798 -0
  568. package/src/oauth/pkce.ts +15 -0
  569. package/src/oauth/store.ts +728 -0
  570. package/src/oauth/token-guardian.ts +309 -0
  571. package/src/oauth/types.ts +62 -0
  572. package/src/oauth/xai.ts +241 -0
  573. package/src/providers/alibaba-region-backup.ts +75 -0
  574. package/src/providers/alibaba-region-migration.ts +156 -0
  575. package/src/providers/alibaba-region-startup.ts +36 -0
  576. package/src/providers/antigravity-models.ts +695 -0
  577. package/src/providers/antigravity-quota.ts +216 -0
  578. package/src/providers/api-keys.ts +140 -0
  579. package/src/providers/base-url-choices.ts +74 -0
  580. package/src/providers/codex-capacity.ts +292 -0
  581. package/src/providers/command-code-efforts.ts +144 -0
  582. package/src/providers/context-cap.ts +82 -0
  583. package/src/providers/cursor-pool.ts +72 -0
  584. package/src/providers/derive.ts +586 -0
  585. package/src/providers/fastwire.ts +501 -0
  586. package/src/providers/free-directory.ts +187 -0
  587. package/src/providers/github-copilot-transport.ts +56 -0
  588. package/src/providers/google-vertex-location.ts +14 -0
  589. package/src/providers/key-failover.ts +271 -0
  590. package/src/providers/kiro-models.ts +67 -0
  591. package/src/providers/label.ts +19 -0
  592. package/src/providers/model-discovery-limits.ts +16 -0
  593. package/src/providers/model-discovery.ts +449 -0
  594. package/src/providers/model-rename-migration.ts +255 -0
  595. package/src/providers/model-rename-startup.ts +28 -0
  596. package/src/providers/openai-sidecar.ts +243 -0
  597. package/src/providers/openai-tier-startup.ts +56 -0
  598. package/src/providers/openai-tiers.ts +423 -0
  599. package/src/providers/openai-virtual-models.ts +83 -0
  600. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  601. package/src/providers/openrouter-routing.ts +102 -0
  602. package/src/providers/provider-id-rewrite.ts +185 -0
  603. package/src/providers/quota.ts +2345 -0
  604. package/src/providers/registry.ts +2918 -0
  605. package/src/providers/replit/constants.ts +27 -0
  606. package/src/providers/replit/derive.ts +85 -0
  607. package/src/providers/replit/headers.ts +28 -0
  608. package/src/providers/replit/origin.ts +55 -0
  609. package/src/providers/replit/pair-install-response.ts +72 -0
  610. package/src/providers/replit/probe.ts +199 -0
  611. package/src/providers/replit/setup.ts +350 -0
  612. package/src/providers/request-pacing.ts +310 -0
  613. package/src/providers/service-tier.ts +277 -0
  614. package/src/providers/slug-codec.ts +103 -0
  615. package/src/providers/static-model-discovery.ts +86 -0
  616. package/src/providers/xai-responses-opt-in.ts +15 -0
  617. package/src/providers/xai-transport.ts +148 -0
  618. package/src/reasoning-effort.ts +183 -0
  619. package/src/responses/compaction.ts +142 -0
  620. package/src/responses/custom-tool-compat.ts +266 -0
  621. package/src/responses/hosted-tool-policy.ts +9 -0
  622. package/src/responses/namespace-tool-compat.ts +355 -0
  623. package/src/responses/parser.ts +838 -0
  624. package/src/responses/provider-continuation.ts +98 -0
  625. package/src/responses/provider-opaque-metadata.ts +73 -0
  626. package/src/responses/reasoning-envelope.ts +60 -0
  627. package/src/responses/reasoning-replay-cache.ts +426 -0
  628. package/src/responses/schema.ts +165 -0
  629. package/src/responses/spill-store.ts +459 -0
  630. package/src/responses/state.ts +1433 -0
  631. package/src/responses/thought-signature-replay.ts +347 -0
  632. package/src/responses/tool-groups.ts +19 -0
  633. package/src/responses/tool-search-compat.ts +301 -0
  634. package/src/responses/truncated-stop-reason.ts +60 -0
  635. package/src/router.ts +761 -0
  636. package/src/routing/analytics.ts +378 -0
  637. package/src/routing/capability.ts +244 -0
  638. package/src/routing/compatibility/assemble.ts +73 -0
  639. package/src/routing/compatibility/behavior.ts +278 -0
  640. package/src/routing/compatibility/catalog.ts +99 -0
  641. package/src/routing/compatibility/endpoint.ts +52 -0
  642. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  643. package/src/routing/compatibility/policy.ts +181 -0
  644. package/src/routing/compatibility/provider-slot.ts +56 -0
  645. package/src/routing/compatibility/reader.ts +110 -0
  646. package/src/routing/compatibility/subject.ts +191 -0
  647. package/src/routing/compatibility/types.ts +64 -0
  648. package/src/routing/compatibility/version.ts +104 -0
  649. package/src/routing/cost.ts +77 -0
  650. package/src/routing/evaluator.ts +495 -0
  651. package/src/routing/health.ts +412 -0
  652. package/src/routing/history/cursor.ts +43 -0
  653. package/src/routing/history/indexer.ts +605 -0
  654. package/src/routing/history/schema.ts +72 -0
  655. package/src/routing/profile-namespace.ts +15 -0
  656. package/src/routing/profile.ts +547 -0
  657. package/src/routing/quota.ts +145 -0
  658. package/src/routing/request-evidence.ts +45 -0
  659. package/src/routing/trace.ts +776 -0
  660. package/src/server/adapter-resolve.ts +53 -0
  661. package/src/server/auth-cors.ts +751 -0
  662. package/src/server/background-lifecycle.ts +182 -0
  663. package/src/server/chat-completions.ts +442 -0
  664. package/src/server/chat-native-sse.ts +331 -0
  665. package/src/server/chat-native.ts +426 -0
  666. package/src/server/claude-messages.ts +1030 -0
  667. package/src/server/direct-local-http.ts +347 -0
  668. package/src/server/effort-policy.ts +190 -0
  669. package/src/server/github-copilot-responses-repair.ts +338 -0
  670. package/src/server/gui-static.ts +152 -0
  671. package/src/server/image-retry.ts +42 -0
  672. package/src/server/images.ts +568 -0
  673. package/src/server/index.ts +1813 -0
  674. package/src/server/lifecycle.ts +498 -0
  675. package/src/server/live.ts +717 -0
  676. package/src/server/local-management-read-client.ts +90 -0
  677. package/src/server/local-provider-reload-client.ts +137 -0
  678. package/src/server/management/agent-settings-routes.ts +1433 -0
  679. package/src/server/management/api-access.ts +141 -0
  680. package/src/server/management/api-key-usage.ts +193 -0
  681. package/src/server/management/body.ts +41 -0
  682. package/src/server/management/combo-routes.ts +263 -0
  683. package/src/server/management/config-routes.ts +835 -0
  684. package/src/server/management/context.ts +113 -0
  685. package/src/server/management/integration-routes.ts +498 -0
  686. package/src/server/management/lab-automation-routes.ts +206 -0
  687. package/src/server/management/lab-routes.ts +563 -0
  688. package/src/server/management/logs-usage-routes.ts +586 -0
  689. package/src/server/management/model-routes.ts +560 -0
  690. package/src/server/management/model-rows.ts +163 -0
  691. package/src/server/management/native-integration-routes.ts +769 -0
  692. package/src/server/management/oauth-account-routes.ts +637 -0
  693. package/src/server/management/provider-capability-config.ts +48 -0
  694. package/src/server/management/provider-routes.ts +1033 -0
  695. package/src/server/management/replit-provider-routes.ts +86 -0
  696. package/src/server/management/request-history-routes.ts +191 -0
  697. package/src/server/management/routing-analytics-routes.ts +74 -0
  698. package/src/server/management/routing-profile-routes.ts +385 -0
  699. package/src/server/management/shared.ts +286 -0
  700. package/src/server/management/sidebar-routes.ts +106 -0
  701. package/src/server/management/storage-log-guard-routes.ts +186 -0
  702. package/src/server/management/sync-response.ts +69 -0
  703. package/src/server/management/system-restart.ts +435 -0
  704. package/src/server/management/system-routes.ts +194 -0
  705. package/src/server/management/usage-summary-cache.ts +94 -0
  706. package/src/server/management/vision-sidecar-options.ts +167 -0
  707. package/src/server/management/web-search-sidecar-options.ts +120 -0
  708. package/src/server/management-api.ts +314 -0
  709. package/src/server/management-auth.ts +482 -0
  710. package/src/server/memory-watchdog.ts +156 -0
  711. package/src/server/passive-route-linker.ts +66 -0
  712. package/src/server/port-reclaim.ts +307 -0
  713. package/src/server/ports.ts +156 -0
  714. package/src/server/proxy-liveness.ts +328 -0
  715. package/src/server/readiness.ts +99 -0
  716. package/src/server/relay-eager.ts +353 -0
  717. package/src/server/relay.ts +1209 -0
  718. package/src/server/request-decompress.ts +239 -0
  719. package/src/server/request-log-conversation.ts +168 -0
  720. package/src/server/request-log.ts +1259 -0
  721. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  722. package/src/server/responses/agent-task-recovery.ts +465 -0
  723. package/src/server/responses/collaboration.ts +551 -0
  724. package/src/server/responses/compact.ts +771 -0
  725. package/src/server/responses/core.ts +5389 -0
  726. package/src/server/responses/empty-completion-guard.ts +276 -0
  727. package/src/server/responses/encrypted-payload.ts +331 -0
  728. package/src/server/responses/fetch-helpers.ts +232 -0
  729. package/src/server/responses/input-admission.ts +185 -0
  730. package/src/server/responses/pacing-overload.ts +13 -0
  731. package/src/server/responses/passthrough-error.ts +78 -0
  732. package/src/server/responses/policy-fallback.ts +178 -0
  733. package/src/server/responses/responses-field-backfill.ts +251 -0
  734. package/src/server/responses/terminal-guard.ts +251 -0
  735. package/src/server/responses/upstream-error.ts +53 -0
  736. package/src/server/responses/ws-upstream.ts +308 -0
  737. package/src/server/responses-custom-tool-repair.ts +282 -0
  738. package/src/server/responses-image-gen-repair.ts +132 -0
  739. package/src/server/responses-item-id-repair.ts +272 -0
  740. package/src/server/responses-json-events.ts +90 -0
  741. package/src/server/responses-model-rewrite.ts +29 -0
  742. package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
  743. package/src/server/responses-snapshot-repair.ts +621 -0
  744. package/src/server/responses-terminal-repair.ts +342 -0
  745. package/src/server/responses-tool-search-repair.ts +267 -0
  746. package/src/server/responses-undeclared-tool-guard.ts +153 -0
  747. package/src/server/responses.ts +25 -0
  748. package/src/server/search.ts +201 -0
  749. package/src/server/sse-frame-buffer.ts +292 -0
  750. package/src/server/sse-payload-rewrite.ts +263 -0
  751. package/src/server/startup-action-control.ts +315 -0
  752. package/src/server/startup-health-cache.ts +131 -0
  753. package/src/server/system-env.ts +484 -0
  754. package/src/server/windows-tcp-drop.ts +184 -0
  755. package/src/server/windows-tray-control.ts +41 -0
  756. package/src/server/ws-bridge.ts +472 -0
  757. package/src/service-manager-probe.ts +892 -0
  758. package/src/service.ts +3575 -0
  759. package/src/sidecar/auth.ts +92 -0
  760. package/src/sidecar/candidates.ts +83 -0
  761. package/src/stall-timeout.ts +20 -0
  762. package/src/storage/cleanup-job.ts +57 -0
  763. package/src/storage/cleanup.ts +3085 -0
  764. package/src/storage/policy-job.ts +457 -0
  765. package/src/storage/policy-scheduler.ts +40 -0
  766. package/src/storage/policy-worker.ts +59 -0
  767. package/src/storage/policy.ts +527 -0
  768. package/src/storage/restore-job.ts +299 -0
  769. package/src/storage/restore-worker.ts +58 -0
  770. package/src/storage/scanner.ts +238 -0
  771. package/src/storage/storage-mutation-coordinator.ts +139 -0
  772. package/src/storage/worker-lifecycle.ts +215 -0
  773. package/src/tray/assets/opencodex-tray-offline.ico +0 -0
  774. package/src/tray/assets/opencodex-tray-online.ico +0 -0
  775. package/src/tray/assets/opencodex-tray-warning.ico +0 -0
  776. package/src/tray/assets/opencodex-tray.png +0 -0
  777. package/src/tray/windows-tray.ps1 +364 -0
  778. package/src/tray/windows.ts +757 -0
  779. package/src/types/accounts.ts +37 -0
  780. package/src/types/config.ts +876 -0
  781. package/src/types/provider.ts +545 -0
  782. package/src/types/request.ts +384 -0
  783. package/src/types/tools.ts +131 -0
  784. package/src/types/wire.ts +80 -0
  785. package/src/types.ts +106 -0
  786. package/src/update/badge.ts +72 -0
  787. package/src/update/index.ts +415 -0
  788. package/src/update/job.ts +1887 -0
  789. package/src/update/notify.ts +263 -0
  790. package/src/update/npm-cache-preflight.d.mts +47 -0
  791. package/src/update/npm-cache-preflight.mjs +201 -0
  792. package/src/update/npm-invocation.d.mts +23 -0
  793. package/src/update/npm-invocation.mjs +94 -0
  794. package/src/update/transactional-install.d.mts +22 -0
  795. package/src/update/transactional-install.mjs +259 -0
  796. package/src/update/tray-update-plan.d.mts +18 -0
  797. package/src/update/tray-update-plan.mjs +38 -0
  798. package/src/usage/cost.ts +625 -0
  799. package/src/usage/debug.ts +97 -0
  800. package/src/usage/expected-prices.ts +416 -0
  801. package/src/usage/log.ts +1223 -0
  802. package/src/usage/summary.ts +753 -0
  803. package/src/usage/totals.ts +14 -0
  804. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  805. package/src/usage/user-cost-overlays.ts +314 -0
  806. package/src/vision/anthropic-describe.ts +189 -0
  807. package/src/vision/backends.ts +97 -0
  808. package/src/vision/describe.ts +131 -0
  809. package/src/vision/eligibility.ts +250 -0
  810. package/src/vision/index.ts +681 -0
  811. package/src/vision/reasoning.ts +55 -0
  812. package/src/vision/routed-describe.ts +175 -0
  813. package/src/vision/timeout-bounds.ts +9 -0
  814. package/src/web-search/anthropic-executor.ts +195 -0
  815. package/src/web-search/backends.ts +108 -0
  816. package/src/web-search/exa-executor.ts +88 -0
  817. package/src/web-search/executor.ts +113 -0
  818. package/src/web-search/format-result.ts +89 -0
  819. package/src/web-search/gemini-executor.ts +141 -0
  820. package/src/web-search/index.ts +331 -0
  821. package/src/web-search/loop.ts +896 -0
  822. package/src/web-search/parse.ts +315 -0
  823. package/src/web-search/progress-stream.ts +342 -0
  824. package/src/web-search/sources.ts +60 -0
  825. package/src/web-search/synthetic-tool.ts +47 -0
  826. package/src/web-search/xai-executor.ts +219 -0
@@ -0,0 +1,2345 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ effectiveCodexAuthAccountId,
4
+ fetchMainAccountInfoSnapshot,
5
+ listCodexAuthAccountsSnapshot,
6
+ } from "../codex/auth-api";
7
+ import { isMainAccountIdentityGenerationLive } from "../codex/main-account-cache";
8
+ import { MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
9
+ import { codexPlanKey } from "../codex/plan";
10
+ import { resolveEnvValue } from "../config";
11
+ import { getValidAccessToken, getValidAccessTokenForAccount } from "../oauth";
12
+ import { getAccountCredential, getAccountSet, getCredential } from "../oauth/store";
13
+ import { antigravityUserAgent } from "../adapters/client-fingerprint";
14
+ import { apiKeyPoolEntryId } from "./api-keys";
15
+ import { XAI_GROK_CLIENT_VERSION, XAI_GROK_COMPATIBILITY } from "./xai-transport";
16
+ import { getProviderRegistryEntry, providerCodexAccountMode, registryEntryForProviderDestination } from "./registry";
17
+ import type { OcxConfig, OcxProviderConfig } from "../types";
18
+ import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "./openai-tiers";
19
+ import {
20
+ captureConfigGeneration,
21
+ sweepExpiredOnWrite,
22
+ type GenerationContext,
23
+ } from "../lib/state-store-sweeper";
24
+ import { readBoundedResponseBody } from "../lib/bounded-body";
25
+ import {
26
+ aggregateCodexPoolCapacity,
27
+ CODEX_CAPACITY_MAX_QUOTA_AGE_MS,
28
+ type CodexCapacityAggregation,
29
+ type CodexCapacityQuota,
30
+ } from "./codex-capacity";
31
+ import {
32
+ AntigravityQuotaRpcError,
33
+ fetchAntigravityLiveQuota,
34
+ isTerminalAntigravityQuotaStatus,
35
+ } from "./antigravity-quota";
36
+ import { antigravityHostCandidates, isAntigravityHttpsHost } from "../adapters/google-antigravity-hosts";
37
+
38
+ /** Match oauth/index REFRESH_SKEW_MS — use stored access without refresh when still fresh. */
39
+ const ACCOUNT_TOKEN_SKEW_MS = 60_000;
40
+
41
+ const CACHE_TTL_MS = 5 * 60_000;
42
+ const REQUEST_TIMEOUT_MS = 8_000;
43
+ /** Successful provider quota payloads are small; reject oversized or stalled JSON before parsing. */
44
+ export const QUOTA_RESPONSE_MAX_BYTES = 512 * 1024;
45
+ const KIMI_CODE_BASE_URL = "https://api.kimi.com/coding/v1";
46
+ const KIMI_CODE_USAGE_URL = `${KIMI_CODE_BASE_URL}/usages`;
47
+ const COMMAND_CODE_BASE_URL = "https://api.commandcode.ai";
48
+ const COMMAND_CODE_WHOAMI_URL = `${COMMAND_CODE_BASE_URL}/alpha/whoami`;
49
+ const COMMAND_CODE_CREDITS_URL = `${COMMAND_CODE_BASE_URL}/alpha/billing/credits`;
50
+ const COMMAND_CODE_SUBSCRIPTIONS_URL = `${COMMAND_CODE_BASE_URL}/alpha/billing/subscriptions`;
51
+ const COMMAND_CODE_USAGE_URL = `${COMMAND_CODE_BASE_URL}/alpha/usage/summary`;
52
+ const A6API_BASE_URL = "https://api.a6api.com";
53
+ const OPENCODE_GO_BASE_URL = "https://opencode.ai/zen/go/v1";
54
+ const OPENCODE_GO_USAGE_URL = `${OPENCODE_GO_BASE_URL}/usage`;
55
+ const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
56
+ const DEEPSEEK_BASE_URL = "https://api.deepseek.com";
57
+ const CLINE_BASE_URL = "https://api.cline.bot";
58
+ const ZAI_BASE_URL = "https://api.z.ai";
59
+ const ZAI_CN_BASE_URL = "https://open.bigmodel.cn";
60
+ const MINIMAX_REMAINS_URL = "https://www.minimax.io/v1/token_plan/remains";
61
+ const MOONSHOT_BASE_URL = "https://api.moonshot.ai/v1";
62
+ const VENICE_BASE_URL = "https://api.venice.ai/api/v1";
63
+ const SYNTHETIC_BASE_URL = "https://api.synthetic.new/v2";
64
+ const DEEPINFRA_BASE_URL = "https://api.deepinfra.com";
65
+ const NEURALWATT_BASE_URL = "https://api.neuralwatt.com/v1";
66
+ const XAI_BILLING_URL = "https://cli-chat-proxy.grok.com/v1/billing";
67
+ const XAI_CREDITS_URL = `${XAI_BILLING_URL}?format=credits`;
68
+ /** Keep a failed probe's previous row at most this long before dropping it. */
69
+ const LAST_GOOD_MAX_AGE_MS = CODEX_CAPACITY_MAX_QUOTA_AGE_MS;
70
+ const nativeMainReportGenerations = new WeakMap<ProviderQuotaReport, number>();
71
+ let providerQuotaBeforePublishForTests: (() => void | Promise<void>) | null = null;
72
+
73
+ /** Test-only seam for identity/config invalidation after probes but before publication. */
74
+ export function setProviderQuotaBeforePublishForTests(
75
+ hook: (() => void | Promise<void>) | null,
76
+ ): void {
77
+ providerQuotaBeforePublishForTests = hook;
78
+ }
79
+ const TERMINAL_QUOTA_FAILURE = Symbol("terminal-quota-failure");
80
+ type ProviderQuotaProbeResult = ProviderQuotaReport | null | typeof TERMINAL_QUOTA_FAILURE;
81
+
82
+ export interface ProviderQuotaWindow {
83
+ label: string;
84
+ percent: number;
85
+ resetAt?: number;
86
+ }
87
+
88
+ export interface ProviderQuotaCreditsUsd {
89
+ used: number;
90
+ limit: number;
91
+ remaining: number;
92
+ percent: number;
93
+ expiresAt?: number;
94
+ unlimited?: boolean;
95
+ }
96
+
97
+ export interface ProviderQuota {
98
+ fiveHourPercent?: number;
99
+ fiveHourResetAt?: number;
100
+ weeklyPercent?: number;
101
+ weeklyResetAt?: number;
102
+ monthlyPercent?: number;
103
+ monthlyResetAt?: number;
104
+ customWindows?: ProviderQuotaWindow[];
105
+ creditsUsd?: ProviderQuotaCreditsUsd;
106
+ updatedAt: number;
107
+ }
108
+
109
+ export interface ProviderQuotaReport {
110
+ provider: string;
111
+ label: string;
112
+ source: string;
113
+ quota: ProviderQuota;
114
+ updatedAt: number;
115
+ reverseEngineered?: boolean;
116
+ aggregation?: CodexCapacityAggregation;
117
+ }
118
+
119
+ export interface ProviderQuotaResponse {
120
+ generatedAt: number;
121
+ reports: ProviderQuotaReport[];
122
+ }
123
+
124
+ let cache: { key: string; ts: number; response: ProviderQuotaResponse } | null = null;
125
+ const inflight = new Map<string, { epoch: number; promise: Promise<ProviderQuotaResponse> }>();
126
+ /** Bumped on cache clear and on force-refresh start; stale-epoch probes lose commit authority. */
127
+ let invalidationEpoch = 0;
128
+
129
+ /** Invalidate the report cache (e.g. after switching a provider's active account). */
130
+ export function clearProviderQuotaCache(): void {
131
+ cache = null;
132
+ invalidationEpoch += 1;
133
+ }
134
+
135
+ function cacheKey(config: OcxConfig): string {
136
+ const providers = Object.entries(config.providers)
137
+ .map(([name, provider]) => {
138
+ const resolvedKey = typeof provider.apiKey === "string"
139
+ ? resolveEnvValue(provider.apiKey)?.trim()
140
+ : undefined;
141
+ const activeKeyId = resolvedKey ? apiKeyPoolEntryId(resolvedKey) : "none";
142
+ return `${name}:${provider.adapter}:${provider.authMode ?? "key"}:${providerCodexAccountMode(name, provider) ?? "none"}:${provider.disabled === true ? "off" : "on"}:${provider.baseUrl}:${activeKeyId}`;
143
+ })
144
+ .sort()
145
+ .join("|");
146
+ return `${config.defaultProvider}|${providers}`;
147
+ }
148
+
149
+ type CodexAuthAccountsSnapshotPromise = ReturnType<typeof listCodexAuthAccountsSnapshot>;
150
+
151
+ function hasCodexPoolProvider(config: OcxConfig): boolean {
152
+ return Object.entries(config.providers).some(([name, provider]) => (
153
+ provider.disabled !== true
154
+ && isBuiltInChatGptForwardProvider(name, provider)
155
+ && providerCodexAccountMode(name, provider) !== "direct"
156
+ ));
157
+ }
158
+
159
+ function quotaSignatureValue(quota: CodexCapacityQuota | null): unknown {
160
+ if (!quota) return null;
161
+ return {
162
+ fiveHourPercent: quota.fiveHourPercent,
163
+ fiveHourResetAt: quota.fiveHourResetAt,
164
+ weeklyPercent: quota.weeklyPercent,
165
+ weeklyResetAt: quota.weeklyResetAt,
166
+ monthlyPercent: quota.monthlyPercent,
167
+ monthlyResetAt: quota.monthlyResetAt,
168
+ updatedAt: quota.updatedAt,
169
+ customWindows: [...(quota.customWindows ?? [])]
170
+ .map(window => ({ label: window.label, percent: window.percent, resetAt: window.resetAt }))
171
+ .sort((a, b) => a.label.localeCompare(b.label)),
172
+ };
173
+ }
174
+
175
+ /** Hash only presentation-relevant state; account ids and email addresses never enter the key. */
176
+ function cacheKeyWithAggregationState(
177
+ config: OcxConfig,
178
+ prefetchedSnapshot?: CodexAuthAccountsSnapshotPromise,
179
+ ): string | Promise<string> {
180
+ const base = cacheKey(config);
181
+ if (!hasCodexPoolProvider(config)) return base;
182
+ return (async () => {
183
+ try {
184
+ const activeId = effectiveCodexAuthAccountId(config);
185
+ const snapshot = await (prefetchedSnapshot ?? listCodexAuthAccountsSnapshot(config, false));
186
+ const rows = snapshot.accounts.map(account => ({
187
+ isMain: account.isMain,
188
+ active: account.id === activeId,
189
+ plan: codexPlanKey(account.plan) ?? null,
190
+ paused: account.paused,
191
+ needsReauth: account.needsReauth === true,
192
+ quota: quotaSignatureValue(account.quota as CodexCapacityQuota | null),
193
+ }));
194
+ const canonicalRows = rows.map(row => JSON.stringify(row)).sort();
195
+ const digest = createHash("sha256").update(JSON.stringify(canonicalRows)).digest("hex").slice(0, 24);
196
+ return `${base}|codex-pool:${digest}`;
197
+ } catch {
198
+ return `${base}|codex-pool:unavailable`;
199
+ }
200
+ })();
201
+ }
202
+
203
+ function publicCapacityWindow(window: import("./codex-capacity").CodexCapacityWindowAggregation) {
204
+ const { totalWeight: _totalWeight, consumedWeight: _consumedWeight, remainingWeight: _remainingWeight, ...safe } = window;
205
+ return safe;
206
+ }
207
+
208
+ /** Management API metadata intentionally omits configured/weighted unit counts. */
209
+ function publicCapacityAggregation(
210
+ aggregation: CodexCapacityAggregation,
211
+ presentation: NonNullable<CodexCapacityAggregation["presentation"]>,
212
+ ): CodexCapacityAggregation {
213
+ const safeCurrentAccount = presentation === "coverage-only" && aggregation.currentAccount
214
+ ? { ...aggregation.currentAccount, quota: null }
215
+ : aggregation.currentAccount;
216
+ return {
217
+ ...aggregation,
218
+ presentation,
219
+ ...(safeCurrentAccount ? { currentAccount: safeCurrentAccount } : {}),
220
+ ...(aggregation.fiveHour ? { fiveHour: publicCapacityWindow(aggregation.fiveHour) } : {}),
221
+ ...(aggregation.weekly ? { weekly: publicCapacityWindow(aggregation.weekly) } : {}),
222
+ ...(aggregation.monthly ? { monthly: publicCapacityWindow(aggregation.monthly) } : {}),
223
+ ...(aggregation.customWindows ? {
224
+ customWindows: aggregation.customWindows.map(window => ({
225
+ label: window.label,
226
+ ...publicCapacityWindow(window),
227
+ })),
228
+ } : {}),
229
+ };
230
+ }
231
+
232
+ function hasQuotaRows(quota: ProviderQuota | null | undefined): quota is ProviderQuota {
233
+ if (!quota) return false;
234
+ return typeof quota.fiveHourPercent === "number"
235
+ || typeof quota.weeklyPercent === "number"
236
+ || typeof quota.monthlyPercent === "number"
237
+ || quota.creditsUsd?.unlimited === true
238
+ || typeof quota.creditsUsd?.percent === "number"
239
+ || !!quota.customWindows?.some(window => typeof window.percent === "number");
240
+ }
241
+
242
+ function providerLabel(providerId: string): string {
243
+ return getProviderRegistryEntry(providerId)?.label ?? providerId;
244
+ }
245
+
246
+ function normalizeResetAt(value: unknown): number | undefined {
247
+ if (typeof value === "number" && Number.isFinite(value)) return epochMillis(value);
248
+ if (typeof value === "string" && value.trim()) {
249
+ const trimmed = value.trim();
250
+ // Cursor Connect RPC returns billingCycleEnd as a unix-ms decimal string ("1771077734000").
251
+ // Date.parse treats that as invalid; numeric epoch strings must be handled explicitly.
252
+ if (/^[+-]?\d+(\.\d+)?$/.test(trimmed)) {
253
+ const numeric = Number(trimmed);
254
+ return epochMillis(numeric);
255
+ }
256
+ const parsed = Date.parse(trimmed);
257
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
258
+ }
259
+ return undefined;
260
+ }
261
+
262
+ /** Unix 0 / negative values are sentinels, not reset clocks (Command Code fiveHour.resetAt: 0). */
263
+ function epochMillis(value: number): number | undefined {
264
+ if (!Number.isFinite(value) || value <= 0) return undefined;
265
+ return value > 10_000_000_000 ? value : value * 1000;
266
+ }
267
+
268
+ function toFiniteNumber(value: unknown): number | undefined {
269
+ if (typeof value === "number" && Number.isFinite(value)) return value;
270
+ if (typeof value === "string" && value.trim()) {
271
+ const parsed = Number(value);
272
+ return Number.isFinite(parsed) ? parsed : undefined;
273
+ }
274
+ return undefined;
275
+ }
276
+
277
+ function normalizePercent(value: unknown): number | undefined {
278
+ const numeric = toFiniteNumber(value);
279
+ return numeric === undefined ? undefined : Math.max(0, Math.min(100, numeric));
280
+ }
281
+
282
+ function asRecord(value: unknown): Record<string, unknown> | null {
283
+ return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null;
284
+ }
285
+
286
+ const QUOTA_JSON_READ_FAILURE = Symbol("quota-json-read-failure");
287
+
288
+ async function readQuotaJson(
289
+ response: Response,
290
+ timeoutMs = REQUEST_TIMEOUT_MS,
291
+ ): Promise<unknown | typeof QUOTA_JSON_READ_FAILURE> {
292
+ const declaredLength = Number(response.headers.get("content-length"));
293
+ if (Number.isFinite(declaredLength) && declaredLength > QUOTA_RESPONSE_MAX_BYTES) {
294
+ try {
295
+ void response.body?.cancel(
296
+ new DOMException("Provider quota response is too large", "QuotaExceededError"),
297
+ ).catch(() => undefined);
298
+ } catch {
299
+ // Best-effort cancellation only.
300
+ }
301
+ return QUOTA_JSON_READ_FAILURE;
302
+ }
303
+
304
+ try {
305
+ const bounded = await readBoundedResponseBody(response, {
306
+ maxBytes: QUOTA_RESPONSE_MAX_BYTES,
307
+ totalTimeoutMs: timeoutMs,
308
+ inactivityTimeoutMs: timeoutMs,
309
+ });
310
+ if (bounded.oversized || bounded.truncated || !bounded.displaySafe) return QUOTA_JSON_READ_FAILURE;
311
+ return JSON.parse(bounded.text) as unknown;
312
+ } catch {
313
+ return QUOTA_JSON_READ_FAILURE;
314
+ }
315
+ }
316
+
317
+ /** Test-only access to the quota reader's deadline and cancellation contract. */
318
+ export async function readProviderQuotaJsonForTests(response: Response, timeoutMs: number): Promise<unknown> {
319
+ const result = await readQuotaJson(response, timeoutMs);
320
+ return result === QUOTA_JSON_READ_FAILURE ? null : result;
321
+ }
322
+
323
+ function isBuiltInChatGptForwardProvider(name: string, provider: OcxProviderConfig): boolean {
324
+ return name === OPENAI_CODEX_PROVIDER_ID && isCanonicalOpenAiForwardProvider(provider);
325
+ }
326
+
327
+ function isCanonicalA6apiBaseUrl(baseUrl: string): boolean {
328
+ const normalized = normalizedBaseUrl(baseUrl);
329
+ return normalized === A6API_BASE_URL || normalized === `${A6API_BASE_URL}/v1`;
330
+ }
331
+
332
+ function isCanonicalOpenCodeGoBaseUrl(baseUrl: string): boolean {
333
+ return normalizedBaseUrl(baseUrl) === OPENCODE_GO_BASE_URL;
334
+ }
335
+
336
+ function isCanonicalOpenRouterBaseUrl(baseUrl: string): boolean {
337
+ const normalized = normalizedBaseUrl(baseUrl);
338
+ return normalized === OPENROUTER_BASE_URL;
339
+ }
340
+
341
+ function isCanonicalDeepSeekBaseUrl(baseUrl: string): boolean {
342
+ const normalized = normalizedBaseUrl(baseUrl);
343
+ return normalized === DEEPSEEK_BASE_URL || normalized === `${DEEPSEEK_BASE_URL}/v1`;
344
+ }
345
+
346
+ function isCanonicalClineBaseUrl(baseUrl: string): boolean {
347
+ const normalized = normalizedBaseUrl(baseUrl);
348
+ return normalized === CLINE_BASE_URL || normalized === `${CLINE_BASE_URL}/api/v1`;
349
+ }
350
+
351
+ function isCanonicalZaiBaseUrl(baseUrl: string): boolean {
352
+ const normalized = normalizedBaseUrl(baseUrl);
353
+ return normalized === ZAI_BASE_URL
354
+ || normalized === `${ZAI_BASE_URL}/api/coding/paas/v4`
355
+ || normalized === ZAI_CN_BASE_URL
356
+ || normalized === `${ZAI_CN_BASE_URL}/api/coding/paas/v4`
357
+ // BigModel serves the same GLM Coding Plan on the OpenAI Responses wire at /api/v1.
358
+ || normalized === `${ZAI_CN_BASE_URL}/api/v1`;
359
+ }
360
+
361
+ function isCanonicalMinimaxBaseUrl(baseUrl: string): boolean {
362
+ const normalized = normalizedBaseUrl(baseUrl);
363
+ return normalized === "https://api.minimax.io/v1" || normalized === "https://api.minimaxi.com/v1";
364
+ }
365
+
366
+ function isCanonicalMoonshotBaseUrl(baseUrl: string): boolean {
367
+ const normalized = normalizedBaseUrl(baseUrl);
368
+ return normalized === MOONSHOT_BASE_URL || normalized === "https://api.moonshot.cn/v1";
369
+ }
370
+
371
+ function isCanonicalVeniceBaseUrl(baseUrl: string): boolean {
372
+ return normalizedBaseUrl(baseUrl) === VENICE_BASE_URL;
373
+ }
374
+
375
+ function isCanonicalSyntheticBaseUrl(baseUrl: string): boolean {
376
+ const normalized = normalizedBaseUrl(baseUrl);
377
+ return normalized === SYNTHETIC_BASE_URL || normalized === "https://api.synthetic.new/openai/v1";
378
+ }
379
+
380
+ function isCanonicalDeepInfraBaseUrl(baseUrl: string): boolean {
381
+ const normalized = normalizedBaseUrl(baseUrl);
382
+ return normalized === DEEPINFRA_BASE_URL || normalized === `${DEEPINFRA_BASE_URL}/v1/openai`;
383
+ }
384
+
385
+ function isCanonicalNeuralwattBaseUrl(baseUrl: string): boolean {
386
+ return normalizedBaseUrl(baseUrl) === NEURALWATT_BASE_URL;
387
+ }
388
+
389
+ function a6apiPayload(value: unknown): Record<string, unknown> | null {
390
+ const body = asRecord(value);
391
+ return asRecord(body?.data) ?? body;
392
+ }
393
+
394
+ function firstFinite(record: Record<string, unknown> | null, names: string[]): number | undefined {
395
+ if (!record) return undefined;
396
+ for (const name of names) {
397
+ const value = toFiniteNumber(record[name]);
398
+ if (value !== undefined) return value;
399
+ }
400
+ return undefined;
401
+ }
402
+
403
+ async function fetchA6apiQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
404
+ // Never send a configured API key to a lookalike host or through a redirect.
405
+ if (!isCanonicalA6apiBaseUrl(config.baseUrl)) return null;
406
+ const apiKey = resolveEnvValue(config.apiKey)?.trim();
407
+ if (!apiKey) return null;
408
+ const headers = { Accept: "application/json", Authorization: `Bearer ${apiKey}` } as const;
409
+ const [subscriptionResponse, tokenResponse] = await Promise.all([
410
+ fetch(`${A6API_BASE_URL}/dashboard/billing/subscription`, {
411
+ headers, redirect: "error", signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
412
+ }),
413
+ fetch(`${A6API_BASE_URL}/api/usage/token/`, {
414
+ headers, redirect: "error", signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
415
+ }),
416
+ ]);
417
+ if (!subscriptionResponse.ok || !tokenResponse.ok) {
418
+ const statuses = [subscriptionResponse.status, tokenResponse.status];
419
+ // 408/429 are transient (timeout/throttle), not invalid-account signals: keep the
420
+ // last-good row like 5xx/network failures. 401/403 (bad key) and 404 (contract change)
421
+ // stay terminal.
422
+ return statuses.some(status => status >= 400 && status < 500 && status !== 429 && status !== 408)
423
+ ? TERMINAL_QUOTA_FAILURE
424
+ : null;
425
+ }
426
+ const [subscriptionBody, tokenBody] = await Promise.all([
427
+ readQuotaJson(subscriptionResponse),
428
+ readQuotaJson(tokenResponse),
429
+ ]);
430
+ if (subscriptionBody === QUOTA_JSON_READ_FAILURE || tokenBody === QUOTA_JSON_READ_FAILURE) return null;
431
+ const subscription = a6apiPayload(subscriptionBody);
432
+ const token = a6apiPayload(tokenBody);
433
+ const unlimited = token?.unlimited_quota === true
434
+ || token?.unlimited_quota === 1
435
+ || token?.unlimited_quota === "true";
436
+ const normalizedExpiry = normalizeResetAt(token?.expires_at);
437
+ const expiry = normalizedExpiry && normalizedExpiry > 0
438
+ ? { expiresAt: normalizedExpiry }
439
+ : {};
440
+ if (unlimited) {
441
+ return report(provider, "a6api:billing", {
442
+ creditsUsd: {
443
+ used: 0,
444
+ limit: 0,
445
+ remaining: 0,
446
+ percent: 0,
447
+ unlimited: true,
448
+ ...expiry,
449
+ },
450
+ customWindows: [{ label: "Unlimited API credits", percent: 0 }],
451
+ updatedAt: Date.now(),
452
+ });
453
+ }
454
+ const limitUsd = firstFinite(subscription, ["hard_limit_usd"]);
455
+ const grantedUnits = firstFinite(token, ["total_granted"]);
456
+ const usedUnits = firstFinite(token, ["total_used"]);
457
+ const availableUnits = firstFinite(token, ["total_available"]);
458
+ const reconciledUnits = usedUnits !== undefined && availableUnits !== undefined
459
+ ? usedUnits + availableUnits
460
+ : undefined;
461
+ const reconciliationTolerance = grantedUnits !== undefined
462
+ ? Math.abs(grantedUnits) * 1e-9
463
+ : 0;
464
+ if (limitUsd === undefined || grantedUnits === undefined || usedUnits === undefined
465
+ || availableUnits === undefined || limitUsd <= 0 || grantedUnits <= 0
466
+ || usedUnits < 0 || availableUnits < 0
467
+ || reconciledUnits === undefined
468
+ || Math.abs(reconciledUnits - grantedUnits) > reconciliationTolerance) return TERMINAL_QUOTA_FAILURE;
469
+ const usdPerUnit = limitUsd / grantedUnits;
470
+ const usedUsd = usedUnits * usdPerUnit;
471
+ const remainingUsd = Math.max(0, availableUnits * usdPerUnit);
472
+ const percent = normalizePercent((usedUsd / limitUsd) * 100);
473
+ if (percent === undefined) return TERMINAL_QUOTA_FAILURE;
474
+ const label = `API credits ($${remainingUsd.toFixed(2)} of $${limitUsd.toFixed(2)} remaining)`;
475
+ return report(provider, "a6api:billing", {
476
+ creditsUsd: {
477
+ used: usedUsd,
478
+ limit: limitUsd,
479
+ remaining: remainingUsd,
480
+ percent,
481
+ ...expiry,
482
+ },
483
+ customWindows: [{ label, percent }],
484
+ updatedAt: Date.now(),
485
+ });
486
+ }
487
+
488
+ function parseOpenCodeGoUsageWindow(value: unknown): { percent: number; resetAt?: number } | null {
489
+ const row = asRecord(value);
490
+ if (!row) return null;
491
+ const percent = normalizePercent(row.percent);
492
+ if (percent === undefined) return null;
493
+ const resetAt = normalizeResetAt(row.resetsAt);
494
+ return { percent, ...(resetAt !== undefined ? { resetAt } : {}) };
495
+ }
496
+
497
+ async function fetchOpenCodeGoQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
498
+ // Never send a configured API key when the provider destination is not the built-in Go endpoint.
499
+ if (!isCanonicalOpenCodeGoBaseUrl(config.baseUrl)) return null;
500
+ const apiKey = resolveEnvValue(config.apiKey)?.trim();
501
+ if (!apiKey) return null;
502
+ const response = await fetch(OPENCODE_GO_USAGE_URL, {
503
+ headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
504
+ redirect: "error",
505
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
506
+ });
507
+ if (!response.ok) {
508
+ return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
509
+ ? TERMINAL_QUOTA_FAILURE
510
+ : null;
511
+ }
512
+ const body = asRecord(await readQuotaJson(response));
513
+ const usage = asRecord(body?.usage);
514
+ if (!usage) return null;
515
+ const rolling = parseOpenCodeGoUsageWindow(usage.rolling);
516
+ const weekly = parseOpenCodeGoUsageWindow(usage.weekly);
517
+ const monthly = parseOpenCodeGoUsageWindow(usage.monthly);
518
+ const quota: ProviderQuota = {
519
+ ...(rolling ? {
520
+ fiveHourPercent: rolling.percent,
521
+ ...(rolling.resetAt !== undefined ? { fiveHourResetAt: rolling.resetAt } : {}),
522
+ } : {}),
523
+ ...(weekly ? {
524
+ weeklyPercent: weekly.percent,
525
+ ...(weekly.resetAt !== undefined ? { weeklyResetAt: weekly.resetAt } : {}),
526
+ } : {}),
527
+ ...(monthly ? {
528
+ monthlyPercent: monthly.percent,
529
+ ...(monthly.resetAt !== undefined ? { monthlyResetAt: monthly.resetAt } : {}),
530
+ } : {}),
531
+ updatedAt: Date.now(),
532
+ };
533
+ return report(provider, "opencode-go:usage", quota);
534
+ }
535
+
536
+ /**
537
+ * OpenRouter `GET /api/v1/key` — the key's own credit balance and optional
538
+ * per-key spending cap. `limit` is the configured cap (absent = uncapped);
539
+ * `usage` is lifetime spend; `limit_remaining` is what is left of the cap.
540
+ * When no cap is set there is no hard limit to meter against, so no bar is
541
+ * produced — the provider falls back to its documented reference.
542
+ */
543
+ async function fetchOpenRouterQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
544
+ // Never send a configured API key to a lookalike host or through a redirect.
545
+ if (!isCanonicalOpenRouterBaseUrl(config.baseUrl)) return null;
546
+ const apiKey = resolveEnvValue(config.apiKey)?.trim();
547
+ if (!apiKey) return null;
548
+ const response = await fetch(`${OPENROUTER_BASE_URL}/key`, {
549
+ headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
550
+ redirect: "error",
551
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
552
+ });
553
+ if (!response.ok) {
554
+ return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
555
+ ? TERMINAL_QUOTA_FAILURE
556
+ : null;
557
+ }
558
+ const body = asRecord(await readQuotaJson(response));
559
+ const data = asRecord(body?.data) ?? body;
560
+ if (!data) return null;
561
+ const limit = toFiniteNumber(data.limit);
562
+ const limitRemaining = toFiniteNumber(data.limit_remaining);
563
+ const usage = toFiniteNumber(data.usage);
564
+ // A successful no-cap response is a DELIBERATE change, not a transient
565
+ // failure: the old capped row must be dropped, not preserved as last-good.
566
+ if (limit === undefined || limit <= 0) return TERMINAL_QUOTA_FAILURE;
567
+ // Prefer the authoritative remaining-cap value when present: `usage` is
568
+ // lifetime accumulated spend and overstates a reset or re-capped key.
569
+ const used = limitRemaining !== undefined
570
+ ? Math.max(0, limit - limitRemaining)
571
+ : usage !== undefined && usage >= 0 ? usage : undefined;
572
+ if (used === undefined) return null;
573
+ const percent = normalizePercent((used / limit) * 100);
574
+ if (percent === undefined) return null;
575
+ const remaining = Math.max(0, limit - used);
576
+ const label = `API credits ($${remaining.toFixed(2)} of $${limit.toFixed(2)} remaining)`;
577
+ return report(provider, "openrouter:key-info", {
578
+ customWindows: [{ label, percent }],
579
+ updatedAt: Date.now(),
580
+ });
581
+ }
582
+
583
+ /**
584
+ * DeepSeek `GET /user/balance` — the account's granted + topped-up credit
585
+ * balance. The payload places `total_balance` / `granted_balance` inside
586
+ * entries of `balance_infos` (one row per currency); the row for the account's
587
+ * currency is selected by preference. `granted_balance` is a CURRENT balance
588
+ * component, not the original grant ceiling, so no consumed percentage is
589
+ * fabricated — the balance is reported as a balance-only window.
590
+ */
591
+ async function fetchDeepSeekQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
592
+ if (!isCanonicalDeepSeekBaseUrl(config.baseUrl)) return null;
593
+ const apiKey = resolveEnvValue(config.apiKey)?.trim();
594
+ if (!apiKey) return null;
595
+ const response = await fetch(`${DEEPSEEK_BASE_URL}/user/balance`, {
596
+ headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
597
+ redirect: "error",
598
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
599
+ });
600
+ if (!response.ok) {
601
+ return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
602
+ ? TERMINAL_QUOTA_FAILURE
603
+ : null;
604
+ }
605
+ const body = asRecord(await readQuotaJson(response));
606
+ // The payload nests balances under `balance_infos` rows keyed by currency;
607
+ // prefer a USD row, then CNY, then the first row that parses.
608
+ const infos = Array.isArray(body?.balance_infos) ? body.balance_infos as unknown[] : null;
609
+ const rows = infos
610
+ ? infos.map((raw): Record<string, unknown> | null => asRecord(raw)).filter((r): r is Record<string, unknown> => r !== null)
611
+ : [];
612
+ const pick = (currency: string): Record<string, unknown> | null =>
613
+ rows.find(row => String(row.currency ?? "").toUpperCase() === currency) ?? null;
614
+ const preferred = pick("USD") ?? pick("CNY") ?? rows[0] ?? null;
615
+ if (!preferred) return null;
616
+ const totalBalance = toFiniteNumber(preferred.total_balance);
617
+ const grantedBalance = toFiniteNumber(preferred.granted_balance);
618
+ const toppedUp = toFiniteNumber(preferred.topped_up_balance);
619
+ const balance = totalBalance ?? grantedBalance ?? toppedUp;
620
+ if (balance === undefined || balance < 0) return null;
621
+ const label = grantedBalance !== undefined && grantedBalance > 0
622
+ ? `API balance ($${balance.toFixed(2)} total, $${grantedBalance.toFixed(2)} granted)`
623
+ : `API balance ($${balance.toFixed(2)})`;
624
+ return report(provider, "deepseek:balance", {
625
+ customWindows: [{ label, percent: 0 }],
626
+ updatedAt: Date.now(),
627
+ });
628
+ }
629
+
630
+ /**
631
+ * ClinePass `GET /api/v1/users/me/plan/usage-limits` — the subscription's
632
+ * rolling five-hour, weekly, and monthly utilization, matching the existing
633
+ * ProviderQuota windows directly. The endpoint 404s (or returns a null plan)
634
+ * for accounts without an active ClinePass, which is a no-report, not an error.
635
+ */
636
+ async function fetchClineQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
637
+ if (!isCanonicalClineBaseUrl(config.baseUrl)) return null;
638
+ const apiKey = resolveEnvValue(config.apiKey)?.trim();
639
+ if (!apiKey) return null;
640
+ const response = await fetch(`${CLINE_BASE_URL}/api/v1/users/me/plan/usage-limits`, {
641
+ headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
642
+ redirect: "error",
643
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
644
+ });
645
+ if (!response.ok) {
646
+ // 404 = no active plan; a plain "no plan" is a no-report, everything else
647
+ // 4xx (except 408/429) is a credential/contract problem.
648
+ if (response.status === 404) return null;
649
+ return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
650
+ ? TERMINAL_QUOTA_FAILURE
651
+ : null;
652
+ }
653
+ const body = asRecord(await readQuotaJson(response));
654
+ const data = asRecord(body?.data) ?? body;
655
+ const limits = Array.isArray(data?.limits) ? data.limits : null;
656
+ if (!limits) return null;
657
+ const quota: ProviderQuota = { updatedAt: Date.now() };
658
+ let windows = 0;
659
+ for (const raw of limits) {
660
+ const row = asRecord(raw);
661
+ if (!row) continue;
662
+ const percent = normalizePercent(row.percentUsed);
663
+ if (percent === undefined) continue;
664
+ const resetAt = normalizeResetAt(row.resetsAt);
665
+ if (row.type === "five_hour") {
666
+ quota.fiveHourPercent = percent;
667
+ if (resetAt !== undefined) quota.fiveHourResetAt = resetAt;
668
+ windows += 1;
669
+ } else if (row.type === "weekly") {
670
+ quota.weeklyPercent = percent;
671
+ if (resetAt !== undefined) quota.weeklyResetAt = resetAt;
672
+ windows += 1;
673
+ } else if (row.type === "monthly") {
674
+ quota.monthlyPercent = percent;
675
+ if (resetAt !== undefined) quota.monthlyResetAt = resetAt;
676
+ windows += 1;
677
+ }
678
+ }
679
+ return windows > 0 ? report(provider, "cline:plan-usage-limits", quota) : null;
680
+ }
681
+
682
+ /**
683
+ * Z.AI GLM Coding Plan `GET /api/monitor/usage/quota/limit` — the coding-plan
684
+ * limits arrive as a `limits` array of `TOKENS_LIMIT` (newer plans call the
685
+ * same rows `CREDIT_LIMIT`) and `TIME_LIMIT` rows. `TOKENS_LIMIT`/`CREDIT_LIMIT`
686
+ * rows carry the window length as `unit`/`number`: unit 3 is hours (number 5 →
687
+ * the rolling five-hour window), unit 6 is weeks (number 1 → the weekly
688
+ * window). `TIME_LIMIT` rows are the monthly MCP tool budget (Web Search / Web
689
+ * Reader / Zread). Every row's `percentage` is the consumed share (falling
690
+ * back to `currentValue`/`usage` when absent) and `nextResetTime` (unix ms)
691
+ * the window reset.
692
+ */
693
+ export function parseZaiQuotaLimits(data: Record<string, unknown> | null): ProviderQuota | null {
694
+ const limits = Array.isArray(data?.limits) ? data.limits as unknown[] : null;
695
+ if (!limits) return null;
696
+ const quota: ProviderQuota = { updatedAt: Date.now() };
697
+ let windows = 0;
698
+ for (const raw of limits) {
699
+ const row = asRecord(raw);
700
+ if (!row) continue;
701
+ const resetAt = normalizeResetAt(row.nextResetTime);
702
+ let percent = normalizePercent(row.percentage);
703
+ if (percent === undefined) {
704
+ const used = toFiniteNumber(row.currentValue);
705
+ const total = toFiniteNumber(row.usage);
706
+ if (used !== undefined && total !== undefined && total > 0) {
707
+ percent = normalizePercent((used / total) * 100);
708
+ }
709
+ }
710
+ if (percent === undefined) continue;
711
+ if (row.type === "TOKENS_LIMIT" || row.type === "CREDIT_LIMIT") {
712
+ const unit = toFiniteNumber(row.unit);
713
+ const number = toFiniteNumber(row.number);
714
+ if (unit === 3 && number === 5) {
715
+ quota.fiveHourPercent = percent;
716
+ if (resetAt !== undefined) quota.fiveHourResetAt = resetAt;
717
+ windows += 1;
718
+ } else if (unit === 6 && number === 1) {
719
+ quota.weeklyPercent = percent;
720
+ if (resetAt !== undefined) quota.weeklyResetAt = resetAt;
721
+ windows += 1;
722
+ }
723
+ } else if (row.type === "TIME_LIMIT") {
724
+ quota.monthlyPercent = percent;
725
+ if (resetAt !== undefined) quota.monthlyResetAt = resetAt;
726
+ windows += 1;
727
+ }
728
+ }
729
+ return windows > 0 ? quota : null;
730
+ }
731
+
732
+ /**
733
+ * Legacy Z.AI payload shape: percent fields with window identifiers directly on
734
+ * the data object (optionally nested under `quota`). Kept as a fallback so
735
+ * older responses keep rendering when the `limits` array is absent.
736
+ */
737
+ function parseZaiQuotaLegacyFields(data: Record<string, unknown> | null): ProviderQuota | null {
738
+ if (!data) return null;
739
+ const quota: ProviderQuota = { updatedAt: Date.now() };
740
+ let windows = 0;
741
+ const percentAt = (key: string): number | undefined => {
742
+ const value = normalizePercent(data[key]);
743
+ if (value !== undefined) return value;
744
+ const nested = asRecord(data.quota);
745
+ return nested ? normalizePercent(nested[key]) : undefined;
746
+ };
747
+ const fiveHour = percentAt("fiveHourPercent") ?? percentAt("fiveHourUsage") ?? percentAt("fiveHourUsed");
748
+ const weekly = percentAt("weeklyPercent") ?? percentAt("weeklyUsage") ?? percentAt("weeklyUsed");
749
+ const monthly = percentAt("monthlyPercent") ?? percentAt("mcpPercent") ?? percentAt("monthlyMCPUsage");
750
+ if (fiveHour !== undefined) {
751
+ quota.fiveHourPercent = fiveHour;
752
+ windows += 1;
753
+ }
754
+ if (weekly !== undefined) {
755
+ quota.weeklyPercent = weekly;
756
+ windows += 1;
757
+ }
758
+ if (monthly !== undefined) {
759
+ quota.monthlyPercent = monthly;
760
+ windows += 1;
761
+ }
762
+ return windows > 0 ? quota : null;
763
+ }
764
+
765
+ /**
766
+ * Fetches the Z.AI GLM Coding Plan quota — on whichever region the provider
767
+ * points at (api.z.ai or open.bigmodel.cn). Authenticates with the API key as
768
+ * a Bearer token per Z.AI's API reference. The `limits` array shape is
769
+ * preferred; older field-name payloads fall back to the legacy parser.
770
+ */
771
+ async function fetchZaiQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
772
+ if (!isCanonicalZaiBaseUrl(config.baseUrl)) return null;
773
+ const apiKey = resolveEnvValue(config.apiKey)?.trim();
774
+ if (!apiKey) return null;
775
+ const normalized = normalizedBaseUrl(config.baseUrl);
776
+ const monitorHost = normalized === ZAI_BASE_URL || normalized === `${ZAI_BASE_URL}/api/coding/paas/v4`
777
+ ? ZAI_BASE_URL
778
+ : ZAI_CN_BASE_URL;
779
+ const response = await fetch(`${monitorHost}/api/monitor/usage/quota/limit`, {
780
+ headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
781
+ redirect: "error",
782
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
783
+ });
784
+ if (!response.ok) {
785
+ return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
786
+ ? TERMINAL_QUOTA_FAILURE
787
+ : null;
788
+ }
789
+ const body = asRecord(await readQuotaJson(response));
790
+ if (!body || body.success === false) return null;
791
+ const data = asRecord(body.data) ?? body;
792
+ const quota = Array.isArray(data?.limits)
793
+ ? parseZaiQuotaLimits(data)
794
+ : parseZaiQuotaLegacyFields(data);
795
+ return quota ? report(provider, "zai:quota-limit", quota) : null;
796
+ }
797
+
798
+ /**
799
+ * MiniMax Token Plan `GET /v1/token_plan/remains` — the subscription's
800
+ * remaining quota as a countdown-time value (ms). The endpoint does not expose
801
+ * the plan's total duration, so no percentage is fabricated from a presumed
802
+ * window: the remaining time is reported as a duration-only window. When the
803
+ * API supplies a total (`total_time` / `plan_duration_ms`), a consumed share
804
+ * is derived from it. Region selects the host: `minimax` → www.minimax.io,
805
+ * `minimax-cn` → api.minimaxi.com.
806
+ */
807
+ async function fetchMinimaxQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
808
+ if (!isCanonicalMinimaxBaseUrl(config.baseUrl)) return null;
809
+ const apiKey = resolveEnvValue(config.apiKey)?.trim();
810
+ if (!apiKey) return null;
811
+ const cnHost = normalizedBaseUrl(config.baseUrl)?.startsWith("https://api.minimaxi.com");
812
+ const remainsUrl = cnHost ? "https://api.minimaxi.com/v1/token_plan/remains" : MINIMAX_REMAINS_URL;
813
+ const response = await fetch(remainsUrl, {
814
+ headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
815
+ redirect: "error",
816
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
817
+ });
818
+ if (!response.ok) {
819
+ return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
820
+ ? TERMINAL_QUOTA_FAILURE
821
+ : null;
822
+ }
823
+ const body = asRecord(await readQuotaJson(response));
824
+ if (!body || body.success === false) return null;
825
+ const data = asRecord(body.data) ?? body;
826
+ const remainsMs = toFiniteNumber(data.remains_time ?? data.remainsTime);
827
+ if (remainsMs === undefined || remainsMs < 0) return null;
828
+ const hours = Math.floor(remainsMs / 3_600_000);
829
+ const label = `Token Plan remaining (${hours}h)`;
830
+ // Only derive a consumed share when the API actually reports the plan total;
831
+ // a presumed window (e.g. 30 days) would fabricate utilization. A valid
832
+ // response that omits the total after a prior refresh had it is a DELIBERATE
833
+ // contract change — the old row must be dropped (terminal), not preserved as
834
+ // a transient last-good.
835
+ const totalMs = toFiniteNumber(data.total_time ?? data.plan_duration_ms ?? data.total_duration_ms);
836
+ if (totalMs === undefined || totalMs <= 0) return TERMINAL_QUOTA_FAILURE;
837
+ const consumed = Math.max(0, totalMs - remainsMs);
838
+ const percent = normalizePercent((consumed / totalMs) * 100);
839
+ if (percent === undefined) return null;
840
+ return report(provider, "minimax:token-plan-remains", {
841
+ customWindows: [{ label, percent }],
842
+ updatedAt: Date.now(),
843
+ });
844
+ }
845
+
846
+ /**
847
+ * Moonshot/Kimi `GET /v1/users/me/balance` — the account's available balance
848
+ * (voucher + cash). Renders a single balance window against the sum of
849
+ * voucher + cash when positive (there is no per-window rate limit to meter).
850
+ */
851
+ async function fetchMoonshotQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
852
+ if (!isCanonicalMoonshotBaseUrl(config.baseUrl)) return null;
853
+ const apiKey = resolveEnvValue(config.apiKey)?.trim();
854
+ if (!apiKey) return null;
855
+ const host = normalizedBaseUrl(config.baseUrl)?.startsWith("https://api.moonshot.cn") ? "https://api.moonshot.cn/v1" : MOONSHOT_BASE_URL;
856
+ const response = await fetch(`${host}/users/me/balance`, {
857
+ headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
858
+ redirect: "error",
859
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
860
+ });
861
+ if (!response.ok) {
862
+ return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
863
+ ? TERMINAL_QUOTA_FAILURE
864
+ : null;
865
+ }
866
+ const body = asRecord(await readQuotaJson(response));
867
+ const data = asRecord(body?.data) ?? body;
868
+ if (!data) return null;
869
+ const available = toFiniteNumber(data.available_balance);
870
+ const voucher = toFiniteNumber(data.voucher_balance);
871
+ const cash = toFiniteNumber(data.cash_balance);
872
+ if (available === undefined || available < 0) return null;
873
+ // Moonshot exposes no per-window quota ceiling, only a balance — report it
874
+ // as a balance-only window (percent 0) rather than a fabricated utilization.
875
+ // Currency is host-scoped: China platform (api.moonshot.cn) bills in CNY;
876
+ // the international platform (api.moonshot.ai) bills in USD. Do not force
877
+ // either side into the other unit — the number is correct, only the unit
878
+ // must match the host.
879
+ const isChinaHost = host.startsWith("https://api.moonshot.cn");
880
+ const money = (n: number) => isChinaHost ? `¥${n.toFixed(2)}` : `$${n.toFixed(2)}`;
881
+ const unit = isChinaHost ? "CNY" : "USD";
882
+ const label = voucher !== undefined && cash !== undefined
883
+ ? `Balance (${money(available)} ${unit} available, ${money(voucher)} voucher)`
884
+ : `Balance (${money(available)} ${unit} available)`;
885
+ return report(provider, "moonshot:balance", {
886
+ customWindows: [{ label, percent: 0 }],
887
+ updatedAt: Date.now(),
888
+ });
889
+ }
890
+
891
+ /**
892
+ * Venice `GET /api/v1/billing/balance` — DIEM (native credits) or USD balance.
893
+ * Shows the remaining balance; epoch allocation progress when present.
894
+ */
895
+ async function fetchVeniceQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
896
+ if (!isCanonicalVeniceBaseUrl(config.baseUrl)) return null;
897
+ const apiKey = resolveEnvValue(config.apiKey)?.trim();
898
+ if (!apiKey) return null;
899
+ const response = await fetch(`${VENICE_BASE_URL}/billing/balance`, {
900
+ headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
901
+ redirect: "error",
902
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
903
+ });
904
+ if (!response.ok) {
905
+ return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
906
+ ? TERMINAL_QUOTA_FAILURE
907
+ : null;
908
+ }
909
+ const body = asRecord(await readQuotaJson(response));
910
+ const data = asRecord(body?.data) ?? body;
911
+ if (!data) return null;
912
+ const diemBalance = toFiniteNumber(data.balance);
913
+ const usdBalance = toFiniteNumber(data.balance_usd);
914
+ const epochUsed = toFiniteNumber(data.diem_epoch_used);
915
+ const epochAllocated = toFiniteNumber(data.diem_epoch_allocated);
916
+ if (diemBalance === undefined && usdBalance === undefined) return null;
917
+ const label = diemBalance !== undefined
918
+ ? `DIEM balance (${Math.round(diemBalance)})`
919
+ : `USD balance ($${usdBalance?.toFixed(2) ?? "?"})`;
920
+ if (epochAllocated !== undefined && epochAllocated > 0 && epochUsed !== undefined) {
921
+ const percent = normalizePercent((epochUsed / epochAllocated) * 100);
922
+ if (percent === undefined) return null;
923
+ return report(provider, "venice:billing-balance", {
924
+ customWindows: [{ label, percent }],
925
+ updatedAt: Date.now(),
926
+ });
927
+ }
928
+ return report(provider, "venice:billing-balance", {
929
+ customWindows: [{ label, percent: 0 }],
930
+ updatedAt: Date.now(),
931
+ });
932
+ }
933
+
934
+ /**
935
+ * Synthetic `GET /v2/quotas` — the known quota lanes (rolling 5-hour,
936
+ * weekly token, search-hourly) mapped onto the quota windows.
937
+ */
938
+ async function fetchSyntheticQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
939
+ if (!isCanonicalSyntheticBaseUrl(config.baseUrl)) return null;
940
+ const apiKey = resolveEnvValue(config.apiKey)?.trim();
941
+ if (!apiKey) return null;
942
+ const response = await fetch(`${SYNTHETIC_BASE_URL}/quotas`, {
943
+ headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
944
+ redirect: "error",
945
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
946
+ });
947
+ if (!response.ok) {
948
+ return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
949
+ ? TERMINAL_QUOTA_FAILURE
950
+ : null;
951
+ }
952
+ const body = asRecord(await readQuotaJson(response));
953
+ const data = asRecord(body?.data) ?? body;
954
+ const quota: ProviderQuota = { updatedAt: Date.now() };
955
+ let windows = 0;
956
+ const percentAt = (key: string): number | undefined => {
957
+ const value = normalizePercent(data?.[key]);
958
+ if (value !== undefined) return value;
959
+ const nested = asRecord(data?.quota) ?? asRecord(data?.quotas);
960
+ return nested ? normalizePercent(nested[key]) : undefined;
961
+ };
962
+ const fiveHour = percentAt("rollingFiveHourLimit");
963
+ const weekly = percentAt("weeklyTokenLimit");
964
+ if (fiveHour !== undefined) {
965
+ quota.fiveHourPercent = fiveHour;
966
+ windows += 1;
967
+ }
968
+ if (weekly !== undefined) {
969
+ quota.weeklyPercent = weekly;
970
+ windows += 1;
971
+ }
972
+ const search = asRecord(data?.search);
973
+ const searchHourly = search ? normalizePercent(search.hourly) : undefined;
974
+ if (searchHourly !== undefined) {
975
+ quota.customWindows = [...(quota.customWindows ?? []), { label: "Search hourly", percent: searchHourly }];
976
+ windows += 1;
977
+ }
978
+ return windows > 0 ? report(provider, "synthetic:quotas", quota) : null;
979
+ }
980
+
981
+ /**
982
+ * DeepInfra `GET /payment/checklist?compute_owed=true` — prepaid balance,
983
+ * recent spend, spending limit, and suspension state. Renders a balance
984
+ * window (prepaid funds are a negative `stripe_balance` → positive available).
985
+ */
986
+ async function fetchDeepInfraQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
987
+ if (!isCanonicalDeepInfraBaseUrl(config.baseUrl)) return null;
988
+ const apiKey = resolveEnvValue(config.apiKey)?.trim();
989
+ if (!apiKey) return null;
990
+ const response = await fetch(`${DEEPINFRA_BASE_URL}/payment/checklist?compute_owed=true`, {
991
+ headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
992
+ redirect: "error",
993
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
994
+ });
995
+ if (!response.ok) {
996
+ return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
997
+ ? TERMINAL_QUOTA_FAILURE
998
+ : null;
999
+ }
1000
+ const body = asRecord(await readQuotaJson(response));
1001
+ const data = asRecord(body?.data) ?? body;
1002
+ if (!data) return null;
1003
+ const stripeBalance = toFiniteNumber(data.stripe_balance);
1004
+ const spendLimit = toFiniteNumber(data.spending_limit);
1005
+ const total = toFiniteNumber(data.total_amount_due);
1006
+ if (stripeBalance === undefined) return null;
1007
+ // Prepaid funds are negative; a positive value is money owed.
1008
+ const available = stripeBalance < 0 ? -stripeBalance : 0;
1009
+ if (spendLimit !== undefined && spendLimit > 0) {
1010
+ const spent = total !== undefined && total > 0 ? total : Math.max(0, spendLimit - available);
1011
+ const percent = normalizePercent((spent / spendLimit) * 100);
1012
+ if (percent === undefined) return null;
1013
+ return report(provider, "deepinfra:billing-checklist", {
1014
+ customWindows: [{ label: `Billing cycle spend ($${spent.toFixed(2)} of $${spendLimit.toFixed(2)})`, percent }],
1015
+ updatedAt: Date.now(),
1016
+ });
1017
+ }
1018
+ return report(provider, "deepinfra:billing-checklist", {
1019
+ customWindows: [{ label: `Prepaid balance ($${available.toFixed(2)})`, percent: 0 }],
1020
+ updatedAt: Date.now(),
1021
+ });
1022
+ }
1023
+
1024
+ /**
1025
+ * Neuralwatt `GET /v1/quota` — subscription kWh usage (primary window) and
1026
+ * prepaid USD credit balance (secondary).
1027
+ */
1028
+ async function fetchNeuralwattQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
1029
+ if (!isCanonicalNeuralwattBaseUrl(config.baseUrl)) return null;
1030
+ const apiKey = resolveEnvValue(config.apiKey)?.trim();
1031
+ if (!apiKey) return null;
1032
+ const response = await fetch(`${NEURALWATT_BASE_URL}/quota`, {
1033
+ headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
1034
+ redirect: "error",
1035
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
1036
+ });
1037
+ if (!response.ok) {
1038
+ return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
1039
+ ? TERMINAL_QUOTA_FAILURE
1040
+ : null;
1041
+ }
1042
+ const body = asRecord(await readQuotaJson(response));
1043
+ const data = asRecord(body?.data) ?? body;
1044
+ const quota: ProviderQuota = { updatedAt: Date.now() };
1045
+ let windows = 0;
1046
+ const subscription = asRecord(data?.subscription);
1047
+ const kwhUsed = subscription ? toFiniteNumber(subscription.kwh_used) : undefined;
1048
+ const kwhIncluded = subscription ? toFiniteNumber(subscription.kwh_included) : undefined;
1049
+ if (kwhUsed !== undefined && kwhIncluded !== undefined && kwhIncluded > 0) {
1050
+ const percent = normalizePercent((kwhUsed / kwhIncluded) * 100);
1051
+ if (percent !== undefined) {
1052
+ quota.fiveHourPercent = percent;
1053
+ const periodEnd = subscription ? normalizeResetAt(subscription.current_period_end) : undefined;
1054
+ if (periodEnd !== undefined) quota.fiveHourResetAt = periodEnd;
1055
+ windows += 1;
1056
+ }
1057
+ }
1058
+ const balance = asRecord(data?.balance);
1059
+ const totalCredits = balance ? toFiniteNumber(balance.total_credits_usd) : undefined;
1060
+ const remainingCredits = balance ? toFiniteNumber(balance.credits_remaining_usd) : undefined;
1061
+ if (totalCredits !== undefined && totalCredits > 0 && remainingCredits !== undefined) {
1062
+ // Utilization is CONSUMED credits, not the remaining share.
1063
+ const used = Math.max(0, totalCredits - remainingCredits);
1064
+ const percent = normalizePercent((used / totalCredits) * 100);
1065
+ if (percent !== undefined) {
1066
+ quota.customWindows = [...(quota.customWindows ?? []), { label: "Prepaid credits", percent }];
1067
+ windows += 1;
1068
+ }
1069
+ }
1070
+ return windows > 0 ? report(provider, "neuralwatt:quota", quota) : null;
1071
+ }
1072
+
1073
+ function report(
1074
+ provider: string,
1075
+ source: string,
1076
+ quota: ProviderQuota,
1077
+ aggregation?: CodexCapacityAggregation,
1078
+ ): ProviderQuotaReport | null {
1079
+ if (!hasQuotaRows(quota)) return null;
1080
+ return {
1081
+ provider,
1082
+ label: providerLabel(provider),
1083
+ source,
1084
+ quota,
1085
+ updatedAt: quota.updatedAt,
1086
+ ...(aggregation ? { aggregation } : {}),
1087
+ };
1088
+ }
1089
+
1090
+ function tagNativeMainReport(
1091
+ value: ProviderQuotaReport | null,
1092
+ generation: number,
1093
+ ): ProviderQuotaReport | null {
1094
+ if (value) nativeMainReportGenerations.set(value, generation);
1095
+ return value;
1096
+ }
1097
+
1098
+ function isProviderQuotaReportCurrent(value: ProviderQuotaReport): boolean {
1099
+ const generation = nativeMainReportGenerations.get(value);
1100
+ return generation === undefined || isMainAccountIdentityGenerationLive(generation);
1101
+ }
1102
+
1103
+ async function fetchChatGptForwardQuota(
1104
+ config: OcxConfig,
1105
+ provider: string,
1106
+ providerConfig: OcxProviderConfig,
1107
+ forceRefresh: boolean,
1108
+ prefetchedSnapshot?: CodexAuthAccountsSnapshotPromise,
1109
+ ): Promise<ProviderQuotaReport | null> {
1110
+ if (providerCodexAccountMode(provider, providerConfig) === "direct") {
1111
+ const snapshot = await fetchMainAccountInfoSnapshot(forceRefresh);
1112
+ const quota = snapshot.info.quota
1113
+ ? { ...snapshot.info.quota, updatedAt: Date.now() } as ProviderQuota
1114
+ : null;
1115
+ return quota
1116
+ ? tagNativeMainReport(report(provider, "chatgpt:wham", quota), snapshot.mainIdentityGeneration)
1117
+ : null;
1118
+ }
1119
+ const snapshot = await (prefetchedSnapshot ?? listCodexAuthAccountsSnapshot(config, forceRefresh));
1120
+ const accounts = snapshot.accounts;
1121
+ const activeId = effectiveCodexAuthAccountId(config);
1122
+ const capacityAccounts = accounts.map(account => ({ ...account, active: account.id === activeId }));
1123
+ const active = capacityAccounts.find(account => account.active)
1124
+ ?? accounts.find(account => account.id === MAIN_CODEX_ACCOUNT_ID)
1125
+ ?? accounts[0];
1126
+ const now = Date.now();
1127
+ const capacity = aggregateCodexPoolCapacity(capacityAccounts, now);
1128
+ if (capacity.aggregation && capacity.quota) {
1129
+ return tagNativeMainReport(
1130
+ report(
1131
+ provider,
1132
+ "chatgpt:wham",
1133
+ capacity.quota as ProviderQuota,
1134
+ publicCapacityAggregation(capacity.aggregation, "aggregate"),
1135
+ ),
1136
+ snapshot.mainIdentityGeneration,
1137
+ );
1138
+ }
1139
+ const activeUsable = !!active && !active.paused && active.needsReauth !== true;
1140
+ const quota = activeUsable && active?.quota
1141
+ ? { ...active.quota, updatedAt: active.quota.updatedAt ?? Date.now() } as CodexCapacityQuota
1142
+ : null;
1143
+ const quotaFresh = !!quota
1144
+ && Number.isFinite(quota.updatedAt)
1145
+ && now - quota.updatedAt < CODEX_CAPACITY_MAX_QUOTA_AGE_MS;
1146
+ if (quota && quotaFresh) {
1147
+ const fallback = report(
1148
+ provider,
1149
+ "chatgpt:wham",
1150
+ quota as ProviderQuota,
1151
+ capacity.aggregation
1152
+ ? publicCapacityAggregation(capacity.aggregation, "effective-account-fallback")
1153
+ : undefined,
1154
+ );
1155
+ return tagNativeMainReport(fallback, snapshot.mainIdentityGeneration);
1156
+ }
1157
+ if (capacity.aggregation) {
1158
+ const updatedAt = Date.now();
1159
+ return tagNativeMainReport(
1160
+ {
1161
+ provider,
1162
+ label: providerLabel(provider),
1163
+ source: "chatgpt:wham",
1164
+ quota: { updatedAt },
1165
+ updatedAt,
1166
+ aggregation: publicCapacityAggregation(capacity.aggregation, "coverage-only"),
1167
+ },
1168
+ snapshot.mainIdentityGeneration,
1169
+ );
1170
+ }
1171
+ return null;
1172
+ }
1173
+
1174
+ function centsValue(value: unknown): number | undefined {
1175
+ const rec = asRecord(value);
1176
+ return rec ? toFiniteNumber(rec.val) : undefined;
1177
+ }
1178
+
1179
+ /** Decode JWT payload `sub` for xAI weekly credits when the stored credential lacks accountId. */
1180
+ function xaiUserIdFromAccessToken(accessToken: string): string | undefined {
1181
+ const parts = accessToken.split(".");
1182
+ if (parts.length < 2 || !parts[1]) return undefined;
1183
+ try {
1184
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8")) as { sub?: unknown };
1185
+ return typeof payload.sub === "string" && payload.sub.trim() ? payload.sub.trim() : undefined;
1186
+ } catch {
1187
+ return undefined;
1188
+ }
1189
+ }
1190
+
1191
+ /**
1192
+ * Grok Build weekly credits envelope:
1193
+ * `{ config: { creditUsagePercent?, currentPeriod: { type: "USAGE_PERIOD_TYPE_WEEKLY", end } } }`.
1194
+ * Omitted percent is treated as 0 (proto3 default).
1195
+ */
1196
+ export function parseXaiCreditsResponse(value: unknown): { percent: number; resetAt?: number } | null {
1197
+ const body = asRecord(value);
1198
+ const config = asRecord(body?.config);
1199
+ if (!config) return null;
1200
+ const period = asRecord(config.currentPeriod);
1201
+ if (!period || period.type !== "USAGE_PERIOD_TYPE_WEEKLY") return null;
1202
+ const resetAt = normalizeResetAt(period.end);
1203
+ if (resetAt === undefined) return null;
1204
+ if (config.creditUsagePercent !== undefined) {
1205
+ const percent = normalizePercent(config.creditUsagePercent);
1206
+ if (percent === undefined) return null;
1207
+ return { percent, resetAt };
1208
+ }
1209
+ return { percent: 0, resetAt };
1210
+ }
1211
+
1212
+ async function fetchXaiWeeklyCredits(accessToken: string, userId: string): Promise<ProviderQuota | null> {
1213
+ try {
1214
+ const response = await fetch(XAI_CREDITS_URL, {
1215
+ headers: {
1216
+ Accept: "application/json",
1217
+ Authorization: `Bearer ${accessToken}`,
1218
+ [XAI_GROK_COMPATIBILITY.headers.tokenAuth]: "xai-grok-cli",
1219
+ [XAI_GROK_COMPATIBILITY.headers.authenticateResponse]: "authenticate-response",
1220
+ "x-userid": userId,
1221
+ [XAI_GROK_COMPATIBILITY.headers.clientVersion]: XAI_GROK_CLIENT_VERSION,
1222
+ },
1223
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
1224
+ });
1225
+ if (!response.ok) return null;
1226
+ const parsed = parseXaiCreditsResponse(await readQuotaJson(response));
1227
+ if (!parsed) return null;
1228
+ return {
1229
+ weeklyPercent: parsed.percent,
1230
+ ...(parsed.resetAt !== undefined ? { weeklyResetAt: parsed.resetAt } : {}),
1231
+ updatedAt: Date.now(),
1232
+ };
1233
+ } catch {
1234
+ return null;
1235
+ }
1236
+ }
1237
+
1238
+ async function fetchXaiQuota(provider: string): Promise<ProviderQuotaReport | null> {
1239
+ let accessToken: string;
1240
+ try {
1241
+ accessToken = await getValidAccessToken("xai");
1242
+ } catch {
1243
+ return null;
1244
+ }
1245
+
1246
+ // Prefer the SuperGrok weekly credits window that actually gates prompting (#1283).
1247
+ const userId = getCredential("xai")?.accountId?.trim() || xaiUserIdFromAccessToken(accessToken);
1248
+ if (userId) {
1249
+ const weekly = await fetchXaiWeeklyCredits(accessToken, userId);
1250
+ if (weekly) return report(provider, "xai:grok-billing-credits", weekly);
1251
+ }
1252
+
1253
+ // Legacy monthly dollar pool — retained when weekly is unavailable.
1254
+ try {
1255
+ const response = await fetch(XAI_BILLING_URL, {
1256
+ headers: { Accept: "application/json", Authorization: `Bearer ${accessToken}` },
1257
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
1258
+ });
1259
+ if (!response.ok) return null;
1260
+ const body = asRecord(await readQuotaJson(response));
1261
+ const config = asRecord(body?.config);
1262
+ if (!config) return null;
1263
+ const limitCents = centsValue(config.monthlyLimit);
1264
+ const usedCents = centsValue(config.used);
1265
+ if (limitCents === undefined || usedCents === undefined || limitCents <= 0) return null;
1266
+ const percent = normalizePercent((usedCents / limitCents) * 100);
1267
+ if (percent === undefined) return null;
1268
+ return report(provider, "xai:grok-billing", {
1269
+ monthlyPercent: percent,
1270
+ monthlyResetAt: normalizeResetAt(config.billingPeriodEnd),
1271
+ updatedAt: Date.now(),
1272
+ });
1273
+ } catch {
1274
+ return null;
1275
+ }
1276
+ }
1277
+
1278
+ function parseClaudeBucket(value: unknown): { percent?: number; resetAt?: number } | null {
1279
+ const rec = asRecord(value);
1280
+ if (!rec) return null;
1281
+ const percent = normalizePercent(rec.utilization);
1282
+ const resetAt = normalizeResetAt(rec.resets_at);
1283
+ if (percent === undefined && resetAt === undefined) return null;
1284
+ return { percent, resetAt };
1285
+ }
1286
+
1287
+ /** Claude's OAuth usage endpoint, probed with ONE account's own bearer token. */
1288
+ const anthropicUsageInflight = new Map<string, Promise<ProviderQuota | null>>();
1289
+
1290
+ async function fetchAnthropicUsageQuota(accessToken: string): Promise<ProviderQuota | null> {
1291
+ const joinable = anthropicUsageInflight.get(accessToken);
1292
+ if (joinable) return joinable;
1293
+
1294
+ const probe = (async (): Promise<ProviderQuota | null> => {
1295
+ const response = await fetch("https://api.anthropic.com/api/oauth/usage", {
1296
+ headers: {
1297
+ Accept: "application/json, text/plain, */*",
1298
+ "Content-Type": "application/json",
1299
+ "User-Agent": "claude-cli/2.1.63 (external, cli)",
1300
+ "anthropic-beta": "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05",
1301
+ Authorization: `Bearer ${accessToken}`,
1302
+ },
1303
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
1304
+ });
1305
+ if (!response.ok) return null;
1306
+ const body = asRecord(await readQuotaJson(response));
1307
+ if (!body) return null;
1308
+ const fiveHour = parseClaudeBucket(body.five_hour);
1309
+ const sevenDay = parseClaudeBucket(body.seven_day);
1310
+ const opus = parseClaudeBucket(body.seven_day_opus);
1311
+ const sonnet = parseClaudeBucket(body.seven_day_sonnet);
1312
+ const customWindows: ProviderQuotaWindow[] = [];
1313
+ if (opus?.percent !== undefined) customWindows.push({ label: "Opus", percent: opus.percent, ...(opus.resetAt !== undefined ? { resetAt: opus.resetAt } : {}) });
1314
+ if (sonnet?.percent !== undefined) customWindows.push({ label: "Sonnet", percent: sonnet.percent, ...(sonnet.resetAt !== undefined ? { resetAt: sonnet.resetAt } : {}) });
1315
+ const quota: ProviderQuota = {
1316
+ // Claude's 5-hour window is a first-class rate limit, same as the Codex login 5h/weekly
1317
+ // rows: report it in the canonical fields so the dashboard renders it with the standard
1318
+ // "5-hour limit" label and ordering instead of as a generic extra window.
1319
+ ...(fiveHour?.percent !== undefined ? { fiveHourPercent: fiveHour.percent } : {}),
1320
+ ...(fiveHour?.resetAt !== undefined ? { fiveHourResetAt: fiveHour.resetAt } : {}),
1321
+ ...(sevenDay?.percent !== undefined ? { weeklyPercent: sevenDay.percent } : {}),
1322
+ ...(sevenDay?.resetAt !== undefined ? { weeklyResetAt: sevenDay.resetAt } : {}),
1323
+ ...(customWindows.length > 0 ? { customWindows } : {}),
1324
+ updatedAt: Date.now(),
1325
+ };
1326
+ // Empty / schema-changed payloads must not cache as "success with no bars".
1327
+ return hasQuotaRows(quota) ? quota : null;
1328
+ })().finally(() => {
1329
+ if (anthropicUsageInflight.get(accessToken) === probe) anthropicUsageInflight.delete(accessToken);
1330
+ });
1331
+ anthropicUsageInflight.set(accessToken, probe);
1332
+ return probe;
1333
+ }
1334
+
1335
+ async function fetchAnthropicQuota(provider: string): Promise<ProviderQuotaReport | null> {
1336
+ // Capture the account we intend to probe before awaiting — a mid-flight active
1337
+ // switch must not seed the wrong account's cache with this response.
1338
+ const probedAccountId = getAccountSet("anthropic")?.activeAccountId;
1339
+ const probedAccountKey = probedAccountId ? accountCacheKey("anthropic", probedAccountId) : null;
1340
+ const writerGeneration = captureConfigGeneration();
1341
+ let accessToken: string;
1342
+ try {
1343
+ accessToken = await getValidAccessToken("anthropic");
1344
+ } catch {
1345
+ return null;
1346
+ }
1347
+ const quota = await fetchAnthropicUsageQuota(accessToken);
1348
+ if (!quota) return null;
1349
+ // Share the active-account probe with the per-account cache so Providers-page
1350
+ // loads do not double-hit Anthropic's rate-limited usage endpoint.
1351
+ if (probedAccountId && probedAccountKey) {
1352
+ const stillOwnsToken = getAccountCredential("anthropic", probedAccountId)?.access === accessToken;
1353
+ if (stillOwnsToken && mayCommitAccountQuotaKey(probedAccountKey, writerGeneration)) {
1354
+ accountQuotaCache.set(probedAccountKey, { ts: Date.now(), quota });
1355
+ }
1356
+ }
1357
+ return report(provider, "anthropic:oauth-usage", quota);
1358
+ }
1359
+
1360
+ // ---------------------------------------------------------------------------
1361
+ // Per-account quota (multiauth)
1362
+ // ---------------------------------------------------------------------------
1363
+
1364
+ /**
1365
+ * Anthropic reports usage per CREDENTIAL, so every logged-in account can be probed with its
1366
+ * own bearer token — the active-account selection and the local usage log are irrelevant here.
1367
+ * Mirrors the Codex pool behaviour (codex/auth-api.ts:fetchPoolAccountQuota), including a
1368
+ * per-account TTL so N accounts cost at most N upstream calls per window.
1369
+ *
1370
+ * The TTL is deliberately longer than the provider-level one: this path multiplies by account
1371
+ * count, and Anthropic rate-limits the usage endpoint (observed 429 under repeated probing).
1372
+ */
1373
+ const ACCOUNT_QUOTA_TTL_MS = 10 * 60_000;
1374
+ type AccountQuotaCacheEntry = {
1375
+ ts: number;
1376
+ quota: ProviderQuota | null;
1377
+ /** Last probe failed (429 / network / expired login); still may hold last-good quota. */
1378
+ unavailable?: true;
1379
+ };
1380
+ const accountQuotaCache = new Map<string, AccountQuotaCacheEntry>();
1381
+ const accountQuotaInflight = new Map<string, Promise<AccountQuotaCacheEntry>>();
1382
+ let lastReconciledGeneration = 0;
1383
+ let liveAccountQuotaKeys = new Set<string>();
1384
+ let liveProviderQuotaKeys = new Set<string>();
1385
+
1386
+ function mayCommitAccountQuotaKey(key: string, writerGeneration: number): boolean {
1387
+ return writerGeneration >= lastReconciledGeneration || liveAccountQuotaKeys.has(key);
1388
+ }
1389
+
1390
+ function mayCommitProviderQuotaKey(key: string, writerGeneration: number): boolean {
1391
+ return writerGeneration >= lastReconciledGeneration || liveProviderQuotaKeys.has(key);
1392
+ }
1393
+
1394
+ export interface ProviderAccountQuota {
1395
+ accountId: string;
1396
+ quota: ProviderQuota | null;
1397
+ /** Set when the probe could not reach upstream (expired login, 429, network). */
1398
+ unavailable?: true;
1399
+ }
1400
+
1401
+ /** Providers whose per-account quota can be probed. Extend as other OAuth APIs are covered. */
1402
+ export function supportsPerAccountQuota(provider: string): boolean {
1403
+ return provider === "anthropic";
1404
+ }
1405
+
1406
+ function accountCacheKey(provider: string, accountId: string): string {
1407
+ return `${provider}\u0000${accountId}`;
1408
+ }
1409
+
1410
+ /**
1411
+ * Synchronous last-good per-account quota read for routing. Never probes the network.
1412
+ * Returns null when nothing is cached (or the cached row has no bars).
1413
+ */
1414
+ export function getCachedProviderAccountQuota(provider: string, accountId: string): ProviderQuota | null {
1415
+ const entry = accountQuotaCache.get(accountCacheKey(provider, accountId));
1416
+ return entry?.quota ?? null;
1417
+ }
1418
+
1419
+ /** Test-only: seed or clear the per-account quota cache without probing upstream. */
1420
+ export function setCachedProviderAccountQuotaForTests(
1421
+ provider: string,
1422
+ accountId: string,
1423
+ quota: ProviderQuota | null,
1424
+ ): void {
1425
+ const key = accountCacheKey(provider, accountId);
1426
+ if (quota === null) {
1427
+ accountQuotaCache.delete(key);
1428
+ return;
1429
+ }
1430
+ accountQuotaCache.set(key, { ts: Date.now(), quota });
1431
+ }
1432
+
1433
+ export function sweepExpiredProviderAccountQuotaRows(now = Date.now()): number {
1434
+ let removed = 0;
1435
+ for (const [key, entry] of accountQuotaCache) {
1436
+ if (entry.ts + ACCOUNT_QUOTA_TTL_MS > now) continue;
1437
+ accountQuotaCache.delete(key);
1438
+ removed += 1;
1439
+ }
1440
+ return removed;
1441
+ }
1442
+
1443
+ export function reconcileProviderAccountQuotaRows(context: GenerationContext): number {
1444
+ if (context.generation <= lastReconciledGeneration) return 0;
1445
+ let removed = 0;
1446
+ for (const key of accountQuotaCache.keys()) {
1447
+ if (context.oauthAccountKeys.has(key)) continue;
1448
+ accountQuotaCache.delete(key);
1449
+ removed += 1;
1450
+ }
1451
+ if (cache) {
1452
+ const reports = cache.response.reports.filter(report => context.providerNames.has(report.provider));
1453
+ removed += cache.response.reports.length - reports.length;
1454
+ cache = { ...cache, response: { ...cache.response, reports } };
1455
+ }
1456
+ liveAccountQuotaKeys = new Set(context.oauthAccountKeys);
1457
+ liveProviderQuotaKeys = new Set(context.providerNames);
1458
+ lastReconciledGeneration = context.generation;
1459
+ return removed;
1460
+ }
1461
+
1462
+ /** Test-only reset so a direct reconcile call in one file cannot leak across files. */
1463
+ export function resetProviderQuotaReconcileStateForTests(): void {
1464
+ lastReconciledGeneration = 0;
1465
+ liveAccountQuotaKeys = new Set();
1466
+ liveProviderQuotaKeys = new Set();
1467
+ }
1468
+
1469
+ /** Drop cached per-account rows (all, or just one provider's). */
1470
+ export function clearAccountQuotaCache(provider?: string): void {
1471
+ if (!provider) {
1472
+ accountQuotaCache.clear();
1473
+ accountQuotaInflight.clear();
1474
+ return;
1475
+ }
1476
+ const prefix = `${provider}\u0000`;
1477
+ for (const key of [...accountQuotaCache.keys()]) {
1478
+ if (key.startsWith(prefix)) accountQuotaCache.delete(key);
1479
+ }
1480
+ // Drop in-flight probes too so a late resolve cannot repopulate after logout/remove.
1481
+ for (const key of [...accountQuotaInflight.keys()]) {
1482
+ if (key.startsWith(prefix)) accountQuotaInflight.delete(key);
1483
+ }
1484
+ }
1485
+
1486
+ /**
1487
+ * Resolve a bearer for quota probing without silently adopting a newer global
1488
+ * Claude CLI credential into a background multiauth slot.
1489
+ *
1490
+ * - Fresh stored access → use as-is (no refresh).
1491
+ * - Active account with expired access → normal refresh path.
1492
+ * - Background `local-cli` with expired access → fail closed (unavailable):
1493
+ * `getValidAccessTokenForAccount` can persist a mismatched Claude CLI identity.
1494
+ * - Background ordinary OAuth (`source !== "local-cli"`) → safe to refresh;
1495
+ * Anthropic's lock only adopts disk credentials for `local-cli` rows.
1496
+ */
1497
+ async function getTokenForAccountQuotaProbe(provider: string, accountId: string): Promise<string> {
1498
+ const stored = getAccountCredential(provider, accountId);
1499
+ if (!stored) throw new Error("account credential missing");
1500
+ if (stored.expires > Date.now() + ACCOUNT_TOKEN_SKEW_MS) return stored.access;
1501
+ const activeId = getAccountSet(provider)?.activeAccountId;
1502
+ if (activeId !== accountId && stored.source === "local-cli") {
1503
+ throw new Error("background local-cli token expired; skip CLI-adopting refresh for quota probe");
1504
+ }
1505
+ return getValidAccessTokenForAccount(provider, accountId);
1506
+ }
1507
+
1508
+ async function fetchAccountQuota(
1509
+ provider: string,
1510
+ accountId: string,
1511
+ forceRefresh: boolean,
1512
+ ): Promise<AccountQuotaCacheEntry> {
1513
+ const key = accountCacheKey(provider, accountId);
1514
+ const writerGeneration = captureConfigGeneration();
1515
+ const cached = accountQuotaCache.get(key);
1516
+ if (!forceRefresh && cached && Date.now() - cached.ts < ACCOUNT_QUOTA_TTL_MS) return cached;
1517
+ const joinable = accountQuotaInflight.get(key);
1518
+ if (joinable) return joinable;
1519
+
1520
+ const probe = (async (): Promise<AccountQuotaCacheEntry> => {
1521
+ try {
1522
+ const token = await getTokenForAccountQuotaProbe(provider, accountId);
1523
+ const quota = await fetchAnthropicUsageQuota(token);
1524
+ if (!quota) {
1525
+ // Preserve last-good bars and mark unavailable; advance TTL so failures
1526
+ // negative-cache instead of re-probing on every GUI poll.
1527
+ const entry: AccountQuotaCacheEntry = {
1528
+ ts: Date.now(),
1529
+ quota: cached?.quota ?? null,
1530
+ unavailable: true,
1531
+ };
1532
+ if (mayCommitAccountQuotaKey(key, writerGeneration)) {
1533
+ accountQuotaCache.set(key, entry);
1534
+ sweepExpiredOnWrite(entry.ts);
1535
+ }
1536
+ return entry;
1537
+ }
1538
+ const entry: AccountQuotaCacheEntry = { ts: Date.now(), quota };
1539
+ if (mayCommitAccountQuotaKey(key, writerGeneration)) {
1540
+ accountQuotaCache.set(key, entry);
1541
+ sweepExpiredOnWrite(entry.ts);
1542
+ }
1543
+ return entry;
1544
+ } catch {
1545
+ const entry: AccountQuotaCacheEntry = {
1546
+ ts: Date.now(),
1547
+ quota: cached?.quota ?? null,
1548
+ unavailable: true,
1549
+ };
1550
+ if (mayCommitAccountQuotaKey(key, writerGeneration)) {
1551
+ accountQuotaCache.set(key, entry);
1552
+ sweepExpiredOnWrite(entry.ts);
1553
+ }
1554
+ return entry;
1555
+ }
1556
+ })().finally(() => {
1557
+ if (accountQuotaInflight.get(key) === probe) accountQuotaInflight.delete(key);
1558
+ });
1559
+ accountQuotaInflight.set(key, probe);
1560
+ return probe;
1561
+ }
1562
+
1563
+ /**
1564
+ * Per-account quota rows for a provider's logged-in accounts. Probes run in parallel; a
1565
+ * single failing account never blocks the others.
1566
+ */
1567
+ export async function fetchProviderAccountQuotas(
1568
+ provider: string,
1569
+ forceRefresh = false,
1570
+ ): Promise<ProviderAccountQuota[]> {
1571
+ if (!supportsPerAccountQuota(provider)) return [];
1572
+ const set = getAccountSet(provider);
1573
+ if (!set) return [];
1574
+ return await Promise.all(set.accounts.map(async account => {
1575
+ const entry = await fetchAccountQuota(provider, account.id, forceRefresh);
1576
+ return {
1577
+ accountId: account.id,
1578
+ quota: entry.quota,
1579
+ ...(entry.unavailable ? { unavailable: true as const } : {}),
1580
+ };
1581
+ }));
1582
+ }
1583
+
1584
+ function normalizedBaseUrl(value: string): string | null {
1585
+ try {
1586
+ const url = new URL(value);
1587
+ if (url.username || url.password || url.search || url.hash) return null;
1588
+ return `${url.origin.toLowerCase()}${url.pathname.replace(/\/+$/, "")}`;
1589
+ } catch {
1590
+ return null;
1591
+ }
1592
+ }
1593
+
1594
+ function quotaResetAt(row: Record<string, unknown>): number | undefined {
1595
+ return normalizeResetAt(row.resetTime ?? row.resetAt ?? row.reset_time ?? row.reset_at);
1596
+ }
1597
+
1598
+ function isCanonicalKimiCodeBaseUrl(baseUrl: string): boolean {
1599
+ return normalizedBaseUrl(baseUrl) === KIMI_CODE_BASE_URL;
1600
+ }
1601
+
1602
+ function isCanonicalCommandCodeBaseUrl(baseUrl: string): boolean {
1603
+ const normalized = normalizedBaseUrl(baseUrl);
1604
+ // OAuth preset points at the API root; the Provider-API preset at /provider/v1.
1605
+ return normalized === COMMAND_CODE_BASE_URL || normalized === `${COMMAND_CODE_BASE_URL}/provider/v1`;
1606
+ }
1607
+
1608
+ /** Prefer the nested `data` shell when the outer object is only an envelope. */
1609
+ function unwrapKimiQuotaPayload(value: unknown): Record<string, unknown> | null {
1610
+ const body = asRecord(value);
1611
+ if (!body) return null;
1612
+ const nested = asRecord(body.data);
1613
+ if (!nested) return body;
1614
+ // A null/non-usable outer field is a placeholder, not data — an envelope like
1615
+ // { usage: null, data: { usage: {...} } } must still unwrap to the nested payload.
1616
+ const usable = (field: unknown): boolean => field !== undefined && field !== null;
1617
+ const outerHasUsage = usable(body.usage) || usable(body.limits) || usable(body.totalQuota);
1618
+ const nestedHasUsage = usable(nested.usage) || usable(nested.limits) || usable(nested.totalQuota);
1619
+ return !outerHasUsage && nestedHasUsage ? nested : body;
1620
+ }
1621
+
1622
+ function kimiLimitLabel(item: Record<string, unknown>, detail: Record<string, unknown>): string {
1623
+ return [item.name, item.title, item.scope, detail.name, detail.title]
1624
+ .filter((value): value is string => typeof value === "string")
1625
+ .join(" ")
1626
+ .toLowerCase();
1627
+ }
1628
+
1629
+ function parseKimiQuotaRow(value: unknown, resetFallback?: Record<string, unknown>): { percent: number; resetAt?: number } | null {
1630
+ const row = asRecord(value);
1631
+ if (!row) return null;
1632
+ const resetAt = quotaResetAt(row) ?? (resetFallback ? quotaResetAt(resetFallback) : undefined);
1633
+ const limit = toFiniteNumber(row.limit);
1634
+ if (limit !== undefined && limit > 0) {
1635
+ let used = toFiniteNumber(row.used);
1636
+ if (used === undefined) {
1637
+ const remaining = toFiniteNumber(row.remaining);
1638
+ if (remaining !== undefined) used = limit - remaining;
1639
+ }
1640
+ if (used !== undefined) {
1641
+ const percent = normalizePercent((used / limit) * 100);
1642
+ if (percent !== undefined) return { percent, ...(resetAt !== undefined ? { resetAt } : {}) };
1643
+ }
1644
+ }
1645
+ // Some payloads expose utilisation directly when limit/used arithmetic is absent.
1646
+ const direct = normalizePercent(row.utilization ?? row.percent ?? row.usedPercent ?? row.used_percent);
1647
+ return direct === undefined ? null : { percent: direct, ...(resetAt !== undefined ? { resetAt } : {}) };
1648
+ }
1649
+
1650
+ function isKimiFiveHourLimit(item: Record<string, unknown>, detail: Record<string, unknown>, window: Record<string, unknown>): boolean {
1651
+ const duration = toFiniteNumber(window.duration ?? item.duration ?? detail.duration);
1652
+ const unit = String(window.timeUnit ?? item.timeUnit ?? detail.timeUnit ?? "").toUpperCase();
1653
+ if ((unit.includes("MINUTE") && duration === 300) || (unit.includes("HOUR") && duration === 5)) return true;
1654
+ return /(^|\b)5\s*(?:h|hour)/.test(kimiLimitLabel(item, detail));
1655
+ }
1656
+
1657
+ function isKimiWeeklyLimit(item: Record<string, unknown>, detail: Record<string, unknown>, window: Record<string, unknown>): boolean {
1658
+ const duration = toFiniteNumber(window.duration ?? item.duration ?? detail.duration);
1659
+ const unit = String(window.timeUnit ?? item.timeUnit ?? detail.timeUnit ?? "").toUpperCase();
1660
+ if ((unit.includes("DAY") && duration === 7) || (unit.includes("HOUR") && duration === 168)) return true;
1661
+ return /weekly|7\s*(?:d|day)/.test(kimiLimitLabel(item, detail));
1662
+ }
1663
+
1664
+ function parseKimiQuotaPayload(value: unknown): ProviderQuota | null {
1665
+ const body = unwrapKimiQuotaPayload(value);
1666
+ if (!body) return null;
1667
+ let weekly = parseKimiQuotaRow(body.usage);
1668
+ const total = parseKimiQuotaRow(body.totalQuota);
1669
+ let fiveHour: { percent: number; resetAt?: number } | null = null;
1670
+ if (Array.isArray(body.limits)) {
1671
+ for (const rawItem of body.limits) {
1672
+ const item = asRecord(rawItem);
1673
+ if (!item) continue;
1674
+ const detail = asRecord(item.detail) ?? item;
1675
+ const window = asRecord(item.window) ?? {};
1676
+ if (!fiveHour && isKimiFiveHourLimit(item, detail, window)) {
1677
+ fiveHour = parseKimiQuotaRow(detail, window);
1678
+ }
1679
+ if (!weekly && isKimiWeeklyLimit(item, detail, window)) {
1680
+ weekly = parseKimiQuotaRow(detail, window);
1681
+ }
1682
+ if (fiveHour && weekly) break;
1683
+ }
1684
+ }
1685
+ const quota: ProviderQuota = {
1686
+ ...(fiveHour ? {
1687
+ fiveHourPercent: fiveHour.percent,
1688
+ ...(fiveHour.resetAt !== undefined ? { fiveHourResetAt: fiveHour.resetAt } : {}),
1689
+ } : {}),
1690
+ ...(weekly ? {
1691
+ weeklyPercent: weekly.percent,
1692
+ ...(weekly.resetAt !== undefined ? { weeklyResetAt: weekly.resetAt } : {}),
1693
+ } : {}),
1694
+ ...(total ? { customWindows: [{ label: "Total subscription credits", percent: total.percent, ...(total.resetAt !== undefined ? { resetAt: total.resetAt } : {}) }] } : {}),
1695
+ updatedAt: Date.now(),
1696
+ };
1697
+ return hasQuotaRows(quota) ? quota : null;
1698
+ }
1699
+
1700
+ async function resolveKimiQuotaBearer(config: OcxProviderConfig): Promise<string | null> {
1701
+ if (config.authMode === "oauth") {
1702
+ try {
1703
+ return await getValidAccessToken("kimi");
1704
+ } catch {
1705
+ return null;
1706
+ }
1707
+ }
1708
+ // ACTIVE key only: silently walking apiKeyPool when the primary env reference is
1709
+ // unresolved would render a quota bar for a DIFFERENT account than the one routing
1710
+ // requests — a wrong meter is worse than no meter.
1711
+ const primary = resolveEnvValue(config.apiKey)?.trim();
1712
+ return primary || null;
1713
+ }
1714
+
1715
+ async function fetchKimiQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaReport | null> {
1716
+ // Never release credentials to a user-edited or lookalike provider host.
1717
+ if (!isCanonicalKimiCodeBaseUrl(config.baseUrl)) return null;
1718
+ const accessToken = await resolveKimiQuotaBearer(config);
1719
+ if (!accessToken) return null;
1720
+ const response = await fetch(KIMI_CODE_USAGE_URL, {
1721
+ headers: { Accept: "application/json", Authorization: `Bearer ${accessToken}` },
1722
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
1723
+ });
1724
+ if (!response.ok) return null;
1725
+ const quota = parseKimiQuotaPayload(await readQuotaJson(response));
1726
+ return quota ? report(provider, "kimi:usages", quota) : null;
1727
+ }
1728
+
1729
+ /**
1730
+ * Command Code rolling window: `{ cap, used, resetAt }` off /alpha/billing/credits,
1731
+ * normalized to a percent with an optional reset timestamp.
1732
+ */
1733
+ function parseCommandCodeWindow(value: unknown): { percent: number; resetAt?: number } | null {
1734
+ const row = asRecord(value);
1735
+ if (!row) return null;
1736
+ const cap = toFiniteNumber(row.cap);
1737
+ const used = toFiniteNumber(row.used);
1738
+ if (cap === undefined || used === undefined || cap <= 0 || used < 0) return null;
1739
+ const percent = normalizePercent((used / cap) * 100);
1740
+ if (percent === undefined) return null;
1741
+ const resetAt = quotaResetAt(row);
1742
+ return { percent, ...(resetAt !== undefined ? { resetAt } : {}) };
1743
+ }
1744
+
1745
+ /** Soft-fail GET returning a parsed record, or null when unavailable. */
1746
+ async function fetchCommandCodeJson(url: string, bearer: string): Promise<Record<string, unknown> | null> {
1747
+ try {
1748
+ const response = await fetch(url, {
1749
+ headers: { Accept: "application/json", Authorization: `Bearer ${bearer}` },
1750
+ redirect: "error",
1751
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
1752
+ });
1753
+ if (!response.ok) return null;
1754
+ return asRecord(await readQuotaJson(response));
1755
+ } catch {
1756
+ return null;
1757
+ }
1758
+ }
1759
+
1760
+ /**
1761
+ * Soft-fail period spend (used) against the remaining credit pools → creditsUsd.
1762
+ * Period scoping: `since=<currentPeriodStart>` keeps spend aligned with the
1763
+ * pools' billing cycle, and `currentPeriodEnd` becomes expiresAt.
1764
+ */
1765
+ async function fetchCommandCodeSpend(
1766
+ bearer: string,
1767
+ credits: Record<string, unknown> | null,
1768
+ orgQuery: string,
1769
+ ): Promise<ProviderQuotaCreditsUsd | undefined> {
1770
+ if (!credits) return undefined;
1771
+ const subscriptionBody = await fetchCommandCodeJson(`${COMMAND_CODE_SUBSCRIPTIONS_URL}${orgQuery}`, bearer);
1772
+ const subscription = asRecord(subscriptionBody?.data) ?? subscriptionBody;
1773
+ const periodStart = typeof subscription?.currentPeriodStart === "string" ? subscription.currentPeriodStart.trim() : "";
1774
+ // Unscoped /usage/summary is lifetime spend; mixing it with current-cycle
1775
+ // remaining pools produces a wrong percent. Omit creditsUsd until a period exists.
1776
+ if (!periodStart) return undefined;
1777
+ const sinceQuery = `${orgQuery ? "&" : "?"}since=${encodeURIComponent(periodStart)}`;
1778
+ const expiresAt = normalizeResetAt(subscription?.currentPeriodEnd);
1779
+ const summaryBody = await fetchCommandCodeJson(`${COMMAND_CODE_USAGE_URL}${orgQuery}${sinceQuery}`, bearer);
1780
+ const summary = asRecord(summaryBody?.data) ?? summaryBody;
1781
+ const used = toFiniteNumber(summary?.totalCost) ?? toFiniteNumber(summary?.totalMonthlyCredits);
1782
+ if (used === undefined || used < 0) return undefined;
1783
+ const pools = [credits.monthlyCredits, credits.purchasedCredits, credits.freeCredits]
1784
+ .map(value => toFiniteNumber(value))
1785
+ .filter((value): value is number => value !== undefined);
1786
+ // Field presence is what separates a real balance from absent data: an exhausted
1787
+ // all-zero account still reports remaining=0, while no remaining-credit field at
1788
+ // all means there is nothing to meter.
1789
+ if (pools.length === 0) return undefined;
1790
+ const remaining = pools.reduce((sum, value) => sum + Math.max(0, value ?? 0), 0);
1791
+ const limit = used + remaining;
1792
+ const percent = normalizePercent(limit > 0 ? (used / limit) * 100 : 0);
1793
+ // Purchased credits roll over past the subscription period end, so an expiry is
1794
+ // only truthful when the aggregate contains no non-expiring purchased pool.
1795
+ const purchased = toFiniteNumber(credits.purchasedCredits) ?? 0;
1796
+ return percent === undefined
1797
+ ? undefined
1798
+ : {
1799
+ used,
1800
+ limit,
1801
+ remaining,
1802
+ percent,
1803
+ ...(expiresAt !== undefined && purchased <= 0 ? { expiresAt } : {}),
1804
+ };
1805
+ }
1806
+
1807
+ /** OAuth access token or ACTIVE Provider-API key for the Command Code quota probe. */
1808
+ async function resolveCommandCodeQuotaBearer(config: OcxProviderConfig): Promise<string | null> {
1809
+ if (config.authMode === "oauth") {
1810
+ try {
1811
+ return await getValidAccessToken("command-code");
1812
+ } catch {
1813
+ return null;
1814
+ }
1815
+ }
1816
+ // ACTIVE key only: a quota bar for a different account than the one routing
1817
+ // requests is a wrong meter, not a helpful one.
1818
+ return resolveEnvValue(config.apiKey)?.trim() || null;
1819
+ }
1820
+
1821
+ /**
1822
+ * Command Code `GET /alpha/billing/credits` — the same Bearer surface the CLI's
1823
+ * usage view uses (windowLimits.fiveHour / windowLimits.weekly), plus soft
1824
+ * whoami (team orgId scoping) and subscription-scoped spend for creditsUsd.
1825
+ */
1826
+ async function fetchCommandCodeQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
1827
+ // Never release credentials to a user-edited or lookalike provider host.
1828
+ if (!isCanonicalCommandCodeBaseUrl(config.baseUrl)) return null;
1829
+ const bearer = await resolveCommandCodeQuotaBearer(config);
1830
+ if (!bearer) return null;
1831
+ const whoamiBody = await fetchCommandCodeJson(COMMAND_CODE_WHOAMI_URL, bearer);
1832
+ const whoami = asRecord(whoamiBody?.data) ?? whoamiBody;
1833
+ const org = asRecord(whoami?.org);
1834
+ const orgId = typeof org?.id === "string" && org.id.trim() ? org.id.trim() : null;
1835
+ const orgQuery = orgId ? `?orgId=${encodeURIComponent(orgId)}` : "";
1836
+ const response = await fetch(`${COMMAND_CODE_CREDITS_URL}${orgQuery}`, {
1837
+ headers: { Accept: "application/json", Authorization: `Bearer ${bearer}` },
1838
+ redirect: "error",
1839
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
1840
+ });
1841
+ if (!response.ok) {
1842
+ return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
1843
+ ? TERMINAL_QUOTA_FAILURE
1844
+ : null;
1845
+ }
1846
+ const raw = asRecord(await readQuotaJson(response));
1847
+ const body = asRecord(raw?.data) ?? raw;
1848
+ const credits = asRecord(body?.credits);
1849
+ const limits = asRecord(body?.windowLimits);
1850
+ if (!credits && !limits) return null;
1851
+ const fiveHour = parseCommandCodeWindow(limits?.fiveHour);
1852
+ const weekly = parseCommandCodeWindow(limits?.weekly);
1853
+ const creditsUsd = await fetchCommandCodeSpend(bearer, credits, orgQuery);
1854
+ return report(provider, "command-code:credits", {
1855
+ ...(fiveHour ? {
1856
+ fiveHourPercent: fiveHour.percent,
1857
+ ...(fiveHour.resetAt !== undefined ? { fiveHourResetAt: fiveHour.resetAt } : {}),
1858
+ } : {}),
1859
+ ...(weekly ? {
1860
+ weeklyPercent: weekly.percent,
1861
+ ...(weekly.resetAt !== undefined ? { weeklyResetAt: weekly.resetAt } : {}),
1862
+ } : {}),
1863
+ ...(creditsUsd ? { creditsUsd } : {}),
1864
+ updatedAt: Date.now(),
1865
+ });
1866
+ }
1867
+
1868
+ /** Cursor included usage via api2.cursor.sh (Bearer from OAuth) — unofficial, may change. */
1869
+ async function fetchCursorQuota(provider: string): Promise<ProviderQuotaReport | null> {
1870
+ let accessToken: string;
1871
+ try {
1872
+ accessToken = await getValidAccessToken("cursor");
1873
+ } catch {
1874
+ return null;
1875
+ }
1876
+
1877
+ const authHeaders = {
1878
+ Accept: "application/json",
1879
+ Authorization: `Bearer ${accessToken}`,
1880
+ "User-Agent": "opencodex-quota",
1881
+ } as const;
1882
+
1883
+ // Prefer dashboard period usage (Pro/Team/Ultra spend allowance in USD cents).
1884
+ // Field names follow Cursor's Connect RPC shape (limit/remaining/includedSpend), not usedCents.
1885
+ try {
1886
+ const periodRes = await fetch("https://api2.cursor.sh/aiserver.v1.DashboardService/GetCurrentPeriodUsage", {
1887
+ method: "POST",
1888
+ headers: {
1889
+ ...authHeaders,
1890
+ "Content-Type": "application/json",
1891
+ "Connect-Protocol-Version": "1",
1892
+ },
1893
+ body: "{}",
1894
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
1895
+ });
1896
+ if (periodRes.ok) {
1897
+ const body = asRecord(await readQuotaJson(periodRes));
1898
+ const planUsage = asRecord(body?.planUsage);
1899
+ if (planUsage) {
1900
+ const resetAt = normalizeResetAt(body?.billingCycleEnd ?? planUsage.billingCycleEnd ?? body?.periodEnd);
1901
+
1902
+ // Primary meter: overall included allowance (Cursor Settings → Usage total %).
1903
+ // autoPercentUsed / apiPercentUsed are secondary pools and must not replace the total.
1904
+ const limit = toFiniteNumber(planUsage.limit ?? planUsage.limitCents ?? planUsage.totalLimitCents);
1905
+ const remaining = toFiniteNumber(planUsage.remaining ?? planUsage.remainingCents);
1906
+ const includedSpend = toFiniteNumber(planUsage.includedSpend ?? planUsage.usedCents ?? planUsage.used);
1907
+ const totalSpend = toFiniteNumber(planUsage.totalSpend);
1908
+ let used: number | undefined;
1909
+ if (includedSpend !== undefined) used = includedSpend;
1910
+ else if (limit !== undefined && remaining !== undefined) used = Math.max(0, limit - remaining);
1911
+ else if (totalSpend !== undefined) used = totalSpend;
1912
+ const totalPercent = normalizePercent(planUsage.totalPercentUsed ?? planUsage.percentUsed)
1913
+ ?? (limit !== undefined && limit > 0 && used !== undefined
1914
+ ? normalizePercent((used / limit) * 100)
1915
+ : undefined);
1916
+
1917
+ const autoPercent = normalizePercent(planUsage.autoPercentUsed);
1918
+ const apiPercent = normalizePercent(planUsage.apiPercentUsed);
1919
+ const customWindows: ProviderQuotaWindow[] = [];
1920
+ if (autoPercent !== undefined) {
1921
+ customWindows.push({
1922
+ label: "First-party models",
1923
+ percent: autoPercent,
1924
+ ...(resetAt !== undefined ? { resetAt } : {}),
1925
+ });
1926
+ }
1927
+ if (apiPercent !== undefined) {
1928
+ customWindows.push({
1929
+ label: "API usage",
1930
+ percent: apiPercent,
1931
+ ...(resetAt !== undefined ? { resetAt } : {}),
1932
+ });
1933
+ }
1934
+
1935
+ if (totalPercent !== undefined || customWindows.length > 0) {
1936
+ const built = report(provider, "cursor:period-usage", {
1937
+ ...(totalPercent !== undefined ? {
1938
+ monthlyPercent: totalPercent,
1939
+ ...(resetAt !== undefined ? { monthlyResetAt: resetAt } : {}),
1940
+ } : {}),
1941
+ ...(customWindows.length > 0 ? { customWindows } : {}),
1942
+ updatedAt: Date.now(),
1943
+ });
1944
+ if (built) return { ...built, reverseEngineered: true };
1945
+ }
1946
+ }
1947
+ }
1948
+ } catch {
1949
+ /* fall through */
1950
+ }
1951
+
1952
+ // /api/usage/summary — same host, sometimes richer than /auth/usage for Team plans.
1953
+ try {
1954
+ const summaryRes = await fetch("https://api2.cursor.sh/api/usage/summary", {
1955
+ headers: authHeaders,
1956
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
1957
+ });
1958
+ if (summaryRes.ok) {
1959
+ const body = asRecord(await readQuotaJson(summaryRes));
1960
+ const individual = asRecord(body?.individualUsage);
1961
+ const plan = asRecord(individual?.plan);
1962
+ if (plan) {
1963
+ const used = toFiniteNumber(plan.used);
1964
+ const limit = toFiniteNumber(plan.limit);
1965
+ const percent = normalizePercent(plan.totalPercentUsed)
1966
+ ?? (used !== undefined && limit !== undefined && limit > 0
1967
+ ? normalizePercent((used / limit) * 100)
1968
+ : undefined);
1969
+ if (percent !== undefined) {
1970
+ const built = report(provider, "cursor:usage-summary", {
1971
+ monthlyPercent: percent,
1972
+ monthlyResetAt: normalizeResetAt(body?.billingCycleEnd),
1973
+ updatedAt: Date.now(),
1974
+ });
1975
+ if (built) return { ...built, reverseEngineered: true };
1976
+ }
1977
+ }
1978
+ }
1979
+ } catch {
1980
+ /* fall through to /auth/usage */
1981
+ }
1982
+
1983
+ const response = await fetch("https://api2.cursor.sh/auth/usage", {
1984
+ headers: authHeaders,
1985
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
1986
+ });
1987
+ if (!response.ok) return null;
1988
+ const body = asRecord(await readQuotaJson(response));
1989
+ if (!body) return null;
1990
+
1991
+ // Prefer the gpt-4 bucket (historical "fast requests"); else first model with used+limit.
1992
+ let used: number | undefined;
1993
+ let limit: number | undefined;
1994
+ const gpt4 = asRecord(body["gpt-4"]);
1995
+ if (gpt4) {
1996
+ used = toFiniteNumber(gpt4.numRequests ?? gpt4.used);
1997
+ limit = toFiniteNumber(gpt4.maxRequestUsage ?? gpt4.limit ?? gpt4.maxRequests);
1998
+ }
1999
+ if (used === undefined || limit === undefined || limit <= 0) {
2000
+ for (const [key, value] of Object.entries(body)) {
2001
+ if (key === "startOfMonth" || key === "billingCycleStart") continue;
2002
+ const bucket = asRecord(value);
2003
+ if (!bucket) continue;
2004
+ const bucketUsed = toFiniteNumber(bucket.numRequests ?? bucket.used);
2005
+ const bucketLimit = toFiniteNumber(bucket.maxRequestUsage ?? bucket.limit ?? bucket.maxRequests);
2006
+ if (bucketUsed !== undefined && bucketLimit !== undefined && bucketLimit > 0) {
2007
+ used = bucketUsed;
2008
+ limit = bucketLimit;
2009
+ break;
2010
+ }
2011
+ }
2012
+ }
2013
+ if (used === undefined || limit === undefined || limit <= 0) return null;
2014
+ const percent = normalizePercent((used / limit) * 100);
2015
+ if (percent === undefined) return null;
2016
+ const startOfMonth = normalizeResetAt(body.startOfMonth ?? body.billingCycleStart);
2017
+ // Next reset = same day next month, computed in UTC to avoid timezone-shifted rollover.
2018
+ const monthlyResetAt = startOfMonth !== undefined
2019
+ ? (() => {
2020
+ const start = new Date(startOfMonth);
2021
+ return Date.UTC(start.getUTCFullYear(), start.getUTCMonth() + 1, start.getUTCDate());
2022
+ })()
2023
+ : undefined;
2024
+ const built = report(provider, "cursor:auth-usage", {
2025
+ monthlyPercent: percent,
2026
+ ...(monthlyResetAt !== undefined ? { monthlyResetAt } : {}),
2027
+ updatedAt: Date.now(),
2028
+ });
2029
+ return built ? { ...built, reverseEngineered: true } : null;
2030
+ }
2031
+
2032
+ function quotaInfoEntries(modelInfo: Record<string, unknown>): Record<string, unknown>[] {
2033
+ const entries: Record<string, unknown>[] = [];
2034
+ const add = (value: unknown, tier?: string) => {
2035
+ const rec = asRecord(value);
2036
+ if (!rec) return;
2037
+ entries.push(tier ? { ...rec, tier } : rec);
2038
+ };
2039
+ const addArray = (value: unknown) => {
2040
+ if (!Array.isArray(value)) return;
2041
+ for (const entry of value) add(entry);
2042
+ };
2043
+
2044
+ if (Array.isArray(modelInfo.quotaInfo)) addArray(modelInfo.quotaInfo);
2045
+ else add(modelInfo.quotaInfo);
2046
+ addArray(modelInfo.quotaInfos);
2047
+
2048
+ const byTier = asRecord(modelInfo.quotaInfoByTier);
2049
+ if (byTier) {
2050
+ for (const [tier, value] of Object.entries(byTier)) {
2051
+ if (Array.isArray(value)) {
2052
+ for (const entry of value) add(entry, tier);
2053
+ } else {
2054
+ add(value, tier);
2055
+ }
2056
+ }
2057
+ }
2058
+ return entries;
2059
+ }
2060
+
2061
+ function classifyAntigravityFamily(modelId: string, modelInfo: Record<string, unknown>, quotaInfo: Record<string, unknown>): "Gem" | "Cla" | null {
2062
+ const displayName = typeof modelInfo.displayName === "string" ? modelInfo.displayName : "";
2063
+ const tier = typeof quotaInfo.tier === "string" ? quotaInfo.tier : "";
2064
+ const haystack = `${modelId} ${displayName} ${tier}`.toLowerCase();
2065
+ if (haystack.includes("gemini")) return "Gem";
2066
+ if (haystack.includes("claude") || haystack.includes("opus") || haystack.includes("sonnet") || haystack.includes("gpt-oss") || haystack.includes("gpt_oss")) return "Cla";
2067
+ return null;
2068
+ }
2069
+
2070
+ function antigravityUsedPercent(quotaInfo: Record<string, unknown>): number | undefined {
2071
+ const remaining = normalizePercent(toFiniteNumber(quotaInfo.remainingFraction) !== undefined
2072
+ ? toFiniteNumber(quotaInfo.remainingFraction)! * 100
2073
+ : toFiniteNumber(quotaInfo.remainingPercentage) !== undefined
2074
+ ? toFiniteNumber(quotaInfo.remainingPercentage)!
2075
+ : undefined);
2076
+ if (remaining === undefined) return undefined;
2077
+ return normalizePercent(100 - remaining);
2078
+ }
2079
+
2080
+ async function fetchAntigravityQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
2081
+ const credential = getCredential("google-antigravity");
2082
+ if (!credential?.projectId) return null;
2083
+ let accessToken: string;
2084
+ try {
2085
+ accessToken = await getValidAccessToken("google-antigravity");
2086
+ } catch {
2087
+ return null;
2088
+ }
2089
+ const baseUrl = (config.baseUrl || "https://daily-cloudcode-pa.googleapis.com").replace(/\/+$/, "");
2090
+ let liveQuota: ProviderQuota | null;
2091
+ try {
2092
+ liveQuota = await fetchAntigravityLiveQuota({
2093
+ accessToken,
2094
+ projectId: credential.projectId,
2095
+ baseUrl,
2096
+ timeoutMs: REQUEST_TIMEOUT_MS,
2097
+ });
2098
+ } catch (error) {
2099
+ if (error instanceof AntigravityQuotaRpcError && isTerminalAntigravityQuotaStatus(error.status)) {
2100
+ return TERMINAL_QUOTA_FAILURE;
2101
+ }
2102
+ liveQuota = null;
2103
+ }
2104
+
2105
+ const windows = new Map<string, ProviderQuotaWindow>();
2106
+ for (const [index, host] of antigravityHostCandidates(baseUrl).entries()) {
2107
+ if (!isAntigravityHttpsHost(host)) continue;
2108
+ try {
2109
+ const response = await fetch(`${host}/v1internal:fetchAvailableModels`, {
2110
+ method: "POST",
2111
+ headers: {
2112
+ Accept: "application/json",
2113
+ "Content-Type": "application/json",
2114
+ "User-Agent": antigravityUserAgent(),
2115
+ Authorization: `Bearer ${accessToken}`,
2116
+ },
2117
+ body: JSON.stringify({ project: credential.projectId }),
2118
+ redirect: "error",
2119
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
2120
+ });
2121
+ if (!response.ok) {
2122
+ if (index === 0 && (response.status === 404 || response.status === 503)) continue;
2123
+ break;
2124
+ }
2125
+ const body = asRecord(await readQuotaJson(response));
2126
+ const models = asRecord(body?.models);
2127
+ if (models) {
2128
+ for (const [modelId, rawModelInfo] of Object.entries(models)) {
2129
+ const modelInfo = asRecord(rawModelInfo);
2130
+ if (!modelInfo) continue;
2131
+ for (const quotaInfo of quotaInfoEntries(modelInfo)) {
2132
+ const label = classifyAntigravityFamily(modelId, modelInfo, quotaInfo);
2133
+ if (!label || windows.has(label)) continue;
2134
+ const percent = antigravityUsedPercent(quotaInfo);
2135
+ if (percent === undefined) continue;
2136
+ windows.set(label, {
2137
+ label,
2138
+ percent,
2139
+ ...(normalizeResetAt(quotaInfo.resetTime) !== undefined ? { resetAt: normalizeResetAt(quotaInfo.resetTime) } : {}),
2140
+ });
2141
+ }
2142
+ }
2143
+ }
2144
+ break;
2145
+ } catch {
2146
+ if (index === 0) continue;
2147
+ break;
2148
+ }
2149
+ }
2150
+
2151
+ if (liveQuota) {
2152
+ const liveWindows = liveQuota.customWindows ?? [];
2153
+ const catalogClaude = windows.get("Cla");
2154
+ const customWindows = [
2155
+ ...liveWindows,
2156
+ ...(liveWindows.some(window => window.label === "Cla") || !catalogClaude ? [] : [catalogClaude]),
2157
+ ];
2158
+ return report(provider, "google-antigravity:retrieveUserQuota", {
2159
+ ...liveQuota,
2160
+ ...(customWindows.length > 0 ? { customWindows } : {}),
2161
+ updatedAt: Date.now(),
2162
+ });
2163
+ }
2164
+
2165
+ const customWindows = ["Gem", "Cla"].flatMap(label => {
2166
+ const window = windows.get(label);
2167
+ return window ? [window] : [];
2168
+ });
2169
+ if (customWindows.length === 0) return null;
2170
+ return report(provider, "google-antigravity:fetchAvailableModels", {
2171
+ customWindows,
2172
+ updatedAt: Date.now(),
2173
+ });
2174
+ }
2175
+
2176
+ async function maybeFetchProviderQuota(
2177
+ name: string,
2178
+ provider: OcxProviderConfig,
2179
+ config: OcxConfig,
2180
+ forceRefresh: boolean,
2181
+ prefetchedCodexSnapshot?: CodexAuthAccountsSnapshotPromise,
2182
+ ): Promise<ProviderQuotaProbeResult> {
2183
+ if (provider.disabled === true) return null;
2184
+ try {
2185
+ if (isBuiltInChatGptForwardProvider(name, provider)) {
2186
+ return fetchChatGptForwardQuota(config, name, provider, forceRefresh, prefetchedCodexSnapshot);
2187
+ }
2188
+ if (provider.authMode === "oauth" && name === "xai") return fetchXaiQuota(name);
2189
+ if (provider.authMode === "oauth" && name === "anthropic") return fetchAnthropicQuota(name);
2190
+ if (provider.authMode === "oauth" && name === "cursor") return fetchCursorQuota(name);
2191
+ if (provider.authMode === "oauth" && name === "google-antigravity") return fetchAntigravityQuota(name, provider);
2192
+ // Kimi Code `/usages` accepts OAuth or coding-plan API keys, but only on the canonical
2193
+ // host and only for real key auth — forward/local modes carry no credential of ours.
2194
+ if (provider.authMode === "oauth" && name === "kimi") return fetchKimiQuota(name, provider);
2195
+ if (provider.authMode === "key" && isCanonicalKimiCodeBaseUrl(provider.baseUrl)) {
2196
+ return fetchKimiQuota(name, provider);
2197
+ }
2198
+ // OAuth account login or Provider-API key only; forward/local modes carry no
2199
+ // credential of ours on the canonical host.
2200
+ if (provider.authMode === "oauth" && name === "command-code") {
2201
+ return fetchCommandCodeQuota(name, provider);
2202
+ }
2203
+ if ((provider.authMode ?? "key") === "key" && name === "commandcode"
2204
+ && isCanonicalCommandCodeBaseUrl(provider.baseUrl)) {
2205
+ return fetchCommandCodeQuota(name, provider);
2206
+ }
2207
+ // Identify OpenCode Go by where it routes, not by what the row is called. Multi-account
2208
+ // setups keep the same destination under names like `opencode-go-2` (#1924), and those rows
2209
+ // silently had no quota panel and no `ocx provider quota --json` report while the literal
2210
+ // name was the gate. `registryEntryForProviderDestination` is the existing predicate for
2211
+ // exactly this question: normalized endpoint + adapter + key auth, so a canonical URL behind
2212
+ // a different adapter is still not OpenCode Go. The defensive URL check inside
2213
+ // `fetchOpenCodeGoQuota` stays — sending a key anywhere must not depend on this gate.
2214
+ if ((provider.authMode ?? "key") === "key" && registryEntryForProviderDestination(provider)?.id === "opencode-go") {
2215
+ return fetchOpenCodeGoQuota(name, provider);
2216
+ }
2217
+ if ((provider.authMode ?? "key") === "key" && isCanonicalA6apiBaseUrl(provider.baseUrl)) {
2218
+ return fetchA6apiQuota(name, provider);
2219
+ }
2220
+ if ((provider.authMode ?? "key") === "key" && name === "openrouter") {
2221
+ return fetchOpenRouterQuota(name, provider);
2222
+ }
2223
+ if ((provider.authMode ?? "key") === "key" && name === "deepseek") {
2224
+ return fetchDeepSeekQuota(name, provider);
2225
+ }
2226
+ if ((provider.authMode ?? "key") === "key" && name === "cline-pass") {
2227
+ return fetchClineQuota(name, provider);
2228
+ }
2229
+ if ((provider.authMode ?? "key") === "key"
2230
+ && (name === "zai" || name === "glm" || name === "glm-cn" || name === "zhipu-bigmodel-coding")) {
2231
+ return fetchZaiQuota(name, provider);
2232
+ }
2233
+ if ((provider.authMode ?? "key") === "key" && (name === "minimax" || name === "minimax-cn")) {
2234
+ return fetchMinimaxQuota(name, provider);
2235
+ }
2236
+ if ((provider.authMode ?? "key") === "key" && name === "moonshot") {
2237
+ return fetchMoonshotQuota(name, provider);
2238
+ }
2239
+ if ((provider.authMode ?? "key") === "key" && name === "venice") {
2240
+ return fetchVeniceQuota(name, provider);
2241
+ }
2242
+ if ((provider.authMode ?? "key") === "key" && name === "synthetic") {
2243
+ return fetchSyntheticQuota(name, provider);
2244
+ }
2245
+ if ((provider.authMode ?? "key") === "key" && name === "deepinfra") {
2246
+ return fetchDeepInfraQuota(name, provider);
2247
+ }
2248
+ if ((provider.authMode ?? "key") === "key" && name === "neuralwatt") {
2249
+ return fetchNeuralwattQuota(name, provider);
2250
+ }
2251
+ return null;
2252
+ } catch {
2253
+ return null;
2254
+ }
2255
+ }
2256
+
2257
+ export async function fetchProviderQuotaReports(config: OcxConfig, forceRefresh = false): Promise<ProviderQuotaResponse> {
2258
+ // A Pool report's cache signature and provider fetch must share one account snapshot.
2259
+ // Preserve force semantics when deciding whether that snapshot refreshes upstream data.
2260
+ const prefetchedCodexSnapshot = hasCodexPoolProvider(config)
2261
+ ? listCodexAuthAccountsSnapshot(config, forceRefresh)
2262
+ : undefined;
2263
+ const keyCandidate = cacheKeyWithAggregationState(config, prefetchedCodexSnapshot);
2264
+ const key = typeof keyCandidate === "string" ? keyCandidate : await keyCandidate;
2265
+ const writerGeneration = captureConfigGeneration();
2266
+ const now = Date.now();
2267
+ // The cache fast path must not extend a preserved last-good row past its 30-minute bound:
2268
+ // a row preserved at age 29:59 plus a full 5-minute TTL would otherwise serve until ~35min.
2269
+ const cacheFresh = cache && cache.key === key && now - cache.ts < CACHE_TTL_MS
2270
+ && cache.response.reports.every(item =>
2271
+ now - item.updatedAt < LAST_GOOD_MAX_AGE_MS && isProviderQuotaReportCurrent(item));
2272
+ if (!forceRefresh && cacheFresh) return cache!.response;
2273
+ const joinable = inflight.get(key);
2274
+ if (!forceRefresh && joinable && joinable.epoch === invalidationEpoch) return joinable.promise;
2275
+ // A forced probe takes commit authority: older in-flight probes must not overwrite its result.
2276
+ if (forceRefresh) invalidationEpoch += 1;
2277
+ const epoch = invalidationEpoch;
2278
+
2279
+ const promise = (async (): Promise<ProviderQuotaResponse> => {
2280
+ const previous = cache && cache.key === key ? cache.response.reports : [];
2281
+ const probeResults = await Promise.all(
2282
+ Object.entries(config.providers).map(([name, provider]) => (
2283
+ maybeFetchProviderQuota(name, provider, config, forceRefresh, prefetchedCodexSnapshot)
2284
+ )),
2285
+ );
2286
+ const fresh = probeResults.filter((item): item is ProviderQuotaReport => item !== null && item !== TERMINAL_QUOTA_FAILURE);
2287
+ const terminalFailures = new Set(
2288
+ Object.keys(config.providers).filter((_, index) => probeResults[index] === TERMINAL_QUOTA_FAILURE),
2289
+ );
2290
+ await providerQuotaBeforePublishForTests?.();
2291
+ let commitKey: string | null = null;
2292
+ if (epoch === invalidationEpoch) {
2293
+ const commitKeyCandidate = cacheKeyWithAggregationState(config);
2294
+ commitKey = typeof commitKeyCandidate === "string" ? commitKeyCandidate : await commitKeyCandidate;
2295
+ }
2296
+
2297
+ // Keep bounded last-good rows when a probe fails transiently; terminal-invalid provider
2298
+ // responses explicitly suppress their old row. Never re-stamp preserved timestamps.
2299
+ // Note: the cache key encodes the provider set (name/adapter/authMode/disabled/baseUrl),
2300
+ // so previous rows always correspond to currently configured, enabled providers — a
2301
+ // disabled or removed provider changes the key and starts from an empty previous set.
2302
+ const cutoff = Date.now() - LAST_GOOD_MAX_AGE_MS;
2303
+ const byProvider = new Map<string, ProviderQuotaReport>();
2304
+ const generationMismatchedProviders = new Set<string>();
2305
+ for (const item of previous) {
2306
+ if (item.updatedAt < cutoff) continue;
2307
+ if (isProviderQuotaReportCurrent(item)) byProvider.set(item.provider, item);
2308
+ else generationMismatchedProviders.add(item.provider);
2309
+ }
2310
+ for (const item of fresh) {
2311
+ if (isProviderQuotaReportCurrent(item)) {
2312
+ byProvider.set(item.provider, item);
2313
+ generationMismatchedProviders.delete(item.provider);
2314
+ } else {
2315
+ byProvider.delete(item.provider);
2316
+ generationMismatchedProviders.add(item.provider);
2317
+ }
2318
+ }
2319
+ // Terminal-invalid probes suppress their previous row (transient failures keep it).
2320
+ for (const provider of terminalFailures) {
2321
+ byProvider.delete(provider);
2322
+ generationMismatchedProviders.delete(provider);
2323
+ }
2324
+
2325
+ const response = { generatedAt: Date.now(), reports: [...byProvider.values()] };
2326
+ // Commit only when this probe still holds authority (no clear/force superseded it).
2327
+ if (
2328
+ epoch === invalidationEpoch
2329
+ && commitKey === key
2330
+ && generationMismatchedProviders.size === 0
2331
+ ) {
2332
+ const reports = response.reports.filter(item => mayCommitProviderQuotaKey(item.provider, writerGeneration));
2333
+ cache = { key, ts: Date.now(), response: { ...response, reports } };
2334
+ }
2335
+ return response;
2336
+ })();
2337
+
2338
+ const entry = { epoch, promise };
2339
+ inflight.set(key, entry);
2340
+ try {
2341
+ return await promise;
2342
+ } finally {
2343
+ if (inflight.get(key) === entry) inflight.delete(key);
2344
+ }
2345
+ }