cartethyia 2.5.2 → 2.5.4

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 (277) hide show
  1. package/dist/dashboard/assets/{BannedPage-MsQaeQTs.js → BannedPage-C2orBsI2.js} +1 -1
  2. package/dist/dashboard/assets/BannedPage-C2orBsI2.js.gz +0 -0
  3. package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js +1 -0
  4. package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolsPage-DfWHTJ7v.js → CliToolsPage-BV6pxVQo.js} +1 -1
  6. package/dist/dashboard/assets/CliToolsPage-BV6pxVQo.js.gz +0 -0
  7. package/dist/dashboard/assets/{CombosPage-BrCBQDAc.js → CombosPage-Bsan4679.js} +1 -1
  8. package/dist/dashboard/assets/CombosPage-Bsan4679.js.gz +0 -0
  9. package/dist/dashboard/assets/{ConsoleLogPage-C6Uh33er.js → ConsoleLogPage-CW-wPmJ6.js} +1 -1
  10. package/dist/dashboard/assets/ConsoleLogPage-CW-wPmJ6.js.gz +0 -0
  11. package/dist/dashboard/assets/{CustomizationPage-8aS9hyJu.js → CustomizationPage-0p8uX7vB.js} +1 -1
  12. package/dist/dashboard/assets/CustomizationPage-0p8uX7vB.js.gz +0 -0
  13. package/dist/dashboard/assets/HealthEventsModal-BjqSMfko.js +1 -0
  14. package/dist/dashboard/assets/HealthEventsModal-BjqSMfko.js.gz +0 -0
  15. package/dist/dashboard/assets/{LoginPage-C8hKyXBe.js → LoginPage-BvzCx8Kr.js} +1 -1
  16. package/dist/dashboard/assets/LoginPage-BvzCx8Kr.js.gz +0 -0
  17. package/dist/dashboard/assets/{ModelPicker-CWkTUtpX.js → ModelPicker-BY1aKEkP.js} +1 -1
  18. package/dist/dashboard/assets/ModelPicker-BY1aKEkP.js.gz +0 -0
  19. package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js +1 -0
  20. package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js.gz +0 -0
  21. package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js +3 -0
  22. package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js.gz +0 -0
  23. package/dist/dashboard/assets/ProvidersPage-Js9jUPSb.js +1 -0
  24. package/dist/dashboard/assets/ProvidersPage-Js9jUPSb.js.gz +0 -0
  25. package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js +7 -0
  26. package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js.gz +0 -0
  27. package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js +1 -0
  28. package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js.gz +0 -0
  29. package/dist/dashboard/assets/SettingsPage-CS86ho5X.js +1 -0
  30. package/dist/dashboard/assets/SettingsPage-CS86ho5X.js.gz +0 -0
  31. package/dist/dashboard/assets/{SetupPage-CXjUlfqr.js → SetupPage-DDbMQsoo.js} +1 -1
  32. package/dist/dashboard/assets/SetupPage-DDbMQsoo.js.gz +0 -0
  33. package/dist/dashboard/assets/{SortableList-C-_oD3F2.js → SortableList-CB3Z7VTf.js} +1 -1
  34. package/dist/dashboard/assets/SortableList-CB3Z7VTf.js.gz +0 -0
  35. package/dist/dashboard/assets/{StudioPage-u9IxmYOI.js → StudioPage-D95UCPHI.js} +3 -3
  36. package/dist/dashboard/assets/StudioPage-D95UCPHI.js.gz +0 -0
  37. package/dist/dashboard/assets/{UsagePage-BEVLVrwP.js → UsagePage-Cnae9SWA.js} +13 -13
  38. package/dist/dashboard/assets/UsagePage-Cnae9SWA.js.gz +0 -0
  39. package/dist/dashboard/assets/{api-keys-uNywYlfv.js → api-keys-VY68Mhd1.js} +1 -1
  40. package/dist/dashboard/assets/api-keys-VY68Mhd1.js.gz +0 -0
  41. package/dist/dashboard/assets/app-COlK95oi.css +2 -0
  42. package/dist/dashboard/assets/app-COlK95oi.css.gz +0 -0
  43. package/dist/dashboard/assets/{assert-NHwYq95t.js → assert-Chi7pCnt.js} +1 -1
  44. package/dist/dashboard/assets/assert-Chi7pCnt.js.gz +0 -0
  45. package/dist/dashboard/assets/{badge-6uzDDvqs.js → badge-D4pQapKj.js} +1 -1
  46. package/dist/dashboard/assets/badge-D4pQapKj.js.gz +0 -0
  47. package/dist/dashboard/assets/{banner-4_WNg2WZ.js → banner-DH6cg-_B.js} +1 -1
  48. package/dist/dashboard/assets/banner-DH6cg-_B.js.gz +0 -0
  49. package/dist/dashboard/assets/{card--AtFedAq.js → card-BKqlUOe4.js} +1 -1
  50. package/dist/dashboard/assets/card-BKqlUOe4.js.gz +0 -0
  51. package/dist/dashboard/assets/{cli-tools-B3e7dx3N.js → cli-tools-fTgljTGA.js} +1 -1
  52. package/dist/dashboard/assets/cli-tools-fTgljTGA.js.gz +0 -0
  53. package/dist/dashboard/assets/{clipboard-button-NreUx6c8.js → clipboard-button-D5Ys_sM0.js} +1 -1
  54. package/dist/dashboard/assets/clipboard-button-D5Ys_sM0.js.gz +0 -0
  55. package/dist/dashboard/assets/database-Dz9N3zGd.js +1 -0
  56. package/dist/dashboard/assets/database-Dz9N3zGd.js.gz +0 -0
  57. package/dist/dashboard/assets/{dialog-BtpsJU98.js → dialog-BYj-8WuK.js} +1 -1
  58. package/dist/dashboard/assets/dialog-BYj-8WuK.js.gz +0 -0
  59. package/dist/dashboard/assets/{drawer-WwwtzihF.js → drawer-CmXIZ6H1.js} +1 -1
  60. package/dist/dashboard/assets/drawer-CmXIZ6H1.js.gz +0 -0
  61. package/dist/dashboard/assets/entry-CBg5zeu8.js +1 -0
  62. package/dist/dashboard/assets/entry-CBg5zeu8.js.gz +0 -0
  63. package/dist/dashboard/assets/entry-CQBr6U9c.js +1 -0
  64. package/dist/dashboard/assets/entry-CQBr6U9c.js.gz +0 -0
  65. package/dist/dashboard/assets/entry-CgdSAzEV.js +4 -0
  66. package/dist/dashboard/assets/entry-CgdSAzEV.js.gz +0 -0
  67. package/dist/dashboard/assets/{github-badge-D_eM3IgG.js → github-badge-BlptL8Uf.js} +1 -1
  68. package/dist/dashboard/assets/github-badge-BlptL8Uf.js.gz +0 -0
  69. package/dist/dashboard/assets/{index-DCBtROoK.js → index-js6TJvdr.js} +2 -2
  70. package/dist/dashboard/assets/index-js6TJvdr.js.gz +0 -0
  71. package/dist/dashboard/assets/{inline-BcNGF8Gr.js → inline-DUWDSmDV.js} +1 -1
  72. package/dist/dashboard/assets/inline-DUWDSmDV.js.gz +0 -0
  73. package/dist/dashboard/assets/{input-BG41et9G.js → input-LVjgZq4f.js} +1 -1
  74. package/dist/dashboard/assets/input-LVjgZq4f.js.gz +0 -0
  75. package/dist/dashboard/assets/{layout-C8mer_Ak.js → layout-CPfxpsfy.js} +1 -1
  76. package/dist/dashboard/assets/layout-CPfxpsfy.js.gz +0 -0
  77. package/dist/dashboard/assets/{list-row-Gh_W4OR_.js → list-row-DupvLfDx.js} +1 -1
  78. package/dist/dashboard/assets/list-row-DupvLfDx.js.gz +0 -0
  79. package/dist/dashboard/assets/{live-BJGYRBnR.js → live-C4pEJUqI.js} +1 -1
  80. package/dist/dashboard/assets/live-C4pEJUqI.js.gz +0 -0
  81. package/dist/dashboard/assets/model-bans-BRiuufkB.js +1 -0
  82. package/dist/dashboard/assets/model-bans-BRiuufkB.js.gz +0 -0
  83. package/dist/dashboard/assets/{mutation-BkoLM5Ba.js → mutation-B4ROwk8L.js} +1 -1
  84. package/dist/dashboard/assets/mutation-B4ROwk8L.js.gz +0 -0
  85. package/dist/dashboard/assets/numbers-YNcHvRY7.js +2 -0
  86. package/dist/dashboard/assets/numbers-YNcHvRY7.js.gz +0 -0
  87. package/dist/dashboard/assets/{query-keys-D7wX2vM5.js → query-keys-gmjzC3RS.js} +1 -1
  88. package/dist/dashboard/assets/{query-keys-D7wX2vM5.js.gz → query-keys-gmjzC3RS.js.gz} +0 -0
  89. package/dist/dashboard/assets/{routing-CcusqXA7.js → routing-C7tBT5o-.js} +1 -1
  90. package/dist/dashboard/assets/routing-C7tBT5o-.js.gz +0 -0
  91. package/dist/dashboard/assets/{select-DsX5QT8K.js → select-WUCXorXw.js} +2 -2
  92. package/dist/dashboard/assets/select-WUCXorXw.js.gz +0 -0
  93. package/dist/dashboard/assets/{settings-mpmXTgUa.js → settings-C_zKl1GS.js} +1 -1
  94. package/dist/dashboard/assets/settings-C_zKl1GS.js.gz +0 -0
  95. package/dist/dashboard/assets/{stack-DwLmjUFK.js → stack-_3O0F8Xn.js} +1 -1
  96. package/dist/dashboard/assets/stack-_3O0F8Xn.js.gz +0 -0
  97. package/dist/dashboard/assets/{state-DYdQmdAl.js → state-BgTxHcJS.js} +1 -1
  98. package/dist/dashboard/assets/state-BgTxHcJS.js.gz +0 -0
  99. package/dist/dashboard/assets/switch-DEjTtOWf.js +1 -0
  100. package/dist/dashboard/assets/switch-DEjTtOWf.js.gz +0 -0
  101. package/dist/dashboard/assets/terminal-0_IjPWTk.js +1 -0
  102. package/dist/dashboard/assets/terminal-0_IjPWTk.js.gz +0 -0
  103. package/dist/dashboard/assets/{useMutation-w4u4CVue.js → useMutation-BWSC0UAS.js} +1 -1
  104. package/dist/dashboard/assets/useMutation-BWSC0UAS.js.gz +0 -0
  105. package/dist/dashboard/assets/zap-C4EeVW4q.js +1 -0
  106. package/dist/dashboard/assets/zap-C4EeVW4q.js.gz +0 -0
  107. package/dist/dashboard/donation/qris-risun-marke.png +0 -0
  108. package/dist/dashboard/index.html +1 -1
  109. package/dist/dashboard/index.html.gz +0 -0
  110. package/dist/dashboard/precompressed-assets.json +54 -54
  111. package/package.json +1 -1
  112. package/scripts/cli/autostart.ts +237 -0
  113. package/scripts/cli/command/console-menu.ts +244 -246
  114. package/scripts/cli/command/tray.ts +208 -204
  115. package/scripts/cli/command/update.ts +27 -10
  116. package/scripts/cli/main.ts +28 -1
  117. package/src/boot/app.ts +693 -693
  118. package/src/boot/dependencies.ts +646 -616
  119. package/src/config/spec.ts +81 -2
  120. package/src/config/telemetry.ts +46 -0
  121. package/src/console/api-keys/bucket-spend.ts +15 -4
  122. package/src/console/backup/contracts.ts +15 -2
  123. package/src/console/backup/routes.ts +52 -1
  124. package/src/console/backup/service.ts +60 -4
  125. package/src/console/console-router.ts +8 -0
  126. package/src/console/data-management/contracts.ts +93 -0
  127. package/src/console/data-management/operations.ts +229 -0
  128. package/src/console/data-management/routes.ts +104 -0
  129. package/src/console/domain-registration.ts +39 -0
  130. package/src/console/observability/contracts.ts +524 -485
  131. package/src/console/observability/store.ts +1115 -1018
  132. package/src/console/providers/catalog/contracts.ts +6 -0
  133. package/src/console/providers/catalog/provider-operations.ts +20 -0
  134. package/src/console/providers/catalog/routes.ts +7 -0
  135. package/src/console/providers/catalog/store.ts +22 -1
  136. package/src/console/quota/account-quota-shared.ts +52 -39
  137. package/src/console/quota/refresh.ts +441 -429
  138. package/src/console/routing/pools/import-parse.ts +206 -0
  139. package/src/console/routing/pools/operations.ts +543 -501
  140. package/src/console/routing/pools/routes.ts +181 -164
  141. package/src/console/settings/contracts.ts +37 -0
  142. package/src/console/settings/store.ts +51 -0
  143. package/src/console/share/share-router.ts +6 -9
  144. package/src/console/updates/routes.ts +16 -0
  145. package/src/main.ts +10 -0
  146. package/src/observability/event-compaction.ts +315 -0
  147. package/src/observability/event-day-index.ts +442 -0
  148. package/src/observability/event-log.ts +152 -3
  149. package/src/observability/payload-capture.ts +373 -344
  150. package/src/observability/payload-store.ts +440 -366
  151. package/src/observability/telemetry-status.ts +28 -16
  152. package/src/persistence/health-event-flush.ts +36 -0
  153. package/src/persistence/schema.ts +1402 -1360
  154. package/src/persistence/stores/api-key-store.ts +6 -0
  155. package/src/persistence/stores/share-store.ts +583 -556
  156. package/src/persistence/stores/telemetry-store.ts +237 -214
  157. package/src/protocol/codecs.ts +1 -2
  158. package/src/protocol/image-clamp.ts +19 -1
  159. package/src/protocol/registry.ts +8 -0
  160. package/src/protocol/request/gemini.ts +9 -2
  161. package/src/protocol/request/messages.ts +14 -2
  162. package/src/protocol/response/gemini.ts +18 -4
  163. package/src/providers/adapters/compatible-adapter.ts +3 -0
  164. package/src/providers/bundled-discovery-providers.ts +122 -119
  165. package/src/providers/configured-openai-providers.ts +136 -134
  166. package/src/providers/default-registry.ts +5 -4
  167. package/src/providers/discovery/openai-model-discovery.ts +5 -0
  168. package/src/providers/discovery/openrouter-free-roster.ts +15 -17
  169. package/src/providers/integrations/antigravity/antigravity-quota.ts +11 -3
  170. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +4 -4
  171. package/src/providers/integrations/buddy/workbuddy/shared.ts +201 -200
  172. package/src/providers/integrations/cline/cline-quota.ts +132 -128
  173. package/src/providers/integrations/codex/codex-headers.ts +25 -39
  174. package/src/providers/integrations/codex/codex.ts +2 -29
  175. package/src/providers/integrations/cursor/cursor-quota.ts +310 -0
  176. package/src/providers/integrations/cursor/cursor-request.ts +616 -471
  177. package/src/providers/integrations/cursor/cursor-session-manager.ts +319 -281
  178. package/src/providers/integrations/cursor/cursor.ts +1485 -960
  179. package/src/providers/integrations/cursor/protocol/messages.ts +25 -0
  180. package/src/providers/integrations/factory/factory.ts +8 -1
  181. package/src/providers/integrations/gemini.ts +6 -1
  182. package/src/providers/integrations/grok/grok-quota.ts +279 -177
  183. package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -0
  184. package/src/providers/integrations/grok/grok-ratelimit.ts +218 -0
  185. package/src/providers/integrations/opencode/provider.ts +531 -532
  186. package/src/providers/integrations/qoder/qoder-quota.ts +192 -0
  187. package/src/providers/integrations/tokenharbor.ts +2 -2
  188. package/src/providers/integrations/trae/trae-catalog.ts +5 -3
  189. package/src/providers/integrations/trae/trae-quota.ts +384 -0
  190. package/src/providers/operations/account-health-service.ts +1523 -1457
  191. package/src/providers/operations/client-versions.ts +9 -2
  192. package/src/providers/provider-contracts.ts +7 -0
  193. package/src/providers/provider-metadata.ts +13 -1
  194. package/src/providers/quota/quota-support.ts +8 -0
  195. package/src/security/api-key-auth.ts +504 -494
  196. package/src/transport/dispatch/attempt-finalize.ts +46 -5
  197. package/src/transport/dispatch/generation.ts +6 -0
  198. package/src/transport/dispatch/proxy-request.ts +33 -1
  199. package/src/transport/dispatch/ratelimit-capture.ts +37 -0
  200. package/src/transport/dispatch/streaming/stream.ts +30 -2
  201. package/src/transport/dispatch/upstream.ts +9 -0
  202. package/src/transport/request/preparer.ts +7 -6
  203. package/src/transport/request/state.ts +9 -0
  204. package/src/transport/request/tenant-preferences.ts +60 -0
  205. package/src/transport/routing/route-catalog.ts +854 -799
  206. package/dist/dashboard/assets/BannedPage-MsQaeQTs.js.gz +0 -0
  207. package/dist/dashboard/assets/CliToolDetailPage-BO9nKg9w.js +0 -1
  208. package/dist/dashboard/assets/CliToolDetailPage-BO9nKg9w.js.gz +0 -0
  209. package/dist/dashboard/assets/CliToolsPage-DfWHTJ7v.js.gz +0 -0
  210. package/dist/dashboard/assets/CombosPage-BrCBQDAc.js.gz +0 -0
  211. package/dist/dashboard/assets/ConsoleLogPage-C6Uh33er.js.gz +0 -0
  212. package/dist/dashboard/assets/CustomizationPage-8aS9hyJu.js.gz +0 -0
  213. package/dist/dashboard/assets/HealthEventsModal-qomXThxX.js +0 -1
  214. package/dist/dashboard/assets/HealthEventsModal-qomXThxX.js.gz +0 -0
  215. package/dist/dashboard/assets/LoginPage-C8hKyXBe.js.gz +0 -0
  216. package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js.gz +0 -0
  217. package/dist/dashboard/assets/OverviewPage-C-ExCy1V.js +0 -1
  218. package/dist/dashboard/assets/OverviewPage-C-ExCy1V.js.gz +0 -0
  219. package/dist/dashboard/assets/ProviderDetailPage-tr811uaF.js +0 -3
  220. package/dist/dashboard/assets/ProviderDetailPage-tr811uaF.js.gz +0 -0
  221. package/dist/dashboard/assets/ProvidersPage-BS9IEpvv.js +0 -1
  222. package/dist/dashboard/assets/ProvidersPage-BS9IEpvv.js.gz +0 -0
  223. package/dist/dashboard/assets/ProxyPage-Rn_qXScb.js +0 -7
  224. package/dist/dashboard/assets/ProxyPage-Rn_qXScb.js.gz +0 -0
  225. package/dist/dashboard/assets/QuotaPage-BihkgQSq.js +0 -1
  226. package/dist/dashboard/assets/QuotaPage-BihkgQSq.js.gz +0 -0
  227. package/dist/dashboard/assets/SettingsPage-DhiEjm6T.js +0 -1
  228. package/dist/dashboard/assets/SettingsPage-DhiEjm6T.js.gz +0 -0
  229. package/dist/dashboard/assets/SetupPage-CXjUlfqr.js.gz +0 -0
  230. package/dist/dashboard/assets/SortableList-C-_oD3F2.js.gz +0 -0
  231. package/dist/dashboard/assets/StudioPage-u9IxmYOI.js.gz +0 -0
  232. package/dist/dashboard/assets/UsagePage-BEVLVrwP.js.gz +0 -0
  233. package/dist/dashboard/assets/api-keys-uNywYlfv.js.gz +0 -0
  234. package/dist/dashboard/assets/app-DxivpfYG.css +0 -2
  235. package/dist/dashboard/assets/app-DxivpfYG.css.gz +0 -0
  236. package/dist/dashboard/assets/assert-NHwYq95t.js.gz +0 -0
  237. package/dist/dashboard/assets/badge-6uzDDvqs.js.gz +0 -0
  238. package/dist/dashboard/assets/banner-4_WNg2WZ.js.gz +0 -0
  239. package/dist/dashboard/assets/card--AtFedAq.js.gz +0 -0
  240. package/dist/dashboard/assets/cli-tools-B3e7dx3N.js.gz +0 -0
  241. package/dist/dashboard/assets/clipboard-button-NreUx6c8.js.gz +0 -0
  242. package/dist/dashboard/assets/dialog-BtpsJU98.js.gz +0 -0
  243. package/dist/dashboard/assets/download-A0SW9OCW.js +0 -1
  244. package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
  245. package/dist/dashboard/assets/drawer-WwwtzihF.js.gz +0 -0
  246. package/dist/dashboard/assets/entry-Dj9AR0gY.js +0 -4
  247. package/dist/dashboard/assets/entry-Dj9AR0gY.js.gz +0 -0
  248. package/dist/dashboard/assets/entry-TJIye_jT.js +0 -1
  249. package/dist/dashboard/assets/entry-TJIye_jT.js.gz +0 -0
  250. package/dist/dashboard/assets/entry-jy9_4-Ke.js +0 -1
  251. package/dist/dashboard/assets/entry-jy9_4-Ke.js.gz +0 -0
  252. package/dist/dashboard/assets/github-badge-D_eM3IgG.js.gz +0 -0
  253. package/dist/dashboard/assets/index-DCBtROoK.js.gz +0 -0
  254. package/dist/dashboard/assets/inline-BcNGF8Gr.js.gz +0 -0
  255. package/dist/dashboard/assets/input-BG41et9G.js.gz +0 -0
  256. package/dist/dashboard/assets/layout-C8mer_Ak.js.gz +0 -0
  257. package/dist/dashboard/assets/list-row-Gh_W4OR_.js.gz +0 -0
  258. package/dist/dashboard/assets/live-BJGYRBnR.js.gz +0 -0
  259. package/dist/dashboard/assets/model-bans-DeVLDA7I.js +0 -1
  260. package/dist/dashboard/assets/model-bans-DeVLDA7I.js.gz +0 -0
  261. package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
  262. package/dist/dashboard/assets/numbers-Bs4LOOuf.js +0 -2
  263. package/dist/dashboard/assets/numbers-Bs4LOOuf.js.gz +0 -0
  264. package/dist/dashboard/assets/routing-CcusqXA7.js.gz +0 -0
  265. package/dist/dashboard/assets/select-DsX5QT8K.js.gz +0 -0
  266. package/dist/dashboard/assets/settings-mpmXTgUa.js.gz +0 -0
  267. package/dist/dashboard/assets/sparkles-BRaRUxAU.js +0 -1
  268. package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
  269. package/dist/dashboard/assets/stack-DwLmjUFK.js.gz +0 -0
  270. package/dist/dashboard/assets/state-DYdQmdAl.js.gz +0 -0
  271. package/dist/dashboard/assets/switch-CidSfbG7.js +0 -1
  272. package/dist/dashboard/assets/switch-CidSfbG7.js.gz +0 -0
  273. package/dist/dashboard/assets/terminal-Bd4ee8TA.js +0 -1
  274. package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
  275. package/dist/dashboard/assets/useMutation-w4u4CVue.js.gz +0 -0
  276. /package/dist/dashboard/assets/{app-CafWtVXA.js → app-DCQZRmHY.js} +0 -0
  277. /package/dist/dashboard/assets/{app-CafWtVXA.js.gz → app-DCQZRmHY.js.gz} +0 -0
@@ -1503,6 +1503,27 @@ export function createMessageTypes(): protobuf.Root {
1503
1503
  { id: 1, name: "request_context", type: "RequestContextPayload" },
1504
1504
  ])
1505
1505
 
1506
+ // Allowlist prechecks (exec #41/#42/#43): Cursor asks whether a shell
1507
+ // command / MCP tool / web fetch would be permitted before it emits the
1508
+ // real call. Without an args frame this is decodable, but its exec id is
1509
+ // then stranded — the server waits forever for the matching result.
1510
+ addType(root, "ShellAllowlistPrecheckArgs", [
1511
+ { id: 1, name: "command", type: "string" },
1512
+ { id: 2, name: "working_directory", type: "string" },
1513
+ { id: 3, name: "parsing_result", type: "bytes" },
1514
+ { id: 4, name: "classifier_result", type: "bytes" },
1515
+ { id: 5, name: "tool_call_id", type: "string" },
1516
+ ])
1517
+ addType(root, "McpAllowlistPrecheckArgs", [
1518
+ { id: 1, name: "provider_identifier", type: "string" },
1519
+ { id: 2, name: "tool_name", type: "string" },
1520
+ { id: 3, name: "tool_call_id", type: "string" },
1521
+ ])
1522
+ addType(root, "WebFetchAllowlistPrecheckArgs", [
1523
+ { id: 1, name: "url", type: "string" },
1524
+ { id: 2, name: "tool_call_id", type: "string" },
1525
+ ])
1526
+
1506
1527
  addType(root, "RequestContextResult", [
1507
1528
  { id: 1, name: "success", type: "RequestContextSuccess" },
1508
1529
  ])
@@ -1568,6 +1589,9 @@ export function createMessageTypes(): protobuf.Root {
1568
1589
  { id: 18, name: "read_mcp_resource_exec_args", type: "ReadMcpResourceExecArgs" },
1569
1590
  { id: 28, name: "subagent_args", type: "SubagentArgs" },
1570
1591
  { id: 36, name: "mcp_state_exec_args", type: "McpStateExecArgs" },
1592
+ { id: 41, name: "shell_allowlist_precheck_args", type: "ShellAllowlistPrecheckArgs" },
1593
+ { id: 42, name: "mcp_allowlist_precheck_args", type: "McpAllowlistPrecheckArgs" },
1594
+ { id: 43, name: "web_fetch_allowlist_precheck_args", type: "WebFetchAllowlistPrecheckArgs" },
1571
1595
  { id: 44, name: "git_diff_request", type: "GetDiffRequest" },
1572
1596
  { id: 45, name: "pi_read_args", type: "PiReadToolArgs" },
1573
1597
  { id: 46, name: "pi_bash_args", type: "PiBashToolArgs" },
@@ -1582,6 +1606,7 @@ export function createMessageTypes(): protobuf.Root {
1582
1606
  "shell_args", "write_args", "delete_args", "grep_args", "read_args", "ls_args",
1583
1607
  "request_context_args", "mcp_args", "shell_stream_args", "background_shell_spawn_args",
1584
1608
  "list_mcp_resources_exec_args", "read_mcp_resource_exec_args", "mcp_state_exec_args",
1609
+ "shell_allowlist_precheck_args", "mcp_allowlist_precheck_args", "web_fetch_allowlist_precheck_args",
1585
1610
  "subagent_args", "git_diff_request",
1586
1611
  "pi_read_args", "pi_bash_args", "pi_edit_args", "pi_write_args",
1587
1612
  "pi_grep_args", "pi_find_args", "pi_ls_args",
@@ -496,7 +496,14 @@ class FactoryDroidAdapter implements ProviderAdapter {
496
496
  response = await outboundFetch(url, {
497
497
  method: "POST",
498
498
  headers,
499
- body: JSON.stringify(buildGeminiPayload(request)),
499
+ body: JSON.stringify(
500
+ buildGeminiPayload(
501
+ request,
502
+ ...(context.image_clamp_max_bytes === undefined
503
+ ? []
504
+ : [{ maxBytesOverride: context.image_clamp_max_bytes }]),
505
+ ),
506
+ ),
500
507
  signal: lifecycle.signal,
501
508
  });
502
509
  } catch (error: unknown) {
@@ -189,7 +189,12 @@ class GeminiAdapter implements ProviderAdapter {
189
189
  const action = request.stream ? "streamGenerateContent?alt=sse" : "generateContent";
190
190
  const url = geminiModelUrl(this.baseUrl, model, action);
191
191
 
192
- const payload = buildGeminiPayload(request);
192
+ const payload = buildGeminiPayload(
193
+ request,
194
+ ...(context.image_clamp_max_bytes === undefined
195
+ ? []
196
+ : [{ maxBytesOverride: context.image_clamp_max_bytes }]),
197
+ );
193
198
  if (
194
199
  request.tools?.some(
195
200
  (tool) =>
@@ -1,177 +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
-
6
- const BILLING_URL = "https://cli-chat-proxy.grok.com/v1/billing?format=credits";
7
- const BILLING_SOURCE = "cli-chat-proxy.grok.com/v1/billing";
8
- const USER_URL = "https://cli-chat-proxy.grok.com/v1/user?include=subscription";
9
- const GRPC_CREDITS_URL = "https://grok.com/grok_api_v2.GrokBuildBilling/GetGrokCreditsConfig";
10
- const GRPC_EMPTY_FRAME = new Uint8Array([0, 0, 0, 0, 0]);
11
-
12
- /**
13
- * Auth headers shared by every xAI billing/userinfo call: bearer token plus
14
- * the CLI client identity the gateway gates on.
15
- */
16
- function xaiHeaders(accessToken: string): Record<string, string> {
17
- const version = getGrokVersion();
18
- return {
19
- authorization: `Bearer ${accessToken}`,
20
- "x-xai-token-auth": "xai-grok-cli",
21
- "x-grok-client-version": version,
22
- "user-agent": buildGrokAuthUserAgent(version),
23
- };
24
- }
25
- interface XaiBillingPayload {
26
- currentPeriod?: unknown;
27
- creditUsagePercent?: unknown;
28
- productUsage?: unknown;
29
- onDemandCap?: unknown;
30
- onDemandUsed?: unknown;
31
- billingPeriodStart?: unknown;
32
- billingPeriodEnd?: unknown;
33
- monthlyLimit?: unknown;
34
- used?: unknown;
35
- subscriptionTier?: unknown;
36
- user?: unknown;
37
- }
38
-
39
- function parsePayload(value: string): XaiBillingPayload {
40
- const payload: unknown = JSON.parse(value);
41
- // Live billing wraps usage in a `config` envelope (free-tier shape);
42
- // paid shapes may carry the fields top-level — read the envelope first.
43
- const parsed = record(payload);
44
- return ((parsed && record(parsed.config)) ?? parsed ?? {}) as XaiBillingPayload;
45
- }
46
-
47
- function billingNumber(value: unknown): number | null {
48
- const nested = record(value);
49
- return number(nested?.val ?? value);
50
- }
51
-
52
- function period(raw: unknown): { start: string; end: string } | undefined {
53
- const value = record(raw);
54
- const type = text(value?.type);
55
- if (type !== null && !type.toUpperCase().includes("WEEK")) return undefined;
56
- const start = isoDate(value?.start);
57
- const end = isoDate(value?.end);
58
- return start && end && Date.parse(end) > Date.parse(start) ? { start, end } : undefined;
59
- }
60
-
61
- function buildWeeklyWindows(payload: XaiBillingPayload): ProviderQuotaWindow[] {
62
- const current = period(payload.currentPeriod);
63
- const creditPercent = billingNumber(payload.creditUsagePercent);
64
- if (!current) return [];
65
- if (creditPercent === null || creditPercent < 0 || creditPercent > 100) {
66
- // Free-tier billing states the period but no usage percent: emit the
67
- // window with the reset date and null percents (the dashboard renders
68
- // the countdown without inventing a number) instead of empty windows.
69
- return [percentWindow("1w", "SuperGrok Weekly Credits", null, current.end, null, null, "independent")];
70
- }
71
- const windows: ProviderQuotaWindow[] = [
72
- percentWindow("1w", "SuperGrok Weekly Credits", creditPercent, current.end, creditPercent, 100, "independent"),
73
- ];
74
- if (Array.isArray(payload.productUsage)) {
75
- for (const entry of payload.productUsage) {
76
- const value = record(entry);
77
- const product = text(value?.product);
78
- const percent = billingNumber(value?.usagePercent);
79
- windows.push(percentWindow(`product:${product}:1w`, `${product} (Weekly)`, percent, current.end, percent, 100, "independent"));
80
- }
81
- }
82
- return windows;
83
- }
84
-
85
- function buildMonthlyWindows(payload: XaiBillingPayload): ProviderQuotaWindow[] {
86
- const start = isoDate(payload.billingPeriodStart);
87
- const end = isoDate(payload.billingPeriodEnd);
88
- const limit = billingNumber(payload.monthlyLimit);
89
- const used = billingNumber(payload.used);
90
- if (!start || !end || limit === null || limit <= 0 || used === null || used < 0) return [];
91
- const usedPercent = Math.min(100, (used / limit) * 100);
92
- return [percentWindow("1mo", "SuperGrok Monthly Included", usedPercent, end, used, limit, "independent")];
93
- }
94
-
95
- function addOnDemandWindow(windows: ProviderQuotaWindow[], payload: XaiBillingPayload): void {
96
- const cap = billingNumber(payload.onDemandCap);
97
- const used = billingNumber(payload.onDemandUsed);
98
- if (cap === null || cap <= 0 || used === null || used < 0) return;
99
- windows.push(percentWindow("on-demand", "On-demand", Math.min(100, (used / cap) * 100), null, used, cap, "independent"));
100
- }
101
-
102
- async function fetchGrpcCredits(
103
- accessToken: string,
104
- fetcher: FetchLike,
105
- ): Promise<{ percentUsed: number; resetAt: string | null } | null> {
106
- try {
107
- const response = await fetcher(GRPC_CREDITS_URL, {
108
- method: "POST",
109
- headers: {
110
- ...xaiHeaders(accessToken),
111
- "content-type": "application/grpc-web+proto",
112
- "x-grpc-web": "1",
113
- },
114
- body: GRPC_EMPTY_FRAME,
115
- signal: AbortSignal.timeout(8_000),
116
- });
117
- if (!response.ok) return null;
118
- const arrayBuffer = await response.arrayBuffer();
119
- return decodeGrokCreditsFrame(new Uint8Array(arrayBuffer));
120
- } catch {
121
- return null;
122
- }
123
- }
124
-
125
- async function fetchUser(
126
- accessToken: string,
127
- fetcher: FetchLike,
128
- ): Promise<Record<string, unknown> | null> {
129
- try {
130
- const response = await fetcher(USER_URL, {
131
- headers: { ...xaiHeaders(accessToken), accept: "application/json" },
132
- signal: AbortSignal.timeout(10_000),
133
- });
134
- if (!response.ok) return null;
135
- return record((await response.json()) as unknown);
136
- } catch {
137
- return null;
138
- }
139
- }
140
-
141
- /** Fetches Grok Build subscription billing, including weekly and unified-monthly fallbacks. */
142
- export async function fetchGrokQuota(
143
- credential: string,
144
- fetcher: FetchLike,
145
- ): Promise<ProviderQuotaResult> {
146
- const accessToken = credential.trim();
147
- if (!accessToken) throw new Error("grok quota requires an OAuth access token");
148
- const response = await fetcher(BILLING_URL, {
149
- headers: { ...xaiHeaders(accessToken), accept: "application/json" },
150
- signal: AbortSignal.timeout(15_000),
151
- });
152
- const raw = await response.text();
153
- if (!response.ok) throw new Error(`Grok Build billing request failed (${response.status}): ${raw.slice(0, 500)}`);
154
- const payload = parsePayload(raw);
155
- const windows = buildWeeklyWindows(payload);
156
- if (windows.length === 0 || windows.every((w) => w.usedPercent === null)) {
157
- const grpc = await fetchGrpcCredits(accessToken, fetcher);
158
- if (grpc) {
159
- const idx = windows.findIndex((w) => w.label.includes("Weekly"));
160
- const grpcWindow = percentWindow("1w", "SuperGrok Weekly Credits", grpc.percentUsed, grpc.resetAt, grpc.percentUsed, 100, "independent");
161
- if (idx >= 0) windows[idx] = grpcWindow;
162
- else windows.unshift(grpcWindow);
163
- }
164
- }
165
- if (windows.length === 0) windows.push(...buildMonthlyWindows(payload));
166
- addOnDemandWindow(windows, payload);
167
- const user = await fetchUser(accessToken, fetcher);
168
- const userRecord = record(user?.user);
169
- const plan =
170
- text(user?.subscriptionTier) ??
171
- text(payload.subscriptionTier) ??
172
- text(userRecord?.subscriptionTier) ??
173
- "SuperGrok";
174
- return { source: "grok", plan, windows, error: null };
175
- }
176
-
177
- 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 };
@@ -0,0 +1,73 @@
1
+ // Where a Grok rate-limit reading lives between dispatch and display.
2
+ //
3
+ // The bucket headers come back on **every** upstream response, so the cheap
4
+ // place to learn them is the traffic the gateway is already sending — not a
5
+ // probe of its own. A dispatch that reads and stores them costs nothing extra;
6
+ // a probe costs a request every time quota is shown.
7
+ //
8
+ // So this store is fed by dispatch (manual test or automatic routing alike) and
9
+ // read by the quota collector. The collector only probes when there is nothing
10
+ // stored, which is the first read after a start and never after that.
11
+ //
12
+ // Keyed by account id: two Grok accounts carry different buckets, and a reading
13
+ // filed against the wrong one is worse than no reading at all.
14
+
15
+ import type { ProviderQuotaWindow } from "../../quota/quota-contracts";
16
+ import { grokHeaderRequestsWindow, grokHeaderWindow, type GrokRateLimitReading } from "./grok-ratelimit";
17
+
18
+ /**
19
+ * How long a stored reading stays useful.
20
+ *
21
+ * The header is eventually consistent, so a fresh value is not meaningfully
22
+ * better than one from a few minutes ago — but a value from last week says
23
+ * nothing about now. Five minutes is short enough to follow real traffic and
24
+ * long enough that a burst of console refreshes costs no requests.
25
+ */
26
+ const SNAPSHOT_TTL_MS = 5 * 60 * 1000;
27
+
28
+ interface Snapshot {
29
+ readonly windows: readonly ProviderQuotaWindow[];
30
+ readonly capturedAt: number;
31
+ }
32
+
33
+ const snapshots = new Map<string, Snapshot>();
34
+
35
+ /** Files a reading captured from a real dispatch against one account. */
36
+ export function rememberGrokRateLimit(
37
+ accountId: string,
38
+ reading: GrokRateLimitReading,
39
+ now = Date.now(),
40
+ ): void {
41
+ const windows = [grokHeaderWindow(reading), grokHeaderRequestsWindow(reading)].filter(
42
+ (value): value is ProviderQuotaWindow => value !== null,
43
+ );
44
+ if (windows.length === 0) return;
45
+ snapshots.set(accountId, { windows, capturedAt: now });
46
+ if (snapshots.size > 4096) {
47
+ // Bounded: an unbounded map keyed by account is a leak dressed as a cache.
48
+ for (const [key, value] of snapshots) {
49
+ if (now - value.capturedAt > SNAPSHOT_TTL_MS) snapshots.delete(key);
50
+ }
51
+ }
52
+ }
53
+
54
+ /** The stored window for one account, or `null` when none is fresh enough. */
55
+ export function storedGrokRateLimit(accountId: string, now = Date.now()): readonly ProviderQuotaWindow[] | null {
56
+ const snapshot = snapshots.get(accountId);
57
+ if (!snapshot) return null;
58
+ if (now - snapshot.capturedAt > SNAPSHOT_TTL_MS) {
59
+ snapshots.delete(accountId);
60
+ return null;
61
+ }
62
+ return snapshot.windows;
63
+ }
64
+
65
+ /** Whether a fresh reading exists, so the collector can skip its probe. */
66
+ export function hasFreshGrokRateLimit(accountId: string, now = Date.now()): boolean {
67
+ return storedGrokRateLimit(accountId, now) !== null;
68
+ }
69
+
70
+ /** Test seam. */
71
+ export function clearGrokRateLimitSnapshots(): void {
72
+ snapshots.clear();
73
+ }