cartethyia 2.5.9 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (386) hide show
  1. package/.env.example +15 -42
  2. package/README.md +11 -10
  3. package/dist/dashboard/assets/{BannedPage-Brvuj_Eb.js → BannedPage-l6XMnfr9.js} +1 -1
  4. package/dist/dashboard/assets/BannedPage-l6XMnfr9.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolDetailPage-CqfXtI3y.js → CliToolDetailPage-DwOzAW45.js} +1 -1
  6. package/dist/dashboard/assets/CliToolDetailPage-DwOzAW45.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-GQeS91bg.js → CliToolsPage-BT3Up3QA.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-BT3Up3QA.js.gz +0 -0
  9. package/dist/dashboard/assets/{CombosPage-BDRsTXBB.js → CombosPage-6kYIUpzR.js} +1 -1
  10. package/dist/dashboard/assets/CombosPage-6kYIUpzR.js.gz +0 -0
  11. package/dist/dashboard/assets/{ConsoleLogPage-zP8AFHX0.js → ConsoleLogPage-zRnj1mlR.js} +1 -1
  12. package/dist/dashboard/assets/ConsoleLogPage-zRnj1mlR.js.gz +0 -0
  13. package/dist/dashboard/assets/{CustomizationPage-BmfrNZ14.js → CustomizationPage-C7m-H1Rz.js} +1 -1
  14. package/dist/dashboard/assets/CustomizationPage-C7m-H1Rz.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js.gz +0 -0
  17. package/dist/dashboard/assets/{LoginPage-DoreIgm8.js → LoginPage-CbphLvjg.js} +1 -1
  18. package/dist/dashboard/assets/LoginPage-CbphLvjg.js.gz +0 -0
  19. package/dist/dashboard/assets/{ModelPicker-C7pDb41R.js → ModelPicker-DVny4S5_.js} +1 -1
  20. package/dist/dashboard/assets/ModelPicker-DVny4S5_.js.gz +0 -0
  21. package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js +1 -0
  22. package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js.gz +0 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js +3 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js.gz +0 -0
  25. package/dist/dashboard/assets/{ProvidersPage-BnoPgA7w.js → ProvidersPage-Dfb2896L.js} +1 -1
  26. package/dist/dashboard/assets/ProvidersPage-Dfb2896L.js.gz +0 -0
  27. package/dist/dashboard/assets/{ProxyPage-Bq_TGgdb.js → ProxyPage-BaCIdAUt.js} +1 -1
  28. package/dist/dashboard/assets/ProxyPage-BaCIdAUt.js.gz +0 -0
  29. package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js +1 -0
  30. package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js.gz +0 -0
  31. package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js +1 -0
  32. package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js.gz +0 -0
  33. package/dist/dashboard/assets/{SetupPage-Day3mCOn.js → SetupPage-D4g6vOGc.js} +1 -1
  34. package/dist/dashboard/assets/SetupPage-D4g6vOGc.js.gz +0 -0
  35. package/dist/dashboard/assets/{SortableList-DQxllnB5.js → SortableList-C93i14BD.js} +1 -1
  36. package/dist/dashboard/assets/SortableList-C93i14BD.js.gz +0 -0
  37. package/dist/dashboard/assets/{StudioPage-uMExoWu1.js → StudioPage-SgQpymjm.js} +1 -1
  38. package/dist/dashboard/assets/{StudioPage-uMExoWu1.js.gz → StudioPage-SgQpymjm.js.gz} +0 -0
  39. package/dist/dashboard/assets/{UsagePage-DXa1oR-4.js → UsagePage-BFME-eba.js} +2 -2
  40. package/dist/dashboard/assets/UsagePage-BFME-eba.js.gz +0 -0
  41. package/dist/dashboard/assets/{api-keys-DQYLQxpW.js → api-keys-C23S9sgp.js} +1 -1
  42. package/dist/dashboard/assets/api-keys-C23S9sgp.js.gz +0 -0
  43. package/dist/dashboard/assets/{app-BIt446yE.js → app-2KhtHCv9.js} +1 -1
  44. package/dist/dashboard/assets/{app-BIt446yE.js.gz → app-2KhtHCv9.js.gz} +0 -0
  45. package/dist/dashboard/assets/app-CEeaMHOy.css +2 -0
  46. package/dist/dashboard/assets/app-CEeaMHOy.css.gz +0 -0
  47. package/dist/dashboard/assets/{assert-C9FD_JwJ.js → assert-DU6qrMA_.js} +1 -1
  48. package/dist/dashboard/assets/assert-DU6qrMA_.js.gz +0 -0
  49. package/dist/dashboard/assets/{badge-CR72dasT.js → badge-CKsF123d.js} +1 -1
  50. package/dist/dashboard/assets/badge-CKsF123d.js.gz +0 -0
  51. package/dist/dashboard/assets/{banner-CI9_eSfO.js → banner-8j7uChA9.js} +1 -1
  52. package/dist/dashboard/assets/banner-8j7uChA9.js.gz +0 -0
  53. package/dist/dashboard/assets/{card-DS7CQEsO.js → card-CdsSlWA3.js} +1 -1
  54. package/dist/dashboard/assets/card-CdsSlWA3.js.gz +0 -0
  55. package/dist/dashboard/assets/{cli-tools-CNqzrfdb.js → cli-tools-BaFUuFFF.js} +1 -1
  56. package/dist/dashboard/assets/cli-tools-BaFUuFFF.js.gz +0 -0
  57. package/dist/dashboard/assets/{clipboard-button-611pSsaX.js → clipboard-button-uL5aYLfW.js} +1 -1
  58. package/dist/dashboard/assets/clipboard-button-uL5aYLfW.js.gz +0 -0
  59. package/dist/dashboard/assets/{dialog-k37exjNt.js → dialog-C07hr5_G.js} +1 -1
  60. package/dist/dashboard/assets/dialog-C07hr5_G.js.gz +0 -0
  61. package/dist/dashboard/assets/{drawer-CX7N8ucR.js → drawer-BPO1TcKH.js} +1 -1
  62. package/dist/dashboard/assets/drawer-BPO1TcKH.js.gz +0 -0
  63. package/dist/dashboard/assets/{entry-CTP8AyiS.js → entry-Bz_CtDfC.js} +1 -1
  64. package/dist/dashboard/assets/entry-Bz_CtDfC.js.gz +0 -0
  65. package/dist/dashboard/assets/entry-CwPRKk0d.js +4 -0
  66. package/dist/dashboard/assets/entry-CwPRKk0d.js.gz +0 -0
  67. package/dist/dashboard/assets/entry-GJRbmrpA.js +1 -0
  68. package/dist/dashboard/assets/entry-GJRbmrpA.js.gz +0 -0
  69. package/dist/dashboard/assets/{github-badge-sltk1qUi.js → github-badge-DmQreSDd.js} +1 -1
  70. package/dist/dashboard/assets/github-badge-DmQreSDd.js.gz +0 -0
  71. package/dist/dashboard/assets/{index-z3zDOSMM.js → index-Bblu6erR.js} +2 -2
  72. package/dist/dashboard/assets/index-Bblu6erR.js.gz +0 -0
  73. package/dist/dashboard/assets/{inline-BhY2sSnO.js → inline-C9dCYaBZ.js} +1 -1
  74. package/dist/dashboard/assets/inline-C9dCYaBZ.js.gz +0 -0
  75. package/dist/dashboard/assets/{input-V1umFiBk.js → input-CjCnXweJ.js} +1 -1
  76. package/dist/dashboard/assets/input-CjCnXweJ.js.gz +0 -0
  77. package/dist/dashboard/assets/{layout-BBOYycyS.js → layout-V6y8DAkl.js} +1 -1
  78. package/dist/dashboard/assets/layout-V6y8DAkl.js.gz +0 -0
  79. package/dist/dashboard/assets/{list-row-1zzZYM0B.js → list-row-CM1AaG_y.js} +1 -1
  80. package/dist/dashboard/assets/list-row-CM1AaG_y.js.gz +0 -0
  81. package/dist/dashboard/assets/{live-BivGfuUs.js → live-ta6GMb-8.js} +1 -1
  82. package/dist/dashboard/assets/live-ta6GMb-8.js.gz +0 -0
  83. package/dist/dashboard/assets/{model-bans-CmffjLuG.js → model-bans-BmYTmbAk.js} +1 -1
  84. package/dist/dashboard/assets/model-bans-BmYTmbAk.js.gz +0 -0
  85. package/dist/dashboard/assets/{mutation-46P0oWMe.js → mutation-g_fduiNw.js} +1 -1
  86. package/dist/dashboard/assets/mutation-g_fduiNw.js.gz +0 -0
  87. package/dist/dashboard/assets/{numbers-DkfMR5Qs.js → numbers-BOevJzdz.js} +1 -1
  88. package/dist/dashboard/assets/numbers-BOevJzdz.js.gz +0 -0
  89. package/dist/dashboard/assets/{query-keys-BFhGpwR1.js → query-keys-UETqfytp.js} +1 -1
  90. package/dist/dashboard/assets/query-keys-UETqfytp.js.gz +0 -0
  91. package/dist/dashboard/assets/{routing-CAb4F37R.js → routing-BPUo-W8d.js} +1 -1
  92. package/dist/dashboard/assets/routing-BPUo-W8d.js.gz +0 -0
  93. package/dist/dashboard/assets/{select-C6MMiaio.js → select-C980c4cz.js} +1 -1
  94. package/dist/dashboard/assets/select-C980c4cz.js.gz +0 -0
  95. package/dist/dashboard/assets/{settings-DNX9sydi.js → settings--3hxSyry.js} +1 -1
  96. package/dist/dashboard/assets/settings--3hxSyry.js.gz +0 -0
  97. package/dist/dashboard/assets/{stack-BHTfXdz_.js → stack-_gfTdLVB.js} +1 -1
  98. package/dist/dashboard/assets/stack-_gfTdLVB.js.gz +0 -0
  99. package/dist/dashboard/assets/{state-mPDRlQkl.js → state-2c5V4zo7.js} +1 -1
  100. package/dist/dashboard/assets/state-2c5V4zo7.js.gz +0 -0
  101. package/dist/dashboard/assets/{switch-DOmJxg7H.js → switch-Dkke7Naf.js} +1 -1
  102. package/dist/dashboard/assets/switch-Dkke7Naf.js.gz +0 -0
  103. package/dist/dashboard/assets/{useMutation-Dem0lXQj.js → useMutation-EDFRkn1i.js} +1 -1
  104. package/dist/dashboard/assets/useMutation-EDFRkn1i.js.gz +0 -0
  105. package/dist/dashboard/index.html +1 -1
  106. package/dist/dashboard/index.html.gz +0 -0
  107. package/dist/dashboard/precompressed-assets.json +51 -51
  108. package/migrations/README.md +52 -52
  109. package/migrations/full-postgres/0020_add_telemetry_event_analytics.sql +77 -0
  110. package/migrations/full-postgres/0021_add_api_key_concurrency_tolerance.sql +13 -0
  111. package/migrations/full-postgres/0022_add_inferhub_config_key.sql +17 -0
  112. package/migrations/full-postgres/0023_drop_api_key_concurrency_tolerance.sql +13 -0
  113. package/migrations/lite-sqlite3/0020_add_telemetry_event_analytics.sql +69 -0
  114. package/migrations/lite-sqlite3/0021_add_api_key_concurrency_tolerance.sql +13 -0
  115. package/migrations/lite-sqlite3/0022_add_inferhub_config_key.sql +17 -0
  116. package/migrations/lite-sqlite3/0023_drop_api_key_concurrency_tolerance.sql +13 -0
  117. package/package.json +96 -97
  118. package/scripts/cli/admin.ts +126 -125
  119. package/scripts/cli/autostart.ts +237 -237
  120. package/scripts/cli/background.ts +120 -110
  121. package/scripts/cli/bin/carte.cjs +8 -8
  122. package/scripts/cli/bin/cartethyia.cjs +167 -167
  123. package/scripts/cli/console-menu.ts +308 -275
  124. package/scripts/cli/controller.ts +145 -0
  125. package/scripts/cli/doctor.ts +1 -1
  126. package/scripts/cli/env.ts +99 -99
  127. package/scripts/cli/gateway.ts +264 -261
  128. package/scripts/{health.ts → cli/health.ts} +4 -4
  129. package/scripts/cli/instance.ts +234 -224
  130. package/scripts/cli/main.ts +188 -164
  131. package/scripts/cli/platform.ts +64 -64
  132. package/scripts/cli/prompt.ts +275 -107
  133. package/scripts/cli/services.ts +88 -88
  134. package/scripts/cli/setup.ts +445 -445
  135. package/scripts/cli/start.ts +85 -82
  136. package/scripts/cli/startup-signal.ts +23 -23
  137. package/scripts/cli/telemetry.ts +70 -0
  138. package/scripts/cli/tray/index.ts +263 -263
  139. package/scripts/cli/tray/windows.ps1 +107 -107
  140. package/scripts/cli/tray.ts +192 -208
  141. package/scripts/cli/update.ts +39 -39
  142. package/scripts/cli/version.ts +67 -67
  143. package/scripts/cli/wordmark.ts +58 -58
  144. package/src/boot/app.ts +16 -1
  145. package/src/boot/dependencies.ts +128 -25
  146. package/src/config/package-paths.ts +24 -0
  147. package/src/config/resources.ts +137 -0
  148. package/src/config/runtime.ts +133 -76
  149. package/src/config/spec.ts +30 -29
  150. package/src/config/telemetry.ts +0 -25
  151. package/src/console/api-keys/bucket-spend.ts +14 -17
  152. package/src/console/api-keys/contracts.ts +410 -410
  153. package/src/console/api-keys/routes.ts +12 -0
  154. package/src/console/audit/store.ts +2 -3
  155. package/src/console/auth/setup-service.ts +8 -1
  156. package/src/console/backup/routes.ts +7 -0
  157. package/src/console/backup/service.ts +463 -463
  158. package/src/console/backup/store.ts +16 -11
  159. package/src/console/backup/validate.ts +1 -4
  160. package/src/console/cli-tools/fs-ops.ts +18 -2
  161. package/src/console/data-management/contracts.ts +1 -8
  162. package/src/console/data-management/operations.ts +61 -48
  163. package/src/console/data-management/routes.ts +4 -2
  164. package/src/console/domain-registration.ts +25 -3
  165. package/src/console/inferhub/client.ts +139 -0
  166. package/src/console/inferhub/key-store.ts +129 -0
  167. package/src/console/inferhub/normalize.ts +347 -0
  168. package/src/console/inferhub/routes.ts +411 -0
  169. package/src/console/inferhub/store.ts +109 -0
  170. package/src/console/inferhub/types.ts +183 -0
  171. package/src/console/observability/contracts.ts +2 -41
  172. package/src/console/observability/store.ts +176 -371
  173. package/src/console/providers/catalog/public-model-store.ts +0 -0
  174. package/src/console/providers/catalog/store.ts +23 -36
  175. package/src/console/quota/account-quota-shared.ts +52 -52
  176. package/src/console/quota/account-quota-tenant.ts +16 -24
  177. package/src/console/quota/refresh.ts +441 -441
  178. package/src/console/remote-support/service.ts +270 -114
  179. package/src/console/routing/pools/import-parse.ts +206 -206
  180. package/src/console/routing/pools/operations.ts +543 -543
  181. package/src/console/routing/pools/probe-verdict.ts +132 -132
  182. package/src/console/routing/pools/routes.ts +181 -181
  183. package/src/console/routing/pools/store.ts +1 -1
  184. package/src/console/settings/contracts.ts +42 -40
  185. package/src/console/settings/store.ts +25 -25
  186. package/src/console/share/share-stats.ts +22 -4
  187. package/src/console/share/share-usage.ts +30 -4
  188. package/src/console/shared/cursor.ts +15 -0
  189. package/src/console/updates/repository-updates.ts +40 -39
  190. package/src/console/updates/routes.ts +12 -0
  191. package/src/main.ts +13 -5
  192. package/src/network/pool/resolver.ts +1104 -1104
  193. package/src/observability/client-name.ts +22 -0
  194. package/src/observability/event-log.ts +5 -29
  195. package/src/observability/event-query.ts +690 -0
  196. package/src/observability/log-ring.ts +26 -2
  197. package/src/observability/log-sanitize.ts +118 -0
  198. package/src/observability/logger.ts +17 -13
  199. package/src/observability/metrics.ts +35 -0
  200. package/src/observability/payload-capture.ts +114 -41
  201. package/src/observability/payload-store.ts +249 -24
  202. package/src/observability/raw-event-writer.ts +118 -0
  203. package/src/observability/runtime-metrics.ts +71 -1
  204. package/src/observability/telemetry-buffer.ts +100 -17
  205. package/src/observability/telemetry-reconciliation.ts +417 -0
  206. package/src/persistence/boot.ts +20 -9
  207. package/src/persistence/db-pg.ts +13 -0
  208. package/src/persistence/db-sqlite.ts +569 -497
  209. package/src/persistence/dialect.ts +3 -2
  210. package/src/persistence/migrate.ts +181 -181
  211. package/src/persistence/schema.ts +1496 -1406
  212. package/src/persistence/shared-cache.ts +5 -1
  213. package/src/persistence/stores/api-key-store.ts +581 -581
  214. package/src/persistence/stores/telemetry-store.ts +124 -63
  215. package/src/protocol/reasoning-text.ts +30 -0
  216. package/src/protocol/registry.ts +2 -0
  217. package/src/protocol/response/chat.ts +33 -7
  218. package/src/protocol/response/codex.ts +14 -2
  219. package/src/protocol/response/messages.ts +5 -2
  220. package/src/providers/authentication/oauth-flow-store.ts +3 -10
  221. package/src/providers/bundled-discovery-providers.ts +122 -122
  222. package/src/providers/default-registry.ts +14 -0
  223. package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
  224. package/src/providers/discovery/models-dev-catalog.ts +354 -354
  225. package/src/providers/discovery/probe-phases.ts +43 -7
  226. package/src/providers/discovery/probing-service.ts +70 -32
  227. package/src/providers/integrations/antigravity/antigravity.ts +9 -2
  228. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +85 -17
  229. package/src/providers/integrations/buddy/buddy-chat-shared.ts +934 -934
  230. package/src/providers/integrations/buddy/buddy-checkin.ts +5 -9
  231. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +3 -7
  232. package/src/providers/integrations/buddy/buddy-quota-shared.ts +3 -1
  233. package/src/providers/integrations/buddy/capabilities.ts +4 -0
  234. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +57 -49
  235. package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -180
  236. package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -206
  237. package/src/providers/integrations/buddy/workbuddy/quota.ts +161 -161
  238. package/src/providers/integrations/claude/claude-quota.ts +2 -2
  239. package/src/providers/integrations/claude/claude-user-profiles.ts +3 -2
  240. package/src/providers/integrations/claude/claude.ts +3 -4
  241. package/src/providers/integrations/cline/cline.ts +94 -8
  242. package/src/providers/integrations/codex/codex.ts +61 -37
  243. package/src/providers/integrations/cursor/cursor-catalog.ts +368 -368
  244. package/src/providers/integrations/cursor/cursor-http2.ts +300 -300
  245. package/src/providers/integrations/cursor/cursor-identity.ts +2 -4
  246. package/src/providers/integrations/cursor/cursor-request.ts +11 -0
  247. package/src/providers/integrations/cursor/cursor.ts +29 -17
  248. package/src/providers/integrations/cursor/protocol/framing.ts +129 -129
  249. package/src/providers/integrations/cursor/protocol/messages.ts +2380 -2380
  250. package/src/providers/integrations/cursor/protocol/struct.ts +305 -305
  251. package/src/providers/integrations/cursor/protocol/tool-canonical.ts +275 -0
  252. package/src/providers/integrations/github/github.ts +3 -1
  253. package/src/providers/integrations/grok/grok-quota.ts +279 -279
  254. package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -73
  255. package/src/providers/integrations/grok/grok-ratelimit.ts +218 -218
  256. package/src/providers/integrations/hermes.ts +7 -1
  257. package/src/providers/integrations/inferhub.ts +201 -73
  258. package/src/providers/integrations/kilo/kilo.ts +152 -152
  259. package/src/providers/integrations/kiro/kiro-machine-id.ts +2 -4
  260. package/src/providers/integrations/opencode/provider.ts +2 -0
  261. package/src/providers/integrations/openrouter/provider.ts +129 -129
  262. package/src/providers/integrations/qoder/qoder-quota.ts +193 -192
  263. package/src/providers/integrations/qoder/qoder-realm.ts +106 -106
  264. package/src/providers/integrations/trae/trae-headers.ts +17 -48
  265. package/src/providers/integrations/trae/trae-oauth.ts +4 -26
  266. package/src/providers/integrations/trae/trae-quota.ts +389 -384
  267. package/src/providers/integrations/trae/trae-realm.ts +15 -2
  268. package/src/providers/integrations/trae/trae.ts +22 -24
  269. package/src/providers/integrations/xiaomi-mimo/think-stream.ts +100 -24
  270. package/src/providers/integrations/zcode/oauth.ts +1 -1
  271. package/src/providers/operations/account-health-service.ts +605 -279
  272. package/src/providers/provider-metadata.ts +12 -1
  273. package/src/providers/provider-model-definition.ts +373 -373
  274. package/src/providers/provider-probe.ts +3 -0
  275. package/src/providers/quota/quota-contracts.ts +3 -7
  276. package/src/providers/search/search-provider.ts +2 -6
  277. package/src/security/admission/contracts.ts +10 -0
  278. package/src/security/admission/in-memory-store.ts +89 -14
  279. package/src/security/admission/lua.ts +191 -182
  280. package/src/security/admission/redis-store.ts +30 -0
  281. package/src/security/admission/service.ts +18 -0
  282. package/src/security/api-key-auth-cache.ts +96 -0
  283. package/src/security/api-key-auth.ts +11 -100
  284. package/src/security/client-router-fingerprint.ts +3 -7
  285. package/src/security/ip-boundary.ts +7 -3
  286. package/src/shared/coerce.ts +67 -0
  287. package/src/shared/hash.ts +6 -0
  288. package/src/shared/time.ts +7 -0
  289. package/src/transport/dispatch/attempt-finalize.ts +26 -138
  290. package/src/transport/dispatch/attempt-loop.ts +103 -81
  291. package/src/transport/dispatch/capture-policy.ts +78 -0
  292. package/src/transport/dispatch/proxy-request.ts +4 -2
  293. package/src/transport/dispatch/streaming/encoder.ts +111 -5
  294. package/src/transport/dispatch/streaming/stream.ts +19 -2
  295. package/src/transport/dispatch/streaming/thinking-loop.ts +300 -0
  296. package/src/transport/failure-policy.ts +27 -0
  297. package/src/transport/guards/abuse-guard.ts +2 -2
  298. package/src/transport/guards/auth-guard.ts +17 -0
  299. package/src/transport/guards/body-policy.ts +3 -2
  300. package/src/transport/guards/concurrency-guard.ts +89 -13
  301. package/src/transport/guards/early-rejection-capture.ts +43 -3
  302. package/src/transport/guards/error-lifecycle.ts +10 -2
  303. package/src/transport/guards/guard-errors.ts +23 -0
  304. package/src/transport/guards/headroom-guard.ts +41 -7
  305. package/src/transport/guards/pipeline.ts +24 -0
  306. package/src/transport/guards/request-context.ts +2 -1
  307. package/src/transport/routing/admission.ts +51 -9
  308. package/src/transport/routing/bounded-lru.ts +49 -0
  309. package/src/transport/routing/candidate-index.ts +88 -0
  310. package/src/transport/routing/reservations.ts +121 -7
  311. package/src/transport/routing/round-robin.ts +10 -22
  312. package/src/transport/routing/route-catalog.ts +26 -13
  313. package/src/transport/routing/route-model.ts +22 -2
  314. package/src/transport/routing/router.ts +53 -31
  315. package/src/transport/surface/chat/encode.ts +2 -3
  316. package/src/transport/surface/chat/parse.ts +2 -1
  317. package/src/transport/surface/dialects.ts +11 -3
  318. package/src/transport/surface/messages/encode.ts +2 -1
  319. package/src/transport/surface/messages/parse.ts +3 -1
  320. package/src/transport/surface/messages/stream.ts +2 -6
  321. package/src/workers/maintenance-lease.ts +200 -0
  322. package/src/workers/scheduler.ts +101 -1
  323. package/dist/dashboard/assets/BannedPage-Brvuj_Eb.js.gz +0 -0
  324. package/dist/dashboard/assets/CliToolDetailPage-CqfXtI3y.js.gz +0 -0
  325. package/dist/dashboard/assets/CliToolsPage-GQeS91bg.js.gz +0 -0
  326. package/dist/dashboard/assets/CombosPage-BDRsTXBB.js.gz +0 -0
  327. package/dist/dashboard/assets/ConsoleLogPage-zP8AFHX0.js.gz +0 -0
  328. package/dist/dashboard/assets/CustomizationPage-BmfrNZ14.js.gz +0 -0
  329. package/dist/dashboard/assets/HealthEventsModal-Dn3M6WZc.js +0 -1
  330. package/dist/dashboard/assets/HealthEventsModal-Dn3M6WZc.js.gz +0 -0
  331. package/dist/dashboard/assets/LoginPage-DoreIgm8.js.gz +0 -0
  332. package/dist/dashboard/assets/ModelPicker-C7pDb41R.js.gz +0 -0
  333. package/dist/dashboard/assets/OverviewPage-CvvD1Ugk.js +0 -1
  334. package/dist/dashboard/assets/OverviewPage-CvvD1Ugk.js.gz +0 -0
  335. package/dist/dashboard/assets/ProviderDetailPage-Clbn1gsW.js +0 -3
  336. package/dist/dashboard/assets/ProviderDetailPage-Clbn1gsW.js.gz +0 -0
  337. package/dist/dashboard/assets/ProvidersPage-BnoPgA7w.js.gz +0 -0
  338. package/dist/dashboard/assets/ProxyPage-Bq_TGgdb.js.gz +0 -0
  339. package/dist/dashboard/assets/QuotaPage-D9DHXvom.js +0 -1
  340. package/dist/dashboard/assets/QuotaPage-D9DHXvom.js.gz +0 -0
  341. package/dist/dashboard/assets/SettingsPage-7xV8WkWL.js +0 -1
  342. package/dist/dashboard/assets/SettingsPage-7xV8WkWL.js.gz +0 -0
  343. package/dist/dashboard/assets/SetupPage-Day3mCOn.js.gz +0 -0
  344. package/dist/dashboard/assets/SortableList-DQxllnB5.js.gz +0 -0
  345. package/dist/dashboard/assets/UsagePage-DXa1oR-4.js.gz +0 -0
  346. package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
  347. package/dist/dashboard/assets/app-CX_Z8cMs.css +0 -2
  348. package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
  349. package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
  350. package/dist/dashboard/assets/badge-CR72dasT.js.gz +0 -0
  351. package/dist/dashboard/assets/banner-CI9_eSfO.js.gz +0 -0
  352. package/dist/dashboard/assets/card-DS7CQEsO.js.gz +0 -0
  353. package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
  354. package/dist/dashboard/assets/clipboard-button-611pSsaX.js.gz +0 -0
  355. package/dist/dashboard/assets/dialog-k37exjNt.js.gz +0 -0
  356. package/dist/dashboard/assets/drawer-CX7N8ucR.js.gz +0 -0
  357. package/dist/dashboard/assets/entry-B9Xvtnfj.js +0 -1
  358. package/dist/dashboard/assets/entry-B9Xvtnfj.js.gz +0 -0
  359. package/dist/dashboard/assets/entry-ByMGkR3C.js +0 -4
  360. package/dist/dashboard/assets/entry-ByMGkR3C.js.gz +0 -0
  361. package/dist/dashboard/assets/entry-CTP8AyiS.js.gz +0 -0
  362. package/dist/dashboard/assets/github-badge-sltk1qUi.js.gz +0 -0
  363. package/dist/dashboard/assets/index-z3zDOSMM.js.gz +0 -0
  364. package/dist/dashboard/assets/inline-BhY2sSnO.js.gz +0 -0
  365. package/dist/dashboard/assets/input-V1umFiBk.js.gz +0 -0
  366. package/dist/dashboard/assets/layout-BBOYycyS.js.gz +0 -0
  367. package/dist/dashboard/assets/list-row-1zzZYM0B.js.gz +0 -0
  368. package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
  369. package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
  370. package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
  371. package/dist/dashboard/assets/numbers-DkfMR5Qs.js.gz +0 -0
  372. package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
  373. package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
  374. package/dist/dashboard/assets/select-C6MMiaio.js.gz +0 -0
  375. package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
  376. package/dist/dashboard/assets/stack-BHTfXdz_.js.gz +0 -0
  377. package/dist/dashboard/assets/state-mPDRlQkl.js.gz +0 -0
  378. package/dist/dashboard/assets/switch-DOmJxg7H.js.gz +0 -0
  379. package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
  380. package/scripts/build/aot.ts +0 -143
  381. package/scripts/build/build.ts +0 -24
  382. package/scripts/build/compile.ts +0 -88
  383. package/scripts/build/restart.ts +0 -120
  384. package/scripts/build/start.ts +0 -25
  385. package/src/observability/event-compaction.ts +0 -317
  386. package/src/observability/event-day-index.ts +0 -440
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Canonical resource policy: how much memory this process may use, and how
3
+ * much work it may hold at once.
4
+ *
5
+ * These two numbers were previously decided in three unrelated places — a
6
+ * hardcoded cap in `concurrency-guard.ts`, a host-wide `os.totalmem()` in the
7
+ * headroom guard, and a fallback constant in `runtime-metrics.ts` — which
8
+ * meant a container limited to 512 MiB admitted the same concurrency as a 4 GiB
9
+ * one and only started shedding once the *host* was nearly full. On a 1-core /
10
+ * 2 GiB VPS that is the difference between shedding early and being
11
+ * OOM-killed, and the OOM kills the dashboard too — the one surface that would
12
+ * have explained what happened.
13
+ *
14
+ * Everything here is derived from one limit and one body ceiling, so the
15
+ * numbers cannot disagree with each other:
16
+ *
17
+ * concurrency = floor(limit × BUDGET_FRACTION / (TRANSIENT_COPIES × body))
18
+ *
19
+ * The transient-copies factor is why this is not simply "limit / body": a
20
+ * request body is held more than once between the socket read, the JSON parse,
21
+ * and the upstream re-serialization, so budgeting one copy per request
22
+ * understates the real hold by a factor of about two.
23
+ */
24
+ import { CONFIG_SPEC, readOptionalInt } from "./spec";
25
+ import { resolveMaxBodyBytes } from "./runtime";
26
+
27
+ /** Used when no limit is discoverable at all (native dev, macOS, Windows). */
28
+ export const FALLBACK_MEMORY_LIMIT_BYTES = 512 * 1024 * 1024;
29
+
30
+ /**
31
+ * Ceiling on concurrent in-flight requests per process.
32
+ *
33
+ * A hard ceiling even when memory would allow more: this process is one core,
34
+ * and past this point queueing delay, not memory, is what degrades. It also
35
+ * bounds every per-request structure that is sized by concurrency.
36
+ */
37
+ export const MAX_PROCESS_CONCURRENCY = 64;
38
+
39
+ /**
40
+ * How many copies of a request body are resident at once.
41
+ *
42
+ * Measured, not assumed: the socket read buffer, the parsed object graph, and
43
+ * the upstream re-serialization each hold a body-sized allocation, and garbage
44
+ * collection lags rather than keeping them to one. Budgeting one copy halves
45
+ * the real peak and lets the process admit twice the concurrency it can hold.
46
+ */
47
+ export const REQUEST_BODY_TRANSIENT_COPIES = 2;
48
+
49
+ /** Share of the memory limit all in-flight request bodies together may hold. */
50
+ export const REQUEST_BODY_BUDGET_FRACTION = 0.25;
51
+
52
+ /** Share of the memory limit kept free for allocation spikes and GC slack. */
53
+ export const MEMORY_HEADROOM_FRACTION = 0.2;
54
+
55
+ /**
56
+ * Concurrency this process should admit, derived from the memory limit and the
57
+ * body ceiling rather than guessed.
58
+ *
59
+ * Worked examples, which are the point of having one formula:
60
+ * - 2 GiB limit, 4 MiB bodies → 64 (the ceiling; memory is not the binder)
61
+ * - 512 MiB limit, 4 MiB bodies → 16
62
+ * - 256 MiB limit, 16 MiB bodies → 2
63
+ *
64
+ * An explicit override is honoured, clamped to 1..MAX_PROCESS_CONCURRENCY. It
65
+ * is the operator's call and is never silently raised back to the derived
66
+ * value; exceeding the derived figure is a deliberate choice, so it is
67
+ * reported at the call site rather than corrected here.
68
+ */
69
+ export function deriveProcessConcurrency(options: {
70
+ readonly memoryLimitBytes: number;
71
+ readonly maxBodyBytes: number;
72
+ readonly override?: number | undefined;
73
+ }): number {
74
+ if (options.override !== undefined) {
75
+ if (!Number.isInteger(options.override)) return 1;
76
+ return Math.min(MAX_PROCESS_CONCURRENCY, Math.max(1, options.override));
77
+ }
78
+ const body = Math.max(1, options.maxBodyBytes);
79
+ const perRequest = REQUEST_BODY_TRANSIENT_COPIES * body;
80
+ const budget = Math.max(1, Math.floor(options.memoryLimitBytes * REQUEST_BODY_BUDGET_FRACTION));
81
+ const derived = Math.floor(budget / perRequest);
82
+ return Math.min(MAX_PROCESS_CONCURRENCY, Math.max(1, derived));
83
+ }
84
+
85
+ /**
86
+ * Bytes kept free below the memory limit.
87
+ *
88
+ * A guard that only reacts once RSS equals the limit has already lost: the
89
+ * allocation that breaches it happens between two samples, and the spike that
90
+ * matters — an upstream response landing all at once — arrives in far less
91
+ * than one sample interval.
92
+ */
93
+ export function deriveMemoryHeadroomBytes(memoryLimitBytes: number): number {
94
+ return Math.ceil(memoryLimitBytes * MEMORY_HEADROOM_FRACTION);
95
+ }
96
+
97
+ /**
98
+ * The optional operator override, read through the config spec so "unset" and
99
+ * "out of range" are treated the way every other knob treats them.
100
+ */
101
+ export function resolveProcessConcurrencyOverride(): number | undefined {
102
+ return readOptionalInt(
103
+ "CARTETHYIA_PROCESS_CONCURRENCY_LIMIT",
104
+ CONFIG_SPEC.CARTETHYIA_PROCESS_CONCURRENCY_LIMIT,
105
+ );
106
+ }
107
+
108
+ /**
109
+ * Slack added on top of every key's concurrency limit.
110
+ *
111
+ * Operator configuration, not something a key carries about itself: it is
112
+ * deliberate hidden allowance so a run that lands a few requests over the
113
+ * number an operator chose is not refused at that number. The effective
114
+ * ceiling is `limit + tolerance`, and only requests past it are refused.
115
+ */
116
+ export function resolveConcurrencyTolerance(): number {
117
+ const value = readOptionalInt(
118
+ "CARTETHYIA_CONCURRENCY_TOLERANCE",
119
+ CONFIG_SPEC.CARTETHYIA_CONCURRENCY_TOLERANCE,
120
+ );
121
+ return value === undefined ? 0 : Math.max(0, value);
122
+ }
123
+
124
+ /**
125
+ * The effective concurrency for this process.
126
+ *
127
+ * Takes the limit as an argument rather than resolving it here: the limit
128
+ * owner is `resolveMemoryLimitBytes()` in `observability/runtime-metrics.ts`,
129
+ * and duplicating that resolution would let the two drift.
130
+ */
131
+ export function resolveProcessConcurrency(memoryLimitBytes: number): number {
132
+ return deriveProcessConcurrency({
133
+ memoryLimitBytes,
134
+ maxBodyBytes: resolveMaxBodyBytes(),
135
+ override: resolveProcessConcurrencyOverride(),
136
+ });
137
+ }
@@ -1,76 +1,133 @@
1
- /**
2
- * HTTP listener, dashboard runtime, and graceful-shutdown drain resolvers.
3
- */
4
-
5
- import { CONFIG_SPEC, readInt, readOptionalText } from "./spec";
6
- import { resolvePackagePath } from "./package-paths";
7
-
8
- /** Resolves the HTTP listener port from the process environment. */
9
- export function resolvePort(): number {
10
- return readInt("PORT", CONFIG_SPEC.PORT);
11
- }
12
-
13
- /** Resolves the dashboard static asset directory. */
14
- export function resolveDashboardDist(): string {
15
- const raw = process.env.DASHBOARD_DIST?.trim();
16
- if (!raw) return resolvePackagePath("dist", "dashboard");
17
- return raw;
18
- }
19
-
20
- /**
21
- * Resolves the configured default gateway API key (`CARTETHYIA_API_KEY`), if
22
- * one is set. Consumers that need the raw secret — first-boot seeding and the
23
- * console's key recovery from existing installations — read it here instead of
24
- * probing `process.env` directly.
25
- */
26
- export function resolveDefaultGatewayApiKey(): string | undefined {
27
- return readOptionalText("CARTETHYIA_API_KEY", CONFIG_SPEC.CARTETHYIA_API_KEY);
28
- }
29
-
30
- /** Resolves the request-body ceiling shared by the Bun listener and ingress middleware. */
31
- export function resolveMaxBodyBytes(): number {
32
- return readInt("CARTETHYIA_SERVER_MAX_BODY_BYTES", CONFIG_SPEC.CARTETHYIA_SERVER_MAX_BODY_BYTES);
33
- }
34
-
35
- /** Resolves the listener idle-socket timeout (seconds). */
36
- export function resolveIdleTimeout(): number {
37
- return readInt("CARTETHYIA_SERVER_IDLE_TIMEOUT", CONFIG_SPEC.CARTETHYIA_SERVER_IDLE_TIMEOUT);
38
- }
39
-
40
- /** Resolves the graceful-shutdown drain window: time for in-flight requests to finish before abort. */
41
- export function resolveShutdownDrainWindowMs(): number {
42
- return readInt(
43
- "CARTETHYIA_SHUTDOWN_DRAIN_WINDOW_MS",
44
- CONFIG_SPEC.CARTETHYIA_SHUTDOWN_DRAIN_WINDOW_MS,
45
- );
46
- }
47
-
48
- /** Resolves the operator drain token, or `undefined` when the drain route is disabled. */
49
- export function resolveDrainToken(): string | undefined {
50
- return readOptionalText("CARTETHYIA_DRAIN_TOKEN", CONFIG_SPEC.CARTETHYIA_DRAIN_TOKEN);
51
- }
52
-
53
- /**
54
- * Whether Elysia precompiles route handlers at startup (AOT).
55
- *
56
- * Precompilation takes the eager validator branch, which calls TypeBox's
57
- * `SchemaCompile(schema)` and then reads `.buildResult.external` off the
58
- * result. In a source-run process that field is absent — the lazy
59
- * `require("typebox/schema")` resolves to a namespace that does not carry it —
60
- * and the process dies at the first schema-bearing route with
61
- * `undefined is not an object (evaluating 'this.tb.buildResult.external')`.
62
- * `scripts/build/aot.ts` avoids this by rewriting the call site and bundling
63
- * the namespaces statically, so only the built artifact can actually take the
64
- * eager path.
65
- *
66
- * So: precompilation is on for production *builds*, and off for a source run
67
- * even when `NODE_ENV=production`. A deployment that runs
68
- * `bun run src/main.ts` must not be pushed onto the eager branch.
69
- *
70
- * `CARTETHYIA_SOURCE_RUN` marks a source run explicitly. It is set by
71
- * `scripts/deploy/*.sh`, which are the only supported way to run from source.
72
- */
73
- export function resolveElysiaPrecompile(): boolean {
74
- if (process.env.CARTETHYIA_SOURCE_RUN === "1") return false;
75
- return process.env.NODE_ENV === "production";
76
- }
1
+ /**
2
+ * HTTP listener, dashboard runtime, and graceful-shutdown drain resolvers.
3
+ */
4
+
5
+ import { CONFIG_SPEC, readInt, readOptionalText } from "./spec";
6
+ import { resolvePackagePath } from "./package-paths";
7
+
8
+ /** Resolves the HTTP listener port from the process environment. */
9
+ export function resolvePort(): number {
10
+ return readInt("PORT", CONFIG_SPEC.PORT);
11
+ }
12
+
13
+ /** Resolves the dashboard static asset directory. */
14
+ export function resolveDashboardDist(): string {
15
+ const raw = process.env.DASHBOARD_DIST?.trim();
16
+ if (!raw) return resolvePackagePath("dist", "dashboard");
17
+ return raw;
18
+ }
19
+
20
+ /**
21
+ * Resolves the configured default gateway API key (`CARTETHYIA_API_KEY`), if
22
+ * one is set. Consumers that need the raw secret — first-boot seeding and the
23
+ * console's key recovery from existing installations — read it here instead of
24
+ * probing `process.env` directly.
25
+ */
26
+ export function resolveDefaultGatewayApiKey(): string | undefined {
27
+ return readOptionalText("CARTETHYIA_API_KEY", CONFIG_SPEC.CARTETHYIA_API_KEY);
28
+ }
29
+
30
+ /** Resolves the request-body ceiling shared by the Bun listener and ingress middleware. */
31
+ export function resolveMaxBodyBytes(): number {
32
+ return readInt("CARTETHYIA_SERVER_MAX_BODY_BYTES", CONFIG_SPEC.CARTETHYIA_SERVER_MAX_BODY_BYTES);
33
+ }
34
+
35
+ /** Resolves the listener idle-socket timeout (seconds). */
36
+ export function resolveIdleTimeout(): number {
37
+ return readInt("CARTETHYIA_SERVER_IDLE_TIMEOUT", CONFIG_SPEC.CARTETHYIA_SERVER_IDLE_TIMEOUT);
38
+ }
39
+
40
+ /** Resolves the graceful-shutdown drain window: time for in-flight requests to finish before abort. */
41
+ export function resolveShutdownDrainWindowMs(): number {
42
+ return readInt(
43
+ "CARTETHYIA_SHUTDOWN_DRAIN_WINDOW_MS",
44
+ CONFIG_SPEC.CARTETHYIA_SHUTDOWN_DRAIN_WINDOW_MS,
45
+ );
46
+ }
47
+
48
+ /** Resolves the operator drain token, or `undefined` when the drain route is disabled. */
49
+ export function resolveDrainToken(): string | undefined {
50
+ return readOptionalText("CARTETHYIA_DRAIN_TOKEN", CONFIG_SPEC.CARTETHYIA_DRAIN_TOKEN);
51
+ }
52
+
53
+ /**
54
+ * Whether Elysia precompiles route handlers at startup (AOT).
55
+ *
56
+ * Precompilation takes the eager validator branch, which calls TypeBox's
57
+ * `SchemaCompile(schema)` and then reads `.buildResult.external` off the
58
+ * result. In a source-run process that field is absent — the lazy
59
+ * `require("typebox/schema")` resolves to a namespace that does not carry it —
60
+ * and the process dies at the first schema-bearing route with
61
+ * `undefined is not an object (evaluating 'this.tb.buildResult.external')`.
62
+ * `scripts/build/aot.ts` avoids this by rewriting the call site and bundling
63
+ * the namespaces statically, so only the built artifact can actually take the
64
+ * eager path.
65
+ *
66
+ * So: precompilation is on for production *builds*, and off for a source run
67
+ * even when `NODE_ENV=production`. A deployment that runs
68
+ * `bun run src/main.ts` must not be pushed onto the eager branch.
69
+ *
70
+ * `CARTETHYIA_SOURCE_RUN` marks a source run explicitly. It is set by
71
+ * `scripts/deploy/*.sh`, which are the only supported way to run from source.
72
+ */
73
+ export function resolveElysiaPrecompile(): boolean {
74
+ if (process.env.CARTETHYIA_SOURCE_RUN === "1") return false;
75
+ return process.env.NODE_ENV === "production";
76
+ }
77
+
78
+ /**
79
+ * How many gateway processes this deployment runs, and which one this is.
80
+ *
81
+ * Multi-process is opt-in and deliberately narrow. Every process shares the
82
+ * port through `SO_REUSEPORT`, so the kernel load-balances across them — which
83
+ * means state that used to be process-local is now per-process unless it lives
84
+ * somewhere shared. A second process without that sharing is not "twice the
85
+ * capacity", it is two processes disagreeing:
86
+ *
87
+ * - **PostgreSQL, not SQLite.** Two processes on one SQLite file corrupt it;
88
+ * there is no cross-process lock in the driver.
89
+ * - **Redis configured AND reachable.** Admission counters, quotas, and
90
+ * cooldowns are per-process without it, so a per-key RPM limit silently
91
+ * becomes N× the configured value.
92
+ * - **Linux.** `SO_REUSEPORT` is the mechanism; elsewhere it is a silent no-op
93
+ * and the second process simply fails to bind.
94
+ *
95
+ * These are checked at resolution time rather than left to fail at bind time,
96
+ * because a half-configured multi-process deploy that starts anyway looks fine
97
+ * until a limit is enforced twice.
98
+ */
99
+ export interface ProcessTopology {
100
+ readonly count: number;
101
+ readonly index: number;
102
+ /** True when more than one process serves this deployment. */
103
+ readonly multiProcess: boolean;
104
+ }
105
+
106
+ /**
107
+ * Resolves the topology from the environment.
108
+ *
109
+ * `count > 1` requires an explicit index: a process that does not know which
110
+ * one it is cannot own a shard of anything, and a guessed index would have two
111
+ * processes claiming the same one.
112
+ */
113
+ export function resolveProcessTopology(): ProcessTopology {
114
+ const count = readInt("CARTETHYIA_PROCESS_COUNT", CONFIG_SPEC.CARTETHYIA_PROCESS_COUNT);
115
+ if (count === 1) return { count: 1, index: 0, multiProcess: false };
116
+ // Mandatory when the count is above one: a process that does not know which
117
+ // one it is cannot own a shard of anything, and a defaulted index would have
118
+ // two processes claiming the same number. The spec's default of 0 cannot
119
+ // express "absent", so the raw value is checked here before it is read.
120
+ const rawIndex = process.env.CARTETHYIA_PROCESS_INDEX?.trim();
121
+ if (rawIndex === undefined || rawIndex.length === 0) {
122
+ throw new Error(
123
+ `CARTETHYIA_PROCESS_INDEX is required when CARTETHYIA_PROCESS_COUNT is ${count} (0 – ${count - 1})`,
124
+ );
125
+ }
126
+ const index = readInt("CARTETHYIA_PROCESS_INDEX", CONFIG_SPEC.CARTETHYIA_PROCESS_INDEX);
127
+ if (index >= count) {
128
+ throw new Error(
129
+ `CARTETHYIA_PROCESS_INDEX must be an integer between 0 and ${count - 1} (got "${rawIndex}")`,
130
+ );
131
+ }
132
+ return { count, index, multiProcess: true };
133
+ }
@@ -110,6 +110,28 @@ export const CONFIG_SPEC = {
110
110
  min: 0,
111
111
  max: 600_000,
112
112
  },
113
+ // Optional ceiling on in-flight requests per process. Left unset, the limit
114
+ // is derived from the memory limit and the body ceiling (see
115
+ // config/resources.ts): 2 GiB/4 MiB → 64, 512 MiB/4 MiB → 16, 256 MiB/16 MiB
116
+ // → 2. Set it to admit more than memory alone would allow — which is the
117
+ // operator's call, and is reported rather than corrected — or less, to keep
118
+ // a slow upstream from being handed more concurrency than it can serve.
119
+ // Optional-int, not int-with-a-zero-default: "unset" and "explicitly zero"
120
+ // must not be the same thing. Zero is not a valid ceiling, so a default of 0
121
+ // would need a second sentinel to mean "derive".
122
+ CARTETHYIA_PROCESS_CONCURRENCY_LIMIT: { kind: "optional-int", min: 1, max: 64 },
123
+ // Slack above each key's concurrency limit. Effective ceiling is
124
+ // limit + tolerance; only requests past it are refused.
125
+ CARTETHYIA_CONCURRENCY_TOLERANCE: { kind: "optional-int", min: 0, max: 1024 },
126
+ // How many gateway processes serve this deployment, and which one this is.
127
+ // Count > 1 requires Linux, CARTETHYIA_DB_MODE=full, and a REACHABLE Redis —
128
+ // checked at boot, because a half-configured multi-process deploy starts
129
+ // fine and then enforces every per-key limit N times over. Lite stays
130
+ // single-process: two processes on one SQLite file corrupt it.
131
+ CARTETHYIA_PROCESS_COUNT: { kind: "int", default: 1, min: 1, max: 16 },
132
+ // Which process this is (0 – count-1). Mandatory when count > 1: a process
133
+ // that does not know its index cannot own a shard of anything.
134
+ CARTETHYIA_PROCESS_INDEX: { kind: "int", default: 0, min: 0, max: 15 },
113
135
  // Optional secret for the operator drain endpoint (`POST /admin/drain`). When
114
136
  // set, a process can be stopped gracefully on platforms where a catchable
115
137
  // signal cannot be delivered to it (notably Windows, where `process.kill` of
@@ -130,9 +152,12 @@ export const CONFIG_SPEC = {
130
152
  min: 1,
131
153
  max: Number.MAX_SAFE_INTEGER,
132
154
  },
133
- // Opt-in forced garbage collection. Off by default: see
134
- // `observability/runtime-metrics.ts` — a forced collection is not
135
- // reclamation, and the synchronous form stalls concurrent requests.
155
+ // Opt-in forced garbage collection under memory pressure. Off by default:
156
+ // see `observability/runtime-metrics.ts` — a forced collection is not
157
+ // reclamation, and a collection that runs while requests are in flight adds
158
+ // a latency spike. This gates the pressure path only; idle reclaim runs
159
+ // unconditionally, because reclaiming when nothing is in flight stalls
160
+ // nothing.
136
161
  CARTETHYIA_FORCED_GC: { kind: "flag" },
137
162
  // How often runtime gauges (memory + bounded-collection sizes) are sampled.
138
163
  CARTETHYIA_RUNTIME_METRICS_INTERVAL_MS: {
@@ -151,37 +176,13 @@ export const CONFIG_SPEC = {
151
176
  /**
152
177
  * How long a day's telemetry is kept, in days.
153
178
  *
154
- * The retention unit is the whole day: a day's event file and its index are
155
- * deleted together, and the current day is never touched. Past this age an
156
- * individual request no longer exists anywhere, so its detail is gone —
157
- * totals, breakdowns and spend are unaffected, because the index keeps
158
- * running sums rather than the rows.
179
+ * Whole raw event files past this age are removed. No summary or compacted
180
+ * form replaces the individual requests.
159
181
  *
160
182
  * This is the knob that decides how long the request list, per-request
161
183
  * filters, and the detail drawer stay complete.
162
184
  */
163
185
  CARTETHYIA_TELEMETRY_RETENTION_DAYS: { kind: "int", default: 30, min: 3, max: 365 },
164
- /**
165
- * Age at which a request's detail is described as no longer stored.
166
- *
167
- * Retention itself is `CARTETHYIA_TELEMETRY_RETENTION_DAYS`; this only words
168
- * the operator-facing message shown when a request that old is asked for.
169
- */
170
- CARTETHYIA_TELEMETRY_COMPACT_AFTER_DAYS: { kind: "int", default: 7, min: 1, max: 365 },
171
- /**
172
- * How often each day's index is brought up to date, in minutes.
173
- *
174
- * Bringing an index up to date means folding the bytes appended since the
175
- * last pass — not re-reading the day. Reads fold before they answer too, so
176
- * raising this does not make the numbers stale; it only leaves the on-disk
177
- * index further behind, and the next read pays for the remainder.
178
- */
179
- CARTETHYIA_TELEMETRY_COMPACT_INTERVAL_MINUTES: {
180
- kind: "int",
181
- default: 60,
182
- min: 5,
183
- max: 1440,
184
- },
185
186
  CARTETHYIA_TELEMETRY_PAYLOAD_MAX_BYTES: {
186
187
  kind: "int",
187
188
  default: 16 * 1024 * 1024,
@@ -20,28 +20,3 @@ export function resolveTelemetryPayloadMaxBytes(): number {
20
20
  );
21
21
  }
22
22
 
23
- /**
24
- * Age at which hourly telemetry files are compacted into a per-day summary.
25
- *
26
- * Clamped below the retention window: compacting at or past the delete age
27
- * would fold a file and throw it away in the same pass, spending the read for
28
- * totals that were about to be discarded.
29
- */
30
- export function resolveTelemetryCompactAfterDays(): number {
31
- const compacted = readInt(
32
- "CARTETHYIA_TELEMETRY_COMPACT_AFTER_DAYS",
33
- CONFIG_SPEC.CARTETHYIA_TELEMETRY_COMPACT_AFTER_DAYS,
34
- );
35
- return Math.min(compacted, resolveTelemetryRetentionDays());
36
- }
37
-
38
- /** Minutes between compaction passes. */
39
- export function resolveTelemetryCompactIntervalMs(): number {
40
- const minutes = readInt(
41
- "CARTETHYIA_TELEMETRY_COMPACT_INTERVAL_MINUTES",
42
- CONFIG_SPEC.CARTETHYIA_TELEMETRY_COMPACT_INTERVAL_MINUTES,
43
- );
44
- return minutes * 60_000;
45
- }
46
-
47
-
@@ -2,7 +2,7 @@ import { eq } from "drizzle-orm";
2
2
 
3
3
  import type { CartethyiaDatabase } from "../../persistence/db-handle";
4
4
  import { apiKeys } from "../../persistence/schema";
5
- import { queryIndexedBuckets } from "../../observability/event-day-index";
5
+ import { sumTelemetryTokens } from "../../observability/event-query";
6
6
  import { utcDayStart, utcMonthStart } from "../shared/day";
7
7
 
8
8
  /**
@@ -27,27 +27,24 @@ export async function familyBucketSpend(
27
27
  keyId: string,
28
28
  now: Date,
29
29
  ): Promise<{ daily: number; monthly: number }> {
30
+ // The tenant comes from the key row itself: the caller passes only the key
31
+ // id, and the SQL sums are tenant-scoped so scoping must not be guessed.
32
+ const [parent] = await db
33
+ .select({ tenantId: apiKeys.tenantId })
34
+ .from(apiKeys)
35
+ .where(eq(apiKeys.id, keyId))
36
+ .limit(1);
37
+ const tenantId = parent?.tenantId;
38
+ if (tenantId === undefined) return { daily: 0, monthly: 0 };
30
39
  const children = await db
31
40
  .select({ id: apiKeys.id })
32
41
  .from(apiKeys)
33
42
  .where(eq(apiKeys.parentKeyId, keyId));
34
43
  const keyIds = [keyId, ...children.map((child) => child.id)];
35
- const spentSince = async (start: Date): Promise<number> => {
36
- // Folded first, so the index covers every event written so far including the
37
- // current day's — then read from the index alone.
38
- //
39
- // Deliberately not index *plus* events: they hold the same requests, and
40
- // adding them doubled every bucket. Folding first is what makes the index
41
- // sufficient on its own; the watermark is the point up to which it is
42
- // complete.
43
- const allowed = new Set(keyIds);
44
- const indexed = await queryIndexedBuckets({ since: start, foldFirst: true }).catch(
45
- () => [],
46
- );
47
- return indexed
48
- .filter((bucket) => bucket.apiKeyId !== null && allowed.has(bucket.apiKeyId))
49
- .reduce((sum, bucket) => sum + bucket.inputTokens + bucket.outputTokens, 0);
50
- };
44
+ // Two SQL sums instead of reading every event of the family: the bucket
45
+ // spend only needs a total, and this runs on the key-creation path.
46
+ const spentSince = async (start: Date): Promise<number> =>
47
+ sumTelemetryTokens(db, tenantId, { apiKeyIds: keyIds, since: start });
51
48
  const [daily, monthly] = await Promise.all([
52
49
  spentSince(utcDayStart(now)),
53
50
  spentSince(utcMonthStart(now)),