cartethyia 2.5.9 → 2.6.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 (386) hide show
  1. package/.env.example +15 -42
  2. package/README.md +11 -10
  3. package/dist/dashboard/assets/{BannedPage-Brvuj_Eb.js → BannedPage-l6XMnfr9.js} +1 -1
  4. package/dist/dashboard/assets/BannedPage-l6XMnfr9.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolDetailPage-CqfXtI3y.js → CliToolDetailPage-DwOzAW45.js} +1 -1
  6. package/dist/dashboard/assets/CliToolDetailPage-DwOzAW45.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-GQeS91bg.js → CliToolsPage-BT3Up3QA.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-BT3Up3QA.js.gz +0 -0
  9. package/dist/dashboard/assets/{CombosPage-BDRsTXBB.js → CombosPage-6kYIUpzR.js} +1 -1
  10. package/dist/dashboard/assets/CombosPage-6kYIUpzR.js.gz +0 -0
  11. package/dist/dashboard/assets/{ConsoleLogPage-zP8AFHX0.js → ConsoleLogPage-zRnj1mlR.js} +1 -1
  12. package/dist/dashboard/assets/ConsoleLogPage-zRnj1mlR.js.gz +0 -0
  13. package/dist/dashboard/assets/{CustomizationPage-BmfrNZ14.js → CustomizationPage-C7m-H1Rz.js} +1 -1
  14. package/dist/dashboard/assets/CustomizationPage-C7m-H1Rz.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js.gz +0 -0
  17. package/dist/dashboard/assets/{LoginPage-DoreIgm8.js → LoginPage-CbphLvjg.js} +1 -1
  18. package/dist/dashboard/assets/LoginPage-CbphLvjg.js.gz +0 -0
  19. package/dist/dashboard/assets/{ModelPicker-C7pDb41R.js → ModelPicker-DVny4S5_.js} +1 -1
  20. package/dist/dashboard/assets/ModelPicker-DVny4S5_.js.gz +0 -0
  21. package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js +1 -0
  22. package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js.gz +0 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js +3 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js.gz +0 -0
  25. package/dist/dashboard/assets/{ProvidersPage-BnoPgA7w.js → ProvidersPage-Dfb2896L.js} +1 -1
  26. package/dist/dashboard/assets/ProvidersPage-Dfb2896L.js.gz +0 -0
  27. package/dist/dashboard/assets/{ProxyPage-Bq_TGgdb.js → ProxyPage-BaCIdAUt.js} +1 -1
  28. package/dist/dashboard/assets/ProxyPage-BaCIdAUt.js.gz +0 -0
  29. package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js +1 -0
  30. package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js.gz +0 -0
  31. package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js +1 -0
  32. package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js.gz +0 -0
  33. package/dist/dashboard/assets/{SetupPage-Day3mCOn.js → SetupPage-D4g6vOGc.js} +1 -1
  34. package/dist/dashboard/assets/SetupPage-D4g6vOGc.js.gz +0 -0
  35. package/dist/dashboard/assets/{SortableList-DQxllnB5.js → SortableList-C93i14BD.js} +1 -1
  36. package/dist/dashboard/assets/SortableList-C93i14BD.js.gz +0 -0
  37. package/dist/dashboard/assets/{StudioPage-uMExoWu1.js → StudioPage-SgQpymjm.js} +1 -1
  38. package/dist/dashboard/assets/{StudioPage-uMExoWu1.js.gz → StudioPage-SgQpymjm.js.gz} +0 -0
  39. package/dist/dashboard/assets/{UsagePage-DXa1oR-4.js → UsagePage-BFME-eba.js} +2 -2
  40. package/dist/dashboard/assets/UsagePage-BFME-eba.js.gz +0 -0
  41. package/dist/dashboard/assets/{api-keys-DQYLQxpW.js → api-keys-C23S9sgp.js} +1 -1
  42. package/dist/dashboard/assets/api-keys-C23S9sgp.js.gz +0 -0
  43. package/dist/dashboard/assets/{app-BIt446yE.js → app-2KhtHCv9.js} +1 -1
  44. package/dist/dashboard/assets/{app-BIt446yE.js.gz → app-2KhtHCv9.js.gz} +0 -0
  45. package/dist/dashboard/assets/app-CEeaMHOy.css +2 -0
  46. package/dist/dashboard/assets/app-CEeaMHOy.css.gz +0 -0
  47. package/dist/dashboard/assets/{assert-C9FD_JwJ.js → assert-DU6qrMA_.js} +1 -1
  48. package/dist/dashboard/assets/assert-DU6qrMA_.js.gz +0 -0
  49. package/dist/dashboard/assets/{badge-CR72dasT.js → badge-CKsF123d.js} +1 -1
  50. package/dist/dashboard/assets/badge-CKsF123d.js.gz +0 -0
  51. package/dist/dashboard/assets/{banner-CI9_eSfO.js → banner-8j7uChA9.js} +1 -1
  52. package/dist/dashboard/assets/banner-8j7uChA9.js.gz +0 -0
  53. package/dist/dashboard/assets/{card-DS7CQEsO.js → card-CdsSlWA3.js} +1 -1
  54. package/dist/dashboard/assets/card-CdsSlWA3.js.gz +0 -0
  55. package/dist/dashboard/assets/{cli-tools-CNqzrfdb.js → cli-tools-BaFUuFFF.js} +1 -1
  56. package/dist/dashboard/assets/cli-tools-BaFUuFFF.js.gz +0 -0
  57. package/dist/dashboard/assets/{clipboard-button-611pSsaX.js → clipboard-button-uL5aYLfW.js} +1 -1
  58. package/dist/dashboard/assets/clipboard-button-uL5aYLfW.js.gz +0 -0
  59. package/dist/dashboard/assets/{dialog-k37exjNt.js → dialog-C07hr5_G.js} +1 -1
  60. package/dist/dashboard/assets/dialog-C07hr5_G.js.gz +0 -0
  61. package/dist/dashboard/assets/{drawer-CX7N8ucR.js → drawer-BPO1TcKH.js} +1 -1
  62. package/dist/dashboard/assets/drawer-BPO1TcKH.js.gz +0 -0
  63. package/dist/dashboard/assets/{entry-CTP8AyiS.js → entry-Bz_CtDfC.js} +1 -1
  64. package/dist/dashboard/assets/entry-Bz_CtDfC.js.gz +0 -0
  65. package/dist/dashboard/assets/entry-CwPRKk0d.js +4 -0
  66. package/dist/dashboard/assets/entry-CwPRKk0d.js.gz +0 -0
  67. package/dist/dashboard/assets/entry-GJRbmrpA.js +1 -0
  68. package/dist/dashboard/assets/entry-GJRbmrpA.js.gz +0 -0
  69. package/dist/dashboard/assets/{github-badge-sltk1qUi.js → github-badge-DmQreSDd.js} +1 -1
  70. package/dist/dashboard/assets/github-badge-DmQreSDd.js.gz +0 -0
  71. package/dist/dashboard/assets/{index-z3zDOSMM.js → index-Bblu6erR.js} +2 -2
  72. package/dist/dashboard/assets/index-Bblu6erR.js.gz +0 -0
  73. package/dist/dashboard/assets/{inline-BhY2sSnO.js → inline-C9dCYaBZ.js} +1 -1
  74. package/dist/dashboard/assets/inline-C9dCYaBZ.js.gz +0 -0
  75. package/dist/dashboard/assets/{input-V1umFiBk.js → input-CjCnXweJ.js} +1 -1
  76. package/dist/dashboard/assets/input-CjCnXweJ.js.gz +0 -0
  77. package/dist/dashboard/assets/{layout-BBOYycyS.js → layout-V6y8DAkl.js} +1 -1
  78. package/dist/dashboard/assets/layout-V6y8DAkl.js.gz +0 -0
  79. package/dist/dashboard/assets/{list-row-1zzZYM0B.js → list-row-CM1AaG_y.js} +1 -1
  80. package/dist/dashboard/assets/list-row-CM1AaG_y.js.gz +0 -0
  81. package/dist/dashboard/assets/{live-BivGfuUs.js → live-ta6GMb-8.js} +1 -1
  82. package/dist/dashboard/assets/live-ta6GMb-8.js.gz +0 -0
  83. package/dist/dashboard/assets/{model-bans-CmffjLuG.js → model-bans-BmYTmbAk.js} +1 -1
  84. package/dist/dashboard/assets/model-bans-BmYTmbAk.js.gz +0 -0
  85. package/dist/dashboard/assets/{mutation-46P0oWMe.js → mutation-g_fduiNw.js} +1 -1
  86. package/dist/dashboard/assets/mutation-g_fduiNw.js.gz +0 -0
  87. package/dist/dashboard/assets/{numbers-DkfMR5Qs.js → numbers-BOevJzdz.js} +1 -1
  88. package/dist/dashboard/assets/numbers-BOevJzdz.js.gz +0 -0
  89. package/dist/dashboard/assets/{query-keys-BFhGpwR1.js → query-keys-UETqfytp.js} +1 -1
  90. package/dist/dashboard/assets/query-keys-UETqfytp.js.gz +0 -0
  91. package/dist/dashboard/assets/{routing-CAb4F37R.js → routing-BPUo-W8d.js} +1 -1
  92. package/dist/dashboard/assets/routing-BPUo-W8d.js.gz +0 -0
  93. package/dist/dashboard/assets/{select-C6MMiaio.js → select-C980c4cz.js} +1 -1
  94. package/dist/dashboard/assets/select-C980c4cz.js.gz +0 -0
  95. package/dist/dashboard/assets/{settings-DNX9sydi.js → settings--3hxSyry.js} +1 -1
  96. package/dist/dashboard/assets/settings--3hxSyry.js.gz +0 -0
  97. package/dist/dashboard/assets/{stack-BHTfXdz_.js → stack-_gfTdLVB.js} +1 -1
  98. package/dist/dashboard/assets/stack-_gfTdLVB.js.gz +0 -0
  99. package/dist/dashboard/assets/{state-mPDRlQkl.js → state-2c5V4zo7.js} +1 -1
  100. package/dist/dashboard/assets/state-2c5V4zo7.js.gz +0 -0
  101. package/dist/dashboard/assets/{switch-DOmJxg7H.js → switch-Dkke7Naf.js} +1 -1
  102. package/dist/dashboard/assets/switch-Dkke7Naf.js.gz +0 -0
  103. package/dist/dashboard/assets/{useMutation-Dem0lXQj.js → useMutation-EDFRkn1i.js} +1 -1
  104. package/dist/dashboard/assets/useMutation-EDFRkn1i.js.gz +0 -0
  105. package/dist/dashboard/index.html +1 -1
  106. package/dist/dashboard/index.html.gz +0 -0
  107. package/dist/dashboard/precompressed-assets.json +51 -51
  108. package/migrations/README.md +52 -52
  109. package/migrations/full-postgres/0020_add_telemetry_event_analytics.sql +77 -0
  110. package/migrations/full-postgres/0021_add_api_key_concurrency_tolerance.sql +13 -0
  111. package/migrations/full-postgres/0022_add_inferhub_config_key.sql +17 -0
  112. package/migrations/full-postgres/0023_drop_api_key_concurrency_tolerance.sql +13 -0
  113. package/migrations/lite-sqlite3/0020_add_telemetry_event_analytics.sql +69 -0
  114. package/migrations/lite-sqlite3/0021_add_api_key_concurrency_tolerance.sql +13 -0
  115. package/migrations/lite-sqlite3/0022_add_inferhub_config_key.sql +17 -0
  116. package/migrations/lite-sqlite3/0023_drop_api_key_concurrency_tolerance.sql +13 -0
  117. package/package.json +96 -97
  118. package/scripts/cli/admin.ts +126 -125
  119. package/scripts/cli/autostart.ts +237 -237
  120. package/scripts/cli/background.ts +120 -110
  121. package/scripts/cli/bin/carte.cjs +8 -8
  122. package/scripts/cli/bin/cartethyia.cjs +167 -167
  123. package/scripts/cli/console-menu.ts +308 -275
  124. package/scripts/cli/controller.ts +145 -0
  125. package/scripts/cli/doctor.ts +1 -1
  126. package/scripts/cli/env.ts +99 -99
  127. package/scripts/cli/gateway.ts +264 -261
  128. package/scripts/{health.ts → cli/health.ts} +4 -4
  129. package/scripts/cli/instance.ts +234 -224
  130. package/scripts/cli/main.ts +188 -164
  131. package/scripts/cli/platform.ts +64 -64
  132. package/scripts/cli/prompt.ts +275 -107
  133. package/scripts/cli/services.ts +88 -88
  134. package/scripts/cli/setup.ts +445 -445
  135. package/scripts/cli/start.ts +85 -82
  136. package/scripts/cli/startup-signal.ts +23 -23
  137. package/scripts/cli/telemetry.ts +70 -0
  138. package/scripts/cli/tray/index.ts +263 -263
  139. package/scripts/cli/tray/windows.ps1 +107 -107
  140. package/scripts/cli/tray.ts +192 -208
  141. package/scripts/cli/update.ts +39 -39
  142. package/scripts/cli/version.ts +67 -67
  143. package/scripts/cli/wordmark.ts +58 -58
  144. package/src/boot/app.ts +16 -1
  145. package/src/boot/dependencies.ts +128 -25
  146. package/src/config/package-paths.ts +24 -0
  147. package/src/config/resources.ts +137 -0
  148. package/src/config/runtime.ts +133 -76
  149. package/src/config/spec.ts +30 -29
  150. package/src/config/telemetry.ts +0 -25
  151. package/src/console/api-keys/bucket-spend.ts +14 -17
  152. package/src/console/api-keys/contracts.ts +410 -410
  153. package/src/console/api-keys/routes.ts +12 -0
  154. package/src/console/audit/store.ts +2 -3
  155. package/src/console/auth/setup-service.ts +8 -1
  156. package/src/console/backup/routes.ts +7 -0
  157. package/src/console/backup/service.ts +463 -463
  158. package/src/console/backup/store.ts +16 -11
  159. package/src/console/backup/validate.ts +1 -4
  160. package/src/console/cli-tools/fs-ops.ts +18 -2
  161. package/src/console/data-management/contracts.ts +1 -8
  162. package/src/console/data-management/operations.ts +61 -48
  163. package/src/console/data-management/routes.ts +4 -2
  164. package/src/console/domain-registration.ts +25 -3
  165. package/src/console/inferhub/client.ts +139 -0
  166. package/src/console/inferhub/key-store.ts +129 -0
  167. package/src/console/inferhub/normalize.ts +347 -0
  168. package/src/console/inferhub/routes.ts +411 -0
  169. package/src/console/inferhub/store.ts +109 -0
  170. package/src/console/inferhub/types.ts +183 -0
  171. package/src/console/observability/contracts.ts +2 -41
  172. package/src/console/observability/store.ts +176 -371
  173. package/src/console/providers/catalog/public-model-store.ts +0 -0
  174. package/src/console/providers/catalog/store.ts +23 -36
  175. package/src/console/quota/account-quota-shared.ts +52 -52
  176. package/src/console/quota/account-quota-tenant.ts +16 -24
  177. package/src/console/quota/refresh.ts +441 -441
  178. package/src/console/remote-support/service.ts +270 -114
  179. package/src/console/routing/pools/import-parse.ts +206 -206
  180. package/src/console/routing/pools/operations.ts +543 -543
  181. package/src/console/routing/pools/probe-verdict.ts +132 -132
  182. package/src/console/routing/pools/routes.ts +181 -181
  183. package/src/console/routing/pools/store.ts +1 -1
  184. package/src/console/settings/contracts.ts +42 -40
  185. package/src/console/settings/store.ts +25 -25
  186. package/src/console/share/share-stats.ts +22 -4
  187. package/src/console/share/share-usage.ts +30 -4
  188. package/src/console/shared/cursor.ts +15 -0
  189. package/src/console/updates/repository-updates.ts +40 -39
  190. package/src/console/updates/routes.ts +12 -0
  191. package/src/main.ts +13 -5
  192. package/src/network/pool/resolver.ts +1104 -1104
  193. package/src/observability/client-name.ts +22 -0
  194. package/src/observability/event-log.ts +5 -29
  195. package/src/observability/event-query.ts +690 -0
  196. package/src/observability/log-ring.ts +26 -2
  197. package/src/observability/log-sanitize.ts +118 -0
  198. package/src/observability/logger.ts +17 -13
  199. package/src/observability/metrics.ts +35 -0
  200. package/src/observability/payload-capture.ts +114 -41
  201. package/src/observability/payload-store.ts +249 -24
  202. package/src/observability/raw-event-writer.ts +118 -0
  203. package/src/observability/runtime-metrics.ts +71 -1
  204. package/src/observability/telemetry-buffer.ts +100 -17
  205. package/src/observability/telemetry-reconciliation.ts +417 -0
  206. package/src/persistence/boot.ts +20 -9
  207. package/src/persistence/db-pg.ts +13 -0
  208. package/src/persistence/db-sqlite.ts +569 -497
  209. package/src/persistence/dialect.ts +3 -2
  210. package/src/persistence/migrate.ts +181 -181
  211. package/src/persistence/schema.ts +1496 -1406
  212. package/src/persistence/shared-cache.ts +5 -1
  213. package/src/persistence/stores/api-key-store.ts +581 -581
  214. package/src/persistence/stores/telemetry-store.ts +124 -63
  215. package/src/protocol/reasoning-text.ts +30 -0
  216. package/src/protocol/registry.ts +2 -0
  217. package/src/protocol/response/chat.ts +33 -7
  218. package/src/protocol/response/codex.ts +14 -2
  219. package/src/protocol/response/messages.ts +5 -2
  220. package/src/providers/authentication/oauth-flow-store.ts +3 -10
  221. package/src/providers/bundled-discovery-providers.ts +122 -122
  222. package/src/providers/default-registry.ts +14 -0
  223. package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
  224. package/src/providers/discovery/models-dev-catalog.ts +354 -354
  225. package/src/providers/discovery/probe-phases.ts +43 -7
  226. package/src/providers/discovery/probing-service.ts +70 -32
  227. package/src/providers/integrations/antigravity/antigravity.ts +9 -2
  228. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +85 -17
  229. package/src/providers/integrations/buddy/buddy-chat-shared.ts +934 -934
  230. package/src/providers/integrations/buddy/buddy-checkin.ts +5 -9
  231. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +3 -7
  232. package/src/providers/integrations/buddy/buddy-quota-shared.ts +3 -1
  233. package/src/providers/integrations/buddy/capabilities.ts +4 -0
  234. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +57 -49
  235. package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -180
  236. package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -206
  237. package/src/providers/integrations/buddy/workbuddy/quota.ts +161 -161
  238. package/src/providers/integrations/claude/claude-quota.ts +2 -2
  239. package/src/providers/integrations/claude/claude-user-profiles.ts +3 -2
  240. package/src/providers/integrations/claude/claude.ts +3 -4
  241. package/src/providers/integrations/cline/cline.ts +94 -8
  242. package/src/providers/integrations/codex/codex.ts +61 -37
  243. package/src/providers/integrations/cursor/cursor-catalog.ts +368 -368
  244. package/src/providers/integrations/cursor/cursor-http2.ts +300 -300
  245. package/src/providers/integrations/cursor/cursor-identity.ts +2 -4
  246. package/src/providers/integrations/cursor/cursor-request.ts +11 -0
  247. package/src/providers/integrations/cursor/cursor.ts +29 -17
  248. package/src/providers/integrations/cursor/protocol/framing.ts +129 -129
  249. package/src/providers/integrations/cursor/protocol/messages.ts +2380 -2380
  250. package/src/providers/integrations/cursor/protocol/struct.ts +305 -305
  251. package/src/providers/integrations/cursor/protocol/tool-canonical.ts +275 -0
  252. package/src/providers/integrations/github/github.ts +3 -1
  253. package/src/providers/integrations/grok/grok-quota.ts +279 -279
  254. package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -73
  255. package/src/providers/integrations/grok/grok-ratelimit.ts +218 -218
  256. package/src/providers/integrations/hermes.ts +7 -1
  257. package/src/providers/integrations/inferhub.ts +201 -73
  258. package/src/providers/integrations/kilo/kilo.ts +152 -152
  259. package/src/providers/integrations/kiro/kiro-machine-id.ts +2 -4
  260. package/src/providers/integrations/opencode/provider.ts +2 -0
  261. package/src/providers/integrations/openrouter/provider.ts +129 -129
  262. package/src/providers/integrations/qoder/qoder-quota.ts +193 -192
  263. package/src/providers/integrations/qoder/qoder-realm.ts +106 -106
  264. package/src/providers/integrations/trae/trae-headers.ts +17 -48
  265. package/src/providers/integrations/trae/trae-oauth.ts +4 -26
  266. package/src/providers/integrations/trae/trae-quota.ts +389 -384
  267. package/src/providers/integrations/trae/trae-realm.ts +15 -2
  268. package/src/providers/integrations/trae/trae.ts +22 -24
  269. package/src/providers/integrations/xiaomi-mimo/think-stream.ts +100 -24
  270. package/src/providers/integrations/zcode/oauth.ts +1 -1
  271. package/src/providers/operations/account-health-service.ts +605 -279
  272. package/src/providers/provider-metadata.ts +12 -1
  273. package/src/providers/provider-model-definition.ts +373 -373
  274. package/src/providers/provider-probe.ts +3 -0
  275. package/src/providers/quota/quota-contracts.ts +3 -7
  276. package/src/providers/search/search-provider.ts +2 -6
  277. package/src/security/admission/contracts.ts +10 -0
  278. package/src/security/admission/in-memory-store.ts +89 -14
  279. package/src/security/admission/lua.ts +191 -182
  280. package/src/security/admission/redis-store.ts +30 -0
  281. package/src/security/admission/service.ts +18 -0
  282. package/src/security/api-key-auth-cache.ts +96 -0
  283. package/src/security/api-key-auth.ts +11 -100
  284. package/src/security/client-router-fingerprint.ts +3 -7
  285. package/src/security/ip-boundary.ts +7 -3
  286. package/src/shared/coerce.ts +67 -0
  287. package/src/shared/hash.ts +6 -0
  288. package/src/shared/time.ts +7 -0
  289. package/src/transport/dispatch/attempt-finalize.ts +26 -138
  290. package/src/transport/dispatch/attempt-loop.ts +103 -81
  291. package/src/transport/dispatch/capture-policy.ts +78 -0
  292. package/src/transport/dispatch/proxy-request.ts +4 -2
  293. package/src/transport/dispatch/streaming/encoder.ts +111 -5
  294. package/src/transport/dispatch/streaming/stream.ts +19 -2
  295. package/src/transport/dispatch/streaming/thinking-loop.ts +300 -0
  296. package/src/transport/failure-policy.ts +27 -0
  297. package/src/transport/guards/abuse-guard.ts +2 -2
  298. package/src/transport/guards/auth-guard.ts +17 -0
  299. package/src/transport/guards/body-policy.ts +3 -2
  300. package/src/transport/guards/concurrency-guard.ts +89 -13
  301. package/src/transport/guards/early-rejection-capture.ts +43 -3
  302. package/src/transport/guards/error-lifecycle.ts +10 -2
  303. package/src/transport/guards/guard-errors.ts +23 -0
  304. package/src/transport/guards/headroom-guard.ts +41 -7
  305. package/src/transport/guards/pipeline.ts +24 -0
  306. package/src/transport/guards/request-context.ts +2 -1
  307. package/src/transport/routing/admission.ts +51 -9
  308. package/src/transport/routing/bounded-lru.ts +49 -0
  309. package/src/transport/routing/candidate-index.ts +88 -0
  310. package/src/transport/routing/reservations.ts +121 -7
  311. package/src/transport/routing/round-robin.ts +10 -22
  312. package/src/transport/routing/route-catalog.ts +26 -13
  313. package/src/transport/routing/route-model.ts +22 -2
  314. package/src/transport/routing/router.ts +53 -31
  315. package/src/transport/surface/chat/encode.ts +2 -3
  316. package/src/transport/surface/chat/parse.ts +2 -1
  317. package/src/transport/surface/dialects.ts +11 -3
  318. package/src/transport/surface/messages/encode.ts +2 -1
  319. package/src/transport/surface/messages/parse.ts +3 -1
  320. package/src/transport/surface/messages/stream.ts +2 -6
  321. package/src/workers/maintenance-lease.ts +200 -0
  322. package/src/workers/scheduler.ts +101 -1
  323. package/dist/dashboard/assets/BannedPage-Brvuj_Eb.js.gz +0 -0
  324. package/dist/dashboard/assets/CliToolDetailPage-CqfXtI3y.js.gz +0 -0
  325. package/dist/dashboard/assets/CliToolsPage-GQeS91bg.js.gz +0 -0
  326. package/dist/dashboard/assets/CombosPage-BDRsTXBB.js.gz +0 -0
  327. package/dist/dashboard/assets/ConsoleLogPage-zP8AFHX0.js.gz +0 -0
  328. package/dist/dashboard/assets/CustomizationPage-BmfrNZ14.js.gz +0 -0
  329. package/dist/dashboard/assets/HealthEventsModal-Dn3M6WZc.js +0 -1
  330. package/dist/dashboard/assets/HealthEventsModal-Dn3M6WZc.js.gz +0 -0
  331. package/dist/dashboard/assets/LoginPage-DoreIgm8.js.gz +0 -0
  332. package/dist/dashboard/assets/ModelPicker-C7pDb41R.js.gz +0 -0
  333. package/dist/dashboard/assets/OverviewPage-CvvD1Ugk.js +0 -1
  334. package/dist/dashboard/assets/OverviewPage-CvvD1Ugk.js.gz +0 -0
  335. package/dist/dashboard/assets/ProviderDetailPage-Clbn1gsW.js +0 -3
  336. package/dist/dashboard/assets/ProviderDetailPage-Clbn1gsW.js.gz +0 -0
  337. package/dist/dashboard/assets/ProvidersPage-BnoPgA7w.js.gz +0 -0
  338. package/dist/dashboard/assets/ProxyPage-Bq_TGgdb.js.gz +0 -0
  339. package/dist/dashboard/assets/QuotaPage-D9DHXvom.js +0 -1
  340. package/dist/dashboard/assets/QuotaPage-D9DHXvom.js.gz +0 -0
  341. package/dist/dashboard/assets/SettingsPage-7xV8WkWL.js +0 -1
  342. package/dist/dashboard/assets/SettingsPage-7xV8WkWL.js.gz +0 -0
  343. package/dist/dashboard/assets/SetupPage-Day3mCOn.js.gz +0 -0
  344. package/dist/dashboard/assets/SortableList-DQxllnB5.js.gz +0 -0
  345. package/dist/dashboard/assets/UsagePage-DXa1oR-4.js.gz +0 -0
  346. package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
  347. package/dist/dashboard/assets/app-CX_Z8cMs.css +0 -2
  348. package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
  349. package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
  350. package/dist/dashboard/assets/badge-CR72dasT.js.gz +0 -0
  351. package/dist/dashboard/assets/banner-CI9_eSfO.js.gz +0 -0
  352. package/dist/dashboard/assets/card-DS7CQEsO.js.gz +0 -0
  353. package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
  354. package/dist/dashboard/assets/clipboard-button-611pSsaX.js.gz +0 -0
  355. package/dist/dashboard/assets/dialog-k37exjNt.js.gz +0 -0
  356. package/dist/dashboard/assets/drawer-CX7N8ucR.js.gz +0 -0
  357. package/dist/dashboard/assets/entry-B9Xvtnfj.js +0 -1
  358. package/dist/dashboard/assets/entry-B9Xvtnfj.js.gz +0 -0
  359. package/dist/dashboard/assets/entry-ByMGkR3C.js +0 -4
  360. package/dist/dashboard/assets/entry-ByMGkR3C.js.gz +0 -0
  361. package/dist/dashboard/assets/entry-CTP8AyiS.js.gz +0 -0
  362. package/dist/dashboard/assets/github-badge-sltk1qUi.js.gz +0 -0
  363. package/dist/dashboard/assets/index-z3zDOSMM.js.gz +0 -0
  364. package/dist/dashboard/assets/inline-BhY2sSnO.js.gz +0 -0
  365. package/dist/dashboard/assets/input-V1umFiBk.js.gz +0 -0
  366. package/dist/dashboard/assets/layout-BBOYycyS.js.gz +0 -0
  367. package/dist/dashboard/assets/list-row-1zzZYM0B.js.gz +0 -0
  368. package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
  369. package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
  370. package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
  371. package/dist/dashboard/assets/numbers-DkfMR5Qs.js.gz +0 -0
  372. package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
  373. package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
  374. package/dist/dashboard/assets/select-C6MMiaio.js.gz +0 -0
  375. package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
  376. package/dist/dashboard/assets/stack-BHTfXdz_.js.gz +0 -0
  377. package/dist/dashboard/assets/state-mPDRlQkl.js.gz +0 -0
  378. package/dist/dashboard/assets/switch-DOmJxg7H.js.gz +0 -0
  379. package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
  380. package/scripts/build/aot.ts +0 -143
  381. package/scripts/build/build.ts +0 -24
  382. package/scripts/build/compile.ts +0 -88
  383. package/scripts/build/restart.ts +0 -120
  384. package/scripts/build/start.ts +0 -25
  385. package/src/observability/event-compaction.ts +0 -317
  386. package/src/observability/event-day-index.ts +0 -440
@@ -1,279 +1,279 @@
1
- import type { FetchLike, ProviderQuotaResult, ProviderQuotaWindow } from "../../quota/quota-contracts";
2
- import { isoDate, number, percentWindow, record, text } from "../../quota/quota-contracts";
3
- import { buildGrokAuthUserAgent, getGrokVersion } from "../../operations/client-versions";
4
- import { decodeGrokCreditsFrame } from "./grok-quota-frame";
5
- import {
6
- grokAuthoritativeWindow,
7
- grokHeaderRequestsWindow,
8
- grokHeaderWindow,
9
- GROK_FREE_USAGE_EXHAUSTED,
10
- parseGrokFreeUsageVerdict,
11
- readGrokRateLimitHeaders,
12
- } from "./grok-ratelimit";
13
- import type { GrokRateLimitProbe } from "./grok-ratelimit";
14
- import { storedGrokRateLimit } from "./grok-ratelimit-store";
15
- import type { QuotaCollectionContext } from "../../quota/quota-support";
16
-
17
- const BILLING_URL = "https://cli-chat-proxy.grok.com/v1/billing?format=credits";
18
- const BILLING_SOURCE = "cli-chat-proxy.grok.com/v1/billing";
19
- const USER_URL = "https://cli-chat-proxy.grok.com/v1/user?include=subscription";
20
- const GRPC_CREDITS_URL = "https://grok.com/grok_api_v2.GrokBuildBilling/GetGrokCreditsConfig";
21
- const GRPC_EMPTY_FRAME = new Uint8Array([0, 0, 0, 0, 0]);
22
-
23
- /**
24
- * The cheapest request that still returns the rate-limit headers.
25
- *
26
- * There is no dedicated quota endpoint — `grok.com/rest/*` does not expose one
27
- * and `/v1/models` carries no rate-limit headers — so reading the bucket costs
28
- * one real request. At ~360-480 tokens against a 1M allowance that is a few
29
- * thousand readings per account, which is affordable; a larger cap would not
30
- * be.
31
- */
32
- const PROBE_URL = "https://cli-chat-proxy.grok.com/v1/responses";
33
- const PROBE_BODY = {
34
- model: "grok-4.6",
35
- stream: false,
36
- max_output_tokens: 1,
37
- input: [{ type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] }],
38
- };
39
-
40
- /**
41
- * Auth headers shared by every xAI billing/userinfo call: bearer token plus
42
- * the CLI client identity the gateway gates on.
43
- */
44
- function xaiHeaders(accessToken: string): Record<string, string> {
45
- const version = getGrokVersion();
46
- return {
47
- authorization: `Bearer ${accessToken}`,
48
- "x-xai-token-auth": "xai-grok-cli",
49
- "x-grok-client-version": version,
50
- "user-agent": buildGrokAuthUserAgent(version),
51
- };
52
- }
53
- interface XaiBillingPayload {
54
- currentPeriod?: unknown;
55
- creditUsagePercent?: unknown;
56
- productUsage?: unknown;
57
- onDemandCap?: unknown;
58
- onDemandUsed?: unknown;
59
- billingPeriodStart?: unknown;
60
- billingPeriodEnd?: unknown;
61
- monthlyLimit?: unknown;
62
- used?: unknown;
63
- subscriptionTier?: unknown;
64
- user?: unknown;
65
- }
66
-
67
- function parsePayload(value: string): XaiBillingPayload {
68
- const payload: unknown = JSON.parse(value);
69
- // Live billing wraps usage in a `config` envelope (free-tier shape);
70
- // paid shapes may carry the fields top-level — read the envelope first.
71
- const parsed = record(payload);
72
- return ((parsed && record(parsed.config)) ?? parsed ?? {}) as XaiBillingPayload;
73
- }
74
-
75
- function billingNumber(value: unknown): number | null {
76
- const nested = record(value);
77
- return number(nested?.val ?? value);
78
- }
79
-
80
- function period(raw: unknown): { start: string; end: string } | undefined {
81
- const value = record(raw);
82
- const type = text(value?.type);
83
- if (type !== null && !type.toUpperCase().includes("WEEK")) return undefined;
84
- const start = isoDate(value?.start);
85
- const end = isoDate(value?.end);
86
- return start && end && Date.parse(end) > Date.parse(start) ? { start, end } : undefined;
87
- }
88
-
89
- function buildWeeklyWindows(payload: XaiBillingPayload): ProviderQuotaWindow[] {
90
- const current = period(payload.currentPeriod);
91
- const creditPercent = billingNumber(payload.creditUsagePercent);
92
- if (!current) return [];
93
- if (creditPercent === null || creditPercent < 0 || creditPercent > 100) {
94
- // Free-tier billing states the period but no usage percent: emit the
95
- // window with the reset date and null percents (the dashboard renders
96
- // the countdown without inventing a number) instead of empty windows.
97
- return [percentWindow("1w", "SuperGrok Weekly Credits", null, current.end, null, null, "independent")];
98
- }
99
- const windows: ProviderQuotaWindow[] = [
100
- percentWindow("1w", "SuperGrok Weekly Credits", creditPercent, current.end, creditPercent, 100, "independent"),
101
- ];
102
- if (Array.isArray(payload.productUsage)) {
103
- for (const entry of payload.productUsage) {
104
- const value = record(entry);
105
- const product = text(value?.product);
106
- const percent = billingNumber(value?.usagePercent);
107
- windows.push(percentWindow(`product:${product}:1w`, `${product} (Weekly)`, percent, current.end, percent, 100, "independent"));
108
- }
109
- }
110
- return windows;
111
- }
112
-
113
- function buildMonthlyWindows(payload: XaiBillingPayload): ProviderQuotaWindow[] {
114
- const start = isoDate(payload.billingPeriodStart);
115
- const end = isoDate(payload.billingPeriodEnd);
116
- const limit = billingNumber(payload.monthlyLimit);
117
- const used = billingNumber(payload.used);
118
- if (!start || !end || limit === null || limit <= 0 || used === null || used < 0) return [];
119
- const usedPercent = Math.min(100, (used / limit) * 100);
120
- return [percentWindow("1mo", "SuperGrok Monthly Included", usedPercent, end, used, limit, "independent")];
121
- }
122
-
123
- function addOnDemandWindow(windows: ProviderQuotaWindow[], payload: XaiBillingPayload): void {
124
- const cap = billingNumber(payload.onDemandCap);
125
- const used = billingNumber(payload.onDemandUsed);
126
- if (cap === null || cap <= 0 || used === null || used < 0) return;
127
- windows.push(percentWindow("on-demand", "On-demand", Math.min(100, (used / cap) * 100), null, used, cap, "independent"));
128
- }
129
-
130
- async function fetchGrpcCredits(
131
- accessToken: string,
132
- fetcher: FetchLike,
133
- ): Promise<{ percentUsed: number; resetAt: string | null } | null> {
134
- try {
135
- const response = await fetcher(GRPC_CREDITS_URL, {
136
- method: "POST",
137
- headers: {
138
- ...xaiHeaders(accessToken),
139
- "content-type": "application/grpc-web+proto",
140
- "x-grpc-web": "1",
141
- },
142
- body: GRPC_EMPTY_FRAME,
143
- signal: AbortSignal.timeout(8_000),
144
- });
145
- if (!response.ok) return null;
146
- const arrayBuffer = await response.arrayBuffer();
147
- return decodeGrokCreditsFrame(new Uint8Array(arrayBuffer));
148
- } catch {
149
- return null;
150
- }
151
- }
152
-
153
- async function fetchUser(
154
- accessToken: string,
155
- fetcher: FetchLike,
156
- ): Promise<Record<string, unknown> | null> {
157
- try {
158
- const response = await fetcher(USER_URL, {
159
- headers: { ...xaiHeaders(accessToken), accept: "application/json" },
160
- signal: AbortSignal.timeout(10_000),
161
- });
162
- if (!response.ok) return null;
163
- return record((await response.json()) as unknown);
164
- } catch {
165
- return null;
166
- }
167
- }
168
-
169
- /**
170
- * One cheap request, read for its rate-limit headers — and, when the account is
171
- * being refused, for the authoritative allowance in the 429 body.
172
- *
173
- * A refusal is not a failure of the probe: it is the only moment upstream
174
- * states the real free allowance, so the body is parsed before anything is
175
- * discarded. A transport failure is the opposite — it says nothing about the
176
- * account, so it yields no reading rather than a zero.
177
- */
178
- async function probeRateLimit(
179
- accessToken: string,
180
- fetcher: FetchLike,
181
- ): Promise<GrokRateLimitProbe> {
182
- try {
183
- const response = await fetcher(PROBE_URL, {
184
- method: "POST",
185
- headers: { ...xaiHeaders(accessToken), "content-type": "application/json" },
186
- body: JSON.stringify(PROBE_BODY),
187
- signal: AbortSignal.timeout(30_000),
188
- });
189
- const reading = readGrokRateLimitHeaders(response.headers);
190
- let authoritative = null;
191
- let note: string | null = null;
192
- if (response.status === 429) {
193
- const body = await response.text();
194
- const verdict = parseGrokFreeUsageVerdict(parsePayload(body), body);
195
- authoritative = verdict ? grokAuthoritativeWindow(verdict) : null;
196
- note = GROK_FREE_USAGE_EXHAUSTED;
197
- } else if (!response.ok) {
198
- // Drain so the connection is reusable, then report nothing: an unrelated
199
- // failure must not read as an exhausted allowance.
200
- await response.text();
201
- }
202
- return {
203
- header: grokHeaderWindow(reading),
204
- requests: grokHeaderRequestsWindow(reading),
205
- authoritative,
206
- refusal: response.status === 429,
207
- note,
208
- };
209
- } catch {
210
- return { header: null, requests: null, authoritative: null, refusal: false, note: null };
211
- }
212
- }
213
-
214
- /** Fetches Grok Build subscription billing, including weekly and unified-monthly fallbacks. */
215
- export async function fetchGrokQuota(
216
- credential: string,
217
- fetcher: FetchLike,
218
- context?: QuotaCollectionContext,
219
- ): Promise<ProviderQuotaResult> {
220
- const accessToken = credential.trim();
221
- if (!accessToken) throw new Error("grok quota requires an OAuth access token");
222
- const response = await fetcher(BILLING_URL, {
223
- headers: { ...xaiHeaders(accessToken), accept: "application/json" },
224
- signal: AbortSignal.timeout(15_000),
225
- });
226
- const raw = await response.text();
227
- if (!response.ok) throw new Error(`Grok Build billing request failed (${response.status}): ${raw.slice(0, 500)}`);
228
- const payload = parsePayload(raw);
229
- const windows = buildWeeklyWindows(payload);
230
- if (windows.length === 0 || windows.every((w) => w.usedPercent === null)) {
231
- const grpc = await fetchGrpcCredits(accessToken, fetcher);
232
- if (grpc) {
233
- const idx = windows.findIndex((w) => w.label.includes("Weekly"));
234
- const grpcWindow = percentWindow("1w", "SuperGrok Weekly Credits", grpc.percentUsed, grpc.resetAt, grpc.percentUsed, 100, "independent");
235
- if (idx >= 0) windows[idx] = grpcWindow;
236
- else windows.unshift(grpcWindow);
237
- }
238
- }
239
- if (windows.length === 0) windows.push(...buildMonthlyWindows(payload));
240
- addOnDemandWindow(windows, payload);
241
- const user = await fetchUser(accessToken, fetcher);
242
- const userRecord = record(user?.user);
243
- const plan =
244
- text(user?.subscriptionTier) ??
245
- text(payload.subscriptionTier) ??
246
- text(userRecord?.subscriptionTier) ??
247
- "SuperGrok";
248
- // The billing windows are the subscription's own accounting; the two CLI
249
- // readings say something different — what this account can still *serve* —
250
- // so they are prepended and appended around it rather than merged in.
251
- //
252
- // Header first: it is what a caller sees every day and it never goes stale
253
- // into "unknown". Authoritative last: it only exists once upstream has
254
- // refused, and it is the number that decides the account, so it sits beneath
255
- // the indicator it overrides.
256
- // A reading captured from real traffic costs nothing, so it is preferred
257
- // whenever one is fresh. The probe is the fallback for the first read after a
258
- // start — never the routine path, or every console refresh would spend a
259
- // request.
260
- const stored = context?.accountId === undefined
261
- ? null
262
- : storedGrokRateLimit(context.accountId);
263
- const probe = stored ? null : await probeRateLimit(accessToken, fetcher);
264
- const ordered: ProviderQuotaWindow[] = [];
265
- if (stored) ordered.push(...stored);
266
- else if (probe) {
267
- if (probe.header) ordered.push(probe.header);
268
- if (probe.requests) ordered.push(probe.requests);
269
- }
270
- ordered.push(...windows);
271
- if (probe?.authoritative) ordered.push(probe.authoritative);
272
- // Deliberately not `probe.note` in `error`: a refusal here is the probe's own
273
- // doing and says the account is out of free usage, which is *account state*,
274
- // not a read failure. Reporting it as an error would make a healthy read look
275
- // broken; the authoritative window below is what carries the verdict.
276
- return { source: "grok", plan, windows: ordered, error: null };
277
- }
278
-
279
- export { BILLING_SOURCE as GROK_BILLING_SOURCE };
1
+ import type { FetchLike, ProviderQuotaResult, ProviderQuotaWindow } from "../../quota/quota-contracts";
2
+ import { isoDate, number, percentWindow, record, text } from "../../quota/quota-contracts";
3
+ import { buildGrokAuthUserAgent, getGrokVersion } from "../../operations/client-versions";
4
+ import { decodeGrokCreditsFrame } from "./grok-quota-frame";
5
+ import {
6
+ grokAuthoritativeWindow,
7
+ grokHeaderRequestsWindow,
8
+ grokHeaderWindow,
9
+ GROK_FREE_USAGE_EXHAUSTED,
10
+ parseGrokFreeUsageVerdict,
11
+ readGrokRateLimitHeaders,
12
+ } from "./grok-ratelimit";
13
+ import type { GrokRateLimitProbe } from "./grok-ratelimit";
14
+ import { storedGrokRateLimit } from "./grok-ratelimit-store";
15
+ import type { QuotaCollectionContext } from "../../quota/quota-support";
16
+
17
+ const BILLING_URL = "https://cli-chat-proxy.grok.com/v1/billing?format=credits";
18
+ const BILLING_SOURCE = "cli-chat-proxy.grok.com/v1/billing";
19
+ const USER_URL = "https://cli-chat-proxy.grok.com/v1/user?include=subscription";
20
+ const GRPC_CREDITS_URL = "https://grok.com/grok_api_v2.GrokBuildBilling/GetGrokCreditsConfig";
21
+ const GRPC_EMPTY_FRAME = new Uint8Array([0, 0, 0, 0, 0]);
22
+
23
+ /**
24
+ * The cheapest request that still returns the rate-limit headers.
25
+ *
26
+ * There is no dedicated quota endpoint — `grok.com/rest/*` does not expose one
27
+ * and `/v1/models` carries no rate-limit headers — so reading the bucket costs
28
+ * one real request. At ~360-480 tokens against a 1M allowance that is a few
29
+ * thousand readings per account, which is affordable; a larger cap would not
30
+ * be.
31
+ */
32
+ const PROBE_URL = "https://cli-chat-proxy.grok.com/v1/responses";
33
+ const PROBE_BODY = {
34
+ model: "grok-4.6",
35
+ stream: false,
36
+ max_output_tokens: 1,
37
+ input: [{ type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] }],
38
+ };
39
+
40
+ /**
41
+ * Auth headers shared by every xAI billing/userinfo call: bearer token plus
42
+ * the CLI client identity the gateway gates on.
43
+ */
44
+ function xaiHeaders(accessToken: string): Record<string, string> {
45
+ const version = getGrokVersion();
46
+ return {
47
+ authorization: `Bearer ${accessToken}`,
48
+ "x-xai-token-auth": "xai-grok-cli",
49
+ "x-grok-client-version": version,
50
+ "user-agent": buildGrokAuthUserAgent(version),
51
+ };
52
+ }
53
+ interface XaiBillingPayload {
54
+ currentPeriod?: unknown;
55
+ creditUsagePercent?: unknown;
56
+ productUsage?: unknown;
57
+ onDemandCap?: unknown;
58
+ onDemandUsed?: unknown;
59
+ billingPeriodStart?: unknown;
60
+ billingPeriodEnd?: unknown;
61
+ monthlyLimit?: unknown;
62
+ used?: unknown;
63
+ subscriptionTier?: unknown;
64
+ user?: unknown;
65
+ }
66
+
67
+ function parsePayload(value: string): XaiBillingPayload {
68
+ const payload: unknown = JSON.parse(value);
69
+ // Live billing wraps usage in a `config` envelope (free-tier shape);
70
+ // paid shapes may carry the fields top-level — read the envelope first.
71
+ const parsed = record(payload);
72
+ return ((parsed && record(parsed.config)) ?? parsed ?? {}) as XaiBillingPayload;
73
+ }
74
+
75
+ function billingNumber(value: unknown): number | null {
76
+ const nested = record(value);
77
+ return number(nested?.val ?? value);
78
+ }
79
+
80
+ function period(raw: unknown): { start: string; end: string } | undefined {
81
+ const value = record(raw);
82
+ const type = text(value?.type);
83
+ if (type !== null && !type.toUpperCase().includes("WEEK")) return undefined;
84
+ const start = isoDate(value?.start);
85
+ const end = isoDate(value?.end);
86
+ return start && end && Date.parse(end) > Date.parse(start) ? { start, end } : undefined;
87
+ }
88
+
89
+ function buildWeeklyWindows(payload: XaiBillingPayload): ProviderQuotaWindow[] {
90
+ const current = period(payload.currentPeriod);
91
+ const creditPercent = billingNumber(payload.creditUsagePercent);
92
+ if (!current) return [];
93
+ if (creditPercent === null || creditPercent < 0 || creditPercent > 100) {
94
+ // Free-tier billing states the period but no usage percent: emit the
95
+ // window with the reset date and null percents (the dashboard renders
96
+ // the countdown without inventing a number) instead of empty windows.
97
+ return [percentWindow("1w", "SuperGrok Weekly Credits", null, current.end, null, null, "independent")];
98
+ }
99
+ const windows: ProviderQuotaWindow[] = [
100
+ percentWindow("1w", "SuperGrok Weekly Credits", creditPercent, current.end, creditPercent, 100, "independent"),
101
+ ];
102
+ if (Array.isArray(payload.productUsage)) {
103
+ for (const entry of payload.productUsage) {
104
+ const value = record(entry);
105
+ const product = text(value?.product);
106
+ const percent = billingNumber(value?.usagePercent);
107
+ windows.push(percentWindow(`product:${product}:1w`, `${product} (Weekly)`, percent, current.end, percent, 100, "independent"));
108
+ }
109
+ }
110
+ return windows;
111
+ }
112
+
113
+ function buildMonthlyWindows(payload: XaiBillingPayload): ProviderQuotaWindow[] {
114
+ const start = isoDate(payload.billingPeriodStart);
115
+ const end = isoDate(payload.billingPeriodEnd);
116
+ const limit = billingNumber(payload.monthlyLimit);
117
+ const used = billingNumber(payload.used);
118
+ if (!start || !end || limit === null || limit <= 0 || used === null || used < 0) return [];
119
+ const usedPercent = Math.min(100, (used / limit) * 100);
120
+ return [percentWindow("1mo", "SuperGrok Monthly Included", usedPercent, end, used, limit, "independent")];
121
+ }
122
+
123
+ function addOnDemandWindow(windows: ProviderQuotaWindow[], payload: XaiBillingPayload): void {
124
+ const cap = billingNumber(payload.onDemandCap);
125
+ const used = billingNumber(payload.onDemandUsed);
126
+ if (cap === null || cap <= 0 || used === null || used < 0) return;
127
+ windows.push(percentWindow("on-demand", "On-demand", Math.min(100, (used / cap) * 100), null, used, cap, "independent"));
128
+ }
129
+
130
+ async function fetchGrpcCredits(
131
+ accessToken: string,
132
+ fetcher: FetchLike,
133
+ ): Promise<{ percentUsed: number; resetAt: string | null } | null> {
134
+ try {
135
+ const response = await fetcher(GRPC_CREDITS_URL, {
136
+ method: "POST",
137
+ headers: {
138
+ ...xaiHeaders(accessToken),
139
+ "content-type": "application/grpc-web+proto",
140
+ "x-grpc-web": "1",
141
+ },
142
+ body: GRPC_EMPTY_FRAME,
143
+ signal: AbortSignal.timeout(8_000),
144
+ });
145
+ if (!response.ok) return null;
146
+ const arrayBuffer = await response.arrayBuffer();
147
+ return decodeGrokCreditsFrame(new Uint8Array(arrayBuffer));
148
+ } catch {
149
+ return null;
150
+ }
151
+ }
152
+
153
+ async function fetchUser(
154
+ accessToken: string,
155
+ fetcher: FetchLike,
156
+ ): Promise<Record<string, unknown> | null> {
157
+ try {
158
+ const response = await fetcher(USER_URL, {
159
+ headers: { ...xaiHeaders(accessToken), accept: "application/json" },
160
+ signal: AbortSignal.timeout(10_000),
161
+ });
162
+ if (!response.ok) return null;
163
+ return record((await response.json()) as unknown);
164
+ } catch {
165
+ return null;
166
+ }
167
+ }
168
+
169
+ /**
170
+ * One cheap request, read for its rate-limit headers — and, when the account is
171
+ * being refused, for the authoritative allowance in the 429 body.
172
+ *
173
+ * A refusal is not a failure of the probe: it is the only moment upstream
174
+ * states the real free allowance, so the body is parsed before anything is
175
+ * discarded. A transport failure is the opposite — it says nothing about the
176
+ * account, so it yields no reading rather than a zero.
177
+ */
178
+ async function probeRateLimit(
179
+ accessToken: string,
180
+ fetcher: FetchLike,
181
+ ): Promise<GrokRateLimitProbe> {
182
+ try {
183
+ const response = await fetcher(PROBE_URL, {
184
+ method: "POST",
185
+ headers: { ...xaiHeaders(accessToken), "content-type": "application/json" },
186
+ body: JSON.stringify(PROBE_BODY),
187
+ signal: AbortSignal.timeout(30_000),
188
+ });
189
+ const reading = readGrokRateLimitHeaders(response.headers);
190
+ let authoritative = null;
191
+ let note: string | null = null;
192
+ if (response.status === 429) {
193
+ const body = await response.text();
194
+ const verdict = parseGrokFreeUsageVerdict(parsePayload(body), body);
195
+ authoritative = verdict ? grokAuthoritativeWindow(verdict) : null;
196
+ note = GROK_FREE_USAGE_EXHAUSTED;
197
+ } else if (!response.ok) {
198
+ // Drain so the connection is reusable, then report nothing: an unrelated
199
+ // failure must not read as an exhausted allowance.
200
+ await response.text();
201
+ }
202
+ return {
203
+ header: grokHeaderWindow(reading),
204
+ requests: grokHeaderRequestsWindow(reading),
205
+ authoritative,
206
+ refusal: response.status === 429,
207
+ note,
208
+ };
209
+ } catch {
210
+ return { header: null, requests: null, authoritative: null, refusal: false, note: null };
211
+ }
212
+ }
213
+
214
+ /** Fetches Grok Build subscription billing, including weekly and unified-monthly fallbacks. */
215
+ export async function fetchGrokQuota(
216
+ credential: string,
217
+ fetcher: FetchLike,
218
+ context?: QuotaCollectionContext,
219
+ ): Promise<ProviderQuotaResult> {
220
+ const accessToken = credential.trim();
221
+ if (!accessToken) throw new Error("grok quota requires an OAuth access token");
222
+ const response = await fetcher(BILLING_URL, {
223
+ headers: { ...xaiHeaders(accessToken), accept: "application/json" },
224
+ signal: AbortSignal.timeout(15_000),
225
+ });
226
+ const raw = await response.text();
227
+ if (!response.ok) throw new Error(`Grok Build billing request failed (${response.status}): ${raw.slice(0, 500)}`);
228
+ const payload = parsePayload(raw);
229
+ const windows = buildWeeklyWindows(payload);
230
+ if (windows.length === 0 || windows.every((w) => w.usedPercent === null)) {
231
+ const grpc = await fetchGrpcCredits(accessToken, fetcher);
232
+ if (grpc) {
233
+ const idx = windows.findIndex((w) => w.label.includes("Weekly"));
234
+ const grpcWindow = percentWindow("1w", "SuperGrok Weekly Credits", grpc.percentUsed, grpc.resetAt, grpc.percentUsed, 100, "independent");
235
+ if (idx >= 0) windows[idx] = grpcWindow;
236
+ else windows.unshift(grpcWindow);
237
+ }
238
+ }
239
+ if (windows.length === 0) windows.push(...buildMonthlyWindows(payload));
240
+ addOnDemandWindow(windows, payload);
241
+ const user = await fetchUser(accessToken, fetcher);
242
+ const userRecord = record(user?.user);
243
+ const plan =
244
+ text(user?.subscriptionTier) ??
245
+ text(payload.subscriptionTier) ??
246
+ text(userRecord?.subscriptionTier) ??
247
+ "SuperGrok";
248
+ // The billing windows are the subscription's own accounting; the two CLI
249
+ // readings say something different — what this account can still *serve* —
250
+ // so they are prepended and appended around it rather than merged in.
251
+ //
252
+ // Header first: it is what a caller sees every day and it never goes stale
253
+ // into "unknown". Authoritative last: it only exists once upstream has
254
+ // refused, and it is the number that decides the account, so it sits beneath
255
+ // the indicator it overrides.
256
+ // A reading captured from real traffic costs nothing, so it is preferred
257
+ // whenever one is fresh. The probe is the fallback for the first read after a
258
+ // start — never the routine path, or every console refresh would spend a
259
+ // request.
260
+ const stored = context?.accountId === undefined
261
+ ? null
262
+ : storedGrokRateLimit(context.accountId);
263
+ const probe = stored ? null : await probeRateLimit(accessToken, fetcher);
264
+ const ordered: ProviderQuotaWindow[] = [];
265
+ if (stored) ordered.push(...stored);
266
+ else if (probe) {
267
+ if (probe.header) ordered.push(probe.header);
268
+ if (probe.requests) ordered.push(probe.requests);
269
+ }
270
+ ordered.push(...windows);
271
+ if (probe?.authoritative) ordered.push(probe.authoritative);
272
+ // Deliberately not `probe.note` in `error`: a refusal here is the probe's own
273
+ // doing and says the account is out of free usage, which is *account state*,
274
+ // not a read failure. Reporting it as an error would make a healthy read look
275
+ // broken; the authoritative window below is what carries the verdict.
276
+ return { source: "grok", plan, windows: ordered, error: null };
277
+ }
278
+
279
+ export { BILLING_SOURCE as GROK_BILLING_SOURCE };