@coseung2/opencodex 2.8.0-cs.1

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 (451) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +219 -0
  3. package/assets/architecture.png +0 -0
  4. package/assets/banner.png +0 -0
  5. package/assets/claude-code-models.gif +0 -0
  6. package/assets/codex-app-picker.png +0 -0
  7. package/bin/ocx-notch.mjs +78 -0
  8. package/bin/ocx.mjs +454 -0
  9. package/bin/package-main.mjs +9 -0
  10. package/gui/dist/assets/index-CAnnes06.js +67 -0
  11. package/gui/dist/assets/index-OY43ubAq.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/antigravity.svg +1 -0
  19. package/gui/dist/provider-icons/claude-color.svg +1 -0
  20. package/gui/dist/provider-icons/claude.svg +1 -0
  21. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
  22. package/gui/dist/provider-icons/copilot-color.svg +1 -0
  23. package/gui/dist/provider-icons/copilot.svg +1 -0
  24. package/gui/dist/provider-icons/cursor-color.svg +2 -0
  25. package/gui/dist/provider-icons/cursor.svg +2 -0
  26. package/gui/dist/provider-icons/deepseek-color.svg +1 -0
  27. package/gui/dist/provider-icons/discord.svg +1 -0
  28. package/gui/dist/provider-icons/firepass-color.svg +1 -0
  29. package/gui/dist/provider-icons/fireworks-color.svg +1 -0
  30. package/gui/dist/provider-icons/gemini-color.svg +1 -0
  31. package/gui/dist/provider-icons/gemini.svg +1 -0
  32. package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
  33. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
  34. package/gui/dist/provider-icons/grok-color.svg +1 -0
  35. package/gui/dist/provider-icons/grok.svg +1 -0
  36. package/gui/dist/provider-icons/groq-color.svg +1 -0
  37. package/gui/dist/provider-icons/huggingface-color.svg +1 -0
  38. package/gui/dist/provider-icons/kimi-color.svg +1 -0
  39. package/gui/dist/provider-icons/kiro-color.svg +15 -0
  40. package/gui/dist/provider-icons/kiro.svg +14 -0
  41. package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
  42. package/gui/dist/provider-icons/mistral-color.svg +1 -0
  43. package/gui/dist/provider-icons/moonshot-color.svg +1 -0
  44. package/gui/dist/provider-icons/nvidia-color.svg +1 -0
  45. package/gui/dist/provider-icons/ollama-color.svg +1 -0
  46. package/gui/dist/provider-icons/openai.svg +1 -0
  47. package/gui/dist/provider-icons/opencode.svg +1 -0
  48. package/gui/dist/provider-icons/openrouter-color.svg +1 -0
  49. package/gui/dist/provider-icons/pi.svg +21 -0
  50. package/gui/dist/provider-icons/qianfan-color.svg +1 -0
  51. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
  52. package/gui/dist/provider-icons/telegram.svg +1 -0
  53. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
  54. package/gui/dist/provider-icons/vllm-color.svg +1 -0
  55. package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
  56. package/package.json +107 -0
  57. package/packages/ocx-notch/README.md +55 -0
  58. package/packages/ocx-notch/THIRD_PARTY_NOTICES.md +45 -0
  59. package/src/AGENTS.md +28 -0
  60. package/src/adapters/anthropic-image-guard.ts +251 -0
  61. package/src/adapters/anthropic-image-normalize.ts +518 -0
  62. package/src/adapters/anthropic.ts +1003 -0
  63. package/src/adapters/azure.ts +36 -0
  64. package/src/adapters/base.ts +72 -0
  65. package/src/adapters/client-fingerprint.ts +59 -0
  66. package/src/adapters/cursor/arg-codec.ts +38 -0
  67. package/src/adapters/cursor/arg-normalize.ts +104 -0
  68. package/src/adapters/cursor/cursor-errors.ts +165 -0
  69. package/src/adapters/cursor/discovery.ts +276 -0
  70. package/src/adapters/cursor/effort-map.ts +127 -0
  71. package/src/adapters/cursor/exec-policy.ts +88 -0
  72. package/src/adapters/cursor/framing.ts +211 -0
  73. package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
  74. package/src/adapters/cursor/kv-store.ts +52 -0
  75. package/src/adapters/cursor/live-models.ts +153 -0
  76. package/src/adapters/cursor/live-smoke-gate.ts +41 -0
  77. package/src/adapters/cursor/live-transport.ts +1214 -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 +55 -0
  82. package/src/adapters/cursor/native-exec-desktop.ts +184 -0
  83. package/src/adapters/cursor/native-exec-fs.ts +329 -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 +548 -0
  87. package/src/adapters/cursor/native-exec-tools.ts +118 -0
  88. package/src/adapters/cursor/native-exec.ts +576 -0
  89. package/src/adapters/cursor/protobuf-events.ts +563 -0
  90. package/src/adapters/cursor/protobuf-request.ts +714 -0
  91. package/src/adapters/cursor/request-builder.ts +255 -0
  92. package/src/adapters/cursor/thread-continuity.ts +67 -0
  93. package/src/adapters/cursor/tool-definitions.ts +505 -0
  94. package/src/adapters/cursor/transport-retry.ts +132 -0
  95. package/src/adapters/cursor/transport.ts +57 -0
  96. package/src/adapters/cursor/types.ts +52 -0
  97. package/src/adapters/cursor.ts +196 -0
  98. package/src/adapters/google-antigravity-replay.ts +303 -0
  99. package/src/adapters/google-antigravity-wire.ts +108 -0
  100. package/src/adapters/google-errors.ts +85 -0
  101. package/src/adapters/google-http.ts +100 -0
  102. package/src/adapters/google-tool-schema.ts +173 -0
  103. package/src/adapters/google-truncation.ts +13 -0
  104. package/src/adapters/google-wire-compiler.ts +232 -0
  105. package/src/adapters/google.ts +758 -0
  106. package/src/adapters/identity.ts +44 -0
  107. package/src/adapters/image.ts +23 -0
  108. package/src/adapters/kiro-constants.ts +16 -0
  109. package/src/adapters/kiro-errors.ts +197 -0
  110. package/src/adapters/kiro-events.ts +179 -0
  111. package/src/adapters/kiro-images.ts +129 -0
  112. package/src/adapters/kiro-retry.ts +312 -0
  113. package/src/adapters/kiro-thinking.ts +96 -0
  114. package/src/adapters/kiro-tool-fallback.ts +36 -0
  115. package/src/adapters/kiro-tools.ts +215 -0
  116. package/src/adapters/kiro-truncation.ts +33 -0
  117. package/src/adapters/kiro-wire.ts +129 -0
  118. package/src/adapters/kiro.ts +1898 -0
  119. package/src/adapters/mimo-free.ts +263 -0
  120. package/src/adapters/openai-chat.ts +1005 -0
  121. package/src/adapters/openai-responses.ts +1137 -0
  122. package/src/adapters/run-turn-queue.ts +114 -0
  123. package/src/adapters/tool-catalog-nudge.ts +71 -0
  124. package/src/adapters/upstream-http-error.ts +48 -0
  125. package/src/bridge.ts +1619 -0
  126. package/src/chat/inbound.ts +295 -0
  127. package/src/chat/outbound.ts +765 -0
  128. package/src/claude/agents-inject.ts +243 -0
  129. package/src/claude/alias.ts +149 -0
  130. package/src/claude/auth-detect.ts +229 -0
  131. package/src/claude/auth-mode-migration.ts +32 -0
  132. package/src/claude/auth-mode.ts +62 -0
  133. package/src/claude/context-windows.ts +189 -0
  134. package/src/claude/desktop-3p-guard.ts +35 -0
  135. package/src/claude/desktop-3p-paths.ts +84 -0
  136. package/src/claude/desktop-3p.ts +381 -0
  137. package/src/claude/desktop-health.ts +26 -0
  138. package/src/claude/desktop-profile.ts +263 -0
  139. package/src/claude/gateway-cache.ts +70 -0
  140. package/src/claude/inbound-debug.ts +163 -0
  141. package/src/claude/inbound.ts +509 -0
  142. package/src/claude/model-info.ts +151 -0
  143. package/src/claude/outbound.ts +872 -0
  144. package/src/cli/access.ts +108 -0
  145. package/src/cli/account-api.ts +268 -0
  146. package/src/cli/account-auth.ts +223 -0
  147. package/src/cli/account-extended.ts +350 -0
  148. package/src/cli/account.ts +275 -0
  149. package/src/cli/agent-driven.ts +70 -0
  150. package/src/cli/agent.ts +184 -0
  151. package/src/cli/catalog-prewarm.ts +27 -0
  152. package/src/cli/claude-desktop.ts +188 -0
  153. package/src/cli/claude.ts +286 -0
  154. package/src/cli/codex-shim-autorestore.ts +45 -0
  155. package/src/cli/combo.ts +119 -0
  156. package/src/cli/config-command.ts +145 -0
  157. package/src/cli/debug.ts +228 -0
  158. package/src/cli/doctor.ts +930 -0
  159. package/src/cli/export-command.ts +187 -0
  160. package/src/cli/help.ts +330 -0
  161. package/src/cli/index.ts +1103 -0
  162. package/src/cli/init.ts +224 -0
  163. package/src/cli/integrations.ts +142 -0
  164. package/src/cli/interactive-confirm.ts +133 -0
  165. package/src/cli/internal-dispatch.ts +20 -0
  166. package/src/cli/models-runtime.ts +212 -0
  167. package/src/cli/models.ts +336 -0
  168. package/src/cli/observe.ts +117 -0
  169. package/src/cli/opencode.ts +586 -0
  170. package/src/cli/provider-runtime.ts +162 -0
  171. package/src/cli/provider.ts +463 -0
  172. package/src/cli/runtime-api.ts +325 -0
  173. package/src/cli/star-prompt.ts +134 -0
  174. package/src/cli/status-oauth.ts +78 -0
  175. package/src/cli/status.ts +321 -0
  176. package/src/cli/system-command.ts +114 -0
  177. package/src/cli/tray-proxy.ts +52 -0
  178. package/src/cli/v2.ts +173 -0
  179. package/src/cli.ts +10 -0
  180. package/src/clients/config-export.ts +377 -0
  181. package/src/codex/account-id.ts +34 -0
  182. package/src/codex/account-label.ts +34 -0
  183. package/src/codex/account-lifecycle.ts +55 -0
  184. package/src/codex/account-namespace-match.ts +63 -0
  185. package/src/codex/account-namespaces.ts +149 -0
  186. package/src/codex/account-pause.ts +20 -0
  187. package/src/codex/account-runtime-state.ts +31 -0
  188. package/src/codex/account-store.ts +517 -0
  189. package/src/codex/account-usability.ts +20 -0
  190. package/src/codex/app-server-processes.ts +756 -0
  191. package/src/codex/auth-api.ts +1540 -0
  192. package/src/codex/auth-collision.ts +107 -0
  193. package/src/codex/auth-context.ts +352 -0
  194. package/src/codex/autostart-health.ts +149 -0
  195. package/src/codex/catalog/aggregation.ts +378 -0
  196. package/src/codex/catalog/bundled.ts +251 -0
  197. package/src/codex/catalog/effort.ts +355 -0
  198. package/src/codex/catalog/metadata.ts +180 -0
  199. package/src/codex/catalog/parsing.ts +456 -0
  200. package/src/codex/catalog/provider-fetch.ts +902 -0
  201. package/src/codex/catalog/sync.ts +620 -0
  202. package/src/codex/catalog.ts +12 -0
  203. package/src/codex/data/upstream-models.json +830 -0
  204. package/src/codex/exec-invocation.ts +22 -0
  205. package/src/codex/features.ts +969 -0
  206. package/src/codex/history-migration-guardian.ts +102 -0
  207. package/src/codex/history-provider.ts +776 -0
  208. package/src/codex/home.ts +206 -0
  209. package/src/codex/inject.ts +799 -0
  210. package/src/codex/injected-marker.ts +72 -0
  211. package/src/codex/journal.ts +163 -0
  212. package/src/codex/main-account-cache.ts +32 -0
  213. package/src/codex/main-account.ts +40 -0
  214. package/src/codex/model-cache.ts +227 -0
  215. package/src/codex/paths.ts +65 -0
  216. package/src/codex/plugins-doctor.ts +242 -0
  217. package/src/codex/pool-rotation.ts +225 -0
  218. package/src/codex/project-config-warnings.ts +411 -0
  219. package/src/codex/quota.ts +411 -0
  220. package/src/codex/refresh.ts +53 -0
  221. package/src/codex/routing.ts +1477 -0
  222. package/src/codex/runtime.ts +538 -0
  223. package/src/codex/shim.ts +1189 -0
  224. package/src/codex/subagent-defaults.ts +550 -0
  225. package/src/codex/subagent-model-fallback.ts +469 -0
  226. package/src/codex/sync.ts +130 -0
  227. package/src/codex/warmup.ts +192 -0
  228. package/src/codex/websocket-registry.ts +100 -0
  229. package/src/combos/failover.ts +140 -0
  230. package/src/combos/index.ts +41 -0
  231. package/src/combos/request.ts +62 -0
  232. package/src/combos/resolve.ts +232 -0
  233. package/src/combos/types.ts +326 -0
  234. package/src/config.ts +2355 -0
  235. package/src/generated/jawcode-model-metadata.ts +104 -0
  236. package/src/github/star-state.ts +203 -0
  237. package/src/grok/inject.ts +519 -0
  238. package/src/grok/status.ts +121 -0
  239. package/src/grok/sync.ts +66 -0
  240. package/src/images/artifacts.ts +516 -0
  241. package/src/images/fulfill-video.ts +163 -0
  242. package/src/images/fulfill.ts +149 -0
  243. package/src/images/index.ts +4 -0
  244. package/src/images/loop.ts +829 -0
  245. package/src/images/plan.ts +133 -0
  246. package/src/images/synthetic-tool.ts +133 -0
  247. package/src/images/types.ts +41 -0
  248. package/src/images/xai-client.ts +141 -0
  249. package/src/images/xai-video-client.ts +163 -0
  250. package/src/index.ts +22 -0
  251. package/src/lib/abort.ts +146 -0
  252. package/src/lib/admin-secrets.ts +25 -0
  253. package/src/lib/admission.ts +83 -0
  254. package/src/lib/app-owned-memory-stores.ts +173 -0
  255. package/src/lib/app-owned-memory.ts +265 -0
  256. package/src/lib/bounded-body.ts +202 -0
  257. package/src/lib/bun-binary-validator.d.mts +3 -0
  258. package/src/lib/bun-binary-validator.mjs +18 -0
  259. package/src/lib/bun-runtime.ts +71 -0
  260. package/src/lib/bun-stream-caps.ts +126 -0
  261. package/src/lib/config-ownership.ts +360 -0
  262. package/src/lib/crash-guard.ts +344 -0
  263. package/src/lib/debug-log-buffer.ts +83 -0
  264. package/src/lib/debug-settings.ts +108 -0
  265. package/src/lib/debug.ts +31 -0
  266. package/src/lib/destination-policy.ts +316 -0
  267. package/src/lib/errors.ts +364 -0
  268. package/src/lib/eventstream-decoder.ts +253 -0
  269. package/src/lib/gcp-adc.ts +341 -0
  270. package/src/lib/injection-debug-log.ts +58 -0
  271. package/src/lib/open-url.ts +25 -0
  272. package/src/lib/pinned-http.ts +151 -0
  273. package/src/lib/privacy.ts +20 -0
  274. package/src/lib/process-control.ts +165 -0
  275. package/src/lib/provider-outbound.ts +170 -0
  276. package/src/lib/provider-url.ts +14 -0
  277. package/src/lib/proxy-env.ts +18 -0
  278. package/src/lib/redact.ts +105 -0
  279. package/src/lib/retry-after.ts +55 -0
  280. package/src/lib/service-secrets.ts +25 -0
  281. package/src/lib/shadow-call.ts +30 -0
  282. package/src/lib/sidecar-tracker.ts +52 -0
  283. package/src/lib/sse-decoder.ts +323 -0
  284. package/src/lib/state-store-registrations.ts +109 -0
  285. package/src/lib/state-store-sweeper.ts +184 -0
  286. package/src/lib/test-home-guard.ts +90 -0
  287. package/src/lib/token-estimate.ts +69 -0
  288. package/src/lib/translator-budget.ts +356 -0
  289. package/src/lib/upstream-retry.ts +239 -0
  290. package/src/lib/win-exec.ts +115 -0
  291. package/src/lib/win-paths.ts +68 -0
  292. package/src/lib/windows-elevation.ts +705 -0
  293. package/src/lib/windows-secret-acl.ts +514 -0
  294. package/src/lib/winsw.ts +375 -0
  295. package/src/oauth/anthropic-routing.ts +594 -0
  296. package/src/oauth/anthropic.ts +177 -0
  297. package/src/oauth/callback-server.ts +294 -0
  298. package/src/oauth/chatgpt.ts +150 -0
  299. package/src/oauth/cursor.ts +211 -0
  300. package/src/oauth/github-copilot.ts +428 -0
  301. package/src/oauth/google-antigravity.ts +230 -0
  302. package/src/oauth/health.ts +399 -0
  303. package/src/oauth/index.ts +1173 -0
  304. package/src/oauth/key-providers.ts +108 -0
  305. package/src/oauth/kimi.ts +213 -0
  306. package/src/oauth/kiro-credentials.ts +726 -0
  307. package/src/oauth/kiro.ts +577 -0
  308. package/src/oauth/local-token-detect.ts +121 -0
  309. package/src/oauth/log.ts +48 -0
  310. package/src/oauth/login-cli.ts +163 -0
  311. package/src/oauth/pkce.ts +15 -0
  312. package/src/oauth/store.ts +630 -0
  313. package/src/oauth/token-guardian.ts +303 -0
  314. package/src/oauth/types.ts +62 -0
  315. package/src/oauth/xai.ts +241 -0
  316. package/src/providers/alibaba-region-backup.ts +75 -0
  317. package/src/providers/alibaba-region-migration.ts +156 -0
  318. package/src/providers/alibaba-region-startup.ts +36 -0
  319. package/src/providers/antigravity-models.ts +205 -0
  320. package/src/providers/api-keys.ts +140 -0
  321. package/src/providers/base-url-choices.ts +64 -0
  322. package/src/providers/context-cap.ts +65 -0
  323. package/src/providers/derive.ts +339 -0
  324. package/src/providers/free-directory.ts +184 -0
  325. package/src/providers/github-copilot-transport.ts +56 -0
  326. package/src/providers/key-failover.ts +203 -0
  327. package/src/providers/kiro-models.ts +67 -0
  328. package/src/providers/label.ts +19 -0
  329. package/src/providers/model-discovery.ts +356 -0
  330. package/src/providers/openai-sidecar.ts +175 -0
  331. package/src/providers/openai-tier-startup.ts +27 -0
  332. package/src/providers/openai-tiers.ts +301 -0
  333. package/src/providers/openai-virtual-models.ts +82 -0
  334. package/src/providers/openrouter-routing.ts +102 -0
  335. package/src/providers/provider-id-rewrite.ts +150 -0
  336. package/src/providers/quota.ts +1045 -0
  337. package/src/providers/registry.ts +1600 -0
  338. package/src/providers/slug-codec.ts +67 -0
  339. package/src/providers/xai-transport.ts +141 -0
  340. package/src/reasoning-effort.ts +135 -0
  341. package/src/responses/compaction.ts +117 -0
  342. package/src/responses/parser.ts +656 -0
  343. package/src/responses/reasoning-envelope.ts +52 -0
  344. package/src/responses/schema.ts +159 -0
  345. package/src/responses/spill-store.ts +394 -0
  346. package/src/responses/state.ts +895 -0
  347. package/src/responses/tool-groups.ts +19 -0
  348. package/src/router.ts +425 -0
  349. package/src/server/adapter-resolve.ts +80 -0
  350. package/src/server/auth-cors.ts +530 -0
  351. package/src/server/chat-completions.ts +368 -0
  352. package/src/server/claude-messages.ts +914 -0
  353. package/src/server/effort-policy.ts +172 -0
  354. package/src/server/gui-static.ts +123 -0
  355. package/src/server/image-retry.ts +42 -0
  356. package/src/server/images.ts +476 -0
  357. package/src/server/index.ts +1126 -0
  358. package/src/server/lifecycle.ts +305 -0
  359. package/src/server/live.ts +598 -0
  360. package/src/server/management/agent-settings-routes.ts +1039 -0
  361. package/src/server/management/api-access.ts +141 -0
  362. package/src/server/management/api-key-usage.ts +167 -0
  363. package/src/server/management/body.ts +35 -0
  364. package/src/server/management/combo-routes.ts +220 -0
  365. package/src/server/management/config-routes.ts +422 -0
  366. package/src/server/management/context.ts +31 -0
  367. package/src/server/management/logs-usage-routes.ts +516 -0
  368. package/src/server/management/model-routes.ts +525 -0
  369. package/src/server/management/oauth-account-routes.ts +561 -0
  370. package/src/server/management/provider-routes.ts +556 -0
  371. package/src/server/management/shared.ts +277 -0
  372. package/src/server/management/sidebar-routes.ts +88 -0
  373. package/src/server/management/system-restart.ts +179 -0
  374. package/src/server/management/system-routes.ts +117 -0
  375. package/src/server/management/usage-summary-cache.ts +86 -0
  376. package/src/server/management-api.ts +211 -0
  377. package/src/server/management-auth.ts +267 -0
  378. package/src/server/memory-watchdog.ts +156 -0
  379. package/src/server/port-reclaim.ts +307 -0
  380. package/src/server/ports.ts +116 -0
  381. package/src/server/proxy-liveness.ts +201 -0
  382. package/src/server/relay-eager.ts +313 -0
  383. package/src/server/relay.ts +1060 -0
  384. package/src/server/request-decompress.ts +132 -0
  385. package/src/server/request-log-conversation.ts +168 -0
  386. package/src/server/request-log.ts +1046 -0
  387. package/src/server/responses/collaboration.ts +354 -0
  388. package/src/server/responses/compact.ts +384 -0
  389. package/src/server/responses/core.ts +2756 -0
  390. package/src/server/responses/encrypted-payload.ts +308 -0
  391. package/src/server/responses/fetch-helpers.ts +157 -0
  392. package/src/server/responses/passthrough-error.ts +78 -0
  393. package/src/server/responses/terminal-guard.ts +230 -0
  394. package/src/server/responses/upstream-error.ts +48 -0
  395. package/src/server/responses-image-gen-repair.ts +132 -0
  396. package/src/server/responses-item-id-repair.ts +224 -0
  397. package/src/server/responses.ts +9 -0
  398. package/src/server/search.ts +136 -0
  399. package/src/server/sse-payload-rewrite.ts +175 -0
  400. package/src/server/startup-action-control.ts +308 -0
  401. package/src/server/startup-health-cache.ts +113 -0
  402. package/src/server/system-env.ts +413 -0
  403. package/src/server/windows-tcp-drop.ts +184 -0
  404. package/src/server/windows-tray-control.ts +41 -0
  405. package/src/server/ws-bridge.ts +471 -0
  406. package/src/service.ts +2554 -0
  407. package/src/stall-timeout.ts +20 -0
  408. package/src/storage/cleanup-job.ts +57 -0
  409. package/src/storage/cleanup.ts +3085 -0
  410. package/src/storage/policy-job.ts +457 -0
  411. package/src/storage/policy-scheduler.ts +40 -0
  412. package/src/storage/policy-worker.ts +59 -0
  413. package/src/storage/policy.ts +527 -0
  414. package/src/storage/restore-job.ts +299 -0
  415. package/src/storage/restore-worker.ts +58 -0
  416. package/src/storage/scanner.ts +238 -0
  417. package/src/storage/storage-mutation-coordinator.ts +139 -0
  418. package/src/storage/worker-lifecycle.ts +215 -0
  419. package/src/tray/assets/opencodex-tray-offline.ico +0 -0
  420. package/src/tray/assets/opencodex-tray-online.ico +0 -0
  421. package/src/tray/assets/opencodex-tray-warning.ico +0 -0
  422. package/src/tray/assets/opencodex-tray.png +0 -0
  423. package/src/tray/windows-tray.ps1 +290 -0
  424. package/src/tray/windows.ts +730 -0
  425. package/src/types.ts +1237 -0
  426. package/src/update/badge.ts +72 -0
  427. package/src/update/index.ts +392 -0
  428. package/src/update/job.ts +1513 -0
  429. package/src/update/notify.ts +257 -0
  430. package/src/update/npm-invocation.d.mts +23 -0
  431. package/src/update/npm-invocation.mjs +94 -0
  432. package/src/update/tray-update-plan.d.mts +18 -0
  433. package/src/update/tray-update-plan.mjs +38 -0
  434. package/src/usage/cost.ts +0 -0
  435. package/src/usage/debug.ts +97 -0
  436. package/src/usage/expected-prices.ts +164 -0
  437. package/src/usage/log.ts +658 -0
  438. package/src/usage/summary.ts +585 -0
  439. package/src/usage/totals.ts +14 -0
  440. package/src/vision/anthropic-describe.ts +185 -0
  441. package/src/vision/describe.ts +125 -0
  442. package/src/vision/index.ts +467 -0
  443. package/src/web-search/anthropic-executor.ts +189 -0
  444. package/src/web-search/executor.ts +105 -0
  445. package/src/web-search/format-result.ts +89 -0
  446. package/src/web-search/index.ts +196 -0
  447. package/src/web-search/loop.ts +664 -0
  448. package/src/web-search/parse.ts +220 -0
  449. package/src/web-search/progress-stream.ts +342 -0
  450. package/src/web-search/synthetic-tool.ts +47 -0
  451. package/vendor/ocx-notch/win32-x64/ocx-notch.exe +0 -0
@@ -0,0 +1,1045 @@
1
+ import { fetchMainAccountInfo, listCodexAuthAccounts } from "../codex/auth-api";
2
+ import { MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
3
+ import { resolveEnvValue } from "../config";
4
+ import { getValidAccessToken, getValidAccessTokenForAccount } from "../oauth";
5
+ import { getAccountCredential, getAccountSet, getCredential } from "../oauth/store";
6
+ import { antigravityUserAgent } from "../adapters/client-fingerprint";
7
+ import { resolveKiroApiRegion, resolveKiroProfileArn } from "../oauth/kiro";
8
+ import type { KiroOAuthMetadata } from "../oauth/types";
9
+ import { getProviderRegistryEntry, providerCodexAccountMode } from "./registry";
10
+ import type { OcxConfig, OcxProviderConfig } from "../types";
11
+ import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "./openai-tiers";
12
+ import {
13
+ captureConfigGeneration,
14
+ sweepExpiredOnWrite,
15
+ type GenerationContext,
16
+ } from "../lib/state-store-sweeper";
17
+
18
+ /** Match oauth/index REFRESH_SKEW_MS — use stored access without refresh when still fresh. */
19
+ const ACCOUNT_TOKEN_SKEW_MS = 60_000;
20
+
21
+ const CACHE_TTL_MS = 5 * 60_000;
22
+ const REQUEST_TIMEOUT_MS = 8_000;
23
+ const KIMI_CODE_BASE_URL = "https://api.kimi.com/coding/v1";
24
+ const KIMI_CODE_USAGE_URL = `${KIMI_CODE_BASE_URL}/usages`;
25
+ const KIRO_USAGE_LIMITS_PATH = "getUsageLimits";
26
+ /** Keep a failed probe's previous row at most this long before dropping it. */
27
+ const LAST_GOOD_MAX_AGE_MS = 30 * 60_000;
28
+
29
+ export interface ProviderQuotaWindow {
30
+ label: string;
31
+ percent: number;
32
+ resetAt?: number;
33
+ }
34
+
35
+ export interface ProviderQuota {
36
+ fiveHourPercent?: number;
37
+ fiveHourResetAt?: number;
38
+ weeklyPercent?: number;
39
+ weeklyResetAt?: number;
40
+ monthlyPercent?: number;
41
+ monthlyResetAt?: number;
42
+ customWindows?: ProviderQuotaWindow[];
43
+ updatedAt: number;
44
+ }
45
+
46
+ export interface ProviderQuotaReport {
47
+ provider: string;
48
+ label: string;
49
+ source: string;
50
+ quota: ProviderQuota;
51
+ updatedAt: number;
52
+ reverseEngineered?: boolean;
53
+ }
54
+
55
+ export interface ProviderQuotaResponse {
56
+ generatedAt: number;
57
+ reports: ProviderQuotaReport[];
58
+ }
59
+
60
+ let cache: { key: string; ts: number; response: ProviderQuotaResponse } | null = null;
61
+ const inflight = new Map<string, { epoch: number; promise: Promise<ProviderQuotaResponse> }>();
62
+ /** Bumped on cache clear and on force-refresh start; stale-epoch probes lose commit authority. */
63
+ let invalidationEpoch = 0;
64
+
65
+ /** Invalidate the report cache (e.g. after switching a provider's active account). */
66
+ export function clearProviderQuotaCache(): void {
67
+ cache = null;
68
+ invalidationEpoch += 1;
69
+ }
70
+
71
+ function cacheKey(config: OcxConfig): string {
72
+ const providers = Object.entries(config.providers)
73
+ .map(([name, provider]) => `${name}:${provider.adapter}:${provider.authMode ?? "key"}:${providerCodexAccountMode(name, provider) ?? "none"}:${provider.disabled === true ? "off" : "on"}:${provider.baseUrl}`)
74
+ .sort()
75
+ .join("|");
76
+ return `${config.defaultProvider}|${config.activeCodexAccountId ?? ""}|${providers}`;
77
+ }
78
+
79
+ function hasQuotaRows(quota: ProviderQuota | null | undefined): quota is ProviderQuota {
80
+ if (!quota) return false;
81
+ return typeof quota.fiveHourPercent === "number"
82
+ || typeof quota.weeklyPercent === "number"
83
+ || typeof quota.monthlyPercent === "number"
84
+ || !!quota.customWindows?.some(window => typeof window.percent === "number");
85
+ }
86
+
87
+ function providerLabel(providerId: string): string {
88
+ return getProviderRegistryEntry(providerId)?.label ?? providerId;
89
+ }
90
+
91
+ function normalizeResetAt(value: unknown): number | undefined {
92
+ if (typeof value === "number" && Number.isFinite(value)) return value > 10_000_000_000 ? value : value * 1000;
93
+ if (typeof value === "string" && value.trim()) {
94
+ const trimmed = value.trim();
95
+ // Cursor Connect RPC returns billingCycleEnd as a unix-ms decimal string ("1771077734000").
96
+ // Date.parse treats that as invalid; numeric epoch strings must be handled explicitly.
97
+ if (/^\d+(\.\d+)?$/.test(trimmed)) {
98
+ const numeric = Number(trimmed);
99
+ if (Number.isFinite(numeric)) return numeric > 10_000_000_000 ? numeric : numeric * 1000;
100
+ }
101
+ const parsed = Date.parse(trimmed);
102
+ return Number.isFinite(parsed) ? parsed : undefined;
103
+ }
104
+ return undefined;
105
+ }
106
+
107
+ function toFiniteNumber(value: unknown): number | undefined {
108
+ if (typeof value === "number" && Number.isFinite(value)) return value;
109
+ if (typeof value === "string" && value.trim()) {
110
+ const parsed = Number(value);
111
+ return Number.isFinite(parsed) ? parsed : undefined;
112
+ }
113
+ return undefined;
114
+ }
115
+
116
+ function normalizePercent(value: unknown): number | undefined {
117
+ const numeric = toFiniteNumber(value);
118
+ return numeric === undefined ? undefined : Math.max(0, Math.min(100, numeric));
119
+ }
120
+
121
+ function asRecord(value: unknown): Record<string, unknown> | null {
122
+ return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null;
123
+ }
124
+
125
+ function isBuiltInChatGptForwardProvider(name: string, provider: OcxProviderConfig): boolean {
126
+ return name === OPENAI_CODEX_PROVIDER_ID && isCanonicalOpenAiForwardProvider(provider);
127
+ }
128
+
129
+ function report(provider: string, source: string, quota: ProviderQuota): ProviderQuotaReport | null {
130
+ if (!hasQuotaRows(quota)) return null;
131
+ return {
132
+ provider,
133
+ label: providerLabel(provider),
134
+ source,
135
+ quota,
136
+ updatedAt: quota.updatedAt,
137
+ };
138
+ }
139
+
140
+ async function fetchChatGptForwardQuota(
141
+ config: OcxConfig,
142
+ provider: string,
143
+ providerConfig: OcxProviderConfig,
144
+ forceRefresh: boolean,
145
+ ): Promise<ProviderQuotaReport | null> {
146
+ if (providerCodexAccountMode(provider, providerConfig) === "direct") {
147
+ const main = await fetchMainAccountInfo(forceRefresh);
148
+ const quota = main.quota ? { ...main.quota, updatedAt: Date.now() } as ProviderQuota : null;
149
+ return quota ? report(provider, "chatgpt:wham", quota) : null;
150
+ }
151
+ const accounts = await listCodexAuthAccounts(config, forceRefresh);
152
+ const activeId = config.activeCodexAccountId || MAIN_CODEX_ACCOUNT_ID;
153
+ const active = accounts.find(account => account.id === activeId)
154
+ ?? accounts.find(account => account.id === MAIN_CODEX_ACCOUNT_ID)
155
+ ?? accounts[0];
156
+ const quota = active?.quota ? { ...active.quota, updatedAt: active.quota.updatedAt ?? Date.now() } as ProviderQuota : null;
157
+ return quota ? report(provider, "chatgpt:wham", quota) : null;
158
+ }
159
+
160
+ function centsValue(value: unknown): number | undefined {
161
+ const rec = asRecord(value);
162
+ return rec ? toFiniteNumber(rec.val) : undefined;
163
+ }
164
+
165
+ async function fetchXaiQuota(provider: string): Promise<ProviderQuotaReport | null> {
166
+ let accessToken: string;
167
+ try {
168
+ accessToken = await getValidAccessToken("xai");
169
+ } catch {
170
+ return null;
171
+ }
172
+ const response = await fetch("https://cli-chat-proxy.grok.com/v1/billing", {
173
+ headers: { Accept: "application/json", Authorization: `Bearer ${accessToken}` },
174
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
175
+ });
176
+ if (!response.ok) return null;
177
+ const body = asRecord(await response.json().catch(() => null));
178
+ const config = asRecord(body?.config);
179
+ if (!config) return null;
180
+ const limitCents = centsValue(config.monthlyLimit);
181
+ const usedCents = centsValue(config.used);
182
+ if (limitCents === undefined || usedCents === undefined || limitCents <= 0) return null;
183
+ const percent = normalizePercent((usedCents / limitCents) * 100);
184
+ if (percent === undefined) return null;
185
+ const quota: ProviderQuota = {
186
+ monthlyPercent: percent,
187
+ monthlyResetAt: normalizeResetAt(config.billingPeriodEnd),
188
+ updatedAt: Date.now(),
189
+ };
190
+ return report(provider, "xai:grok-billing", quota);
191
+ }
192
+
193
+ function parseClaudeBucket(value: unknown): { percent?: number; resetAt?: number } | null {
194
+ const rec = asRecord(value);
195
+ if (!rec) return null;
196
+ const percent = normalizePercent(rec.utilization);
197
+ const resetAt = normalizeResetAt(rec.resets_at);
198
+ if (percent === undefined && resetAt === undefined) return null;
199
+ return { percent, resetAt };
200
+ }
201
+
202
+ function kiroCreditRow(body: Record<string, unknown>): Record<string, unknown> | null {
203
+ const rows = Array.isArray(body.usageBreakdownList) ? body.usageBreakdownList : [];
204
+ const credit = rows
205
+ .map(row => asRecord(row))
206
+ .find((row): row is Record<string, unknown> => row?.resourceType === "CREDIT");
207
+ return credit ?? asRecord(body.usageBreakdown);
208
+ }
209
+
210
+ /**
211
+ * Probe one Kiro credential using only that account's persisted routing metadata.
212
+ * Passing an explicit metadata object is load-bearing: the Kiro resolvers then
213
+ * cannot borrow environment or local-CLI state from a different account.
214
+ */
215
+ async function fetchKiroUsageQuota(
216
+ accessToken: string,
217
+ metadata: Pick<KiroOAuthMetadata, "profileArn" | "apiRegion" | "ssoRegion">,
218
+ ): Promise<ProviderQuota | null> {
219
+ const region = resolveKiroApiRegion(metadata);
220
+ const profileArn = resolveKiroProfileArn(metadata);
221
+ const url = new URL(`https://q.${region}.amazonaws.com/${KIRO_USAGE_LIMITS_PATH}`);
222
+ url.searchParams.set("origin", "KIRO_CLI");
223
+ url.searchParams.set("resourceType", "AGENTIC_REQUEST");
224
+ if (profileArn) url.searchParams.set("profileArn", profileArn);
225
+
226
+ const response = await fetch(url, {
227
+ headers: { Accept: "application/json", Authorization: `Bearer ${accessToken}` },
228
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
229
+ });
230
+ if (!response.ok) return null;
231
+ const body = asRecord(await response.json().catch(() => null));
232
+ if (!body) return null;
233
+ const credit = kiroCreditRow(body);
234
+ if (!credit) return null;
235
+
236
+ const used = toFiniteNumber(credit.currentUsageWithPrecision)
237
+ ?? toFiniteNumber(credit.currentUsage);
238
+ const limit = toFiniteNumber(credit.usageLimitWithPrecision)
239
+ ?? toFiniteNumber(credit.usageLimit);
240
+ if (used === undefined || limit === undefined || limit <= 0) return null;
241
+ const percent = normalizePercent((used / limit) * 100);
242
+ if (percent === undefined) return null;
243
+ const resetAt = normalizeResetAt(credit.nextDateReset)
244
+ ?? normalizeResetAt(body.nextDateReset);
245
+ return {
246
+ monthlyPercent: percent,
247
+ ...(resetAt !== undefined ? { monthlyResetAt: resetAt } : {}),
248
+ updatedAt: Date.now(),
249
+ };
250
+ }
251
+
252
+ /** Claude's OAuth usage endpoint, probed with ONE account's own bearer token. */
253
+ const anthropicUsageInflight = new Map<string, Promise<ProviderQuota | null>>();
254
+
255
+ async function fetchAnthropicUsageQuota(accessToken: string): Promise<ProviderQuota | null> {
256
+ const joinable = anthropicUsageInflight.get(accessToken);
257
+ if (joinable) return joinable;
258
+
259
+ const probe = (async (): Promise<ProviderQuota | null> => {
260
+ const response = await fetch("https://api.anthropic.com/api/oauth/usage", {
261
+ headers: {
262
+ Accept: "application/json, text/plain, */*",
263
+ "Content-Type": "application/json",
264
+ "User-Agent": "claude-cli/2.1.63 (external, cli)",
265
+ "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",
266
+ Authorization: `Bearer ${accessToken}`,
267
+ },
268
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
269
+ });
270
+ if (!response.ok) return null;
271
+ const body = asRecord(await response.json().catch(() => null));
272
+ if (!body) return null;
273
+ const fiveHour = parseClaudeBucket(body.five_hour);
274
+ const sevenDay = parseClaudeBucket(body.seven_day);
275
+ const opus = parseClaudeBucket(body.seven_day_opus);
276
+ const sonnet = parseClaudeBucket(body.seven_day_sonnet);
277
+ const customWindows: ProviderQuotaWindow[] = [];
278
+ if (opus?.percent !== undefined) customWindows.push({ label: "Opus", percent: opus.percent, ...(opus.resetAt !== undefined ? { resetAt: opus.resetAt } : {}) });
279
+ if (sonnet?.percent !== undefined) customWindows.push({ label: "Sonnet", percent: sonnet.percent, ...(sonnet.resetAt !== undefined ? { resetAt: sonnet.resetAt } : {}) });
280
+ const quota: ProviderQuota = {
281
+ // Claude's 5-hour window is a first-class rate limit, same as the Codex login 5h/weekly
282
+ // rows: report it in the canonical fields so the dashboard renders it with the standard
283
+ // "5-hour limit" label and ordering instead of as a generic extra window.
284
+ ...(fiveHour?.percent !== undefined ? { fiveHourPercent: fiveHour.percent } : {}),
285
+ ...(fiveHour?.resetAt !== undefined ? { fiveHourResetAt: fiveHour.resetAt } : {}),
286
+ ...(sevenDay?.percent !== undefined ? { weeklyPercent: sevenDay.percent } : {}),
287
+ ...(sevenDay?.resetAt !== undefined ? { weeklyResetAt: sevenDay.resetAt } : {}),
288
+ ...(customWindows.length > 0 ? { customWindows } : {}),
289
+ updatedAt: Date.now(),
290
+ };
291
+ // Empty / schema-changed payloads must not cache as "success with no bars".
292
+ return hasQuotaRows(quota) ? quota : null;
293
+ })().finally(() => {
294
+ if (anthropicUsageInflight.get(accessToken) === probe) anthropicUsageInflight.delete(accessToken);
295
+ });
296
+ anthropicUsageInflight.set(accessToken, probe);
297
+ return probe;
298
+ }
299
+
300
+ async function fetchAnthropicQuota(provider: string): Promise<ProviderQuotaReport | null> {
301
+ // Capture the account we intend to probe before awaiting — a mid-flight active
302
+ // switch must not seed the wrong account's cache with this response.
303
+ const probedAccountId = getAccountSet("anthropic")?.activeAccountId;
304
+ const probedAccountKey = probedAccountId ? accountCacheKey("anthropic", probedAccountId) : null;
305
+ const writerGeneration = captureConfigGeneration();
306
+ let accessToken: string;
307
+ try {
308
+ accessToken = await getValidAccessToken("anthropic");
309
+ } catch {
310
+ return null;
311
+ }
312
+ const quota = await fetchAnthropicUsageQuota(accessToken);
313
+ if (!quota) return null;
314
+ // Share the active-account probe with the per-account cache so Providers-page
315
+ // loads do not double-hit Anthropic's rate-limited usage endpoint.
316
+ if (probedAccountId && probedAccountKey) {
317
+ const stillOwnsToken = getAccountCredential("anthropic", probedAccountId)?.access === accessToken;
318
+ if (stillOwnsToken && mayCommitAccountQuotaKey(probedAccountKey, writerGeneration)) {
319
+ accountQuotaCache.set(probedAccountKey, { ts: Date.now(), quota });
320
+ }
321
+ }
322
+ return report(provider, "anthropic:oauth-usage", quota);
323
+ }
324
+
325
+ // ---------------------------------------------------------------------------
326
+ // Per-account quota (multiauth)
327
+ // ---------------------------------------------------------------------------
328
+
329
+ /**
330
+ * Anthropic reports usage per CREDENTIAL, so every logged-in account can be probed with its
331
+ * own bearer token — the active-account selection and the local usage log are irrelevant here.
332
+ * Mirrors the Codex pool behaviour (codex/auth-api.ts:fetchPoolAccountQuota), including a
333
+ * per-account TTL so N accounts cost at most N upstream calls per window.
334
+ *
335
+ * The TTL is deliberately longer than the provider-level one: this path multiplies by account
336
+ * count, and Anthropic rate-limits the usage endpoint (observed 429 under repeated probing).
337
+ */
338
+ const ACCOUNT_QUOTA_TTL_MS = 10 * 60_000;
339
+ type AccountQuotaCacheEntry = {
340
+ ts: number;
341
+ quota: ProviderQuota | null;
342
+ /** Last probe failed (429 / network / expired login); still may hold last-good quota. */
343
+ unavailable?: true;
344
+ };
345
+ const accountQuotaCache = new Map<string, AccountQuotaCacheEntry>();
346
+ const accountQuotaInflight = new Map<string, Promise<AccountQuotaCacheEntry>>();
347
+ let lastReconciledGeneration = 0;
348
+ let liveAccountQuotaKeys = new Set<string>();
349
+ let liveProviderQuotaKeys = new Set<string>();
350
+
351
+ function mayCommitAccountQuotaKey(key: string, writerGeneration: number): boolean {
352
+ return writerGeneration >= lastReconciledGeneration || liveAccountQuotaKeys.has(key);
353
+ }
354
+
355
+ function mayCommitProviderQuotaKey(key: string, writerGeneration: number): boolean {
356
+ return writerGeneration >= lastReconciledGeneration || liveProviderQuotaKeys.has(key);
357
+ }
358
+
359
+ export interface ProviderAccountQuota {
360
+ accountId: string;
361
+ quota: ProviderQuota | null;
362
+ /** Set when the probe could not reach upstream (expired login, 429, network). */
363
+ unavailable?: true;
364
+ }
365
+
366
+ /** Providers whose per-account quota can be probed. Extend as other OAuth APIs are covered. */
367
+ export function supportsPerAccountQuota(provider: string): boolean {
368
+ return provider === "anthropic" || provider === "kiro";
369
+ }
370
+
371
+ function accountCacheKey(provider: string, accountId: string): string {
372
+ return `${provider}\u0000${accountId}`;
373
+ }
374
+
375
+ /**
376
+ * Synchronous last-good per-account quota read for routing. Never probes the network.
377
+ * Returns null when nothing is cached (or the cached row has no bars).
378
+ */
379
+ export function getCachedProviderAccountQuota(provider: string, accountId: string): ProviderQuota | null {
380
+ const entry = accountQuotaCache.get(accountCacheKey(provider, accountId));
381
+ return entry?.quota ?? null;
382
+ }
383
+
384
+ /** Test-only: seed or clear the per-account quota cache without probing upstream. */
385
+ export function setCachedProviderAccountQuotaForTests(
386
+ provider: string,
387
+ accountId: string,
388
+ quota: ProviderQuota | null,
389
+ ): void {
390
+ const key = accountCacheKey(provider, accountId);
391
+ if (quota === null) {
392
+ accountQuotaCache.delete(key);
393
+ return;
394
+ }
395
+ accountQuotaCache.set(key, { ts: Date.now(), quota });
396
+ }
397
+
398
+ export function sweepExpiredProviderAccountQuotaRows(now = Date.now()): number {
399
+ let removed = 0;
400
+ for (const [key, entry] of accountQuotaCache) {
401
+ if (entry.ts + ACCOUNT_QUOTA_TTL_MS > now) continue;
402
+ accountQuotaCache.delete(key);
403
+ removed += 1;
404
+ }
405
+ return removed;
406
+ }
407
+
408
+ export function reconcileProviderAccountQuotaRows(context: GenerationContext): number {
409
+ if (context.generation <= lastReconciledGeneration) return 0;
410
+ let removed = 0;
411
+ for (const key of accountQuotaCache.keys()) {
412
+ if (context.oauthAccountKeys.has(key)) continue;
413
+ accountQuotaCache.delete(key);
414
+ removed += 1;
415
+ }
416
+ if (cache) {
417
+ const reports = cache.response.reports.filter(report => context.providerNames.has(report.provider));
418
+ removed += cache.response.reports.length - reports.length;
419
+ cache = { ...cache, response: { ...cache.response, reports } };
420
+ }
421
+ liveAccountQuotaKeys = new Set(context.oauthAccountKeys);
422
+ liveProviderQuotaKeys = new Set(context.providerNames);
423
+ lastReconciledGeneration = context.generation;
424
+ return removed;
425
+ }
426
+
427
+ /** Drop cached per-account rows (all, or just one provider's). */
428
+ export function clearAccountQuotaCache(provider?: string): void {
429
+ if (!provider) {
430
+ accountQuotaCache.clear();
431
+ accountQuotaInflight.clear();
432
+ return;
433
+ }
434
+ const prefix = `${provider}\u0000`;
435
+ for (const key of [...accountQuotaCache.keys()]) {
436
+ if (key.startsWith(prefix)) accountQuotaCache.delete(key);
437
+ }
438
+ // Drop in-flight probes too so a late resolve cannot repopulate after logout/remove.
439
+ for (const key of [...accountQuotaInflight.keys()]) {
440
+ if (key.startsWith(prefix)) accountQuotaInflight.delete(key);
441
+ }
442
+ }
443
+
444
+ /**
445
+ * Resolve a bearer for quota probing without silently adopting a newer global
446
+ * Claude CLI credential into a background multiauth slot.
447
+ *
448
+ * - Fresh stored access → use as-is (no refresh).
449
+ * - Active account with expired access → normal refresh path.
450
+ * - Background `local-cli` with expired access → fail closed (unavailable):
451
+ * `getValidAccessTokenForAccount` can persist a mismatched Claude CLI identity.
452
+ * - Background ordinary OAuth (`source !== "local-cli"`) → safe to refresh;
453
+ * Anthropic's lock only adopts disk credentials for `local-cli` rows.
454
+ */
455
+ async function getTokenForAccountQuotaProbe(provider: string, accountId: string): Promise<string> {
456
+ const stored = getAccountCredential(provider, accountId);
457
+ if (!stored) throw new Error("account credential missing");
458
+ if (stored.expires > Date.now() + ACCOUNT_TOKEN_SKEW_MS) return stored.access;
459
+ const activeId = getAccountSet(provider)?.activeAccountId;
460
+ if (activeId !== accountId && stored.source === "local-cli") {
461
+ throw new Error("background local-cli token expired; skip CLI-adopting refresh for quota probe");
462
+ }
463
+ return getValidAccessTokenForAccount(provider, accountId);
464
+ }
465
+
466
+ async function fetchUsageQuotaForAccount(
467
+ provider: string,
468
+ accountId: string,
469
+ accessToken: string,
470
+ ): Promise<ProviderQuota | null> {
471
+ if (provider === "anthropic") return fetchAnthropicUsageQuota(accessToken);
472
+ if (provider === "kiro") {
473
+ const credential = getAccountCredential(provider, accountId);
474
+ if (!credential || credential.access !== accessToken) {
475
+ throw new Error("account credential changed during Kiro quota probe");
476
+ }
477
+ return fetchKiroUsageQuota(accessToken, credential.kiro ?? {});
478
+ }
479
+ return null;
480
+ }
481
+
482
+ async function fetchAccountQuota(
483
+ provider: string,
484
+ accountId: string,
485
+ forceRefresh: boolean,
486
+ ): Promise<AccountQuotaCacheEntry> {
487
+ const key = accountCacheKey(provider, accountId);
488
+ const writerGeneration = captureConfigGeneration();
489
+ const cached = accountQuotaCache.get(key);
490
+ if (!forceRefresh && cached && Date.now() - cached.ts < ACCOUNT_QUOTA_TTL_MS) return cached;
491
+ const joinable = accountQuotaInflight.get(key);
492
+ if (joinable) return joinable;
493
+
494
+ const probe = (async (): Promise<AccountQuotaCacheEntry> => {
495
+ try {
496
+ const token = await getTokenForAccountQuotaProbe(provider, accountId);
497
+ const quota = await fetchUsageQuotaForAccount(provider, accountId, token);
498
+ if (!quota) {
499
+ // Preserve last-good bars and mark unavailable; advance TTL so failures
500
+ // negative-cache instead of re-probing on every GUI poll.
501
+ const entry: AccountQuotaCacheEntry = {
502
+ ts: Date.now(),
503
+ quota: cached?.quota ?? null,
504
+ unavailable: true,
505
+ };
506
+ if (mayCommitAccountQuotaKey(key, writerGeneration)) {
507
+ accountQuotaCache.set(key, entry);
508
+ sweepExpiredOnWrite(entry.ts);
509
+ }
510
+ return entry;
511
+ }
512
+ const entry: AccountQuotaCacheEntry = { ts: Date.now(), quota };
513
+ if (mayCommitAccountQuotaKey(key, writerGeneration)) {
514
+ accountQuotaCache.set(key, entry);
515
+ sweepExpiredOnWrite(entry.ts);
516
+ }
517
+ return entry;
518
+ } catch {
519
+ const entry: AccountQuotaCacheEntry = {
520
+ ts: Date.now(),
521
+ quota: cached?.quota ?? null,
522
+ unavailable: true,
523
+ };
524
+ if (mayCommitAccountQuotaKey(key, writerGeneration)) {
525
+ accountQuotaCache.set(key, entry);
526
+ sweepExpiredOnWrite(entry.ts);
527
+ }
528
+ return entry;
529
+ }
530
+ })().finally(() => {
531
+ if (accountQuotaInflight.get(key) === probe) accountQuotaInflight.delete(key);
532
+ });
533
+ accountQuotaInflight.set(key, probe);
534
+ return probe;
535
+ }
536
+
537
+ /**
538
+ * Per-account quota rows for a provider's logged-in accounts. Probes run in parallel; a
539
+ * single failing account never blocks the others.
540
+ */
541
+ export async function fetchProviderAccountQuotas(
542
+ provider: string,
543
+ forceRefresh = false,
544
+ ): Promise<ProviderAccountQuota[]> {
545
+ if (!supportsPerAccountQuota(provider)) return [];
546
+ const set = getAccountSet(provider);
547
+ if (!set) return [];
548
+ return await Promise.all(set.accounts.map(async account => {
549
+ const entry = await fetchAccountQuota(provider, account.id, forceRefresh);
550
+ return {
551
+ accountId: account.id,
552
+ quota: entry.quota,
553
+ ...(entry.unavailable ? { unavailable: true as const } : {}),
554
+ };
555
+ }));
556
+ }
557
+
558
+ async function fetchKiroQuota(provider: string, forceRefresh: boolean): Promise<ProviderQuotaReport | null> {
559
+ const activeAccountId = getAccountSet("kiro")?.activeAccountId;
560
+ if (!activeAccountId) return null;
561
+ const entry = await fetchAccountQuota("kiro", activeAccountId, forceRefresh);
562
+ return entry.quota ? report(provider, "kiro:getUsageLimits", entry.quota) : null;
563
+ }
564
+
565
+ function normalizedBaseUrl(value: string): string | null {
566
+ try {
567
+ const url = new URL(value);
568
+ if (url.search || url.hash) return null;
569
+ return `${url.origin.toLowerCase()}${url.pathname.replace(/\/+$/, "")}`;
570
+ } catch {
571
+ return null;
572
+ }
573
+ }
574
+
575
+ function quotaResetAt(row: Record<string, unknown>): number | undefined {
576
+ return normalizeResetAt(row.resetTime ?? row.resetAt ?? row.reset_time ?? row.reset_at);
577
+ }
578
+
579
+ function isCanonicalKimiCodeBaseUrl(baseUrl: string): boolean {
580
+ return normalizedBaseUrl(baseUrl) === KIMI_CODE_BASE_URL;
581
+ }
582
+
583
+ /** Prefer the nested `data` shell when the outer object is only an envelope. */
584
+ function unwrapKimiQuotaPayload(value: unknown): Record<string, unknown> | null {
585
+ const body = asRecord(value);
586
+ if (!body) return null;
587
+ const nested = asRecord(body.data);
588
+ if (!nested) return body;
589
+ // A null/non-usable outer field is a placeholder, not data — an envelope like
590
+ // { usage: null, data: { usage: {...} } } must still unwrap to the nested payload.
591
+ const usable = (field: unknown): boolean => field !== undefined && field !== null;
592
+ const outerHasUsage = usable(body.usage) || usable(body.limits) || usable(body.totalQuota);
593
+ const nestedHasUsage = usable(nested.usage) || usable(nested.limits) || usable(nested.totalQuota);
594
+ return !outerHasUsage && nestedHasUsage ? nested : body;
595
+ }
596
+
597
+ function kimiLimitLabel(item: Record<string, unknown>, detail: Record<string, unknown>): string {
598
+ return [item.name, item.title, item.scope, detail.name, detail.title]
599
+ .filter((value): value is string => typeof value === "string")
600
+ .join(" ")
601
+ .toLowerCase();
602
+ }
603
+
604
+ function parseKimiQuotaRow(value: unknown, resetFallback?: Record<string, unknown>): { percent: number; resetAt?: number } | null {
605
+ const row = asRecord(value);
606
+ if (!row) return null;
607
+ const resetAt = quotaResetAt(row) ?? (resetFallback ? quotaResetAt(resetFallback) : undefined);
608
+ const limit = toFiniteNumber(row.limit);
609
+ if (limit !== undefined && limit > 0) {
610
+ let used = toFiniteNumber(row.used);
611
+ if (used === undefined) {
612
+ const remaining = toFiniteNumber(row.remaining);
613
+ if (remaining !== undefined) used = limit - remaining;
614
+ }
615
+ if (used !== undefined) {
616
+ const percent = normalizePercent((used / limit) * 100);
617
+ if (percent !== undefined) return { percent, ...(resetAt !== undefined ? { resetAt } : {}) };
618
+ }
619
+ }
620
+ // Some payloads expose utilisation directly when limit/used arithmetic is absent.
621
+ const direct = normalizePercent(row.utilization ?? row.percent ?? row.usedPercent ?? row.used_percent);
622
+ return direct === undefined ? null : { percent: direct, ...(resetAt !== undefined ? { resetAt } : {}) };
623
+ }
624
+
625
+ function isKimiFiveHourLimit(item: Record<string, unknown>, detail: Record<string, unknown>, window: Record<string, unknown>): boolean {
626
+ const duration = toFiniteNumber(window.duration ?? item.duration ?? detail.duration);
627
+ const unit = String(window.timeUnit ?? item.timeUnit ?? detail.timeUnit ?? "").toUpperCase();
628
+ if ((unit.includes("MINUTE") && duration === 300) || (unit.includes("HOUR") && duration === 5)) return true;
629
+ return /(^|\b)5\s*(?:h|hour)/.test(kimiLimitLabel(item, detail));
630
+ }
631
+
632
+ function isKimiWeeklyLimit(item: Record<string, unknown>, detail: Record<string, unknown>, window: Record<string, unknown>): boolean {
633
+ const duration = toFiniteNumber(window.duration ?? item.duration ?? detail.duration);
634
+ const unit = String(window.timeUnit ?? item.timeUnit ?? detail.timeUnit ?? "").toUpperCase();
635
+ if ((unit.includes("DAY") && duration === 7) || (unit.includes("HOUR") && duration === 168)) return true;
636
+ return /weekly|7\s*(?:d|day)/.test(kimiLimitLabel(item, detail));
637
+ }
638
+
639
+ function parseKimiQuotaPayload(value: unknown): ProviderQuota | null {
640
+ const body = unwrapKimiQuotaPayload(value);
641
+ if (!body) return null;
642
+ let weekly = parseKimiQuotaRow(body.usage);
643
+ const total = parseKimiQuotaRow(body.totalQuota);
644
+ let fiveHour: { percent: number; resetAt?: number } | null = null;
645
+ if (Array.isArray(body.limits)) {
646
+ for (const rawItem of body.limits) {
647
+ const item = asRecord(rawItem);
648
+ if (!item) continue;
649
+ const detail = asRecord(item.detail) ?? item;
650
+ const window = asRecord(item.window) ?? {};
651
+ if (!fiveHour && isKimiFiveHourLimit(item, detail, window)) {
652
+ fiveHour = parseKimiQuotaRow(detail, window);
653
+ }
654
+ if (!weekly && isKimiWeeklyLimit(item, detail, window)) {
655
+ weekly = parseKimiQuotaRow(detail, window);
656
+ }
657
+ if (fiveHour && weekly) break;
658
+ }
659
+ }
660
+ const quota: ProviderQuota = {
661
+ ...(fiveHour ? {
662
+ fiveHourPercent: fiveHour.percent,
663
+ ...(fiveHour.resetAt !== undefined ? { fiveHourResetAt: fiveHour.resetAt } : {}),
664
+ } : {}),
665
+ ...(weekly ? {
666
+ weeklyPercent: weekly.percent,
667
+ ...(weekly.resetAt !== undefined ? { weeklyResetAt: weekly.resetAt } : {}),
668
+ } : {}),
669
+ ...(total ? { customWindows: [{ label: "Total subscription credits", percent: total.percent, ...(total.resetAt !== undefined ? { resetAt: total.resetAt } : {}) }] } : {}),
670
+ updatedAt: Date.now(),
671
+ };
672
+ return hasQuotaRows(quota) ? quota : null;
673
+ }
674
+
675
+ async function resolveKimiQuotaBearer(config: OcxProviderConfig): Promise<string | null> {
676
+ if (config.authMode === "oauth") {
677
+ try {
678
+ return await getValidAccessToken("kimi");
679
+ } catch {
680
+ return null;
681
+ }
682
+ }
683
+ // ACTIVE key only: silently walking apiKeyPool when the primary env reference is
684
+ // unresolved would render a quota bar for a DIFFERENT account than the one routing
685
+ // requests — a wrong meter is worse than no meter.
686
+ const primary = resolveEnvValue(config.apiKey)?.trim();
687
+ return primary || null;
688
+ }
689
+
690
+ async function fetchKimiQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaReport | null> {
691
+ // Never release credentials to a user-edited or lookalike provider host.
692
+ if (!isCanonicalKimiCodeBaseUrl(config.baseUrl)) return null;
693
+ const accessToken = await resolveKimiQuotaBearer(config);
694
+ if (!accessToken) return null;
695
+ const response = await fetch(KIMI_CODE_USAGE_URL, {
696
+ headers: { Accept: "application/json", Authorization: `Bearer ${accessToken}` },
697
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
698
+ });
699
+ if (!response.ok) return null;
700
+ const quota = parseKimiQuotaPayload(await response.json().catch(() => null));
701
+ return quota ? report(provider, "kimi:usages", quota) : null;
702
+ }
703
+
704
+ /** Cursor included usage via api2.cursor.sh (Bearer from OAuth) — unofficial, may change. */
705
+ async function fetchCursorQuota(provider: string): Promise<ProviderQuotaReport | null> {
706
+ let accessToken: string;
707
+ try {
708
+ accessToken = await getValidAccessToken("cursor");
709
+ } catch {
710
+ return null;
711
+ }
712
+
713
+ const authHeaders = {
714
+ Accept: "application/json",
715
+ Authorization: `Bearer ${accessToken}`,
716
+ "User-Agent": "opencodex-quota",
717
+ } as const;
718
+
719
+ // Prefer dashboard period usage (Pro/Team/Ultra spend allowance in USD cents).
720
+ // Field names follow Cursor's Connect RPC shape (limit/remaining/includedSpend), not usedCents.
721
+ try {
722
+ const periodRes = await fetch("https://api2.cursor.sh/aiserver.v1.DashboardService/GetCurrentPeriodUsage", {
723
+ method: "POST",
724
+ headers: {
725
+ ...authHeaders,
726
+ "Content-Type": "application/json",
727
+ "Connect-Protocol-Version": "1",
728
+ },
729
+ body: "{}",
730
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
731
+ });
732
+ if (periodRes.ok) {
733
+ const body = asRecord(await periodRes.json().catch(() => null));
734
+ const planUsage = asRecord(body?.planUsage);
735
+ if (planUsage) {
736
+ const resetAt = normalizeResetAt(body?.billingCycleEnd ?? planUsage.billingCycleEnd ?? body?.periodEnd);
737
+
738
+ // Primary meter: overall included allowance (Cursor Settings → Usage total %).
739
+ // autoPercentUsed / apiPercentUsed are secondary pools and must not replace the total.
740
+ const limit = toFiniteNumber(planUsage.limit ?? planUsage.limitCents ?? planUsage.totalLimitCents);
741
+ const remaining = toFiniteNumber(planUsage.remaining ?? planUsage.remainingCents);
742
+ const includedSpend = toFiniteNumber(planUsage.includedSpend ?? planUsage.usedCents ?? planUsage.used);
743
+ const totalSpend = toFiniteNumber(planUsage.totalSpend);
744
+ let used: number | undefined;
745
+ if (includedSpend !== undefined) used = includedSpend;
746
+ else if (limit !== undefined && remaining !== undefined) used = Math.max(0, limit - remaining);
747
+ else if (totalSpend !== undefined) used = totalSpend;
748
+ const totalPercent = normalizePercent(planUsage.totalPercentUsed ?? planUsage.percentUsed)
749
+ ?? (limit !== undefined && limit > 0 && used !== undefined
750
+ ? normalizePercent((used / limit) * 100)
751
+ : undefined);
752
+
753
+ const autoPercent = normalizePercent(planUsage.autoPercentUsed);
754
+ const apiPercent = normalizePercent(planUsage.apiPercentUsed);
755
+ const customWindows: ProviderQuotaWindow[] = [];
756
+ if (autoPercent !== undefined) {
757
+ customWindows.push({
758
+ label: "First-party models",
759
+ percent: autoPercent,
760
+ ...(resetAt !== undefined ? { resetAt } : {}),
761
+ });
762
+ }
763
+ if (apiPercent !== undefined) {
764
+ customWindows.push({
765
+ label: "API usage",
766
+ percent: apiPercent,
767
+ ...(resetAt !== undefined ? { resetAt } : {}),
768
+ });
769
+ }
770
+
771
+ if (totalPercent !== undefined || customWindows.length > 0) {
772
+ const built = report(provider, "cursor:period-usage", {
773
+ ...(totalPercent !== undefined ? {
774
+ monthlyPercent: totalPercent,
775
+ ...(resetAt !== undefined ? { monthlyResetAt: resetAt } : {}),
776
+ } : {}),
777
+ ...(customWindows.length > 0 ? { customWindows } : {}),
778
+ updatedAt: Date.now(),
779
+ });
780
+ if (built) return { ...built, reverseEngineered: true };
781
+ }
782
+ }
783
+ }
784
+ } catch {
785
+ /* fall through */
786
+ }
787
+
788
+ // /api/usage/summary — same host, sometimes richer than /auth/usage for Team plans.
789
+ try {
790
+ const summaryRes = await fetch("https://api2.cursor.sh/api/usage/summary", {
791
+ headers: authHeaders,
792
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
793
+ });
794
+ if (summaryRes.ok) {
795
+ const body = asRecord(await summaryRes.json().catch(() => null));
796
+ const individual = asRecord(body?.individualUsage);
797
+ const plan = asRecord(individual?.plan);
798
+ if (plan) {
799
+ const used = toFiniteNumber(plan.used);
800
+ const limit = toFiniteNumber(plan.limit);
801
+ const percent = normalizePercent(plan.totalPercentUsed)
802
+ ?? (used !== undefined && limit !== undefined && limit > 0
803
+ ? normalizePercent((used / limit) * 100)
804
+ : undefined);
805
+ if (percent !== undefined) {
806
+ const built = report(provider, "cursor:usage-summary", {
807
+ monthlyPercent: percent,
808
+ monthlyResetAt: normalizeResetAt(body?.billingCycleEnd),
809
+ updatedAt: Date.now(),
810
+ });
811
+ if (built) return { ...built, reverseEngineered: true };
812
+ }
813
+ }
814
+ }
815
+ } catch {
816
+ /* fall through to /auth/usage */
817
+ }
818
+
819
+ const response = await fetch("https://api2.cursor.sh/auth/usage", {
820
+ headers: authHeaders,
821
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
822
+ });
823
+ if (!response.ok) return null;
824
+ const body = asRecord(await response.json().catch(() => null));
825
+ if (!body) return null;
826
+
827
+ // Prefer the gpt-4 bucket (historical "fast requests"); else first model with used+limit.
828
+ let used: number | undefined;
829
+ let limit: number | undefined;
830
+ const gpt4 = asRecord(body["gpt-4"]);
831
+ if (gpt4) {
832
+ used = toFiniteNumber(gpt4.numRequests ?? gpt4.used);
833
+ limit = toFiniteNumber(gpt4.maxRequestUsage ?? gpt4.limit ?? gpt4.maxRequests);
834
+ }
835
+ if (used === undefined || limit === undefined || limit <= 0) {
836
+ for (const [key, value] of Object.entries(body)) {
837
+ if (key === "startOfMonth" || key === "billingCycleStart") continue;
838
+ const bucket = asRecord(value);
839
+ if (!bucket) continue;
840
+ const bucketUsed = toFiniteNumber(bucket.numRequests ?? bucket.used);
841
+ const bucketLimit = toFiniteNumber(bucket.maxRequestUsage ?? bucket.limit ?? bucket.maxRequests);
842
+ if (bucketUsed !== undefined && bucketLimit !== undefined && bucketLimit > 0) {
843
+ used = bucketUsed;
844
+ limit = bucketLimit;
845
+ break;
846
+ }
847
+ }
848
+ }
849
+ if (used === undefined || limit === undefined || limit <= 0) return null;
850
+ const percent = normalizePercent((used / limit) * 100);
851
+ if (percent === undefined) return null;
852
+ const startOfMonth = normalizeResetAt(body.startOfMonth ?? body.billingCycleStart);
853
+ // Next reset = same day next month, computed in UTC to avoid timezone-shifted rollover.
854
+ const monthlyResetAt = startOfMonth !== undefined
855
+ ? (() => {
856
+ const start = new Date(startOfMonth);
857
+ return Date.UTC(start.getUTCFullYear(), start.getUTCMonth() + 1, start.getUTCDate());
858
+ })()
859
+ : undefined;
860
+ const built = report(provider, "cursor:auth-usage", {
861
+ monthlyPercent: percent,
862
+ ...(monthlyResetAt !== undefined ? { monthlyResetAt } : {}),
863
+ updatedAt: Date.now(),
864
+ });
865
+ return built ? { ...built, reverseEngineered: true } : null;
866
+ }
867
+
868
+ function quotaInfoEntries(modelInfo: Record<string, unknown>): Record<string, unknown>[] {
869
+ const entries: Record<string, unknown>[] = [];
870
+ const add = (value: unknown, tier?: string) => {
871
+ const rec = asRecord(value);
872
+ if (!rec) return;
873
+ entries.push(tier ? { ...rec, tier } : rec);
874
+ };
875
+ const addArray = (value: unknown) => {
876
+ if (!Array.isArray(value)) return;
877
+ for (const entry of value) add(entry);
878
+ };
879
+
880
+ if (Array.isArray(modelInfo.quotaInfo)) addArray(modelInfo.quotaInfo);
881
+ else add(modelInfo.quotaInfo);
882
+ addArray(modelInfo.quotaInfos);
883
+
884
+ const byTier = asRecord(modelInfo.quotaInfoByTier);
885
+ if (byTier) {
886
+ for (const [tier, value] of Object.entries(byTier)) {
887
+ if (Array.isArray(value)) {
888
+ for (const entry of value) add(entry, tier);
889
+ } else {
890
+ add(value, tier);
891
+ }
892
+ }
893
+ }
894
+ return entries;
895
+ }
896
+
897
+ function classifyAntigravityFamily(modelId: string, modelInfo: Record<string, unknown>, quotaInfo: Record<string, unknown>): "Gem" | "Cla" | null {
898
+ const displayName = typeof modelInfo.displayName === "string" ? modelInfo.displayName : "";
899
+ const tier = typeof quotaInfo.tier === "string" ? quotaInfo.tier : "";
900
+ const haystack = `${modelId} ${displayName} ${tier}`.toLowerCase();
901
+ if (haystack.includes("gemini")) return "Gem";
902
+ if (haystack.includes("claude") || haystack.includes("opus") || haystack.includes("sonnet") || haystack.includes("gpt-oss") || haystack.includes("gpt_oss")) return "Cla";
903
+ return null;
904
+ }
905
+
906
+ function antigravityUsedPercent(quotaInfo: Record<string, unknown>): number | undefined {
907
+ const remaining = normalizePercent(toFiniteNumber(quotaInfo.remainingFraction) !== undefined
908
+ ? toFiniteNumber(quotaInfo.remainingFraction)! * 100
909
+ : toFiniteNumber(quotaInfo.remainingPercentage) !== undefined
910
+ ? toFiniteNumber(quotaInfo.remainingPercentage)! * 100
911
+ : undefined);
912
+ if (remaining === undefined) return undefined;
913
+ return normalizePercent(100 - remaining);
914
+ }
915
+
916
+ async function fetchAntigravityQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaReport | null> {
917
+ const credential = getCredential("google-antigravity");
918
+ if (!credential?.projectId) return null;
919
+ let accessToken: string;
920
+ try {
921
+ accessToken = await getValidAccessToken("google-antigravity");
922
+ } catch {
923
+ return null;
924
+ }
925
+ const baseUrl = (config.baseUrl || "https://daily-cloudcode-pa.googleapis.com").replace(/\/+$/, "");
926
+ const response = await fetch(`${baseUrl}/v1internal:fetchAvailableModels`, {
927
+ method: "POST",
928
+ headers: {
929
+ Accept: "application/json",
930
+ "Content-Type": "application/json",
931
+ "User-Agent": antigravityUserAgent(),
932
+ Authorization: `Bearer ${accessToken}`,
933
+ },
934
+ body: JSON.stringify({ project: credential.projectId }),
935
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
936
+ });
937
+ if (!response.ok) return null;
938
+ const body = asRecord(await response.json().catch(() => null));
939
+ const models = asRecord(body?.models);
940
+ if (!models) return null;
941
+
942
+ const windows = new Map<string, ProviderQuotaWindow>();
943
+ for (const [modelId, rawModelInfo] of Object.entries(models)) {
944
+ const modelInfo = asRecord(rawModelInfo);
945
+ if (!modelInfo) continue;
946
+ for (const quotaInfo of quotaInfoEntries(modelInfo)) {
947
+ const label = classifyAntigravityFamily(modelId, modelInfo, quotaInfo);
948
+ if (!label || windows.has(label)) continue;
949
+ const percent = antigravityUsedPercent(quotaInfo);
950
+ if (percent === undefined) continue;
951
+ windows.set(label, {
952
+ label,
953
+ percent,
954
+ ...(normalizeResetAt(quotaInfo.resetTime) !== undefined ? { resetAt: normalizeResetAt(quotaInfo.resetTime) } : {}),
955
+ });
956
+ }
957
+ }
958
+
959
+ const customWindows = ["Gem", "Cla"].flatMap(label => {
960
+ const window = windows.get(label);
961
+ return window ? [window] : [];
962
+ });
963
+ if (customWindows.length === 0) return null;
964
+ return report(provider, "google-antigravity:fetchAvailableModels", {
965
+ customWindows,
966
+ updatedAt: Date.now(),
967
+ });
968
+ }
969
+
970
+ async function maybeFetchProviderQuota(
971
+ name: string,
972
+ provider: OcxProviderConfig,
973
+ config: OcxConfig,
974
+ forceRefresh: boolean,
975
+ ): Promise<ProviderQuotaReport | null> {
976
+ if (provider.disabled === true) return null;
977
+ try {
978
+ if (isBuiltInChatGptForwardProvider(name, provider)) return fetchChatGptForwardQuota(config, name, provider, forceRefresh);
979
+ if (provider.authMode === "oauth" && name === "xai") return fetchXaiQuota(name);
980
+ if (provider.authMode === "oauth" && name === "anthropic") return fetchAnthropicQuota(name);
981
+ if (provider.authMode === "oauth" && name === "kiro") return fetchKiroQuota(name, forceRefresh);
982
+ if (provider.authMode === "oauth" && name === "cursor") return fetchCursorQuota(name);
983
+ if (provider.authMode === "oauth" && name === "google-antigravity") return fetchAntigravityQuota(name, provider);
984
+ // Kimi Code `/usages` accepts OAuth or coding-plan API keys, but only on the canonical
985
+ // host and only for real key auth — forward/local modes carry no credential of ours.
986
+ if (provider.authMode === "oauth" && name === "kimi") return fetchKimiQuota(name, provider);
987
+ if (provider.authMode === "key" && isCanonicalKimiCodeBaseUrl(provider.baseUrl)) {
988
+ return fetchKimiQuota(name, provider);
989
+ }
990
+ return null;
991
+ } catch {
992
+ return null;
993
+ }
994
+ }
995
+
996
+ export async function fetchProviderQuotaReports(config: OcxConfig, forceRefresh = false): Promise<ProviderQuotaResponse> {
997
+ const key = cacheKey(config);
998
+ const writerGeneration = captureConfigGeneration();
999
+ const now = Date.now();
1000
+ // The cache fast path must not extend a preserved last-good row past its 30-minute bound:
1001
+ // a row preserved at age 29:59 plus a full 5-minute TTL would otherwise serve until ~35min.
1002
+ const cacheFresh = cache && cache.key === key && now - cache.ts < CACHE_TTL_MS
1003
+ && cache.response.reports.every(item => now - item.updatedAt < LAST_GOOD_MAX_AGE_MS);
1004
+ if (!forceRefresh && cacheFresh) return cache!.response;
1005
+ const joinable = inflight.get(key);
1006
+ if (!forceRefresh && joinable && joinable.epoch === invalidationEpoch) return joinable.promise;
1007
+ // A forced probe takes commit authority: older in-flight probes must not overwrite its result.
1008
+ if (forceRefresh) invalidationEpoch += 1;
1009
+ const epoch = invalidationEpoch;
1010
+
1011
+ const promise = (async (): Promise<ProviderQuotaResponse> => {
1012
+ const previous = cache && cache.key === key ? cache.response.reports : [];
1013
+ const fresh = (await Promise.all(
1014
+ Object.entries(config.providers).map(([name, provider]) => maybeFetchProviderQuota(name, provider, config, forceRefresh)),
1015
+ )).filter((item): item is ProviderQuotaReport => item !== null);
1016
+
1017
+ // Keep bounded last-good rows when a probe fails (e.g. transient upstream flake); never
1018
+ // re-stamp their timestamps, and drop rows older than LAST_GOOD_MAX_AGE_MS.
1019
+ // Note: the cache key encodes the provider set (name/adapter/authMode/disabled/baseUrl),
1020
+ // so previous rows always correspond to currently configured, enabled providers — a
1021
+ // disabled or removed provider changes the key and starts from an empty previous set.
1022
+ const cutoff = Date.now() - LAST_GOOD_MAX_AGE_MS;
1023
+ const byProvider = new Map<string, ProviderQuotaReport>();
1024
+ for (const item of previous) {
1025
+ if (item.updatedAt >= cutoff) byProvider.set(item.provider, item);
1026
+ }
1027
+ for (const item of fresh) byProvider.set(item.provider, item);
1028
+
1029
+ const response = { generatedAt: Date.now(), reports: [...byProvider.values()] };
1030
+ // Commit only when this probe still holds authority (no clear/force superseded it).
1031
+ if (epoch === invalidationEpoch) {
1032
+ const reports = response.reports.filter(item => mayCommitProviderQuotaKey(item.provider, writerGeneration));
1033
+ cache = { key, ts: Date.now(), response: { ...response, reports } };
1034
+ }
1035
+ return response;
1036
+ })();
1037
+
1038
+ const entry = { epoch, promise };
1039
+ inflight.set(key, entry);
1040
+ try {
1041
+ return await promise;
1042
+ } finally {
1043
+ if (inflight.get(key) === entry) inflight.delete(key);
1044
+ }
1045
+ }