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
@@ -39,6 +39,27 @@ export async function shouldCaptureProviderBody(
39
39
  return payloadCaptureKeepsBodies(db, tenantId);
40
40
  }
41
41
 
42
+ /**
43
+ * Per-stage cap for the provider request/response tee at one capture depth.
44
+ *
45
+ * The tee is what the drawer's "Provider Request" and "Provider Response"
46
+ * panels read, so its cap is the most a provider panel can ever show. It used
47
+ * to be a flat 256 KiB at every depth: on a large request the translated
48
+ * provider body was clipped to 256 KiB before the shared record budget ever
49
+ * ran, so a tenant who raised the depth to Maximum — 16 MiB — still saw a
50
+ * quarter-megabyte provider request and an empty provider response. Derived
51
+ * from the same depth tiers as the record budget, so raising the depth now
52
+ * raises both.
53
+ *
54
+ * Capped at a quarter of the depth's total budget: four panels share it, and
55
+ * one of them must never be able to spend the whole thing.
56
+ */
57
+ export function providerCaptureMaxBytes(depth?: TelemetryPayloadDepth): number {
58
+ if (depth === "moderate") return 2 * 1024 * 1024;
59
+ if (depth === "maximum") return 4 * 1024 * 1024;
60
+ return 256 * 1024;
61
+ }
62
+
42
63
  /**
43
64
  * Same answer as {@link shouldCaptureProviderBody}, memoized on the request.
44
65
  *
@@ -62,6 +83,28 @@ export async function shouldCaptureProviderBodyForRequest(
62
83
  return resolved.ok === true && resolved.mode === "full";
63
84
  }
64
85
 
86
+ /**
87
+ * The tenant's capture depth, memoized on the request like the mode above.
88
+ *
89
+ * Read *after* {@link shouldCaptureProviderBodyForRequest} has populated the
90
+ * memo, so the two answers come from one preference read. It sizes the
91
+ * provider tee: a tenant who raised the depth is asking for more of every
92
+ * panel, and a flat cap would answer with the same clipped body regardless.
93
+ */
94
+ export async function resolveCaptureDepthForRequest(
95
+ db: CartethyiaDatabase,
96
+ tenantId: string | null,
97
+ state: { payloadCapturePreference?: unknown },
98
+ ): Promise<TelemetryPayloadDepth> {
99
+ if (state.payloadCapturePreference === undefined) {
100
+ state.payloadCapturePreference = await resolvePayloadCapture(db, tenantId);
101
+ }
102
+ const resolved = state.payloadCapturePreference as
103
+ | { readonly ok: true; readonly depth: TelemetryPayloadDepth }
104
+ | { readonly ok: false };
105
+ return resolved.ok === true ? resolved.depth : "minimum";
106
+ }
107
+
65
108
  /**
66
109
  * Combined body cap for one capture depth. Fixed size tiers: every depth
67
110
  * stores all four drawer panels (client request, translated provider
@@ -103,7 +146,6 @@ export function parseCapturedBody(value: unknown): unknown {
103
146
  }
104
147
  }
105
148
 
106
- const PROVIDER_CAPTURE_MAX_BYTES = 256 * 1024;
107
149
  const PROVIDER_CAPTURE_AWAIT_MS = 2_000;
108
150
  const CAPTURABLE_RESPONSE_TYPES = /^(text\/|application\/(json|x-ndjson|sse))/i;
109
151
 
@@ -258,6 +300,7 @@ export function captureProviderExchange(
258
300
  inner: ValidatedOutboundFetch,
259
301
  sink: ProviderExchangeCapture,
260
302
  captureBody = true,
303
+ maxBytes = providerCaptureMaxBytes(),
261
304
  ): ValidatedOutboundFetch {
262
305
  return async (input, init) => {
263
306
  const url =
@@ -276,14 +319,12 @@ export function captureProviderExchange(
276
319
  // unbounded translated request body per attempt — up to `maxAttempts` of
277
320
  // them, all alive until the loop returns — costs more than it can tell
278
321
  // an operator.
279
- ...(rawBody === null
280
- ? {}
281
- : { body: parseCapturedBody(rawBody.slice(0, PROVIDER_CAPTURE_MAX_BYTES)) }),
322
+ ...(rawBody === null ? {} : { body: parseCapturedBody(rawBody.slice(0, maxBytes)) }),
282
323
  };
283
324
  const response = await inner(input, init);
284
325
  sink.response =
285
326
  captureBody && shouldCaptureProviderResponse(response)
286
- ? readBoundedBody(response.clone(), PROVIDER_CAPTURE_MAX_BYTES).then(parseCapturedBody)
327
+ ? readBoundedBody(response.clone(), maxBytes).then(parseCapturedBody)
287
328
  : Promise.resolve(null);
288
329
  // Read regardless of `captureBody`: this is metadata about the dispatch, not
289
330
  // payload retention, so a tenant in `metadata` mode still records which
@@ -34,6 +34,7 @@ import { ProxyRequestStateStore } from "../request/state";
34
34
  import { ProxyRequestPreparer } from "../request/preparer";
35
35
  import { completeAttempt, estimatedUsage } from "./attempt-finalize";
36
36
  import { runAttemptLoop } from "./attempt-loop";
37
+ import { captureProviderRateLimit } from "./ratelimit-capture";
37
38
 
38
39
  export interface GenerationHandlerDeps {
39
40
  readonly db: CartethyiaDatabase;
@@ -125,6 +126,11 @@ async function passThrough(
125
126
  },
126
127
  ): Promise<Response> {
127
128
  const usage = estimatedUsage(0, 0);
129
+ // The CLI bucket comes back on every upstream response, so this is the free
130
+ // place to learn it: the quota surface then reads the stored value instead of
131
+ // spending a request of its own. Both the manual account test and automatic
132
+ // routing land here, so a captured reading follows real traffic.
133
+ captureProviderRateLimit(args.candidate.provider_id, args.candidate.provider_account_id, upstream.headers);
128
134
  await completeAttempt(state, {
129
135
  status: "completed",
130
136
  providerId: args.candidate.provider_id,
@@ -15,7 +15,11 @@ import { messagesAdapter } from "../surface/messages/adapter";
15
15
  import { completionAdapter } from "../surface/completion";
16
16
  import { forwardedRequestHeaders, proxySuccessHeaders, buildUpstreamDispatchContext, reportedModelFrom } from "./upstream";
17
17
  import { resolvePromptCacheKey } from "../../providers/operations/session-resolution";
18
- import { applyTenantPreferences } from "../request/tenant-preferences";
18
+ import {
19
+ applyTenantPreferences,
20
+ applyTenantDeadlineOverride,
21
+ resolveTenantImageClampMaxBytes,
22
+ } from "../request/tenant-preferences";
19
23
  import { metrics } from "../../observability/metrics";
20
24
  import type { NetworkPoolSelector } from "../../network/pool/selector";
21
25
  import type { TelemetryBatchBuffer } from "../../observability/telemetry-buffer";
@@ -28,6 +32,8 @@ import {
28
32
  completionContext,
29
33
  estimatedUsage,
30
34
  parseCapturedBody,
35
+ providerCaptureMaxBytes,
36
+ resolveCaptureDepthForRequest,
31
37
  shouldCaptureProviderBodyForRequest,
32
38
  terminalFailure,
33
39
  } from "./attempt-finalize";
@@ -126,6 +132,22 @@ export async function handleProviderProxyRequest(
126
132
  if (!prepared)
127
133
  throw new GatewayError("admission_unavailable", 503, "The request was not prepared, so no route can be dialed. Resend the request.");
128
134
  const canonicalRequest = await applyTenantPreferences(prepared, deps.db);
135
+ // Per-tenant timeout, applied as soon as the tenant is known. The deadline
136
+ // was armed at admission with the deployment default; this re-arms it.
137
+ await applyTenantDeadlineOverride(
138
+ prepared.authorization.snapshot.tenant_id,
139
+ deps.db,
140
+ (ms) => state.extendDeadline(ms),
141
+ );
142
+ // Memoized on the request: the ceiling is a tenant setting, so it cannot
143
+ // change between failover attempts, and each wire encoder that clamps an
144
+ // image reads it from here instead of paying for a preference read per
145
+ // attempt.
146
+ const imageClampMaxBytes = await resolveTenantImageClampMaxBytes(
147
+ deps.db,
148
+ prepared.authorization.snapshot.tenant_id,
149
+ );
150
+ if (imageClampMaxBytes !== undefined) state.imageClampMaxBytes = imageClampMaxBytes;
129
151
  const candidates =
130
152
  prepared.eligibleRouteCandidates.length > 0 ? prepared.eligibleRouteCandidates : [prepared.candidate];
131
153
  const attemptCandidates = candidates;
@@ -224,6 +246,9 @@ export async function handleProviderProxyRequest(
224
246
  headers: inboundHeaders,
225
247
  requestId: state.requestId,
226
248
  requestIdentity: state,
249
+ ...(state.imageClampMaxBytes === undefined
250
+ ? {}
251
+ : { imageClampMaxBytes: state.imageClampMaxBytes }),
227
252
  ...(conversationAffinity ? { conversationAffinity } : {}),
228
253
  ...(candidate.user_agent === undefined ? {} : { userAgent: candidate.user_agent }),
229
254
  ...(deps.networkBindingFactory
@@ -239,6 +264,13 @@ export async function handleProviderProxyRequest(
239
264
  prepared.authorization.snapshot.tenant_id,
240
265
  state,
241
266
  ),
267
+ providerCaptureMaxBytes(
268
+ await resolveCaptureDepthForRequest(
269
+ deps.db,
270
+ prepared.authorization.snapshot.tenant_id,
271
+ state,
272
+ ),
273
+ ),
242
274
  ),
243
275
  }
244
276
  : {}),
@@ -0,0 +1,37 @@
1
+ // Files a provider's rate-limit headers against the account that earned them.
2
+ //
3
+ // Providers answer real traffic with the bucket they are metering — Grok's CLI
4
+ // plane returns `x-ratelimit-*` on every response — and that is a reading the
5
+ // gateway already paid for. Capturing it here is what lets the quota surface
6
+ // show a balance without spending a request of its own.
7
+ //
8
+ // Kept provider-scoped and fail-open: a provider with no bucket parser is
9
+ // skipped, and any throw is swallowed, because a bookkeeping miss must never
10
+ // take down a request that already succeeded.
11
+
12
+ import { rememberGrokRateLimit } from "../../providers/integrations/grok/grok-ratelimit-store";
13
+ import { readGrokRateLimitHeaders } from "../../providers/integrations/grok/grok-ratelimit";
14
+
15
+ /** Providers whose response headers carry a readable bucket. */
16
+ const GROK_PROVIDER_IDS: readonly string[] = ["grok"];
17
+
18
+ /**
19
+ * Stores whatever bucket the response carried, keyed by account.
20
+ *
21
+ * No account id means no home for the reading: a bucket filed under the wrong
22
+ * account is worse than none, since it would report another account's balance.
23
+ */
24
+ export function captureProviderRateLimit(
25
+ providerId: string,
26
+ accountId: string | undefined,
27
+ headers: { get(name: string): string | null },
28
+ ): void {
29
+ if (!accountId) return;
30
+ const id = providerId.toLowerCase();
31
+ if (!GROK_PROVIDER_IDS.includes(id)) return;
32
+ try {
33
+ rememberGrokRateLimit(accountId, readGrokRateLimitHeaders(headers));
34
+ } catch {
35
+ // Swallowed: advisory bookkeeping on a request that already succeeded.
36
+ }
37
+ }
@@ -43,7 +43,13 @@ import type { AdmissionLease } from "../../../security/admission/contracts";
43
43
  import type { NetworkPoolSelector } from "../../../network/pool/selector";
44
44
  import type { TelemetryBatchBuffer } from "../../../observability/telemetry-buffer";
45
45
  import type { ProviderExchangeCapture } from "../attempt-finalize";
46
- import { captureProviderExchange, shouldCaptureProviderBodyForRequest } from "../attempt-finalize";
46
+ import {
47
+ captureProviderExchange,
48
+ providerCaptureMaxBytes,
49
+ resolveCaptureDepthForRequest,
50
+ shouldCaptureProviderBodyForRequest,
51
+ } from "../attempt-finalize";
52
+ import { resolveTenantImageClampMaxBytes } from "../../request/tenant-preferences";
47
53
  import { proxySuccessHeaders, buildUpstreamDispatchContext } from "../upstream";
48
54
  import type { AttemptLeaseOwnership } from "../leases";
49
55
  import type { ProviderContinuationScope } from "../../../providers/provider-contracts";
@@ -165,13 +171,32 @@ export async function dispatchStreamingAttempt(input: StreamingDispatchInput): P
165
171
  networkPoolId,
166
172
  prepared.authorization.snapshot.tenant_id,
167
173
  );
174
+ const captureBody = outboundFetch
175
+ ? await shouldCaptureProviderBodyForRequest(deps.db, prepared.authorization.snapshot.tenant_id, state)
176
+ : false;
177
+ const captureDepth = await resolveCaptureDepthForRequest(
178
+ deps.db,
179
+ prepared.authorization.snapshot.tenant_id,
180
+ state,
181
+ );
168
182
  const wrappedOutboundFetch = outboundFetch
169
183
  ? captureProviderExchange(
170
184
  outboundFetch,
171
185
  providerCapture,
172
- await shouldCaptureProviderBodyForRequest(deps.db, prepared.authorization.snapshot.tenant_id, state),
186
+ captureBody,
187
+ providerCaptureMaxBytes(captureDepth),
173
188
  )
174
189
  : undefined;
190
+ // Same memoized ceiling the non-streaming path resolves, read before the
191
+ // dispatch context is built so a stream gets the same limit as a normal
192
+ // request rather than silently falling back to the deployment default.
193
+ if (state.imageClampMaxBytes === undefined) {
194
+ const clampMaxBytes = await resolveTenantImageClampMaxBytes(
195
+ deps.db,
196
+ prepared.authorization.snapshot.tenant_id,
197
+ );
198
+ if (clampMaxBytes !== undefined) state.imageClampMaxBytes = clampMaxBytes;
199
+ }
175
200
  const dispatchContext = buildUpstreamDispatchContext({
176
201
  credential,
177
202
  deadline: state.deadlineMs,
@@ -179,6 +204,9 @@ export async function dispatchStreamingAttempt(input: StreamingDispatchInput): P
179
204
  headers: inboundHeaders,
180
205
  requestId: state.requestId,
181
206
  requestIdentity: state,
207
+ ...(state.imageClampMaxBytes === undefined
208
+ ? {}
209
+ : { imageClampMaxBytes: state.imageClampMaxBytes }),
182
210
  ...(conversationAffinity ? { conversationAffinity } : {}),
183
211
  ...(candidate.user_agent === undefined ? {} : { userAgent: candidate.user_agent }),
184
212
  ...(wrappedOutboundFetch ? { outboundFetch: wrappedOutboundFetch } : {}),
@@ -56,6 +56,12 @@ export function buildUpstreamDispatchContext(input: {
56
56
  readonly continuationScope?: ProviderContinuationScope;
57
57
  /** Stable per-conversation cache affinity (preferred over random ids). */
58
58
  readonly conversationAffinity?: string;
59
+ /**
60
+ * Per-tenant inline-attachment ceiling in bytes, resolved once per request.
61
+ * Carried through dispatch because wire encoders clamp images but hold no
62
+ * database handle.
63
+ */
64
+ readonly imageClampMaxBytes?: number;
59
65
  /** Opaque identity shared by retries of one logical gateway request. */
60
66
  readonly requestIdentity?: object;
61
67
  }): Record<string, unknown> {
@@ -94,6 +100,9 @@ export function buildUpstreamDispatchContext(input: {
94
100
  ...(input.leaseOwnership ? { lease_ownership: input.leaseOwnership } : {}),
95
101
  ...(input.continuationScope ? { continuation_scope: input.continuationScope } : {}),
96
102
  ...(input.conversationAffinity ? { conversation_affinity: input.conversationAffinity } : {}),
103
+ ...(input.imageClampMaxBytes === undefined
104
+ ? {}
105
+ : { image_clamp_max_bytes: input.imageClampMaxBytes }),
97
106
  ...(input.requestIdentity ? { request_identity: input.requestIdentity } : {}),
98
107
  } as Record<string, unknown>;
99
108
  }
@@ -85,12 +85,13 @@ export function resolveContinuationAffinity(
85
85
  // id replayed from another key or tenant resolves nothing.
86
86
  const record = lookupPendingCall(callId, scope);
87
87
  if (record === undefined) {
88
- throw new GatewayError(
89
- "invalid_request",
90
- 400,
91
- "This tool result refers to a Cursor tool call that is no longer pending. Resend the complete conversation.",
92
- { callId },
93
- );
88
+ // A replayed conversation carries every earlier tool result, and results
89
+ // previous legs already answered resolve nothing here — they are history,
90
+ // not the flight being continued. The request is a continuation only
91
+ // through the ids that still resolve; when none do, the ordinary routing
92
+ // path decides (a Cursor adapter may rebase the full history into a
93
+ // fresh Run).
94
+ continue;
94
95
  }
95
96
  if (affinity !== undefined && affinity.providerAccountId !== record.providerAccountId) {
96
97
  throw new GatewayError(
@@ -117,6 +117,15 @@ export interface ProxyRequestState {
117
117
  payloadCapturePreference?:
118
118
  | { readonly ok: true; readonly mode: TelemetryPayloadMode; readonly depth: TelemetryPayloadDepth }
119
119
  | { readonly ok: false };
120
+ /**
121
+ * This request's resolved inline-attachment ceiling, memoized for the same
122
+ * reason the capture preference is: it is a tenant setting, so it cannot
123
+ * change between attempts, and reading it per attempt would pay for it
124
+ * several times on a request that fails over.
125
+ *
126
+ * `undefined` means the deployment default.
127
+ */
128
+ imageClampMaxBytes?: number;
120
129
  /**
121
130
  * Set by the dispatch handler synchronously before returning a streaming
122
131
  * response. `afterResponse` lifecycle hooks read it to skip finalization
@@ -13,6 +13,66 @@ import type { PreparedProxyRequest } from "./preparer";
13
13
  import { normalizeThinkingConfig } from "../translation/thinking";
14
14
  import { compressRequest } from "./rtk/compress-request";
15
15
  import { preferencesReaderFor } from "../../persistence/tenant-preferences";
16
+ import { resolveUpstreamTimeoutMs } from "../../config/timeouts";
17
+
18
+ /**
19
+ * Re-arms the request deadline from the tenant's timeout override.
20
+ *
21
+ * The initial deadline is armed before authentication, when the tenant is not
22
+ * yet known, so a per-tenant timeout can only be applied here — once the
23
+ * snapshot says who is asking. `extendDeadline` keeps the timer, the state
24
+ * field, and the in-flight record in step, so the backstop sweep cannot
25
+ * force-release a request whose budget legitimately grew.
26
+ *
27
+ * Non-fatal: a settings read outage leaves the deployment default in place
28
+ * rather than failing a dispatchable request.
29
+ */
30
+ export async function applyTenantDeadlineOverride(
31
+ tenantId: string,
32
+ db: CartethyiaDatabase,
33
+ extend: (ms: number) => void,
34
+ ): Promise<void> {
35
+ try {
36
+ const prefs = await preferencesReaderFor(db).readPreferences(tenantId);
37
+ const seconds = prefs?.upstreamTimeoutSeconds;
38
+ if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds <= 0) return;
39
+ const wanted = Math.floor(seconds * 1000);
40
+ // Only ever shorten or leave alone a deadline that has already begun: an
41
+ // override longer than the default cannot give back time the request has
42
+ // already spent, and arming a longer one would silently extend a budget
43
+ // the caller's own client has likely abandoned.
44
+ const remaining = Math.min(wanted, resolveUpstreamTimeoutMs());
45
+ extend(remaining);
46
+ } catch {
47
+ // Non-fatal
48
+ }
49
+ }
50
+
51
+ /**
52
+ * The tenant's inline-attachment byte ceiling, or `undefined` for the default.
53
+ *
54
+ * Read through the shared revision-keyed preference reader, so the cost is a
55
+ * memory hit rather than a Postgres round trip — this is consulted while
56
+ * building a request body, on the dispatch path.
57
+ *
58
+ * Non-fatal: a settings read outage falls back to the deployment default,
59
+ * which is the safe direction (the default refuses less, but it never admits
60
+ * something no upstream will take).
61
+ */
62
+ export async function resolveTenantImageClampMaxBytes(
63
+ db: CartethyiaDatabase,
64
+ tenantId: string | undefined,
65
+ ): Promise<number | undefined> {
66
+ if (tenantId === undefined) return undefined;
67
+ try {
68
+ const prefs = await preferencesReaderFor(db).readPreferences(tenantId);
69
+ const bytes = prefs?.imageClampMaxBytes;
70
+ if (typeof bytes !== "number" || !Number.isFinite(bytes) || bytes <= 0) return undefined;
71
+ return Math.floor(bytes);
72
+ } catch {
73
+ return undefined;
74
+ }
75
+ }
16
76
 
17
77
  /**
18
78
  * Applies request shaping owned by tenant preferences and deployment config: