cartethyia 2.5.10 → 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 (367) hide show
  1. package/.env.example +8 -0
  2. package/dist/dashboard/assets/{BannedPage-Cd30Z0b9.js → BannedPage-l6XMnfr9.js} +1 -1
  3. package/dist/dashboard/assets/BannedPage-l6XMnfr9.js.gz +0 -0
  4. package/dist/dashboard/assets/{CliToolDetailPage-wf5u-ww6.js → CliToolDetailPage-DwOzAW45.js} +1 -1
  5. package/dist/dashboard/assets/CliToolDetailPage-DwOzAW45.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolsPage-DykPvuck.js → CliToolsPage-BT3Up3QA.js} +1 -1
  7. package/dist/dashboard/assets/CliToolsPage-BT3Up3QA.js.gz +0 -0
  8. package/dist/dashboard/assets/{CombosPage-DwvSD3vs.js → CombosPage-6kYIUpzR.js} +1 -1
  9. package/dist/dashboard/assets/CombosPage-6kYIUpzR.js.gz +0 -0
  10. package/dist/dashboard/assets/{ConsoleLogPage-CIr7KDex.js → ConsoleLogPage-zRnj1mlR.js} +1 -1
  11. package/dist/dashboard/assets/ConsoleLogPage-zRnj1mlR.js.gz +0 -0
  12. package/dist/dashboard/assets/{CustomizationPage-82X0DDdH.js → CustomizationPage-C7m-H1Rz.js} +1 -1
  13. package/dist/dashboard/assets/CustomizationPage-C7m-H1Rz.js.gz +0 -0
  14. package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js +1 -0
  15. package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js.gz +0 -0
  16. package/dist/dashboard/assets/{LoginPage-CS5mPuVF.js → LoginPage-CbphLvjg.js} +1 -1
  17. package/dist/dashboard/assets/LoginPage-CbphLvjg.js.gz +0 -0
  18. package/dist/dashboard/assets/{ModelPicker-C0UCUsVJ.js → ModelPicker-DVny4S5_.js} +1 -1
  19. package/dist/dashboard/assets/ModelPicker-DVny4S5_.js.gz +0 -0
  20. package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js +1 -0
  21. package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js.gz +0 -0
  22. package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js +3 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js.gz +0 -0
  24. package/dist/dashboard/assets/{ProvidersPage-vdja5ZPK.js → ProvidersPage-Dfb2896L.js} +1 -1
  25. package/dist/dashboard/assets/ProvidersPage-Dfb2896L.js.gz +0 -0
  26. package/dist/dashboard/assets/{ProxyPage-BB4CNm4-.js → ProxyPage-BaCIdAUt.js} +1 -1
  27. package/dist/dashboard/assets/ProxyPage-BaCIdAUt.js.gz +0 -0
  28. package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js +1 -0
  29. package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js.gz +0 -0
  30. package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js +1 -0
  31. package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js.gz +0 -0
  32. package/dist/dashboard/assets/{SetupPage-FadUOl9S.js → SetupPage-D4g6vOGc.js} +1 -1
  33. package/dist/dashboard/assets/SetupPage-D4g6vOGc.js.gz +0 -0
  34. package/dist/dashboard/assets/{SortableList-Cfy6_Khv.js → SortableList-C93i14BD.js} +1 -1
  35. package/dist/dashboard/assets/SortableList-C93i14BD.js.gz +0 -0
  36. package/dist/dashboard/assets/{StudioPage-CM92fgJf.js → StudioPage-SgQpymjm.js} +1 -1
  37. package/dist/dashboard/assets/{StudioPage-CM92fgJf.js.gz → StudioPage-SgQpymjm.js.gz} +0 -0
  38. package/dist/dashboard/assets/{UsagePage-BT4MyQeT.js → UsagePage-BFME-eba.js} +2 -2
  39. package/dist/dashboard/assets/UsagePage-BFME-eba.js.gz +0 -0
  40. package/dist/dashboard/assets/{api-keys-DQYLQxpW.js → api-keys-C23S9sgp.js} +1 -1
  41. package/dist/dashboard/assets/api-keys-C23S9sgp.js.gz +0 -0
  42. package/dist/dashboard/assets/{app-B7pVRhJ1.js → app-2KhtHCv9.js} +1 -1
  43. package/dist/dashboard/assets/{app-B7pVRhJ1.js.gz → app-2KhtHCv9.js.gz} +0 -0
  44. package/dist/dashboard/assets/app-CEeaMHOy.css +2 -0
  45. package/dist/dashboard/assets/app-CEeaMHOy.css.gz +0 -0
  46. package/dist/dashboard/assets/{assert-C9FD_JwJ.js → assert-DU6qrMA_.js} +1 -1
  47. package/dist/dashboard/assets/assert-DU6qrMA_.js.gz +0 -0
  48. package/dist/dashboard/assets/{badge-Bvyr7D0u.js → badge-CKsF123d.js} +1 -1
  49. package/dist/dashboard/assets/badge-CKsF123d.js.gz +0 -0
  50. package/dist/dashboard/assets/{banner-DIvKAZih.js → banner-8j7uChA9.js} +1 -1
  51. package/dist/dashboard/assets/banner-8j7uChA9.js.gz +0 -0
  52. package/dist/dashboard/assets/{card-D5_gYIS_.js → card-CdsSlWA3.js} +1 -1
  53. package/dist/dashboard/assets/card-CdsSlWA3.js.gz +0 -0
  54. package/dist/dashboard/assets/{cli-tools-CNqzrfdb.js → cli-tools-BaFUuFFF.js} +1 -1
  55. package/dist/dashboard/assets/cli-tools-BaFUuFFF.js.gz +0 -0
  56. package/dist/dashboard/assets/{clipboard-button-B6vDBtbm.js → clipboard-button-uL5aYLfW.js} +1 -1
  57. package/dist/dashboard/assets/clipboard-button-uL5aYLfW.js.gz +0 -0
  58. package/dist/dashboard/assets/{dialog-CpjrgdwJ.js → dialog-C07hr5_G.js} +1 -1
  59. package/dist/dashboard/assets/dialog-C07hr5_G.js.gz +0 -0
  60. package/dist/dashboard/assets/{drawer-BWmt3b5y.js → drawer-BPO1TcKH.js} +1 -1
  61. package/dist/dashboard/assets/drawer-BPO1TcKH.js.gz +0 -0
  62. package/dist/dashboard/assets/{entry-C_oakV3z.js → entry-Bz_CtDfC.js} +1 -1
  63. package/dist/dashboard/assets/entry-Bz_CtDfC.js.gz +0 -0
  64. package/dist/dashboard/assets/entry-CwPRKk0d.js +4 -0
  65. package/dist/dashboard/assets/entry-CwPRKk0d.js.gz +0 -0
  66. package/dist/dashboard/assets/entry-GJRbmrpA.js +1 -0
  67. package/dist/dashboard/assets/entry-GJRbmrpA.js.gz +0 -0
  68. package/dist/dashboard/assets/{github-badge-RRHHWGxh.js → github-badge-DmQreSDd.js} +1 -1
  69. package/dist/dashboard/assets/github-badge-DmQreSDd.js.gz +0 -0
  70. package/dist/dashboard/assets/{index-Bhey_rg_.js → index-Bblu6erR.js} +2 -2
  71. package/dist/dashboard/assets/index-Bblu6erR.js.gz +0 -0
  72. package/dist/dashboard/assets/{inline-C2_EQ5Uf.js → inline-C9dCYaBZ.js} +1 -1
  73. package/dist/dashboard/assets/inline-C9dCYaBZ.js.gz +0 -0
  74. package/dist/dashboard/assets/{input-BMRlQ450.js → input-CjCnXweJ.js} +1 -1
  75. package/dist/dashboard/assets/input-CjCnXweJ.js.gz +0 -0
  76. package/dist/dashboard/assets/{layout-XR5xP8ew.js → layout-V6y8DAkl.js} +1 -1
  77. package/dist/dashboard/assets/layout-V6y8DAkl.js.gz +0 -0
  78. package/dist/dashboard/assets/{list-row-Cw07dioc.js → list-row-CM1AaG_y.js} +1 -1
  79. package/dist/dashboard/assets/list-row-CM1AaG_y.js.gz +0 -0
  80. package/dist/dashboard/assets/{live-BivGfuUs.js → live-ta6GMb-8.js} +1 -1
  81. package/dist/dashboard/assets/live-ta6GMb-8.js.gz +0 -0
  82. package/dist/dashboard/assets/{model-bans-CmffjLuG.js → model-bans-BmYTmbAk.js} +1 -1
  83. package/dist/dashboard/assets/model-bans-BmYTmbAk.js.gz +0 -0
  84. package/dist/dashboard/assets/{mutation-46P0oWMe.js → mutation-g_fduiNw.js} +1 -1
  85. package/dist/dashboard/assets/mutation-g_fduiNw.js.gz +0 -0
  86. package/dist/dashboard/assets/{numbers-DrdA9oSM.js → numbers-BOevJzdz.js} +1 -1
  87. package/dist/dashboard/assets/numbers-BOevJzdz.js.gz +0 -0
  88. package/dist/dashboard/assets/{query-keys-BFhGpwR1.js → query-keys-UETqfytp.js} +1 -1
  89. package/dist/dashboard/assets/query-keys-UETqfytp.js.gz +0 -0
  90. package/dist/dashboard/assets/{routing-CAb4F37R.js → routing-BPUo-W8d.js} +1 -1
  91. package/dist/dashboard/assets/routing-BPUo-W8d.js.gz +0 -0
  92. package/dist/dashboard/assets/{select-CxVVB__N.js → select-C980c4cz.js} +1 -1
  93. package/dist/dashboard/assets/select-C980c4cz.js.gz +0 -0
  94. package/dist/dashboard/assets/{settings-DNX9sydi.js → settings--3hxSyry.js} +1 -1
  95. package/dist/dashboard/assets/settings--3hxSyry.js.gz +0 -0
  96. package/dist/dashboard/assets/{stack-uPOwM9Hs.js → stack-_gfTdLVB.js} +1 -1
  97. package/dist/dashboard/assets/stack-_gfTdLVB.js.gz +0 -0
  98. package/dist/dashboard/assets/{state-D1Yz79iL.js → state-2c5V4zo7.js} +1 -1
  99. package/dist/dashboard/assets/state-2c5V4zo7.js.gz +0 -0
  100. package/dist/dashboard/assets/{switch-sevsRrRo.js → switch-Dkke7Naf.js} +1 -1
  101. package/dist/dashboard/assets/switch-Dkke7Naf.js.gz +0 -0
  102. package/dist/dashboard/assets/{useMutation-Dem0lXQj.js → useMutation-EDFRkn1i.js} +1 -1
  103. package/dist/dashboard/assets/useMutation-EDFRkn1i.js.gz +0 -0
  104. package/dist/dashboard/index.html +1 -1
  105. package/dist/dashboard/index.html.gz +0 -0
  106. package/dist/dashboard/precompressed-assets.json +51 -51
  107. package/migrations/README.md +52 -52
  108. package/migrations/full-postgres/0020_add_telemetry_event_analytics.sql +77 -0
  109. package/migrations/full-postgres/0021_add_api_key_concurrency_tolerance.sql +13 -0
  110. package/migrations/full-postgres/0022_add_inferhub_config_key.sql +17 -0
  111. package/migrations/full-postgres/0023_drop_api_key_concurrency_tolerance.sql +13 -0
  112. package/migrations/lite-sqlite3/0020_add_telemetry_event_analytics.sql +69 -0
  113. package/migrations/lite-sqlite3/0021_add_api_key_concurrency_tolerance.sql +13 -0
  114. package/migrations/lite-sqlite3/0022_add_inferhub_config_key.sql +17 -0
  115. package/migrations/lite-sqlite3/0023_drop_api_key_concurrency_tolerance.sql +13 -0
  116. package/package.json +96 -95
  117. package/scripts/cli/admin.ts +126 -128
  118. package/scripts/cli/autostart.ts +237 -237
  119. package/scripts/cli/background.ts +120 -110
  120. package/scripts/cli/bin/carte.cjs +8 -8
  121. package/scripts/cli/bin/cartethyia.cjs +167 -167
  122. package/scripts/cli/console-menu.ts +39 -30
  123. package/scripts/cli/controller.ts +145 -0
  124. package/scripts/cli/env.ts +99 -99
  125. package/scripts/cli/gateway.ts +264 -260
  126. package/scripts/cli/instance.ts +234 -224
  127. package/scripts/cli/main.ts +188 -167
  128. package/scripts/cli/platform.ts +64 -64
  129. package/scripts/cli/prompt.ts +217 -79
  130. package/scripts/cli/services.ts +88 -88
  131. package/scripts/cli/setup.ts +445 -445
  132. package/scripts/cli/start.ts +85 -85
  133. package/scripts/cli/startup-signal.ts +23 -23
  134. package/scripts/cli/telemetry.ts +70 -0
  135. package/scripts/cli/tray/index.ts +263 -263
  136. package/scripts/cli/tray/windows.ps1 +107 -107
  137. package/scripts/cli/tray.ts +29 -4
  138. package/scripts/cli/update.ts +39 -39
  139. package/scripts/cli/version.ts +67 -67
  140. package/scripts/cli/wordmark.ts +58 -58
  141. package/src/boot/app.ts +16 -1
  142. package/src/boot/dependencies.ts +128 -25
  143. package/src/config/resources.ts +137 -0
  144. package/src/config/runtime.ts +133 -76
  145. package/src/config/spec.ts +30 -29
  146. package/src/config/telemetry.ts +0 -25
  147. package/src/console/api-keys/bucket-spend.ts +14 -17
  148. package/src/console/api-keys/contracts.ts +410 -410
  149. package/src/console/api-keys/routes.ts +12 -0
  150. package/src/console/audit/store.ts +2 -3
  151. package/src/console/auth/setup-service.ts +8 -1
  152. package/src/console/backup/routes.ts +7 -0
  153. package/src/console/backup/service.ts +463 -463
  154. package/src/console/backup/store.ts +16 -11
  155. package/src/console/backup/validate.ts +1 -4
  156. package/src/console/cli-tools/fs-ops.ts +18 -2
  157. package/src/console/data-management/contracts.ts +1 -8
  158. package/src/console/data-management/operations.ts +61 -48
  159. package/src/console/data-management/routes.ts +4 -2
  160. package/src/console/domain-registration.ts +25 -3
  161. package/src/console/inferhub/client.ts +139 -0
  162. package/src/console/inferhub/key-store.ts +129 -0
  163. package/src/console/inferhub/normalize.ts +347 -0
  164. package/src/console/inferhub/routes.ts +411 -0
  165. package/src/console/inferhub/store.ts +109 -0
  166. package/src/console/inferhub/types.ts +183 -0
  167. package/src/console/observability/contracts.ts +2 -41
  168. package/src/console/observability/store.ts +176 -371
  169. package/src/console/providers/catalog/public-model-store.ts +0 -0
  170. package/src/console/providers/catalog/store.ts +23 -36
  171. package/src/console/quota/account-quota-shared.ts +52 -52
  172. package/src/console/quota/account-quota-tenant.ts +16 -24
  173. package/src/console/quota/refresh.ts +441 -441
  174. package/src/console/routing/pools/import-parse.ts +206 -206
  175. package/src/console/routing/pools/operations.ts +543 -543
  176. package/src/console/routing/pools/probe-verdict.ts +132 -132
  177. package/src/console/routing/pools/routes.ts +181 -181
  178. package/src/console/settings/contracts.ts +42 -40
  179. package/src/console/settings/store.ts +25 -25
  180. package/src/console/share/share-stats.ts +22 -4
  181. package/src/console/share/share-usage.ts +30 -4
  182. package/src/console/shared/cursor.ts +15 -0
  183. package/src/console/updates/routes.ts +12 -0
  184. package/src/main.ts +13 -5
  185. package/src/network/pool/resolver.ts +1104 -1104
  186. package/src/observability/client-name.ts +22 -0
  187. package/src/observability/event-log.ts +5 -29
  188. package/src/observability/event-query.ts +690 -0
  189. package/src/observability/log-ring.ts +26 -2
  190. package/src/observability/log-sanitize.ts +118 -0
  191. package/src/observability/logger.ts +17 -13
  192. package/src/observability/metrics.ts +35 -0
  193. package/src/observability/payload-capture.ts +114 -41
  194. package/src/observability/payload-store.ts +249 -24
  195. package/src/observability/raw-event-writer.ts +118 -0
  196. package/src/observability/runtime-metrics.ts +71 -1
  197. package/src/observability/telemetry-buffer.ts +100 -17
  198. package/src/observability/telemetry-reconciliation.ts +417 -0
  199. package/src/persistence/boot.ts +20 -9
  200. package/src/persistence/db-pg.ts +13 -0
  201. package/src/persistence/db-sqlite.ts +569 -497
  202. package/src/persistence/dialect.ts +3 -2
  203. package/src/persistence/migrate.ts +181 -181
  204. package/src/persistence/schema.ts +1496 -1406
  205. package/src/persistence/shared-cache.ts +5 -1
  206. package/src/persistence/stores/api-key-store.ts +581 -581
  207. package/src/persistence/stores/telemetry-store.ts +124 -63
  208. package/src/protocol/reasoning-text.ts +30 -0
  209. package/src/protocol/response/chat.ts +33 -7
  210. package/src/protocol/response/codex.ts +14 -2
  211. package/src/protocol/response/messages.ts +5 -2
  212. package/src/providers/authentication/oauth-flow-store.ts +3 -10
  213. package/src/providers/bundled-discovery-providers.ts +122 -122
  214. package/src/providers/default-registry.ts +10 -0
  215. package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
  216. package/src/providers/discovery/models-dev-catalog.ts +354 -354
  217. package/src/providers/discovery/probe-phases.ts +43 -7
  218. package/src/providers/discovery/probing-service.ts +70 -32
  219. package/src/providers/integrations/antigravity/antigravity.ts +9 -2
  220. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +85 -17
  221. package/src/providers/integrations/buddy/buddy-chat-shared.ts +934 -934
  222. package/src/providers/integrations/buddy/buddy-checkin.ts +5 -9
  223. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +3 -7
  224. package/src/providers/integrations/buddy/buddy-quota-shared.ts +3 -1
  225. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +57 -49
  226. package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -180
  227. package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -206
  228. package/src/providers/integrations/buddy/workbuddy/quota.ts +161 -161
  229. package/src/providers/integrations/claude/claude-quota.ts +2 -2
  230. package/src/providers/integrations/claude/claude-user-profiles.ts +3 -2
  231. package/src/providers/integrations/claude/claude.ts +3 -4
  232. package/src/providers/integrations/cline/cline.ts +94 -8
  233. package/src/providers/integrations/codex/codex.ts +61 -37
  234. package/src/providers/integrations/cursor/cursor-catalog.ts +368 -368
  235. package/src/providers/integrations/cursor/cursor-http2.ts +300 -300
  236. package/src/providers/integrations/cursor/cursor-identity.ts +2 -4
  237. package/src/providers/integrations/cursor/cursor-request.ts +11 -0
  238. package/src/providers/integrations/cursor/cursor.ts +29 -17
  239. package/src/providers/integrations/cursor/protocol/framing.ts +129 -129
  240. package/src/providers/integrations/cursor/protocol/messages.ts +2380 -2380
  241. package/src/providers/integrations/cursor/protocol/struct.ts +305 -305
  242. package/src/providers/integrations/cursor/protocol/tool-canonical.ts +275 -0
  243. package/src/providers/integrations/grok/grok-quota.ts +279 -279
  244. package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -73
  245. package/src/providers/integrations/grok/grok-ratelimit.ts +218 -218
  246. package/src/providers/integrations/hermes.ts +7 -1
  247. package/src/providers/integrations/inferhub.ts +201 -73
  248. package/src/providers/integrations/kilo/kilo.ts +152 -152
  249. package/src/providers/integrations/kiro/kiro-machine-id.ts +2 -4
  250. package/src/providers/integrations/openrouter/provider.ts +129 -129
  251. package/src/providers/integrations/qoder/qoder-quota.ts +193 -192
  252. package/src/providers/integrations/qoder/qoder-realm.ts +106 -106
  253. package/src/providers/integrations/trae/trae-oauth.ts +4 -26
  254. package/src/providers/integrations/trae/trae-quota.ts +389 -384
  255. package/src/providers/integrations/trae/trae-realm.ts +15 -2
  256. package/src/providers/integrations/xiaomi-mimo/think-stream.ts +100 -24
  257. package/src/providers/integrations/zcode/oauth.ts +1 -1
  258. package/src/providers/operations/account-health-service.ts +605 -279
  259. package/src/providers/provider-metadata.ts +12 -1
  260. package/src/providers/provider-model-definition.ts +373 -373
  261. package/src/providers/quota/quota-contracts.ts +3 -7
  262. package/src/providers/search/search-provider.ts +2 -6
  263. package/src/security/admission/contracts.ts +10 -0
  264. package/src/security/admission/in-memory-store.ts +89 -14
  265. package/src/security/admission/lua.ts +191 -182
  266. package/src/security/admission/redis-store.ts +30 -0
  267. package/src/security/admission/service.ts +18 -0
  268. package/src/security/api-key-auth-cache.ts +96 -0
  269. package/src/security/api-key-auth.ts +11 -100
  270. package/src/security/client-router-fingerprint.ts +3 -7
  271. package/src/security/ip-boundary.ts +7 -3
  272. package/src/shared/coerce.ts +67 -0
  273. package/src/shared/hash.ts +6 -0
  274. package/src/shared/time.ts +7 -0
  275. package/src/transport/dispatch/attempt-finalize.ts +26 -138
  276. package/src/transport/dispatch/attempt-loop.ts +103 -81
  277. package/src/transport/dispatch/capture-policy.ts +78 -0
  278. package/src/transport/dispatch/proxy-request.ts +4 -2
  279. package/src/transport/dispatch/streaming/encoder.ts +111 -5
  280. package/src/transport/dispatch/streaming/stream.ts +19 -2
  281. package/src/transport/dispatch/streaming/thinking-loop.ts +300 -0
  282. package/src/transport/failure-policy.ts +27 -0
  283. package/src/transport/guards/abuse-guard.ts +2 -2
  284. package/src/transport/guards/auth-guard.ts +17 -0
  285. package/src/transport/guards/body-policy.ts +3 -2
  286. package/src/transport/guards/concurrency-guard.ts +89 -13
  287. package/src/transport/guards/early-rejection-capture.ts +43 -3
  288. package/src/transport/guards/error-lifecycle.ts +10 -2
  289. package/src/transport/guards/guard-errors.ts +23 -0
  290. package/src/transport/guards/headroom-guard.ts +41 -7
  291. package/src/transport/guards/pipeline.ts +24 -0
  292. package/src/transport/guards/request-context.ts +2 -1
  293. package/src/transport/routing/admission.ts +51 -9
  294. package/src/transport/routing/bounded-lru.ts +49 -0
  295. package/src/transport/routing/candidate-index.ts +88 -0
  296. package/src/transport/routing/reservations.ts +121 -7
  297. package/src/transport/routing/round-robin.ts +10 -22
  298. package/src/transport/routing/route-catalog.ts +26 -13
  299. package/src/transport/routing/route-model.ts +22 -2
  300. package/src/transport/routing/router.ts +53 -31
  301. package/src/transport/surface/chat/encode.ts +2 -3
  302. package/src/transport/surface/chat/parse.ts +2 -1
  303. package/src/transport/surface/dialects.ts +11 -3
  304. package/src/transport/surface/messages/encode.ts +2 -1
  305. package/src/transport/surface/messages/parse.ts +3 -1
  306. package/src/transport/surface/messages/stream.ts +2 -6
  307. package/src/workers/maintenance-lease.ts +200 -0
  308. package/src/workers/scheduler.ts +101 -1
  309. package/dist/dashboard/assets/BannedPage-Cd30Z0b9.js.gz +0 -0
  310. package/dist/dashboard/assets/CliToolDetailPage-wf5u-ww6.js.gz +0 -0
  311. package/dist/dashboard/assets/CliToolsPage-DykPvuck.js.gz +0 -0
  312. package/dist/dashboard/assets/CombosPage-DwvSD3vs.js.gz +0 -0
  313. package/dist/dashboard/assets/ConsoleLogPage-CIr7KDex.js.gz +0 -0
  314. package/dist/dashboard/assets/CustomizationPage-82X0DDdH.js.gz +0 -0
  315. package/dist/dashboard/assets/HealthEventsModal-BYPucTTn.js +0 -1
  316. package/dist/dashboard/assets/HealthEventsModal-BYPucTTn.js.gz +0 -0
  317. package/dist/dashboard/assets/LoginPage-CS5mPuVF.js.gz +0 -0
  318. package/dist/dashboard/assets/ModelPicker-C0UCUsVJ.js.gz +0 -0
  319. package/dist/dashboard/assets/OverviewPage-Dk_ew3YU.js +0 -1
  320. package/dist/dashboard/assets/OverviewPage-Dk_ew3YU.js.gz +0 -0
  321. package/dist/dashboard/assets/ProviderDetailPage-CAiRn2Q0.js +0 -3
  322. package/dist/dashboard/assets/ProviderDetailPage-CAiRn2Q0.js.gz +0 -0
  323. package/dist/dashboard/assets/ProvidersPage-vdja5ZPK.js.gz +0 -0
  324. package/dist/dashboard/assets/ProxyPage-BB4CNm4-.js.gz +0 -0
  325. package/dist/dashboard/assets/QuotaPage-FEc4iNyq.js +0 -1
  326. package/dist/dashboard/assets/QuotaPage-FEc4iNyq.js.gz +0 -0
  327. package/dist/dashboard/assets/SettingsPage-Dlfi5L4N.js +0 -1
  328. package/dist/dashboard/assets/SettingsPage-Dlfi5L4N.js.gz +0 -0
  329. package/dist/dashboard/assets/SetupPage-FadUOl9S.js.gz +0 -0
  330. package/dist/dashboard/assets/SortableList-Cfy6_Khv.js.gz +0 -0
  331. package/dist/dashboard/assets/UsagePage-BT4MyQeT.js.gz +0 -0
  332. package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
  333. package/dist/dashboard/assets/app-CX_Z8cMs.css +0 -2
  334. package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
  335. package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
  336. package/dist/dashboard/assets/badge-Bvyr7D0u.js.gz +0 -0
  337. package/dist/dashboard/assets/banner-DIvKAZih.js.gz +0 -0
  338. package/dist/dashboard/assets/card-D5_gYIS_.js.gz +0 -0
  339. package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
  340. package/dist/dashboard/assets/clipboard-button-B6vDBtbm.js.gz +0 -0
  341. package/dist/dashboard/assets/dialog-CpjrgdwJ.js.gz +0 -0
  342. package/dist/dashboard/assets/drawer-BWmt3b5y.js.gz +0 -0
  343. package/dist/dashboard/assets/entry-B-gvimrg.js +0 -1
  344. package/dist/dashboard/assets/entry-B-gvimrg.js.gz +0 -0
  345. package/dist/dashboard/assets/entry-C_oakV3z.js.gz +0 -0
  346. package/dist/dashboard/assets/entry-Dj0cHfEc.js +0 -4
  347. package/dist/dashboard/assets/entry-Dj0cHfEc.js.gz +0 -0
  348. package/dist/dashboard/assets/github-badge-RRHHWGxh.js.gz +0 -0
  349. package/dist/dashboard/assets/index-Bhey_rg_.js.gz +0 -0
  350. package/dist/dashboard/assets/inline-C2_EQ5Uf.js.gz +0 -0
  351. package/dist/dashboard/assets/input-BMRlQ450.js.gz +0 -0
  352. package/dist/dashboard/assets/layout-XR5xP8ew.js.gz +0 -0
  353. package/dist/dashboard/assets/list-row-Cw07dioc.js.gz +0 -0
  354. package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
  355. package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
  356. package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
  357. package/dist/dashboard/assets/numbers-DrdA9oSM.js.gz +0 -0
  358. package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
  359. package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
  360. package/dist/dashboard/assets/select-CxVVB__N.js.gz +0 -0
  361. package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
  362. package/dist/dashboard/assets/stack-uPOwM9Hs.js.gz +0 -0
  363. package/dist/dashboard/assets/state-D1Yz79iL.js.gz +0 -0
  364. package/dist/dashboard/assets/switch-sevsRrRo.js.gz +0 -0
  365. package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
  366. package/src/observability/event-compaction.ts +0 -317
  367. package/src/observability/event-day-index.ts +0 -439
@@ -1,73 +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
- }
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
+ }
@@ -1,218 +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
- }
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
+ }
@@ -92,7 +92,13 @@ export const HERMES_MODELS: readonly ModelDefinition[] = [
92
92
  defineModel({ id: "stepfun/step-3.7-flash:free", ctx: 262144, out: 256000, vision: true, reasoning: true, free: true, freeTier: true }),
93
93
  defineModel({ id: "upstage/solar-mini4:free", ctx: 524288, out: 131072, reasoning: true, free: true, freeTier: true }),
94
94
  defineModel({ id: "reflection/beam-1.0-501b-a23b:free", ctx: 262144, out: 131072, reasoning: true, free: true, freeTier: true }),
95
- defineModel({ id: "meituan/longcat-2.5-preview:free", ctx: 1048576, out: 131072, reasoning: true, free: true, freeTier: true }),
95
+ // Vision-corrected against the live catalog: `architecture.input_modalities`
96
+ // lists `image` for this row, so it was published without vision it has.
97
+ defineModel({ id: "meituan/longcat-2.5-preview:free", ctx: 1048576, out: 131072, vision: true, reasoning: true, free: true, freeTier: true }),
98
+ // Added after a live read: the catalog carries it, it is free, and a dispatch
99
+ // answers 200 — it was simply absent from this list, so a client asking for
100
+ // it resolved to nothing. 262144 out is the catalog's own cap.
101
+ defineModel({ id: "ornith/ornith-1.5-122b:free", ctx: 262144, out: 262144, vision: true, reasoning: true, free: true, freeTier: true }),
96
102
  ];
97
103
 
98
104