cartethyia 2.5.3 → 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 (262) hide show
  1. package/dist/dashboard/assets/{BannedPage-hfCzi-Ux.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-DEDyKBlC.js → CliToolsPage-BV6pxVQo.js} +1 -1
  6. package/dist/dashboard/assets/CliToolsPage-BV6pxVQo.js.gz +0 -0
  7. package/dist/dashboard/assets/{CombosPage-CfuYiQ8Z.js → CombosPage-Bsan4679.js} +1 -1
  8. package/dist/dashboard/assets/CombosPage-Bsan4679.js.gz +0 -0
  9. package/dist/dashboard/assets/{ConsoleLogPage-bsLuZmYH.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-_UWhwziJ.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-DDVIfZGW.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-D6pmQ86L.js → SetupPage-DDbMQsoo.js} +1 -1
  32. package/dist/dashboard/assets/SetupPage-DDbMQsoo.js.gz +0 -0
  33. package/dist/dashboard/assets/{SortableList-DWvKiuSl.js → SortableList-CB3Z7VTf.js} +1 -1
  34. package/dist/dashboard/assets/SortableList-CB3Z7VTf.js.gz +0 -0
  35. package/dist/dashboard/assets/{StudioPage-CmkUrgdj.js → StudioPage-D95UCPHI.js} +3 -3
  36. package/dist/dashboard/assets/StudioPage-D95UCPHI.js.gz +0 -0
  37. package/dist/dashboard/assets/{UsagePage-B47uOydr.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-BydyKaDz.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-BO6h8yNv.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 +4 -8
  114. package/scripts/cli/command/update.ts +27 -10
  115. package/scripts/cli/main.ts +28 -1
  116. package/src/boot/app.ts +693 -693
  117. package/src/boot/dependencies.ts +646 -616
  118. package/src/config/spec.ts +55 -0
  119. package/src/config/telemetry.ts +46 -0
  120. package/src/console/api-keys/bucket-spend.ts +15 -4
  121. package/src/console/backup/contracts.ts +15 -2
  122. package/src/console/console-router.ts +8 -0
  123. package/src/console/data-management/contracts.ts +93 -0
  124. package/src/console/data-management/operations.ts +229 -0
  125. package/src/console/data-management/routes.ts +104 -0
  126. package/src/console/domain-registration.ts +39 -0
  127. package/src/console/observability/contracts.ts +524 -485
  128. package/src/console/observability/store.ts +1115 -1018
  129. package/src/console/providers/catalog/contracts.ts +6 -0
  130. package/src/console/providers/catalog/provider-operations.ts +20 -0
  131. package/src/console/providers/catalog/routes.ts +7 -0
  132. package/src/console/providers/catalog/store.ts +22 -1
  133. package/src/console/quota/account-quota-shared.ts +52 -39
  134. package/src/console/quota/refresh.ts +441 -429
  135. package/src/console/routing/pools/import-parse.ts +206 -0
  136. package/src/console/routing/pools/operations.ts +543 -501
  137. package/src/console/routing/pools/routes.ts +181 -164
  138. package/src/console/settings/contracts.ts +37 -0
  139. package/src/console/settings/store.ts +51 -0
  140. package/src/console/updates/routes.ts +16 -0
  141. package/src/main.ts +10 -0
  142. package/src/observability/event-compaction.ts +315 -0
  143. package/src/observability/event-day-index.ts +442 -0
  144. package/src/observability/event-log.ts +152 -3
  145. package/src/observability/payload-capture.ts +8 -1
  146. package/src/observability/payload-store.ts +440 -376
  147. package/src/persistence/health-event-flush.ts +36 -0
  148. package/src/persistence/schema.ts +1402 -1360
  149. package/src/persistence/stores/telemetry-store.ts +237 -214
  150. package/src/protocol/codecs.ts +1 -2
  151. package/src/protocol/image-clamp.ts +19 -1
  152. package/src/protocol/registry.ts +8 -0
  153. package/src/protocol/request/gemini.ts +9 -2
  154. package/src/protocol/request/messages.ts +14 -2
  155. package/src/providers/adapters/compatible-adapter.ts +3 -0
  156. package/src/providers/bundled-discovery-providers.ts +122 -119
  157. package/src/providers/configured-openai-providers.ts +136 -134
  158. package/src/providers/default-registry.ts +5 -4
  159. package/src/providers/discovery/openai-model-discovery.ts +5 -0
  160. package/src/providers/discovery/openrouter-free-roster.ts +15 -17
  161. package/src/providers/integrations/cline/cline-quota.ts +132 -125
  162. package/src/providers/integrations/codex/codex-headers.ts +25 -39
  163. package/src/providers/integrations/codex/codex.ts +2 -29
  164. package/src/providers/integrations/cursor/cursor-quota.ts +310 -0
  165. package/src/providers/integrations/cursor/cursor-request.ts +616 -471
  166. package/src/providers/integrations/cursor/cursor-session-manager.ts +319 -281
  167. package/src/providers/integrations/cursor/cursor.ts +1485 -960
  168. package/src/providers/integrations/cursor/protocol/messages.ts +25 -0
  169. package/src/providers/integrations/factory/factory.ts +8 -1
  170. package/src/providers/integrations/gemini.ts +6 -1
  171. package/src/providers/integrations/grok/grok-quota.ts +279 -177
  172. package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -0
  173. package/src/providers/integrations/grok/grok-ratelimit.ts +218 -0
  174. package/src/providers/integrations/opencode/provider.ts +12 -13
  175. package/src/providers/integrations/qoder/qoder-quota.ts +192 -0
  176. package/src/providers/integrations/trae/trae-quota.ts +384 -0
  177. package/src/providers/operations/account-health-service.ts +1523 -1480
  178. package/src/providers/provider-contracts.ts +7 -0
  179. package/src/providers/provider-metadata.ts +6 -0
  180. package/src/providers/quota/quota-support.ts +8 -0
  181. package/src/security/api-key-auth.ts +504 -494
  182. package/src/transport/dispatch/generation.ts +6 -0
  183. package/src/transport/dispatch/proxy-request.ts +24 -1
  184. package/src/transport/dispatch/ratelimit-capture.ts +37 -0
  185. package/src/transport/dispatch/streaming/stream.ts +14 -0
  186. package/src/transport/dispatch/upstream.ts +9 -0
  187. package/src/transport/request/preparer.ts +7 -6
  188. package/src/transport/request/state.ts +9 -0
  189. package/src/transport/request/tenant-preferences.ts +60 -0
  190. package/src/transport/routing/route-catalog.ts +58 -10
  191. package/dist/dashboard/assets/BannedPage-hfCzi-Ux.js.gz +0 -0
  192. package/dist/dashboard/assets/CliToolDetailPage-DLGqYEwI.js +0 -1
  193. package/dist/dashboard/assets/CliToolDetailPage-DLGqYEwI.js.gz +0 -0
  194. package/dist/dashboard/assets/CliToolsPage-DEDyKBlC.js.gz +0 -0
  195. package/dist/dashboard/assets/CombosPage-CfuYiQ8Z.js.gz +0 -0
  196. package/dist/dashboard/assets/ConsoleLogPage-bsLuZmYH.js.gz +0 -0
  197. package/dist/dashboard/assets/CustomizationPage-_UWhwziJ.js.gz +0 -0
  198. package/dist/dashboard/assets/HealthEventsModal-Cjfc5WFS.js +0 -1
  199. package/dist/dashboard/assets/HealthEventsModal-Cjfc5WFS.js.gz +0 -0
  200. package/dist/dashboard/assets/LoginPage-DDVIfZGW.js.gz +0 -0
  201. package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js.gz +0 -0
  202. package/dist/dashboard/assets/OverviewPage-B53YYMng.js +0 -1
  203. package/dist/dashboard/assets/OverviewPage-B53YYMng.js.gz +0 -0
  204. package/dist/dashboard/assets/ProviderDetailPage-B0KaAQf0.js +0 -3
  205. package/dist/dashboard/assets/ProviderDetailPage-B0KaAQf0.js.gz +0 -0
  206. package/dist/dashboard/assets/ProvidersPage-Dx31DgIO.js +0 -1
  207. package/dist/dashboard/assets/ProvidersPage-Dx31DgIO.js.gz +0 -0
  208. package/dist/dashboard/assets/ProxyPage-BpnR4kXT.js +0 -7
  209. package/dist/dashboard/assets/ProxyPage-BpnR4kXT.js.gz +0 -0
  210. package/dist/dashboard/assets/QuotaPage-BpOQCnun.js +0 -1
  211. package/dist/dashboard/assets/QuotaPage-BpOQCnun.js.gz +0 -0
  212. package/dist/dashboard/assets/SettingsPage-DV1eo2J_.js +0 -1
  213. package/dist/dashboard/assets/SettingsPage-DV1eo2J_.js.gz +0 -0
  214. package/dist/dashboard/assets/SetupPage-D6pmQ86L.js.gz +0 -0
  215. package/dist/dashboard/assets/SortableList-DWvKiuSl.js.gz +0 -0
  216. package/dist/dashboard/assets/StudioPage-CmkUrgdj.js.gz +0 -0
  217. package/dist/dashboard/assets/UsagePage-B47uOydr.js.gz +0 -0
  218. package/dist/dashboard/assets/api-keys-uNywYlfv.js.gz +0 -0
  219. package/dist/dashboard/assets/app-DxivpfYG.css +0 -2
  220. package/dist/dashboard/assets/app-DxivpfYG.css.gz +0 -0
  221. package/dist/dashboard/assets/assert-NHwYq95t.js.gz +0 -0
  222. package/dist/dashboard/assets/badge-6uzDDvqs.js.gz +0 -0
  223. package/dist/dashboard/assets/banner-4_WNg2WZ.js.gz +0 -0
  224. package/dist/dashboard/assets/card--AtFedAq.js.gz +0 -0
  225. package/dist/dashboard/assets/cli-tools-B3e7dx3N.js.gz +0 -0
  226. package/dist/dashboard/assets/clipboard-button-NreUx6c8.js.gz +0 -0
  227. package/dist/dashboard/assets/dialog-BtpsJU98.js.gz +0 -0
  228. package/dist/dashboard/assets/download-A0SW9OCW.js +0 -1
  229. package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
  230. package/dist/dashboard/assets/drawer-WwwtzihF.js.gz +0 -0
  231. package/dist/dashboard/assets/entry-CkryrIea.js +0 -4
  232. package/dist/dashboard/assets/entry-CkryrIea.js.gz +0 -0
  233. package/dist/dashboard/assets/entry-TJIye_jT.js +0 -1
  234. package/dist/dashboard/assets/entry-TJIye_jT.js.gz +0 -0
  235. package/dist/dashboard/assets/entry-jy9_4-Ke.js +0 -1
  236. package/dist/dashboard/assets/entry-jy9_4-Ke.js.gz +0 -0
  237. package/dist/dashboard/assets/github-badge-D_eM3IgG.js.gz +0 -0
  238. package/dist/dashboard/assets/index-BydyKaDz.js.gz +0 -0
  239. package/dist/dashboard/assets/inline-BcNGF8Gr.js.gz +0 -0
  240. package/dist/dashboard/assets/input-BG41et9G.js.gz +0 -0
  241. package/dist/dashboard/assets/layout-C8mer_Ak.js.gz +0 -0
  242. package/dist/dashboard/assets/list-row-Gh_W4OR_.js.gz +0 -0
  243. package/dist/dashboard/assets/live-BJGYRBnR.js.gz +0 -0
  244. package/dist/dashboard/assets/model-bans-DeVLDA7I.js +0 -1
  245. package/dist/dashboard/assets/model-bans-DeVLDA7I.js.gz +0 -0
  246. package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
  247. package/dist/dashboard/assets/numbers-Bs4LOOuf.js +0 -2
  248. package/dist/dashboard/assets/numbers-Bs4LOOuf.js.gz +0 -0
  249. package/dist/dashboard/assets/routing-CcusqXA7.js.gz +0 -0
  250. package/dist/dashboard/assets/select-BO6h8yNv.js.gz +0 -0
  251. package/dist/dashboard/assets/settings-mpmXTgUa.js.gz +0 -0
  252. package/dist/dashboard/assets/sparkles-BRaRUxAU.js +0 -1
  253. package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
  254. package/dist/dashboard/assets/stack-DwLmjUFK.js.gz +0 -0
  255. package/dist/dashboard/assets/state-DYdQmdAl.js.gz +0 -0
  256. package/dist/dashboard/assets/switch-CidSfbG7.js +0 -1
  257. package/dist/dashboard/assets/switch-CidSfbG7.js.gz +0 -0
  258. package/dist/dashboard/assets/terminal-Bd4ee8TA.js +0 -1
  259. package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
  260. package/dist/dashboard/assets/useMutation-w4u4CVue.js.gz +0 -0
  261. /package/dist/dashboard/assets/{app-CafWtVXA.js → app-DCQZRmHY.js} +0 -0
  262. /package/dist/dashboard/assets/{app-CafWtVXA.js.gz → app-DCQZRmHY.js.gz} +0 -0
@@ -0,0 +1,218 @@
1
+ // Grok's two quota numbers, and why they disagree.
2
+ //
3
+ // The CLI plane answers every response with a token bucket:
4
+ //
5
+ // x-ratelimit-limit-tokens: 1000000
6
+ // x-ratelimit-remaining-tokens: 996169
7
+ // x-ratelimit-remaining-requests: 20
8
+ //
9
+ // and that bucket is *eventually consistent*. Right after a request it can
10
+ // still read a full `1000000` and only drop later. Worse, the figure it
11
+ // advertises is not the figure that binds: the real free allowance for a model
12
+ // like `grok-4.6` is 600k tokens per rolling 24 hours, and the only place
13
+ // upstream states it is the body of a 429:
14
+ //
15
+ // {"code":"subscription:free-usage-exhausted",
16
+ // "error":"... tokens (actual/limit): 603024/600000 ... Usage resets over a
17
+ // rolling 24-hour window"}
18
+ //
19
+ // So there are two readings with different authority. The header is available
20
+ // on every call and is the coarse indicator; the 429 body is only available
21
+ // once the account is actually being refused, and is the authoritative one.
22
+ //
23
+ // They are therefore kept as separate windows rather than merged: a merged
24
+ // number would look precise and be neither. The header window is listed first
25
+ // because it is what a caller sees day to day, and the authoritative one is
26
+ // appended beneath it.
27
+
28
+ import type { ProviderQuotaWindow } from "../../quota/quota-contracts";
29
+ import { isoDate, number, percentWindow, record, text } from "../../quota/quota-contracts";
30
+
31
+ /** Header names the CLI plane uses for its token bucket. */
32
+ const HEADER_LIMIT_TOKENS = "x-ratelimit-limit-tokens";
33
+ const HEADER_REMAINING_TOKENS = "x-ratelimit-remaining-tokens";
34
+ const HEADER_REMAINING_REQUESTS = "x-ratelimit-remaining-requests";
35
+ const HEADER_RESET_TOKENS = "x-ratelimit-reset-tokens";
36
+
37
+ /** The upstream code that carries the authoritative free-usage allowance. */
38
+ export const GROK_FREE_USAGE_EXHAUSTED = "subscription:free-usage-exhausted";
39
+
40
+ /** Kinds, so the view can tell an indicator from a verdict. */
41
+ export const GROK_HEADER_WINDOW_KIND = "grok:cli-header";
42
+ export const GROK_HEADER_REQUESTS_WINDOW_KIND = "grok:cli-header-requests";
43
+ export const GROK_AUTHORITATIVE_WINDOW_KIND = "grok:free-usage-authoritative";
44
+
45
+ /** The per-minute request cap the CLI plane applies. */
46
+ const REQUEST_WINDOW_LIMIT = 21;
47
+
48
+ /** One reading of the CLI token bucket. */
49
+ export interface GrokRateLimitReading {
50
+ readonly limitTokens: number | null;
51
+ readonly remainingTokens: number | null;
52
+ readonly remainingRequests: number | null;
53
+ readonly resetsAt: string | null;
54
+ }
55
+
56
+ /** A `Headers`-like reader, narrowed at the boundary rather than asserted. */
57
+ function headerReader(
58
+ headers: { get(name: string): string | null } | Record<string, string>,
59
+ ): (name: string) => string | null {
60
+ if (typeof headers.get === "function") {
61
+ const reader = headers as { get(name: string): string | null };
62
+ return (name) => reader.get(name);
63
+ }
64
+ const literal = headers as Record<string, string>;
65
+ return (name) => literal[name] ?? null;
66
+ }
67
+
68
+ /**
69
+ * Reads the CLI token bucket off a response.
70
+ *
71
+ * `Headers` and a plain object both reach here — the quota probe gets the real
72
+ * thing, a test gets a literal — so both shapes are accepted rather than
73
+ * forcing one caller to adapt.
74
+ */
75
+ export function readGrokRateLimitHeaders(
76
+ headers: { get(name: string): string | null } | Record<string, string>,
77
+ ): GrokRateLimitReading {
78
+ const get = headerReader(headers);
79
+ const resetSeconds = number(get(HEADER_RESET_TOKENS));
80
+ return {
81
+ limitTokens: number(get(HEADER_LIMIT_TOKENS)),
82
+ remainingTokens: number(get(HEADER_REMAINING_TOKENS)),
83
+ remainingRequests: number(get(HEADER_REMAINING_REQUESTS)),
84
+ resetsAt:
85
+ isoDate(get(HEADER_RESET_TOKENS)) ??
86
+ (resetSeconds !== null && resetSeconds > 0
87
+ ? new Date(Date.now() + resetSeconds * 1000).toISOString()
88
+ : null),
89
+ };
90
+ }
91
+
92
+ /**
93
+ * The header bucket as a quota window.
94
+ *
95
+ * Marked independent: this is the CLI request bucket, a different budget from
96
+ * the free-usage allowance, so it must never be summed into a credit total.
97
+ */
98
+ export function grokHeaderWindow(reading: GrokRateLimitReading): ProviderQuotaWindow | null {
99
+ if (reading.remainingTokens === null && reading.limitTokens === null) return null;
100
+ const used =
101
+ reading.remainingTokens !== null && reading.limitTokens !== null
102
+ ? reading.limitTokens - reading.remainingTokens
103
+ : null;
104
+ // The third argument is the *percentage*, not the count. Passing null left
105
+ // `usedPercent` null, and the bar renders a null percentage as grey — so a
106
+ // window that knew its exact used and limit still drew as unknown.
107
+ const usedPercent =
108
+ reading.limitTokens !== null && reading.limitTokens > 0 && used !== null
109
+ ? (used / reading.limitTokens) * 100
110
+ : null;
111
+ return percentWindow(
112
+ GROK_HEADER_WINDOW_KIND,
113
+ "Rate limit token (header)",
114
+ usedPercent,
115
+ reading.resetsAt,
116
+ used,
117
+ reading.limitTokens,
118
+ "independent",
119
+ );
120
+ }
121
+
122
+ /**
123
+ * The request bucket as its own window.
124
+ *
125
+ * Separate from the token bucket on purpose: `x-ratelimit-remaining-requests`
126
+ * is a per-minute cap (21) with no token equivalent, and folding it into the
127
+ * token window produced one number that described neither budget.
128
+ */
129
+ export function grokHeaderRequestsWindow(
130
+ reading: GrokRateLimitReading,
131
+ ): ProviderQuotaWindow | null {
132
+ if (reading.remainingRequests === null) return null;
133
+ const used = REQUEST_WINDOW_LIMIT - reading.remainingRequests;
134
+ const usedPercent = (used / REQUEST_WINDOW_LIMIT) * 100;
135
+ return percentWindow(
136
+ GROK_HEADER_REQUESTS_WINDOW_KIND,
137
+ "Rate limit request (header)",
138
+ usedPercent,
139
+ reading.resetsAt,
140
+ used,
141
+ REQUEST_WINDOW_LIMIT,
142
+ "independent",
143
+ );
144
+ }
145
+
146
+ /** The authoritative allowance a 429 states, parsed out of its message. */
147
+ export interface GrokFreeUsageVerdict {
148
+ readonly actual: number | null;
149
+ readonly limit: number | null;
150
+ readonly resetsAt: string | null;
151
+ readonly rolling: boolean;
152
+ }
153
+
154
+ /**
155
+ * Parses `tokens (actual/limit): 603024/600000` out of a refusal.
156
+ *
157
+ * The numbers are in prose, not in a field, so this is a text match — but the
158
+ * shape is the only place upstream publishes the real allowance, and a refusal
159
+ * that does not carry it yields `null` rather than a guess.
160
+ */
161
+ export function parseGrokFreeUsageVerdict(body: unknown, message?: string): GrokFreeUsageVerdict | null {
162
+ const root = record(body) ?? {};
163
+ const error = text(root["error"] ?? root["message"]) ?? message ?? "";
164
+ const haystack = `${error} ${text(root["code"]) ?? ""}`;
165
+ const match = /tokens\s*\(actual\/limit\)\s*:?\s*([\d_,]+)\s*\/\s*([\d_,]+)/i.exec(haystack);
166
+ const actual = match?.[1] ? number(match[1].replaceAll(/[_,]/g, "")) : null;
167
+ const limit = match?.[2] ? number(match[2].replaceAll(/[_,]/g, "")) : null;
168
+ if (actual === null && limit === null) {
169
+ // A refusal without the figures still tells us the code; it just cannot
170
+ // tell us the allowance.
171
+ if (!haystack.toLowerCase().includes("free-usage") && !haystack.toLowerCase().includes("rolling 24")) return null;
172
+ }
173
+ const rolling = /rolling\s*24[\s-]*hour/i.test(haystack);
174
+ const resetMatch = /resets?\s+(?:over|in|at)\s+([^.;]{0,60})/i.exec(haystack);
175
+ return {
176
+ actual,
177
+ limit,
178
+ resetsAt: resetMatch ? isoDate(resetMatch[1]) ?? null : null,
179
+ rolling,
180
+ };
181
+ }
182
+
183
+ /**
184
+ * The authoritative allowance as a quota window.
185
+ *
186
+ * `remaining` is what decides whether the account can serve, so it is derived
187
+ * rather than left null: an account that has spent 603024 of 600000 has none
188
+ * left, and reporting "unknown" there would leave a refused account looking
189
+ * merely unmeasured.
190
+ */
191
+ export function grokAuthoritativeWindow(verdict: GrokFreeUsageVerdict): ProviderQuotaWindow | null {
192
+ if (verdict.limit === null && verdict.actual === null) return null;
193
+ return percentWindow(
194
+ GROK_AUTHORITATIVE_WINDOW_KIND,
195
+ "Free usage allowance (authoritative)",
196
+ null,
197
+ verdict.resetsAt,
198
+ verdict.actual,
199
+ verdict.limit,
200
+ // Independent: the free allowance is per model per rolling day, not part of
201
+ // a pooled credit balance.
202
+ "independent",
203
+ );
204
+ }
205
+
206
+ /** One probe's two readings: the coarse header and the authoritative refusal. */
207
+ export interface GrokRateLimitProbe {
208
+ /** The CLI token bucket from the response headers. */
209
+ readonly header: ProviderQuotaWindow | null;
210
+ /** The per-minute request bucket, which has no token equivalent. */
211
+ readonly requests: ProviderQuotaWindow | null;
212
+ /** The free-usage allowance from a 429 body, or `null` when not refused. */
213
+ readonly authoritative: ProviderQuotaWindow | null;
214
+ /** Whether upstream refused the probe at all. */
215
+ readonly refusal: boolean;
216
+ /** The refusal code, for the console note. */
217
+ readonly note: string | null;
218
+ }
@@ -116,8 +116,12 @@ export function isSystemoneZenModel(modelId: string): boolean {
116
116
  * fail a probe.
117
117
  */
118
118
  export function isFreeTierZenModel(modelId: string): boolean {
119
- if (unavailableFreeZenIds().includes(modelId)) return false;
120
- return modelId.endsWith("-free") || KNOWN_FREE_ZEN_IDS.includes(modelId);
119
+ // Dispatch ids arrive provider-prefixed (`opencode/big-pickle`) while the
120
+ // catalog and `KNOWN_FREE_ZEN_IDS` hold the bare id, so match on the bare
121
+ // id rather than the prefixed one.
122
+ const bare = modelId.slice(modelId.indexOf("/") + 1);
123
+ if (unavailableFreeZenIds().includes(bare)) return false;
124
+ return bare.endsWith("-free") || KNOWN_FREE_ZEN_IDS.includes(bare);
121
125
  }
122
126
 
123
127
  /**
@@ -493,22 +497,17 @@ export function systemoneLimits(): Pick<ModelDefinition, "contextLimit" | "outpu
493
497
 
494
498
  export const OPENCODE_FREE_MODELS: readonly ModelDefinition[] = [
495
499
  ...sharedZenChat,
496
- // Free-tier chat models the shared Zen listing advertises with the `-free`
497
- // convention. Each was dispatched through this adapter (its own fingerprint
498
- // headers, agent tools) and answered, so the row is routable, not merely
499
- // listed. Limits come from the models.dev snapshot under `opencode`; a row
500
- // the snapshot does not file keeps an explicit limit rather than the generic
501
- // default.
502
- defineModel({ id: "nemotron-3-ultra-free", providerId: "opencode", wireFamily: "chat", endpoint: `${ZEN_PATH_PREFIX}/chat/completions`, reasoning: true, free: true }),
503
- defineModel({ id: "nemotron-3.5-lightning-free", providerId: "opencode", wireFamily: "chat", endpoint: `${ZEN_PATH_PREFIX}/chat/completions`, reasoning: true, free: true }),
504
- // Absent from the snapshot: keep declared limits rather than the generic default.
505
- defineModel({ id: "longcat-2.5-preview-free", providerId: "opencode", wireFamily: "chat", endpoint: `${ZEN_PATH_PREFIX}/chat/completions`, ctx: 1_000_000, out: 131_072, reasoning: true, free: true }),
500
+ // `nemotron-3-ultra-free`, `nemotron-3.5-lightning-free` and
501
+ // `longcat-2.5-preview-free` are gone: the live Zen roster no longer lists
502
+ // them. A row the provider's own roster dropped can disappear from the wire
503
+ // without notice, which makes it a stale row — one that routes to a 404 at
504
+ // dispatch time rather than failing at catalog load. `big-pickle` and the
505
+ // rest of the shared Zen listing remain.
506
506
  systemoneZen("jev-1.13-free", true),
507
507
  ];
508
508
 
509
509
  export const OPENCODE_ZEN_MODELS: readonly ModelDefinition[] = [
510
510
  ...sharedZenChat,
511
- defineModel({ id: "nemotron-3-ultra-free", providerId: "opencode", wireFamily: "chat", endpoint: `${ZEN_PATH_PREFIX}/chat/completions`, reasoning: true }),
512
511
  defineModel({ id: "muse-spark-1.2", providerId: "opencode", wireFamily: "responses", endpoint: `${ZEN_PATH_PREFIX}/responses`, vision: true, reasoning: true, reasoningEfforts: MUSE_SPARK_REASONING_EFFORTS }),
513
512
  defineModel({ id: "muse-spark-1.3", providerId: "opencode", wireFamily: "responses", endpoint: `${ZEN_PATH_PREFIX}/responses`, vision: true, reasoning: true, reasoningEfforts: MUSE_SPARK_REASONING_EFFORTS }),
514
513
  systemoneZen("jev-1.13", false),
@@ -0,0 +1,192 @@
1
+ // Qoder quota.
2
+ //
3
+ // `GET /api/v2/quota/usage` on the **openapi** host, with a plain bearer token
4
+ // — no COSY signature on this surface, which is the one place the business API
5
+ // differs from inference.
6
+ //
7
+ // The response carries two quota objects and they are not interchangeable:
8
+ //
9
+ // - `userQuota` is the plan allowance — what the account bought.
10
+ // - `addOnQuota` is granted credits (daily check-in, promotions) — what it was
11
+ // given on top.
12
+ //
13
+ // The donor sums them into one balance, and that is right for a credit balance:
14
+ // both are credits in the same unit and either can be spent. It would be wrong
15
+ // for two *rate limits*, which is why these windows are marked `pooled` here
16
+ // and the Trae request buckets are not.
17
+ //
18
+ // Teams add a third object (`orgResourcePackage`), a shared pool that sits
19
+ // outside the account's own allowance. It is only real when it is available,
20
+ // and an account whose plan is exhausted but whose shared pool still has
21
+ // credit is *not* exhausted.
22
+
23
+ import type {
24
+ FetchLike,
25
+ ProviderQuotaResult,
26
+ ProviderQuotaWindow,
27
+ } from "../../quota/quota-contracts";
28
+ import {
29
+ cleanError,
30
+ isoDate,
31
+ number,
32
+ percentWindow,
33
+ quotaRecord,
34
+ record,
35
+ } from "../../quota/quota-contracts";
36
+ import type { QuotaCollectionContext } from "../../quota/quota-support";
37
+ import { QODER_REALMS, type QoderRealm } from "./qoder-realm";
38
+
39
+ /** Paths on the realm's openapi host. */
40
+ const QUOTA_PATH = "/api/v2/quota/usage";
41
+
42
+ /** Shared-pool keys, in the order the donor tries them. */
43
+ const SHARED_KEYS: readonly string[] = [
44
+ "orgResourcePackage",
45
+ "sharedQuota",
46
+ "teamQuota",
47
+ "poolQuota",
48
+ "sharedCredits",
49
+ "sharedCredit",
50
+ ];
51
+
52
+ /**
53
+ * Whether a shared pool is usable.
54
+ *
55
+ * Absent means "not declared unavailable", which is usable. Only an explicit
56
+ * false/null/0 rules it out — reading absence as unavailable is what made a
57
+ * team account with a live pool look exhausted.
58
+ */
59
+ function sharedAvailable(source: Record<string, unknown>): boolean {
60
+ const value = source["available"];
61
+ if (value === undefined || value === null) return true;
62
+ if (typeof value === "boolean") return value;
63
+ if (typeof value === "number") return value !== 0;
64
+ if (typeof value === "string") return value.trim().toLowerCase() !== "false";
65
+ return true;
66
+ }
67
+
68
+ function creditWindow(
69
+ kind: string,
70
+ label: string,
71
+ remaining: number | null,
72
+ limit: number | null,
73
+ resetsAt: string | null,
74
+ ): ProviderQuotaWindow | null {
75
+ if (remaining === null && limit === null) return null;
76
+ const used = remaining !== null && limit !== null ? limit - remaining : null;
77
+ return percentWindow(kind, label, null, resetsAt, used, limit, "pooled");
78
+ }
79
+
80
+ /**
81
+ * Reads one Qoder account's quota.
82
+ *
83
+ * A degenerate body — both quota objects missing, or `isQuotaExceeded` with a
84
+ * zero remaining *and* a zero total — is reported as an error rather than as a
85
+ * zero balance. "No quota" parks the account; "could not read the quota" does
86
+ * not, and those two are not the same claim.
87
+ */
88
+ /** Records an unreadable body where an operator can act on it, not as an error. */
89
+ function logQoderQuotaMiss(detail: string): void {
90
+ console.warn(`[quota] qoder quota unreadable: ${detail}`);
91
+ }
92
+
93
+ export async function fetchQoderQuota(
94
+ credential: string,
95
+ fetcher: FetchLike,
96
+ context?: QuotaCollectionContext,
97
+ ): Promise<ProviderQuotaResult> {
98
+ const raw = context?.auth_state ?? {};
99
+ const realm: QoderRealm = raw["realm"] === "cn" || raw["region"] === "CN" ? "cn" : "intl";
100
+ const config = QODER_REALMS[realm];
101
+ const url = `${config.openapi}${QUOTA_PATH}`;
102
+
103
+ let payload: unknown;
104
+ try {
105
+ const response = await fetcher(url, {
106
+ method: "GET",
107
+ headers: {
108
+ accept: "application/json",
109
+ authorization: `Bearer ${credential}`,
110
+ "user-agent": config.userAgent,
111
+ },
112
+ });
113
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
114
+ payload = await response.json();
115
+ } catch (error) {
116
+ // Unreachable is not a balance. See the note on the degenerate branch.
117
+ logQoderQuotaMiss(cleanError(error));
118
+ return { source: "qoder", plan: null, windows: [], error: null };
119
+ }
120
+
121
+ const root = quotaRecord(payload);
122
+ const userQuota = record(root["userQuota"]);
123
+ const addOnQuota = record(root["addOnQuota"]);
124
+ const owned = [userQuota, addOnQuota].filter(
125
+ (value): value is Record<string, unknown> => value !== null,
126
+ );
127
+
128
+ const bothMissing = owned.length === 0;
129
+ // A live account on the free tier really does answer `userQuota` all zeroes
130
+ // with `isQuotaExceeded: true` — verified against a real credential:
131
+ // `{"isQuotaExceeded":true,"userQuota":{"total":0,"used":0,"remaining":0},
132
+ // "totalUsagePercentage":0.0,...}`. That is a balance of zero, not an
133
+ // unreadable body. Only a response with *both* quota objects absent is
134
+ // unreadable, and it is logged rather than returned — never as an error,
135
+ // which would cool the account.
136
+ const degenerate = bothMissing;
137
+ if (degenerate) {
138
+ // A body we cannot trust is not a zero balance, and it is not a failed
139
+ // read either: `error` is what flags the account `quota_fetch_failed` and
140
+ // cools it, so a shape we simply do not recognise would park a working
141
+ // account. Logged instead of returned.
142
+ logQoderQuotaMiss(
143
+ bothMissing
144
+ ? "Qoder quota response carried no quota objects."
145
+ : "Qoder reported quota exceeded with a zero total; not read as a balance.",
146
+ );
147
+ return { source: "qoder", plan: null, windows: [], error: null };
148
+ }
149
+
150
+ const resetsAt = isoDate(root["expiresAt"] ?? root["expireAt"] ?? root["expires_at"]);
151
+ const windows: ProviderQuotaWindow[] = [];
152
+ const plan = creditWindow("qoder:plan", "Plan credits", number(userQuota?.["remaining"]), number(userQuota?.["total"]), resetsAt);
153
+ if (plan) windows.push(plan);
154
+ const addOn = creditWindow("qoder:addon", "Granted credits", number(addOnQuota?.["remaining"]), number(addOnQuota?.["total"]), resetsAt);
155
+ if (addOn) windows.push(addOn);
156
+
157
+ for (const key of SHARED_KEYS) {
158
+ const shared = record(root[key]);
159
+ if (!shared) continue;
160
+ if (!sharedAvailable(shared)) continue;
161
+ const window = creditWindow(
162
+ "qoder:shared",
163
+ "Shared pool",
164
+ number(shared["remaining"]),
165
+ number(shared["cap"] ?? shared["total"]),
166
+ null,
167
+ );
168
+ if (window) windows.push(window);
169
+ break;
170
+ }
171
+
172
+ if (windows.length === 0) {
173
+ logQoderQuotaMiss("Qoder quota response carried no readable balance.");
174
+ return { source: "qoder", plan: null, windows: [], error: null };
175
+ }
176
+ return { source: "qoder", plan: null, windows, error: null };
177
+ }
178
+
179
+ /**
180
+ * The `qodercn` provider's quota.
181
+ *
182
+ * Same collector, realm forced to `cn`: a CN account is refused by
183
+ * `openapi.qoder.sh`, and the collector cannot see the provider id — the
184
+ * context carries auth state only.
185
+ */
186
+ export function fetchQoderCnQuota(
187
+ credential: string,
188
+ fetcher: FetchLike,
189
+ context?: QuotaCollectionContext,
190
+ ): Promise<ProviderQuotaResult> {
191
+ return fetchQoderQuota(credential, fetcher, { ...context, auth_state: { ...(context?.auth_state ?? {}), realm: "cn" } });
192
+ }