cartethyia 2.5.3 → 2.5.6

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 (456) hide show
  1. package/.env.example +13 -4
  2. package/README.md +2 -2
  3. package/dist/dashboard/assets/{BannedPage-hfCzi-Ux.js → BannedPage-CADDq4IU.js} +1 -1
  4. package/dist/dashboard/assets/BannedPage-CADDq4IU.js.gz +0 -0
  5. package/dist/dashboard/assets/CliToolDetailPage-_z1hID7E.js +1 -0
  6. package/dist/dashboard/assets/CliToolDetailPage-_z1hID7E.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-DEDyKBlC.js → CliToolsPage-DuO1Z7dq.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-DuO1Z7dq.js.gz +0 -0
  9. package/dist/dashboard/assets/CombosPage-DNEPi3xU.js +1 -0
  10. package/dist/dashboard/assets/CombosPage-DNEPi3xU.js.gz +0 -0
  11. package/dist/dashboard/assets/{ConsoleLogPage-bsLuZmYH.js → ConsoleLogPage-BVdpfiz1.js} +1 -1
  12. package/dist/dashboard/assets/ConsoleLogPage-BVdpfiz1.js.gz +0 -0
  13. package/dist/dashboard/assets/{CustomizationPage-_UWhwziJ.js → CustomizationPage-BR5HHvbi.js} +1 -1
  14. package/dist/dashboard/assets/CustomizationPage-BR5HHvbi.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-mUfcknqm.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-mUfcknqm.js.gz +0 -0
  17. package/dist/dashboard/assets/{LoginPage-DDVIfZGW.js → LoginPage-V5faV9V2.js} +1 -1
  18. package/dist/dashboard/assets/LoginPage-V5faV9V2.js.gz +0 -0
  19. package/dist/dashboard/assets/ModelPicker-C7pDb41R.js +1 -0
  20. package/dist/dashboard/assets/ModelPicker-C7pDb41R.js.gz +0 -0
  21. package/dist/dashboard/assets/OverviewPage-BDu20Jdx.js +1 -0
  22. package/dist/dashboard/assets/OverviewPage-BDu20Jdx.js.gz +0 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-Dllr_aFO.js +3 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-Dllr_aFO.js.gz +0 -0
  25. package/dist/dashboard/assets/ProvidersPage-uPiGVTTK.js +1 -0
  26. package/dist/dashboard/assets/ProvidersPage-uPiGVTTK.js.gz +0 -0
  27. package/dist/dashboard/assets/ProxyPage-BaUkO-4O.js +7 -0
  28. package/dist/dashboard/assets/ProxyPage-BaUkO-4O.js.gz +0 -0
  29. package/dist/dashboard/assets/QuotaPage-o0IFAsFb.js +1 -0
  30. package/dist/dashboard/assets/QuotaPage-o0IFAsFb.js.gz +0 -0
  31. package/dist/dashboard/assets/SettingsPage-CYWqbXjp.js +1 -0
  32. package/dist/dashboard/assets/SettingsPage-CYWqbXjp.js.gz +0 -0
  33. package/dist/dashboard/assets/{SetupPage-D6pmQ86L.js → SetupPage-UB690SE2.js} +1 -1
  34. package/dist/dashboard/assets/SetupPage-UB690SE2.js.gz +0 -0
  35. package/dist/dashboard/assets/{SortableList-DWvKiuSl.js → SortableList-BLujdX7N.js} +1 -1
  36. package/dist/dashboard/assets/SortableList-BLujdX7N.js.gz +0 -0
  37. package/dist/dashboard/assets/{StudioPage-CmkUrgdj.js → StudioPage-D5C2raAc.js} +3 -3
  38. package/dist/dashboard/assets/StudioPage-D5C2raAc.js.gz +0 -0
  39. package/dist/dashboard/assets/UsagePage-8uIcxa1W.js +56 -0
  40. package/dist/dashboard/assets/UsagePage-8uIcxa1W.js.gz +0 -0
  41. package/dist/dashboard/assets/api-keys-DQYLQxpW.js +1 -0
  42. package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
  43. package/dist/dashboard/assets/app-CX_Z8cMs.css +2 -0
  44. package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
  45. package/dist/dashboard/assets/{assert-NHwYq95t.js → assert-C9FD_JwJ.js} +1 -1
  46. package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
  47. package/dist/dashboard/assets/{badge-6uzDDvqs.js → badge-CR72dasT.js} +1 -1
  48. package/dist/dashboard/assets/badge-CR72dasT.js.gz +0 -0
  49. package/dist/dashboard/assets/{banner-4_WNg2WZ.js → banner-CI9_eSfO.js} +1 -1
  50. package/dist/dashboard/assets/banner-CI9_eSfO.js.gz +0 -0
  51. package/dist/dashboard/assets/{card--AtFedAq.js → card-DS7CQEsO.js} +1 -1
  52. package/dist/dashboard/assets/card-DS7CQEsO.js.gz +0 -0
  53. package/dist/dashboard/assets/{cli-tools-B3e7dx3N.js → cli-tools-CNqzrfdb.js} +1 -1
  54. package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
  55. package/dist/dashboard/assets/{clipboard-button-NreUx6c8.js → clipboard-button-611pSsaX.js} +1 -1
  56. package/dist/dashboard/assets/clipboard-button-611pSsaX.js.gz +0 -0
  57. package/dist/dashboard/assets/{fingerprint-pattern-BWuHtbW4.js → concurrency-BDF0ok7y.js} +1 -1
  58. package/dist/dashboard/assets/concurrency-BDF0ok7y.js.gz +0 -0
  59. package/dist/dashboard/assets/database-Dz9N3zGd.js +1 -0
  60. package/dist/dashboard/assets/database-Dz9N3zGd.js.gz +0 -0
  61. package/dist/dashboard/assets/dialog-k37exjNt.js +1 -0
  62. package/dist/dashboard/assets/dialog-k37exjNt.js.gz +0 -0
  63. package/dist/dashboard/assets/drawer-CX7N8ucR.js +1 -0
  64. package/dist/dashboard/assets/drawer-CX7N8ucR.js.gz +0 -0
  65. package/dist/dashboard/assets/entry-B9Xvtnfj.js +1 -0
  66. package/dist/dashboard/assets/entry-B9Xvtnfj.js.gz +0 -0
  67. package/dist/dashboard/assets/entry-CTP8AyiS.js +1 -0
  68. package/dist/dashboard/assets/entry-CTP8AyiS.js.gz +0 -0
  69. package/dist/dashboard/assets/entry-DxBHnauy.js +4 -0
  70. package/dist/dashboard/assets/entry-DxBHnauy.js.gz +0 -0
  71. package/dist/dashboard/assets/{github-badge-D_eM3IgG.js → github-badge-sltk1qUi.js} +1 -1
  72. package/dist/dashboard/assets/github-badge-sltk1qUi.js.gz +0 -0
  73. package/dist/dashboard/assets/{index-BydyKaDz.js → index-CmxZr14F.js} +2 -2
  74. package/dist/dashboard/assets/index-CmxZr14F.js.gz +0 -0
  75. package/dist/dashboard/assets/{inline-BcNGF8Gr.js → inline-BhY2sSnO.js} +1 -1
  76. package/dist/dashboard/assets/inline-BhY2sSnO.js.gz +0 -0
  77. package/dist/dashboard/assets/{input-BG41et9G.js → input-V1umFiBk.js} +1 -1
  78. package/dist/dashboard/assets/input-V1umFiBk.js.gz +0 -0
  79. package/dist/dashboard/assets/layout-BBOYycyS.js +1 -0
  80. package/dist/dashboard/assets/layout-BBOYycyS.js.gz +0 -0
  81. package/dist/dashboard/assets/{list-row-Gh_W4OR_.js → list-row-1zzZYM0B.js} +1 -1
  82. package/dist/dashboard/assets/list-row-1zzZYM0B.js.gz +0 -0
  83. package/dist/dashboard/assets/{live-BJGYRBnR.js → live-BivGfuUs.js} +1 -1
  84. package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
  85. package/dist/dashboard/assets/model-bans-CmffjLuG.js +1 -0
  86. package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
  87. package/dist/dashboard/assets/{mutation-BkoLM5Ba.js → mutation-46P0oWMe.js} +1 -1
  88. package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
  89. package/dist/dashboard/assets/numbers-DkfMR5Qs.js +2 -0
  90. package/dist/dashboard/assets/numbers-DkfMR5Qs.js.gz +0 -0
  91. package/dist/dashboard/assets/{query-keys-D7wX2vM5.js → query-keys-BFhGpwR1.js} +1 -1
  92. package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
  93. package/dist/dashboard/assets/{routing-CcusqXA7.js → routing-CAb4F37R.js} +1 -1
  94. package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
  95. package/dist/dashboard/assets/{select-BO6h8yNv.js → select-BYR_5K2c.js} +2 -2
  96. package/dist/dashboard/assets/select-BYR_5K2c.js.gz +0 -0
  97. package/dist/dashboard/assets/{settings-mpmXTgUa.js → settings-DNX9sydi.js} +1 -1
  98. package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
  99. package/dist/dashboard/assets/{stack-DwLmjUFK.js → stack-BHTfXdz_.js} +1 -1
  100. package/dist/dashboard/assets/stack-BHTfXdz_.js.gz +0 -0
  101. package/dist/dashboard/assets/{state-DYdQmdAl.js → state-mPDRlQkl.js} +1 -1
  102. package/dist/dashboard/assets/state-mPDRlQkl.js.gz +0 -0
  103. package/dist/dashboard/assets/switch-DOmJxg7H.js +1 -0
  104. package/dist/dashboard/assets/switch-DOmJxg7H.js.gz +0 -0
  105. package/dist/dashboard/assets/terminal-0_IjPWTk.js +1 -0
  106. package/dist/dashboard/assets/terminal-0_IjPWTk.js.gz +0 -0
  107. package/dist/dashboard/assets/tokens-COzario9.js +1 -0
  108. package/dist/dashboard/assets/tokens-COzario9.js.gz +0 -0
  109. package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js +1 -0
  110. package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js.gz +0 -0
  111. package/dist/dashboard/assets/{useMutation-w4u4CVue.js → useMutation-Dem0lXQj.js} +1 -1
  112. package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
  113. package/dist/dashboard/assets/zap-C4EeVW4q.js +1 -0
  114. package/dist/dashboard/assets/zap-C4EeVW4q.js.gz +0 -0
  115. package/dist/dashboard/donation/qris-risun-marke.png +0 -0
  116. package/dist/dashboard/index.html +1 -1
  117. package/dist/dashboard/index.html.gz +0 -0
  118. package/dist/dashboard/precompressed-assets.json +57 -57
  119. package/migrations/README.md +52 -52
  120. package/migrations/lite-sqlite3/0000_baseline.sql +2 -2
  121. package/package.json +96 -95
  122. package/scripts/cli/admin.ts +125 -0
  123. package/scripts/cli/autostart.ts +237 -0
  124. package/scripts/cli/{ui/background.ts → background.ts} +1 -1
  125. package/scripts/cli/{command/console-menu.ts → console-menu.ts} +275 -248
  126. package/scripts/cli/doctor.ts +37 -0
  127. package/scripts/cli/{support/env.ts → env.ts} +99 -99
  128. package/scripts/cli/{gateway/spawn.ts → gateway.ts} +112 -24
  129. package/scripts/cli/instance.ts +224 -0
  130. package/scripts/cli/main.ts +88 -18
  131. package/scripts/cli/{support/platform.ts → platform.ts} +64 -64
  132. package/scripts/cli/port.ts +100 -0
  133. package/scripts/cli/{support/services.ts → services.ts} +88 -88
  134. package/scripts/cli/{command/setup.ts → setup.ts} +35 -34
  135. package/scripts/cli/{command/start.ts → start.ts} +10 -5
  136. package/scripts/cli/{support/startup-signal.ts → startup-signal.ts} +23 -23
  137. package/scripts/cli/tray/index.ts +62 -12
  138. package/scripts/cli/{command/tray.ts → tray.ts} +208 -208
  139. package/scripts/cli/update.ts +40 -0
  140. package/scripts/cli/version.ts +67 -0
  141. package/scripts/cli/{ui/wordmark.ts → wordmark.ts} +58 -58
  142. package/src/boot/app.ts +11 -7
  143. package/src/boot/dependencies.ts +50 -19
  144. package/src/boot/timeout.ts +2 -13
  145. package/src/config/cooldowns.ts +186 -138
  146. package/src/config/network.ts +8 -14
  147. package/src/config/providers.ts +7 -10
  148. package/src/config/runtime.ts +76 -76
  149. package/src/config/security.ts +15 -0
  150. package/src/config/spec.ts +52 -7
  151. package/src/config/telemetry.ts +26 -0
  152. package/src/config/timeouts.ts +0 -6
  153. package/src/console/admin/model-abuse/contracts.ts +28 -6
  154. package/src/console/admin/model-abuse/routes.ts +56 -3
  155. package/src/console/api-keys/bucket-spend.ts +15 -4
  156. package/src/console/api-keys/contracts.ts +410 -414
  157. package/src/console/api-keys/routes.ts +881 -880
  158. package/src/console/auth/credentials.ts +3 -3
  159. package/src/console/auth/setup-service.ts +2 -2
  160. package/src/console/backup/contracts.ts +15 -2
  161. package/src/console/backup/export-endpoint.ts +6 -2
  162. package/src/console/backup/nine-router.ts +2 -5
  163. package/src/console/backup/service.ts +4 -0
  164. package/src/console/backup/store.ts +192 -29
  165. package/src/console/backup/validate.ts +3 -45
  166. package/src/console/cli-tools/fs-ops.ts +487 -487
  167. package/src/console/console-router-guards.ts +1 -1
  168. package/src/console/console-router.ts +8 -0
  169. package/src/console/data-management/contracts.ts +100 -0
  170. package/src/console/data-management/operations.ts +234 -0
  171. package/src/console/data-management/routes.ts +103 -0
  172. package/src/console/domain-registration.ts +45 -1
  173. package/src/console/observability/contracts.ts +524 -485
  174. package/src/console/observability/live.ts +22 -0
  175. package/src/console/observability/store.ts +1138 -1018
  176. package/src/console/observability/usage-dimensions.ts +4 -4
  177. package/src/console/providers/catalog/catalog-projections.ts +6 -7
  178. package/src/console/providers/catalog/contracts.ts +6 -0
  179. package/src/console/providers/catalog/model-operations.ts +5 -2
  180. package/src/console/providers/catalog/provider-operations.ts +24 -4
  181. package/src/console/providers/catalog/public-model-store.ts +8 -11
  182. package/src/console/providers/catalog/routes.ts +7 -0
  183. package/src/console/providers/catalog/store.ts +1553 -1532
  184. package/src/console/providers/oauth/routes.ts +8 -12
  185. package/src/console/quota/account-quota-shared.ts +52 -39
  186. package/src/console/quota/account-quota-tenant.ts +6 -6
  187. package/src/console/quota/quota-view.ts +1 -1
  188. package/src/console/quota/refresh.ts +441 -429
  189. package/src/console/remote-support/contracts.ts +61 -0
  190. package/src/console/remote-support/routes.ts +65 -0
  191. package/src/console/remote-support/service.ts +207 -0
  192. package/src/console/response-compression.ts +0 -3
  193. package/src/console/routing/pools/import-parse.ts +206 -0
  194. package/src/console/routing/pools/operations.ts +543 -501
  195. package/src/console/routing/pools/probe-result.ts +3 -2
  196. package/src/console/routing/pools/probe-verdict.ts +132 -132
  197. package/src/console/routing/pools/routes.ts +181 -164
  198. package/src/console/routing/pools/store.ts +8 -8
  199. package/src/console/settings/contracts.ts +95 -2
  200. package/src/console/settings/store.ts +81 -3
  201. package/src/console/shared/cursor.ts +8 -8
  202. package/src/console/shared/errors.ts +1 -3
  203. package/src/console/updates/routes.ts +16 -0
  204. package/src/main.ts +10 -0
  205. package/src/network/http2-bidi.ts +2 -4
  206. package/src/network/pool/agent.ts +478 -478
  207. package/src/network/pool/selector.ts +5 -4
  208. package/src/network/retry.ts +1 -2
  209. package/src/network/ssrf.ts +5 -4
  210. package/src/observability/event-compaction.ts +317 -0
  211. package/src/observability/event-day-index.ts +440 -0
  212. package/src/observability/event-log.ts +155 -6
  213. package/src/observability/logger.ts +6 -6
  214. package/src/observability/payload-capture.ts +158 -33
  215. package/src/observability/payload-store.ts +82 -61
  216. package/src/observability/redaction.ts +5 -6
  217. package/src/observability/request-notify.ts +43 -0
  218. package/src/observability/telemetry-status.ts +8 -9
  219. package/src/observability/token-speed.ts +5 -4
  220. package/src/persistence/db-mode.ts +7 -7
  221. package/src/persistence/health-event-flush.ts +36 -0
  222. package/src/persistence/readiness.ts +1 -1
  223. package/src/persistence/schema.ts +75 -29
  224. package/src/persistence/shared-cache.ts +1 -2
  225. package/src/persistence/stores/api-key-store.ts +18 -6
  226. package/src/persistence/stores/share-store.ts +4 -3
  227. package/src/persistence/stores/telemetry-store.ts +33 -11
  228. package/src/protocol/canonical-model.ts +11 -3
  229. package/src/protocol/codecs.ts +139 -6
  230. package/src/protocol/gateway-error.ts +6 -4
  231. package/src/protocol/image-clamp.ts +19 -1
  232. package/src/protocol/registry.ts +8 -0
  233. package/src/protocol/request/chat.ts +21 -1
  234. package/src/protocol/request/codex.ts +10 -1
  235. package/src/protocol/request/gemini.ts +9 -2
  236. package/src/protocol/request/messages.ts +18 -7
  237. package/src/protocol/request/responses.ts +9 -1
  238. package/src/protocol/response/chat.ts +34 -4
  239. package/src/protocol/response/codex.ts +7 -4
  240. package/src/protocol/response/dsml.ts +140 -28
  241. package/src/protocol/response/responses.ts +11 -5
  242. package/src/protocol/stream-error-frames.ts +4 -5
  243. package/src/providers/adapters/compatible-adapter.ts +3 -0
  244. package/src/providers/authentication/jwt-validator.ts +9 -10
  245. package/src/providers/authentication/oauth-refresh-service.ts +9 -11
  246. package/src/providers/bundled-discovery-providers.ts +122 -119
  247. package/src/providers/configured-openai-providers.ts +4 -4
  248. package/src/providers/default-registry.ts +5 -4
  249. package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
  250. package/src/providers/discovery/catalog/catalog-format.ts +3 -6
  251. package/src/providers/discovery/models-dev-catalog.ts +354 -354
  252. package/src/providers/discovery/openai-model-discovery.ts +5 -0
  253. package/src/providers/discovery/openrouter-free-roster.ts +15 -17
  254. package/src/providers/discovery/probe-phases.ts +4 -4
  255. package/src/providers/discovery/probe-wire.ts +6 -13
  256. package/src/providers/discovery/probing-service.ts +1117 -1045
  257. package/src/providers/integrations/alibabacp/alibabacp.ts +1 -1
  258. package/src/providers/integrations/antigravity/antigravity-quota.ts +5 -7
  259. package/src/providers/integrations/buddy/buddy-chat-shared.ts +23 -2
  260. package/src/providers/integrations/buddy/buddy-checkin.ts +1 -4
  261. package/src/providers/integrations/buddy/buddy-cli-headers.ts +23 -6
  262. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -1
  263. package/src/providers/integrations/buddy/buddy-identity.ts +52 -0
  264. package/src/providers/integrations/buddy/buddy-media.ts +2 -2
  265. package/src/providers/integrations/buddy/buddy-usage-store.ts +3 -4
  266. package/src/providers/integrations/buddy/codebuddy/quota.ts +2 -13
  267. package/src/providers/integrations/buddy/codebuddy/shared.ts +118 -115
  268. package/src/providers/integrations/buddy/workbuddy/quota.ts +0 -11
  269. package/src/providers/integrations/buddy/workbuddy/shared.ts +5 -2
  270. package/src/providers/integrations/claude/claude.ts +608 -608
  271. package/src/providers/integrations/cline/cline-quota.ts +27 -20
  272. package/src/providers/integrations/cline/cline.ts +537 -539
  273. package/src/providers/integrations/codex/codex-headers.ts +25 -39
  274. package/src/providers/integrations/codex/codex-oauth.ts +1 -1
  275. package/src/providers/integrations/codex/codex.ts +2 -29
  276. package/src/providers/integrations/commandcode/commandcode.ts +674 -675
  277. package/src/providers/integrations/commandcode/discovery.ts +117 -116
  278. package/src/providers/integrations/cursor/cursor-auth.ts +418 -418
  279. package/src/providers/integrations/cursor/cursor-quota.ts +309 -0
  280. package/src/providers/integrations/cursor/cursor-request.ts +616 -471
  281. package/src/providers/integrations/cursor/cursor-session-manager.ts +319 -281
  282. package/src/providers/integrations/cursor/cursor.ts +1333 -960
  283. package/src/providers/integrations/cursor/protocol/messages.ts +25 -0
  284. package/src/providers/integrations/factory/factory-models.ts +3 -5
  285. package/src/providers/integrations/factory/factory.ts +8 -1
  286. package/src/providers/integrations/gemini.ts +6 -1
  287. package/src/providers/integrations/grok/grok-install-id.ts +2 -3
  288. package/src/providers/integrations/grok/grok-quota.ts +279 -177
  289. package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -0
  290. package/src/providers/integrations/grok/grok-ratelimit.ts +218 -0
  291. package/src/providers/integrations/grok/grok.ts +6 -7
  292. package/src/providers/integrations/hermes.ts +14 -18
  293. package/src/providers/integrations/kiro/kiro-discovery.ts +1 -1
  294. package/src/providers/integrations/kiro/kiro.ts +1 -1
  295. package/src/providers/integrations/mistral.ts +6 -6
  296. package/src/providers/integrations/opencode/fingerprint.ts +3 -3
  297. package/src/providers/integrations/opencode/provider.ts +19 -20
  298. package/src/providers/integrations/qoder/qoder-catalog.ts +450 -450
  299. package/src/providers/integrations/qoder/qoder-oauth.ts +388 -310
  300. package/src/providers/integrations/qoder/qoder-quota.ts +192 -0
  301. package/src/providers/integrations/qoder/qoder.ts +884 -865
  302. package/src/providers/integrations/shared/connect.ts +6 -6
  303. package/src/providers/integrations/trae/trae-catalog.ts +1 -2
  304. package/src/providers/integrations/trae/trae-quota.ts +384 -0
  305. package/src/providers/integrations/trae/trae.ts +2 -2
  306. package/src/providers/integrations/zcode/oauth.ts +6 -1
  307. package/src/providers/operations/account-health-service.ts +75 -27
  308. package/src/providers/operations/account-reset-service.ts +1 -1
  309. package/src/providers/operations/client-versions.ts +6 -6
  310. package/src/providers/operations/custom-cli-headers.ts +3 -4
  311. package/src/providers/operations/provider-catalog-seeder.ts +95 -95
  312. package/src/providers/operations/provider-catalog-service.ts +3 -4
  313. package/src/providers/provider-contracts.ts +7 -0
  314. package/src/providers/provider-metadata.ts +10 -8
  315. package/src/providers/provider-registry.ts +6 -7
  316. package/src/providers/provider-usage.ts +1 -1
  317. package/src/providers/quota/quota-support.ts +8 -0
  318. package/src/security/abuse.ts +15 -10
  319. package/src/security/access-control.ts +144 -146
  320. package/src/security/admission/contracts.ts +11 -0
  321. package/src/security/admission/in-memory-store.ts +4 -3
  322. package/src/security/admission/lease-sweep.ts +6 -6
  323. package/src/security/admission/reasons.ts +5 -1
  324. package/src/security/admission/redis-store.ts +211 -210
  325. package/src/security/admission/service.ts +4 -0
  326. package/src/security/api-key-auth.ts +27 -17
  327. package/src/security/ip-boundary.ts +155 -15
  328. package/src/security/model-abuse.ts +366 -94
  329. package/src/security/outbound-headers.ts +3 -4
  330. package/src/shared/sleep.ts +4 -0
  331. package/src/transport/dispatch/attempt-finalize.ts +47 -5
  332. package/src/transport/dispatch/attempt-loop.ts +531 -530
  333. package/src/transport/dispatch/generation.ts +6 -0
  334. package/src/transport/dispatch/proxy-request.ts +24 -1
  335. package/src/transport/dispatch/ratelimit-capture.ts +37 -0
  336. package/src/transport/dispatch/stream-bridge.ts +1 -3
  337. package/src/transport/dispatch/streaming/finalize.ts +1 -1
  338. package/src/transport/dispatch/streaming/stream.ts +19 -6
  339. package/src/transport/dispatch/upstream.ts +9 -0
  340. package/src/transport/failure-policy.ts +162 -8
  341. package/src/transport/guards/auth-guard.ts +7 -4
  342. package/src/transport/guards/concurrency-guard.ts +0 -5
  343. package/src/transport/guards/error-lifecycle.ts +13 -6
  344. package/src/transport/guards/pipeline.ts +15 -3
  345. package/src/transport/guards/request-context.ts +8 -2
  346. package/src/transport/request/inflight.ts +5 -6
  347. package/src/transport/request/preparer.ts +9 -8
  348. package/src/transport/request/state.ts +21 -8
  349. package/src/transport/request/tenant-preferences.ts +60 -0
  350. package/src/transport/routing/admission.ts +4 -0
  351. package/src/transport/routing/route-catalog.ts +65 -10
  352. package/src/transport/routing/route-model.ts +46 -14
  353. package/src/transport/routing/router.ts +21 -14
  354. package/src/transport/surface/completion.ts +2 -3
  355. package/src/transport/surface/dialects.ts +5 -6
  356. package/src/transport/surface/responses/encode.ts +38 -0
  357. package/src/transport/surface/responses/parse.ts +4 -0
  358. package/src/transport/translation/capabilities.ts +2 -3
  359. package/src/transport/translation/quirks.ts +3 -3
  360. package/src/workers/buddy-growth-tasks.ts +5 -10
  361. package/src/workers/daily-checkin.ts +17 -23
  362. package/src/workers/quota-refresh-sweep.ts +26 -26
  363. package/dist/dashboard/assets/BannedPage-hfCzi-Ux.js.gz +0 -0
  364. package/dist/dashboard/assets/CliToolDetailPage-DLGqYEwI.js +0 -1
  365. package/dist/dashboard/assets/CliToolDetailPage-DLGqYEwI.js.gz +0 -0
  366. package/dist/dashboard/assets/CliToolsPage-DEDyKBlC.js.gz +0 -0
  367. package/dist/dashboard/assets/CombosPage-CfuYiQ8Z.js +0 -1
  368. package/dist/dashboard/assets/CombosPage-CfuYiQ8Z.js.gz +0 -0
  369. package/dist/dashboard/assets/ConsoleLogPage-bsLuZmYH.js.gz +0 -0
  370. package/dist/dashboard/assets/CustomizationPage-_UWhwziJ.js.gz +0 -0
  371. package/dist/dashboard/assets/HealthEventsModal-Cjfc5WFS.js +0 -1
  372. package/dist/dashboard/assets/HealthEventsModal-Cjfc5WFS.js.gz +0 -0
  373. package/dist/dashboard/assets/LoginPage-DDVIfZGW.js.gz +0 -0
  374. package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js +0 -1
  375. package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js.gz +0 -0
  376. package/dist/dashboard/assets/OverviewPage-B53YYMng.js +0 -1
  377. package/dist/dashboard/assets/OverviewPage-B53YYMng.js.gz +0 -0
  378. package/dist/dashboard/assets/ProviderDetailPage-B0KaAQf0.js +0 -3
  379. package/dist/dashboard/assets/ProviderDetailPage-B0KaAQf0.js.gz +0 -0
  380. package/dist/dashboard/assets/ProvidersPage-Dx31DgIO.js +0 -1
  381. package/dist/dashboard/assets/ProvidersPage-Dx31DgIO.js.gz +0 -0
  382. package/dist/dashboard/assets/ProxyPage-BpnR4kXT.js +0 -7
  383. package/dist/dashboard/assets/ProxyPage-BpnR4kXT.js.gz +0 -0
  384. package/dist/dashboard/assets/QuotaPage-BpOQCnun.js +0 -1
  385. package/dist/dashboard/assets/QuotaPage-BpOQCnun.js.gz +0 -0
  386. package/dist/dashboard/assets/SettingsPage-DV1eo2J_.js +0 -1
  387. package/dist/dashboard/assets/SettingsPage-DV1eo2J_.js.gz +0 -0
  388. package/dist/dashboard/assets/SetupPage-D6pmQ86L.js.gz +0 -0
  389. package/dist/dashboard/assets/SortableList-DWvKiuSl.js.gz +0 -0
  390. package/dist/dashboard/assets/StudioPage-CmkUrgdj.js.gz +0 -0
  391. package/dist/dashboard/assets/UsagePage-B47uOydr.js +0 -56
  392. package/dist/dashboard/assets/UsagePage-B47uOydr.js.gz +0 -0
  393. package/dist/dashboard/assets/api-keys-uNywYlfv.js +0 -1
  394. package/dist/dashboard/assets/api-keys-uNywYlfv.js.gz +0 -0
  395. package/dist/dashboard/assets/app-DxivpfYG.css +0 -2
  396. package/dist/dashboard/assets/app-DxivpfYG.css.gz +0 -0
  397. package/dist/dashboard/assets/assert-NHwYq95t.js.gz +0 -0
  398. package/dist/dashboard/assets/badge-6uzDDvqs.js.gz +0 -0
  399. package/dist/dashboard/assets/banner-4_WNg2WZ.js.gz +0 -0
  400. package/dist/dashboard/assets/card--AtFedAq.js.gz +0 -0
  401. package/dist/dashboard/assets/cli-tools-B3e7dx3N.js.gz +0 -0
  402. package/dist/dashboard/assets/clipboard-button-NreUx6c8.js.gz +0 -0
  403. package/dist/dashboard/assets/dialog-BtpsJU98.js +0 -1
  404. package/dist/dashboard/assets/dialog-BtpsJU98.js.gz +0 -0
  405. package/dist/dashboard/assets/download-A0SW9OCW.js +0 -1
  406. package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
  407. package/dist/dashboard/assets/drawer-WwwtzihF.js +0 -1
  408. package/dist/dashboard/assets/drawer-WwwtzihF.js.gz +0 -0
  409. package/dist/dashboard/assets/entry-CkryrIea.js +0 -4
  410. package/dist/dashboard/assets/entry-CkryrIea.js.gz +0 -0
  411. package/dist/dashboard/assets/entry-TJIye_jT.js +0 -1
  412. package/dist/dashboard/assets/entry-TJIye_jT.js.gz +0 -0
  413. package/dist/dashboard/assets/entry-jy9_4-Ke.js +0 -1
  414. package/dist/dashboard/assets/entry-jy9_4-Ke.js.gz +0 -0
  415. package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
  416. package/dist/dashboard/assets/github-badge-D_eM3IgG.js.gz +0 -0
  417. package/dist/dashboard/assets/index-BydyKaDz.js.gz +0 -0
  418. package/dist/dashboard/assets/inline-BcNGF8Gr.js.gz +0 -0
  419. package/dist/dashboard/assets/input-BG41et9G.js.gz +0 -0
  420. package/dist/dashboard/assets/layout-C8mer_Ak.js +0 -1
  421. package/dist/dashboard/assets/layout-C8mer_Ak.js.gz +0 -0
  422. package/dist/dashboard/assets/list-row-Gh_W4OR_.js.gz +0 -0
  423. package/dist/dashboard/assets/live-BJGYRBnR.js.gz +0 -0
  424. package/dist/dashboard/assets/model-bans-DeVLDA7I.js +0 -1
  425. package/dist/dashboard/assets/model-bans-DeVLDA7I.js.gz +0 -0
  426. package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
  427. package/dist/dashboard/assets/numbers-Bs4LOOuf.js +0 -2
  428. package/dist/dashboard/assets/numbers-Bs4LOOuf.js.gz +0 -0
  429. package/dist/dashboard/assets/query-keys-D7wX2vM5.js.gz +0 -0
  430. package/dist/dashboard/assets/routing-CcusqXA7.js.gz +0 -0
  431. package/dist/dashboard/assets/select-BO6h8yNv.js.gz +0 -0
  432. package/dist/dashboard/assets/settings-mpmXTgUa.js.gz +0 -0
  433. package/dist/dashboard/assets/sparkles-BRaRUxAU.js +0 -1
  434. package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
  435. package/dist/dashboard/assets/stack-DwLmjUFK.js.gz +0 -0
  436. package/dist/dashboard/assets/state-DYdQmdAl.js.gz +0 -0
  437. package/dist/dashboard/assets/switch-CidSfbG7.js +0 -1
  438. package/dist/dashboard/assets/switch-CidSfbG7.js.gz +0 -0
  439. package/dist/dashboard/assets/terminal-Bd4ee8TA.js +0 -1
  440. package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
  441. package/dist/dashboard/assets/tokens-DDxMbSRL.js +0 -1
  442. package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
  443. package/dist/dashboard/assets/use-presence-BSZEBx7U.js +0 -1
  444. package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
  445. package/dist/dashboard/assets/useMutation-w4u4CVue.js.gz +0 -0
  446. package/scripts/cli/command/admin.ts +0 -65
  447. package/scripts/cli/command/doctor.ts +0 -257
  448. package/scripts/cli/command/update.ts +0 -23
  449. package/scripts/cli/gateway/drain-token.ts +0 -78
  450. package/scripts/cli/gateway/instance-lock.ts +0 -105
  451. package/scripts/cli/gateway/stop.ts +0 -51
  452. package/scripts/cli/support/bun.ts +0 -27
  453. package/scripts/cli/support/version.ts +0 -37
  454. /package/dist/dashboard/assets/{app-CafWtVXA.js → app-BIt446yE.js} +0 -0
  455. /package/dist/dashboard/assets/{app-CafWtVXA.js.gz → app-BIt446yE.js.gz} +0 -0
  456. /package/scripts/cli/{ui/prompt.ts → prompt.ts} +0 -0
@@ -8,17 +8,17 @@ export type DbMode = "full" | "lite";
8
8
  /**
9
9
  * Reads `CARTETHYIA_DB_MODE`, which is required and has no default.
10
10
  *
11
- * It used to default to `full`, which left the same key carrying three
12
- * different answers: `.env.example` shipped `lite`, the `bun setup` prompt
13
- * offered `lite`, and an environment that set nothing at all booted `full`.
14
- * The third one is the one that bit — a clone without a `.env` came up in Full
15
- * mode and then failed on a missing `DATABASE_URL`, when the documented intent
16
- * was the self-contained Lite path.
11
+ * It must not default to `full`: that leaves the same key carrying three
12
+ * different answers, since `.env.example` ships `lite`, the `bun setup` prompt
13
+ * offers `lite`, and an environment that sets nothing at all would boot
14
+ * `full`. The third one is the one that bites — a clone without a `.env` comes
15
+ * up in Full mode and then fails on a missing `DATABASE_URL`, when the
16
+ * documented intent is the self-contained Lite path.
17
17
  *
18
18
  * Failing closed is the honest fix: the two modes are different databases with
19
19
  * different migration sets (`migrations/lite-sqlite3/` vs
20
20
  * `migrations/full-postgres/`), so guessing one silently picks which migrations
21
- * an operator's data is read against. `bun setup`, `scripts/install/lite.sh`,
21
+ * an operator's data is read against. `bun setup`, `scripts/install.sh`,
22
22
  * `.env.example`, and `docker-compose.yml` all set it explicitly,
23
23
  * so requiring it costs an existing deployment nothing.
24
24
  */
@@ -67,6 +67,12 @@ export class HealthEventFlushBuffer {
67
67
  private timer: ReturnType<typeof setInterval> | undefined;
68
68
  private flushing = false;
69
69
  private dropped = 0;
70
+ /**
71
+ * Settles once while an INSERT is in flight, so a purge that drops the
72
+ * queue can wait that write out instead of letting it land behind its own
73
+ * `DELETE` and resurrect the rows it just removed.
74
+ */
75
+ private inflightDone: Promise<void> | undefined;
70
76
 
71
77
  constructor(
72
78
  private readonly db: CartethyiaDatabase,
@@ -97,10 +103,38 @@ export class HealthEventFlushBuffer {
97
103
  return this.dropped;
98
104
  }
99
105
 
106
+ /** Drops queued rows — everything, or one account's — without writing them. */
107
+ clear(accountId?: string): void {
108
+ if (accountId === undefined) {
109
+ this.pending.length = 0;
110
+ return;
111
+ }
112
+ for (let i = this.pending.length - 1; i >= 0; i -= 1) {
113
+ if (this.pending[i]?.accountId === accountId) this.pending.splice(i, 1);
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Drops queued rows and then blocks until a write already in flight lands.
119
+ *
120
+ * The barrier is what makes a purge sound: deleting only the rows visible at
121
+ * one instant is final, while an insert in flight for the same instant would
122
+ * re-add rows after the `DELETE` finished. Rows enqueued *after* the drop
123
+ * are new events, not resurrected ones, so they are left alone.
124
+ */
125
+ async drain(accountId?: string): Promise<void> {
126
+ this.clear(accountId);
127
+ await this.inflightDone;
128
+ }
129
+
100
130
  /** Writes everything accumulated so far. Safe to call concurrently. */
101
131
  async flush(): Promise<void> {
102
132
  if (this.flushing || this.pending.length === 0) return;
103
133
  this.flushing = true;
134
+ let settle!: () => void;
135
+ this.inflightDone = new Promise<void>((resolve) => {
136
+ settle = resolve;
137
+ });
104
138
  // Drained before the await: rows enqueued while the insert runs stay for
105
139
  // the next pass rather than being lost if this one throws.
106
140
  const batch = this.pending.splice(0, this.pending.length);
@@ -123,6 +157,8 @@ export class HealthEventFlushBuffer {
123
157
  // are dropped; the durable status change was written synchronously.
124
158
  } finally {
125
159
  this.flushing = false;
160
+ this.inflightDone = undefined;
161
+ settle();
126
162
  }
127
163
  }
128
164
 
@@ -9,7 +9,7 @@ import { withTimeout } from "../boot/timeout";
9
9
  /**
10
10
  * Memoized readiness: the first caller runs the real `SELECT 1`/`PING`/
11
11
  * migration checks, and every subsequent caller within the TTL window reuses
12
- * that result — so a burst of concurrent requests no longer each spend a
12
+ * that result — so a burst of concurrent requests does not each spend a
13
13
  * Postgres pool checkout plus a Redis PING on readiness validation.
14
14
  */
15
15
 
@@ -182,7 +182,7 @@ export const providers = pgTable(
182
182
  // `tenant_id` null means the account is shared pool-wide; populated means
183
183
  // tenant-owned/BYOK. The concurrency ceiling lives in provider routing
184
184
  // (`provider_routing_settings.max_inflight`) and network pools
185
- // (`network_pools.max_inflight`); the per-account override no longer exists.
185
+ // (`network_pools.max_inflight`); there is no per-account override.
186
186
  export const providerAccounts = pgTable("provider_accounts", {
187
187
  id: uuid("id").primaryKey().defaultRandom(),
188
188
  providerId: text("provider_id")
@@ -469,11 +469,11 @@ export const networkPools = pgTable("network_pools", {
469
469
  * (`egress_ip`, `last_latency_ms`, the speed-test columns) rather than in the
470
470
  * browser.
471
471
  *
472
- * It lived in `localStorage` first, and that made it decorative: the verdict
473
- * was visible to the operator who ran it and to nobody else — not to another
474
- * machine, not to the gateway, and not to admission. A pool could read
475
- * "OpenCode blocked" on screen while dispatch kept sending traffic through
476
- * it, because nothing outside that one browser had ever heard the verdict.
472
+ * Browser storage is not enough: a verdict held only there is visible to the
473
+ * operator who ran it and to nobody else — not to another machine, not to the
474
+ * gateway, and not to admission. A pool would read "OpenCode blocked" on
475
+ * screen while dispatch kept sending traffic through it, because nothing
476
+ * outside that one browser had heard the verdict.
477
477
  *
478
478
  * `null` means never probed. These describe the *address* recorded in
479
479
  * `egress_ip` at the time, so a changed endpoint clears them.
@@ -527,7 +527,7 @@ export const healthEvents = pgTable(
527
527
  index("health_events_account_id_created_at_idx").on(table.accountId, table.createdAt),
528
528
  // Descending `created_at` is deliberate: health history is always read
529
529
  // newest-first. The SQLite baseline drops the direction (see
530
- // `scripts/generate/sqlite-ddl.ts` — SQLite scans an index either way),
530
+ // `scripts/tools/generate-sqlite.ts` — SQLite scans an index either way),
531
531
  // so this is a Postgres-only access-path choice, not a cross-backend
532
532
  // disagreement.
533
533
  index("health_events_account_model_created_idx").on(
@@ -544,8 +544,7 @@ export const healthEvents = pgTable(
544
544
  );
545
545
 
546
546
 
547
- // Model aliasing & combos: replaces the dormant, disconnected
548
- // `routing_policies` scaffolding. Aliases resolve a client-facing name to a
547
+ // Model aliasing & combos. Aliases resolve a client-facing name to a
549
548
  // real model; combos back a client-facing name with >=1 real models,
550
549
  // auto-selected via `fallback` or `round_robin`.
551
550
  export const modelComboStrategy = pgEnum("model_combo_strategy", [
@@ -556,7 +555,7 @@ export const modelComboStrategy = pgEnum("model_combo_strategy", [
556
555
  /** Canonical `ComboStrategy` union, derived from the enum above. Console
557
556
  * domain and routing layers import this instead of hand-typing their own mirror.
558
557
  * `ProviderRoutingStrategy` covers the same two values but persists on
559
- * `provider_routing_settings` beside `rotateCount`; sticky routing is gone. */
558
+ * `provider_routing_settings` beside `rotateCount`. */
560
559
  export type ComboStrategy = (typeof modelComboStrategy.enumValues)[number];
561
560
 
562
561
  export const modelAliases = pgTable(
@@ -635,9 +634,8 @@ export const providerRoutingSettings = pgTable(
635
634
  * provider; this one bounds one provider's share of one pool. All three
636
635
  * are enforced together — none replaces another.
637
636
  *
638
- * `null` = unlimited, and that is the default: a provider nobody has
639
- * configured behaves exactly as it did before this column existed, with no
640
- * extra Redis key and no extra round trip.
637
+ * `null` = unlimited, and that is the default: an unconfigured provider
638
+ * costs no extra Redis key and no extra round trip.
641
639
  */
642
640
  maxInflightPerProxy: integer("max_inflight_per_proxy"),
643
641
  /**
@@ -752,7 +750,7 @@ export const apiKeys = pgTable(
752
750
  /**
753
751
  * `whitelist` = only the names in `modelList` may be used (empty = all);
754
752
  * `blacklist` = the names in `modelList` are refused (empty = none). One
755
- * list, one mode — the old separate allow/deny columns are retired.
753
+ * list, one mode.
756
754
  */
757
755
  modelAccessMode: text("model_access_mode")
758
756
  .$type<ApiKeyModelAccessMode>()
@@ -787,11 +785,9 @@ export const apiKeys = pgTable(
787
785
  /**
788
786
  * One *live* default key per tenant, enforced by the database.
789
787
  *
790
- * The baseline SQL has always created this; the schema never declared it,
791
- * so the two descriptions disagreed and nothing noticed. It is the only
792
- * database-level guarantee behind `isDefault`, and
788
+ * This is the only database-level guarantee behind `isDefault`, and
793
789
  * `test/persistence/sqlite-baseline.test.ts` pins it by name — so the
794
- * schema was the incomplete side, not the migration.
790
+ * schema must keep declaring it to match the baseline.
795
791
  *
796
792
  * Partial on `revoked_at IS NULL` so a tenant can rotate its default key
797
793
  * without the old, revoked row blocking the new one.
@@ -879,8 +875,7 @@ export const shareLinks = pgTable(
879
875
  .references(() => apiKeys.id, { onDelete: "cascade" }),
880
876
  tokenHash: text("token_hash").notNull(),
881
877
  // Encrypted bearer token. Present so the console can re-display a stable
882
- // link; historical rows written before this column carry NULL and simply
883
- // cannot be shown again.
878
+ // link; nullable, and a NULL row simply cannot be shown again.
884
879
  tokenEncrypted: bytea("token_encrypted"),
885
880
  /**
886
881
  * The public slug this page is reachable at, in the clear.
@@ -891,7 +886,7 @@ export const shareLinks = pgTable(
891
886
  * unencrypted keeps every restored share page reachable under the address
892
887
  * it was created with.
893
888
  *
894
- * Nullable: rows written before this column have no slug, and inventing one
889
+ * Nullable: a row without a slug has no address, and inventing one
895
890
  * would publish an address the page never had.
896
891
  */
897
892
  slug: text("slug"),
@@ -903,8 +898,8 @@ export const shareLinks = pgTable(
903
898
  },
904
899
  (table) => [
905
900
  uniqueIndex("share_links_token_hash_idx").on(table.tokenHash),
906
- // Partial: rows without a slug (written before the column) are not
907
- // constrained, so backfilling is never blocked by a uniqueness collision.
901
+ // Partial: rows without a slug are not constrained, so backfilling is never
902
+ // blocked by a uniqueness collision.
908
903
  uniqueIndex("share_links_slug_uidx").on(table.slug).where(sql`${table.slug} IS NOT NULL`),
909
904
  index("share_links_api_key_idx").on(table.apiKeyId),
910
905
  index("share_links_active_kind_expires_idx").on(table.active, table.kind, table.expiresAt),
@@ -1045,7 +1040,59 @@ export interface ConsoleSettingsPreferences {
1045
1040
  * - `maximum` — 16 MiB combined cap, expect CPU/memory spikes while active
1046
1041
  */
1047
1042
  telemetryPayloadDepth?: "minimum" | "moderate" | "maximum";
1043
+ /**
1044
+ * How long a captured payload frame is kept, in whole minutes.
1045
+ *
1046
+ * `null` or absent means the deployment default
1047
+ * (`CARTETHYIA_TELEMETRY_PAYLOAD_RETENTION_MS`, 15 minutes). Raising it is a
1048
+ * real trade: a frame is a gzipped copy of up to four request bodies, so
1049
+ * keeping them for a week is keeping a week of bodies on disk. The upper
1050
+ * bound is seven days, enforced by the capture path rather than trusted here.
1051
+ */
1052
+ telemetryPayloadRetentionMinutes?: number | null;
1048
1053
  privacyMode?: "masked" | "full";
1054
+ /**
1055
+ * Ignores an account-wide cooldown when building route candidates.
1056
+ *
1057
+ * Off by default, and deliberately dangerous: an account in cooldown was
1058
+ * parked because the upstream refused it, and dialing it anyway reproduces
1059
+ * that refusal on every attempt. Turning this on can exhaust the candidate
1060
+ * list and fail a request that would otherwise have been routed.
1061
+ *
1062
+ * The override is read-only — the cooldown is still recorded, still shown in
1063
+ * Health, and still recovered by the sweep. Nothing is written back, so the
1064
+ * toggle can be turned off again without cleaning anything up.
1065
+ *
1066
+ * Per-(account, model) cooldowns are NOT overridden. Those are usually a
1067
+ * named quota ("daily free limit reached, try again in 11h") where the
1068
+ * upstream stated a real reset; ignoring it only buys repeated refusals.
1069
+ */
1070
+ forceDisableCooldown?: boolean;
1071
+ /**
1072
+ * Per-tenant upstream request timeout in seconds, overriding
1073
+ * `CARTETHYIA_UPSTREAM_TIMEOUT_MS`. `null` or absent means use the
1074
+ * deployment default.
1075
+ */
1076
+ upstreamTimeoutSeconds?: number | null;
1077
+ /**
1078
+ * Per-tenant telemetry compaction age in days, overriding
1079
+ * `CARTETHYIA_TELEMETRY_COMPACT_AFTER_DAYS`. `null` or absent means use the
1080
+ * deployment default. Lower keeps less history but bounds the usage-list
1081
+ * scan; see the compaction notes in `event-compaction.ts`.
1082
+ */
1083
+ telemetryWindowMinutes?: number | null;
1084
+ /**
1085
+ * Per-tenant ceiling for one inline attachment, in bytes, overriding the
1086
+ * deployment default of 10 MB.
1087
+ *
1088
+ * Raising this asserts that this tenant's upstreams accept larger
1089
+ * attachments. The clamp is a hard send gate precisely because an oversized
1090
+ * block poisons every retry and every replayed turn — the upstream rejects
1091
+ * the whole request rather than the one block — so an override larger than
1092
+ * the provider actually accepts trades that protection away. Lowering it is
1093
+ * the safer direction and the more common need.
1094
+ */
1095
+ imageClampMaxBytes?: number | null;
1049
1096
  }
1050
1097
 
1051
1098
  export const consoleSettings = pgTable("console_settings", {
@@ -1247,9 +1294,8 @@ export const telemetryPayloads = pgTable(
1247
1294
  expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
1248
1295
  /**
1249
1296
  * Typed index into the on-disk frame store. Prompt/response bodies live
1250
- * only in the frame file these columns name — never in Postgres. The
1251
- * previous `request_body jsonb` column is gone so a body cannot be
1252
- * written here by accident.
1297
+ * only in the frame file these columns name — never in Postgres, which is
1298
+ * why this table carries no body column to write one into by accident.
1253
1299
  */
1254
1300
  storage: text("storage").notNull(),
1255
1301
  file: text("file").notNull(),
@@ -1264,8 +1310,8 @@ export const telemetryPayloads = pgTable(
1264
1310
  * reads it off the indexed row.
1265
1311
  *
1266
1312
  * Bits: 1=request, 2=provider_request, 4=provider_response,
1267
- * 8=client_response. Nullable: NULL means the row predates this column,
1268
- * which renders as all icons greyed rather than inventing presence.
1313
+ * 8=client_response. Nullable: NULL renders as all icons greyed rather
1314
+ * than inventing presence.
1269
1315
  */
1270
1316
  stages: integer("stages"),
1271
1317
  },
@@ -1273,7 +1319,7 @@ export const telemetryPayloads = pgTable(
1273
1319
  index("telemetry_payloads_request_id_idx").on(table.requestId),
1274
1320
  index("telemetry_payloads_tenant_request_idx").on(table.tenantId, table.requestId),
1275
1321
  index("telemetry_payloads_expires_idx").on(table.expiresAt),
1276
- check("telemetry_payloads_storage_check", sql`${table.storage} IN ('jsonb-file', 'jsonl-file')`),
1322
+ check("telemetry_payloads_storage_check", sql`${table.storage} = 'jsonl-file'`),
1277
1323
  check("telemetry_payloads_version_check", sql`${table.version} = 1`),
1278
1324
  check("telemetry_payloads_offset_check", sql`${table.offset} >= 0`),
1279
1325
  check("telemetry_payloads_length_check", sql`${table.length} > 0`),
@@ -1,7 +1,6 @@
1
1
  // Cross-process cache coordination: a shared L2 in front of Postgres.
2
2
  //
3
- // Every hot read in the request path is cached in-process (see
4
- // `documentation/research/iomemory/01-request-path-census.md`). That is correct
3
+ // Every hot read in the request path is cached in-process. That is correct
5
4
  // for one process and wrong for several: `main.ts` sets `reusePort: true`, so N
6
5
  // gateway processes each hold their own copy. With no shared layer, a key
7
6
  // revoked through the console stays valid for up to a full TTL in every other
@@ -187,6 +187,7 @@ export interface ApiKeyStore {
187
187
  reorder(tenantId: string, keyIds: readonly string[]): Promise<void>;
188
188
  update(tenantId: string, keyId: string, patch: ApiKeyPatch): Promise<ApiKeyRecord | undefined>;
189
189
  revoke(tenantId: string, keyId: string, revokedAt: Date): Promise<boolean>;
190
+ remove(tenantId: string, keyId: string): Promise<boolean>;
190
191
  findActiveByHash?(hash: string): Promise<typeof apiKeys.$inferSelect | undefined>;
191
192
  findActiveById?(keyId: string): Promise<typeof apiKeys.$inferSelect | undefined>;
192
193
  sumChildrenConsumed?(parentKeyId: string): Promise<number>;
@@ -380,10 +381,6 @@ export class DrizzleApiKeyStore implements ApiKeyStore {
380
381
  .set({
381
382
  ...(patch.enabled !== undefined ? { enabled: patch.enabled } : {}),
382
383
  ...(patch.label !== undefined ? { label: patch.label } : {}),
383
- // Promotes a legacy default key: `isDefaultGatewayApiKey` recognizes
384
- // it by its canonical label, but recognition was never written back,
385
- // so the row stayed unflagged and a later rename — which changes the
386
- // only field that identifies it — dropped the default entirely.
387
384
  ...(patch.isDefault !== undefined ? { isDefault: patch.isDefault } : {}),
388
385
  ...(patch.keyHash !== undefined ? { keyHash: patch.keyHash } : {}),
389
386
  ...(patch.keyMode !== undefined ? { keyMode: patch.keyMode } : {}),
@@ -413,8 +410,8 @@ export class DrizzleApiKeyStore implements ApiKeyStore {
413
410
  : {}),
414
411
  // Scopes are validated once, in `validateApiKeyRequest`, against the
415
412
  // tenant scope set; the store writes what it is handed. Omitting the
416
- // field here meant an update was type-checked, accepted, and then
417
- // silently dropped — a grant like `routing:cli_mapping` could be
413
+ // field here means an update is type-checked, accepted, and then
414
+ // silently dropped — a grant like `routing:cli_mapping` can be
418
415
  // turned on and never stick.
419
416
  ...(patch.scopes === undefined ? {} : { scopes: patch.scopes }),
420
417
  })
@@ -453,6 +450,21 @@ export class DrizzleApiKeyStore implements ApiKeyStore {
453
450
  return true;
454
451
  });
455
452
  }
453
+ async remove(tenantId: string, keyId: string): Promise<boolean> {
454
+ return this.db.transaction(async (tx) => {
455
+ const rows = await tx
456
+ .delete(apiKeys)
457
+ .where(
458
+ and(
459
+ eq(apiKeys.tenantId, tenantId),
460
+ eq(apiKeys.id, keyId),
461
+ eq(apiKeys.isDefault, false),
462
+ ),
463
+ )
464
+ .returning({ id: apiKeys.id });
465
+ return rows.length > 0;
466
+ });
467
+ }
456
468
  async findActiveByHash(hash: string): Promise<typeof apiKeys.$inferSelect | undefined> {
457
469
  // The SQLite backend holds this lookup prepared. The builder path rebuilds
458
470
  // the query per call, which measured 60x the lookup itself, so the hot
@@ -308,9 +308,10 @@ export class DrizzleShareLinkStore implements ShareLinkStore {
308
308
  .set({
309
309
  tokenHash: input.tokenHash,
310
310
  tokenEncrypted: input.tokenEncrypted,
311
- // The slug is the address and survives a rotate, so an operator who
312
- // rotates does not invalidate every copy of the link they shared.
313
- ...(current.slug === null ? { slug: input.slug } : {}),
311
+ // Rotating the slug changes the public URL, so the old link stops resolving.
312
+ // Callers always generate a fresh slug via generateShareSlug() before calling
313
+ // create() with rotate: true.
314
+ slug: input.slug,
314
315
  expiresAt: input.expiresAt,
315
316
  lastViewedAt: null,
316
317
  })
@@ -10,6 +10,8 @@ import {
10
10
  type TelemetryUsageIdentityType,
11
11
  } from "../schema";
12
12
  import { isGatewayError } from "../../observability/telemetry-status";
13
+ import { TELEMETRY_EVENT_DIR } from "../../observability/event-log";
14
+ import { dayEventFileName, utcDay } from "../../observability/event-day-index";
13
15
 
14
16
  interface UsageTotalDelta {
15
17
  readonly tenantId: string;
@@ -66,27 +68,47 @@ function aggregateUsage(rows: readonly (typeof telemetryEvents.$inferInsert)[])
66
68
  * operations, lifetime aggregates, and retention.
67
69
  */
68
70
 
69
- const EVENT_DIR = "./data/telemetry-events";
70
71
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
71
72
 
72
73
  /**
73
- * Deletes hourly event files whose whole hour is older than the retention
74
- * cutoff. The current hour's file is never touched: it is still being appended.
74
+ * Deletes whole days past the retention cutoff: the event file and its index
75
+ * together.
76
+ *
77
+ * The day is the retention unit now, so both go at once. Removing one without
78
+ * the other is the failure this avoids — an index with no events reports totals
79
+ * for requests that no longer exist, and events with no index are invisible to
80
+ * every usage read.
81
+ *
82
+ * The cutoff is a whole day, so today's file — still being appended to — is
83
+ * never a candidate. Comparing filenames hour by hour would match a day file's
84
+ * `.events` suffix against an hour stamp and could drop the open file.
75
85
  */
76
86
  async function pruneEventFiles(before: Date): Promise<void> {
77
- const files = await readdir(EVENT_DIR).catch(() => [] as string[]);
78
- const cutoff = before.toISOString().slice(0, 13).replaceAll(":", "-");
87
+ const files = await readdir(TELEMETRY_EVENT_DIR).catch(() => [] as string[]);
88
+ const cutoffDay = before.toISOString().slice(0, 10);
79
89
  for (const file of files) {
80
- if (!file.endsWith(".jsonl")) continue;
81
- const hour = file.slice(0, 13);
82
- if (hour < cutoff) await rm(join(EVENT_DIR, file), { force: true });
90
+ const day = file.slice(0, 10);
91
+ if (day.length !== 10 || day >= cutoffDay) continue;
92
+ if (!file.endsWith(".events.jsonl") && !file.endsWith(".index.json")) continue;
93
+ await rm(join(TELEMETRY_EVENT_DIR, file), { force: true });
83
94
  }
84
95
  }
85
96
 
86
- /** Appends one JSON line per event. One file per hour, so retention is a file delete. */
97
+ /**
98
+ * Appends one JSON line per event to the day's event file.
99
+ *
100
+ * One file per UTC day, appended to and never rewritten while the day is open.
101
+ * A file per `windowMinutes` window — 96 a day at the default — would have to be
102
+ * found, opened and parsed one by one on every usage query, and which of those
103
+ * files was still being written could only be inferred from the name, which is
104
+ * what lets the open one be deleted mid-write.
105
+ *
106
+ * A single growing file has no such ambiguity, and the day index tracks how much
107
+ * of it has been counted by byte offset.
108
+ */
87
109
  async function appendEvents(rows: readonly (typeof telemetryEvents.$inferInsert)[]): Promise<void> {
88
- await mkdir(EVENT_DIR, { recursive: true });
89
- const file = join(EVENT_DIR, new Date().toISOString().slice(0, 13).replaceAll(":", "-") + ".jsonl");
110
+ await mkdir(TELEMETRY_EVENT_DIR, { recursive: true });
111
+ const file = join(TELEMETRY_EVENT_DIR, dayEventFileName(utcDay(new Date())));
90
112
  const newline = String.fromCharCode(10);
91
113
  const body = rows.map((row) => JSON.stringify(row)).join(newline) + newline;
92
114
  await appendFile(file, body, "utf8");
@@ -308,6 +308,13 @@ export type ContentPart =
308
308
  /** Responses reasoning summary block index, preserved across surfaces. */
309
309
  summary_index?: number;
310
310
  signature?: string;
311
+ /**
312
+ * Responses item id the `encrypted_content` was issued for. The upstream
313
+ * binds encrypted reasoning to its item and rejects a replay whose item
314
+ * id does not match, so the id travels with the payload it belongs to
315
+ * rather than being re-derived at request time.
316
+ */
317
+ item_id?: string;
311
318
  encrypted_content?: string;
312
319
  }
313
320
  | { kind: "extension"; name: string; payload: unknown };
@@ -596,9 +603,10 @@ export function hasEncryptedReasoning(parts: readonly ContentPart[]): boolean {
596
603
  * Single-pass summary of a request's content parts.
597
604
  *
598
605
  * `deriveRequiredCapabilities` + `assessTranslation` run on every candidate
599
- * preflight and previously each flattened the whole request (`allParts`) and
600
- * then scanned the copy up to ~6 times. This walks the messages once, builds
601
- * no intermediate array, and answers every predicate the pipeline needs.
606
+ * preflight, and flattening the whole request (`allParts`) in each and then
607
+ * scanning the copy up to ~6 times is the cost this avoids. It walks the
608
+ * messages once, builds no intermediate array, and answers every predicate the
609
+ * pipeline needs.
602
610
  * Results are memoized per request object in a `WeakMap`, so the preflight +
603
611
  * translate passes over one request share a single summary and entries are
604
612
  * collected with the request itself — no manual invalidation, no leak.