cartethyia 2.7.9 → 2.8.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 (375) hide show
  1. package/.env.example +3 -2
  2. package/dist/dashboard/assets/{BannedPage-BxoG1MrL.js → BannedPage-Cc0vKpMG.js} +1 -1
  3. package/dist/dashboard/assets/BannedPage-Cc0vKpMG.js.gz +0 -0
  4. package/dist/dashboard/assets/{CliToolDetailPage-ixwgKk53.js → CliToolDetailPage-Bqj2aa1T.js} +1 -1
  5. package/dist/dashboard/assets/CliToolDetailPage-Bqj2aa1T.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolsPage-BmttNV3U.js → CliToolsPage-CXsaOAO2.js} +1 -1
  7. package/dist/dashboard/assets/CliToolsPage-CXsaOAO2.js.gz +0 -0
  8. package/dist/dashboard/assets/{CombosPage-BE3qwwfC.js → CombosPage-BeLpJ4ma.js} +1 -1
  9. package/dist/dashboard/assets/CombosPage-BeLpJ4ma.js.gz +0 -0
  10. package/dist/dashboard/assets/{ConsoleLogPage-CRZdmMIX.js → ConsoleLogPage-DnV0uZ3m.js} +1 -1
  11. package/dist/dashboard/assets/ConsoleLogPage-DnV0uZ3m.js.gz +0 -0
  12. package/dist/dashboard/assets/{CustomizationPage-Dv_FJ6Er.js → CustomizationPage-CBxAjo63.js} +1 -1
  13. package/dist/dashboard/assets/CustomizationPage-CBxAjo63.js.gz +0 -0
  14. package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js +1 -0
  15. package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js.gz +0 -0
  16. package/dist/dashboard/assets/{InjectSystemPromptPanel-BOoO8rpz.js → InjectSystemPromptPanel-BWFLECmz.js} +2 -2
  17. package/dist/dashboard/assets/InjectSystemPromptPanel-BWFLECmz.js.gz +0 -0
  18. package/dist/dashboard/assets/{LoginPage-ChJuNNVN.js → LoginPage-CgoPc5B9.js} +1 -1
  19. package/dist/dashboard/assets/LoginPage-CgoPc5B9.js.gz +0 -0
  20. package/dist/dashboard/assets/{ModelPicker-BZAJtPg1.js → ModelPicker-D9bFQern.js} +1 -1
  21. package/dist/dashboard/assets/ModelPicker-D9bFQern.js.gz +0 -0
  22. package/dist/dashboard/assets/{OverviewPage-BE40s3u5.js → OverviewPage-B_p4tL8k.js} +1 -1
  23. package/dist/dashboard/assets/OverviewPage-B_p4tL8k.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js +8 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js.gz +0 -0
  26. package/dist/dashboard/assets/{ProviderIcon-CQ6bPUf_.js → ProviderIcon-D_0PiZtg.js} +1 -1
  27. package/dist/dashboard/assets/ProviderIcon-D_0PiZtg.js.gz +0 -0
  28. package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js +1 -0
  29. package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js.gz +0 -0
  30. package/dist/dashboard/assets/{ProxyPage-BJ87Pp0U.js → ProxyPage-BwNC_-Cx.js} +1 -1
  31. package/dist/dashboard/assets/ProxyPage-BwNC_-Cx.js.gz +0 -0
  32. package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js +1 -0
  33. package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js.gz +0 -0
  34. package/dist/dashboard/assets/{SettingsPage-C5HPWqEv.js → SettingsPage-CctTga2C.js} +1 -1
  35. package/dist/dashboard/assets/SettingsPage-CctTga2C.js.gz +0 -0
  36. package/dist/dashboard/assets/{SetupPage-CFNcATJJ.js → SetupPage-febLU4r1.js} +1 -1
  37. package/dist/dashboard/assets/SetupPage-febLU4r1.js.gz +0 -0
  38. package/dist/dashboard/assets/{StudioPage-YoD4tY5l.js → StudioPage-M6fvR5Wz.js} +1 -1
  39. package/dist/dashboard/assets/StudioPage-M6fvR5Wz.js.gz +0 -0
  40. package/dist/dashboard/assets/{UsagePage-DxjaAhb_.js → UsagePage-nyeuRqz9.js} +1 -1
  41. package/dist/dashboard/assets/UsagePage-nyeuRqz9.js.gz +0 -0
  42. package/dist/dashboard/assets/{api-keys-BTlkq_Uk.js → api-keys-BFOlvrJC.js} +1 -1
  43. package/dist/dashboard/assets/api-keys-BFOlvrJC.js.gz +0 -0
  44. package/dist/dashboard/assets/app-Dx2QgsNj.css +2 -0
  45. package/dist/dashboard/assets/app-Dx2QgsNj.css.gz +0 -0
  46. package/dist/dashboard/assets/assert-CqTLw5e7.js +1 -0
  47. package/dist/dashboard/assets/assert-CqTLw5e7.js.gz +0 -0
  48. package/dist/dashboard/assets/{badge-DdbT_7Am.js → badge-C83EqLB0.js} +1 -1
  49. package/dist/dashboard/assets/badge-C83EqLB0.js.gz +0 -0
  50. package/dist/dashboard/assets/{banner-CsGN-nbm.js → banner-D1xn7pIe.js} +1 -1
  51. package/dist/dashboard/assets/banner-D1xn7pIe.js.gz +0 -0
  52. package/dist/dashboard/assets/{card-mgZ_TLLZ.js → card-DU3Lq5Lm.js} +1 -1
  53. package/dist/dashboard/assets/card-DU3Lq5Lm.js.gz +0 -0
  54. package/dist/dashboard/assets/{cli-tools-A1-v8-03.js → cli-tools-Dw1U_drK.js} +1 -1
  55. package/dist/dashboard/assets/cli-tools-Dw1U_drK.js.gz +0 -0
  56. package/dist/dashboard/assets/{clipboard-button-CJ4Zty89.js → clipboard-button-CEL5Sg_J.js} +1 -1
  57. package/dist/dashboard/assets/clipboard-button-CEL5Sg_J.js.gz +0 -0
  58. package/dist/dashboard/assets/{dialog-B5L2jPQ_.js → dialog-BjFR5UHy.js} +1 -1
  59. package/dist/dashboard/assets/dialog-BjFR5UHy.js.gz +0 -0
  60. package/dist/dashboard/assets/{drawer-DkjUouAj.js → drawer-HvBW0AjI.js} +1 -1
  61. package/dist/dashboard/assets/drawer-HvBW0AjI.js.gz +0 -0
  62. package/dist/dashboard/assets/entry-BSu3gFlt.js +4 -0
  63. package/dist/dashboard/assets/entry-BSu3gFlt.js.gz +0 -0
  64. package/dist/dashboard/assets/{entry-B-LVLV6o.js → entry-CECVELyy.js} +1 -1
  65. package/dist/dashboard/assets/entry-CECVELyy.js.gz +0 -0
  66. package/dist/dashboard/assets/{entry-CAhdYl3B.js → entry-DYYofQ7W.js} +1 -1
  67. package/dist/dashboard/assets/entry-DYYofQ7W.js.gz +0 -0
  68. package/dist/dashboard/assets/{formatters-CrsdLVsn.js → formatters-DLW4i8zK.js} +1 -1
  69. package/dist/dashboard/assets/formatters-DLW4i8zK.js.gz +0 -0
  70. package/dist/dashboard/assets/{index-BU64Lqvp.js → index-1-fCqe64.js} +2 -2
  71. package/dist/dashboard/assets/index-1-fCqe64.js.gz +0 -0
  72. package/dist/dashboard/assets/{inline-B2aDLyyg.js → inline-DGvU2oLK.js} +1 -1
  73. package/dist/dashboard/assets/inline-DGvU2oLK.js.gz +0 -0
  74. package/dist/dashboard/assets/{input-BCVkdR9d.js → input-iNnO1X2K.js} +1 -1
  75. package/dist/dashboard/assets/input-iNnO1X2K.js.gz +0 -0
  76. package/dist/dashboard/assets/{layout-DNcAdMK-.js → layout-0q8UGypb.js} +1 -1
  77. package/dist/dashboard/assets/layout-0q8UGypb.js.gz +0 -0
  78. package/dist/dashboard/assets/{list-row-DgrIH0hw.js → list-row-C0w2bQjJ.js} +1 -1
  79. package/dist/dashboard/assets/list-row-C0w2bQjJ.js.gz +0 -0
  80. package/dist/dashboard/assets/{live-Cae01iu8.js → live-M5aJ_xCa.js} +1 -1
  81. package/dist/dashboard/assets/live-M5aJ_xCa.js.gz +0 -0
  82. package/dist/dashboard/assets/{network-CRib7H_2.js → network-BKeQGT3K.js} +1 -1
  83. package/dist/dashboard/assets/network-BKeQGT3K.js.gz +0 -0
  84. package/dist/dashboard/assets/{numbers-BKQE8Xpz.js → numbers-CivAk3da.js} +1 -1
  85. package/dist/dashboard/assets/numbers-CivAk3da.js.gz +0 -0
  86. package/dist/dashboard/assets/{pagination-ecPvG_8g.js → pagination-CpzvI5ep.js} +1 -1
  87. package/dist/dashboard/assets/pagination-CpzvI5ep.js.gz +0 -0
  88. package/dist/dashboard/assets/providers/zro.svg +3 -0
  89. package/dist/dashboard/assets/providers/zro.svg.gz +0 -0
  90. package/dist/dashboard/assets/quota-DiPD6UKH.js +1 -0
  91. package/dist/dashboard/assets/quota-DiPD6UKH.js.gz +0 -0
  92. package/dist/dashboard/assets/{routing-Ckm1UXuC.js → routing-BFQX2VBc.js} +1 -1
  93. package/dist/dashboard/assets/routing-BFQX2VBc.js.gz +0 -0
  94. package/dist/dashboard/assets/{select-ChoFWyKz.js → select-CXbck0jG.js} +2 -2
  95. package/dist/dashboard/assets/select-CXbck0jG.js.gz +0 -0
  96. package/dist/dashboard/assets/{settings-CKjFPlKW.js → settings-C_XvMHh9.js} +1 -1
  97. package/dist/dashboard/assets/settings-C_XvMHh9.js.gz +0 -0
  98. package/dist/dashboard/assets/{stack-DWCwU3fh.js → stack-DggRiCxE.js} +1 -1
  99. package/dist/dashboard/assets/stack-DggRiCxE.js.gz +0 -0
  100. package/dist/dashboard/assets/{state-Cg3VL5J_.js → state-ClFzfbYp.js} +1 -1
  101. package/dist/dashboard/assets/state-ClFzfbYp.js.gz +0 -0
  102. package/dist/dashboard/assets/{switch-Db70BMix.js → switch-CTIFxKqS.js} +1 -1
  103. package/dist/dashboard/assets/switch-CTIFxKqS.js.gz +0 -0
  104. package/dist/dashboard/assets/{useMutation-BqdE0LuM.js → useMutation-BClG3yWj.js} +1 -1
  105. package/dist/dashboard/assets/useMutation-BClG3yWj.js.gz +0 -0
  106. package/dist/dashboard/index.html +2 -2
  107. package/dist/dashboard/index.html.gz +0 -0
  108. package/dist/dashboard/precompressed-assets.json +54 -53
  109. package/migrations/full-postgres/0046_add_accounts_quota_plan_fetched_at.sql +7 -0
  110. package/migrations/lite-sqlite3/0046_add_accounts_quota_plan_fetched_at.sql +4 -0
  111. package/package.json +1 -1
  112. package/src/boot/app.ts +10 -13
  113. package/src/boot/dependencies.ts +1 -1
  114. package/src/boot/flush.ts +20 -0
  115. package/src/boot/lifecycle.ts +7 -5
  116. package/src/config/routing.ts +26 -0
  117. package/src/config/spec-metadata.ts +72 -0
  118. package/src/config/spec.ts +20 -0
  119. package/src/console/api-keys/contracts.ts +12 -1
  120. package/src/console/api-keys/routes.ts +40 -28
  121. package/src/console/auth/account-reset.ts +7 -9
  122. package/src/console/backup/contracts.ts +4 -0
  123. package/src/console/backup/export-endpoint.ts +7 -5
  124. package/src/console/cli-tools/service.ts +6 -12
  125. package/src/console/console-router-guards.ts +23 -16
  126. package/src/console/domain-registration.ts +0 -2
  127. package/src/console/inferhub/normalize.ts +24 -28
  128. package/src/console/inferhub/types.ts +3 -0
  129. package/src/console/ip-bans/routes.ts +7 -13
  130. package/src/console/observability/store.ts +0 -53
  131. package/src/console/providers/catalog/contracts.ts +2 -2
  132. package/src/console/providers/catalog/discover-constants.ts +6 -0
  133. package/src/console/providers/catalog/routes.ts +1 -0
  134. package/src/console/providers/catalog/store.ts +21 -16
  135. package/src/console/quota/account-quota-global.ts +14 -24
  136. package/src/console/quota/account-quota-shared.ts +49 -0
  137. package/src/console/quota/account-quota-tenant.ts +6 -18
  138. package/src/console/quota/quota-view.ts +19 -3
  139. package/src/console/routing/pools/operations.ts +31 -46
  140. package/src/console/shared/errors.ts +33 -6
  141. package/src/main.ts +2 -3
  142. package/src/network/pool/byte-accounting.ts +3 -2
  143. package/src/network/pool/selector.ts +182 -39
  144. package/src/network/pool-cooldown-store.ts +89 -14
  145. package/src/network/pool-health.ts +49 -61
  146. package/src/network/rate-limit-verdict.ts +1 -1
  147. package/src/network/ssrf.ts +6 -6
  148. package/src/observability/event-log.ts +7 -361
  149. package/src/observability/performance-metrics.ts +9 -0
  150. package/src/observability/runtime-metrics.ts +14 -9
  151. package/src/observability/telemetry-buffer.ts +15 -25
  152. package/src/observability/telemetry-reconciliation.ts +9 -37
  153. package/src/observability/telemetry-row.ts +125 -0
  154. package/src/persistence/db-mode.ts +4 -1
  155. package/src/persistence/dialect.ts +18 -0
  156. package/src/persistence/migrate.ts +6 -3
  157. package/src/persistence/schema.ts +8 -0
  158. package/src/persistence/ttl-cache.ts +20 -2
  159. package/src/protocol/codecs.ts +1 -1
  160. package/src/protocol/dsml.ts +34 -86
  161. package/src/protocol/markup-stream-parser.ts +195 -0
  162. package/src/protocol/reasoning-text.ts +19 -0
  163. package/src/protocol/request/chat.ts +10 -10
  164. package/src/protocol/request/gemini.ts +8 -2
  165. package/src/protocol/request/messages.ts +3 -4
  166. package/src/protocol/response/chat.ts +12 -20
  167. package/src/protocol/response/omp-tool-call-markup.ts +43 -86
  168. package/src/protocol/response/responses.ts +18 -2
  169. package/src/protocol/stop-reason.ts +23 -1
  170. package/src/protocol/think-split.ts +112 -0
  171. package/src/protocol/translation/dialect/qwen.ts +26 -58
  172. package/src/providers/adapters/compatible-adapter.ts +71 -12
  173. package/src/providers/authentication/oauth-client.ts +1 -1
  174. package/src/providers/authentication/oauth-device-flow.ts +98 -8
  175. package/src/providers/authentication/oauth-refresh-service.ts +30 -49
  176. package/src/providers/configured-provider.ts +148 -0
  177. package/src/providers/default-registry.ts +61 -36
  178. package/src/providers/discovery/probe-phases.ts +14 -4
  179. package/src/providers/discovery/probing-service.ts +15 -2
  180. package/src/providers/integrations/alibabacp/alibabacp-models.ts +1 -2
  181. package/src/providers/integrations/{dashscope-models.ts → alibabacp/dashscope-models.ts} +2 -2
  182. package/src/providers/integrations/antigravity/antigravity-protocol.ts +5 -1
  183. package/src/providers/integrations/buddy/buddy-chat-shared.ts +173 -17
  184. package/src/providers/integrations/buddy/buddy-identity.ts +19 -6
  185. package/src/providers/integrations/buddy/capabilities.ts +2 -2
  186. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +3 -0
  187. package/src/providers/integrations/buddy/codebuddy/provider.ts +4 -0
  188. package/src/providers/integrations/buddy/codebuddy/shared.ts +37 -22
  189. package/src/providers/integrations/claude/claude-quota.ts +14 -17
  190. package/src/providers/integrations/cline/cline.ts +2 -2
  191. package/src/providers/integrations/commandcode/quota.ts +22 -28
  192. package/src/providers/integrations/cursor/cursor-auth.ts +0 -8
  193. package/src/providers/integrations/cursor/cursor-exec.ts +9 -2
  194. package/src/providers/integrations/cursor/cursor-request.ts +26 -12
  195. package/src/providers/integrations/cursor/cursor-shared.ts +7 -4
  196. package/src/providers/integrations/cursor/cursor.ts +89 -40
  197. package/src/providers/integrations/cursor/protocol/messages.ts +22 -29
  198. package/src/providers/integrations/cursor/protocol/protobuf-schema.ts +37 -0
  199. package/src/providers/integrations/devin/protocol/devin-proto.ts +1 -27
  200. package/src/providers/integrations/factory/factory-oauth.ts +17 -44
  201. package/src/providers/integrations/freebuff/assets/official-system-manager.txt +88 -0
  202. package/src/providers/integrations/freebuff/assets/official-system-worker.txt +57 -0
  203. package/src/providers/integrations/freebuff/assets/official-tools.json +1070 -0
  204. package/src/providers/integrations/freebuff/freebuff-catalog.ts +120 -0
  205. package/src/providers/integrations/freebuff/freebuff-constants.ts +1233 -0
  206. package/src/providers/integrations/freebuff/freebuff-discovery.ts +108 -0
  207. package/src/providers/integrations/freebuff/freebuff-oauth.ts +240 -0
  208. package/src/providers/integrations/freebuff/freebuff-quota.ts +260 -0
  209. package/src/providers/integrations/freebuff/freebuff-virtual.ts +89 -0
  210. package/src/providers/integrations/freebuff/freebuff.ts +682 -0
  211. package/src/providers/integrations/grok/grok-oauth.ts +48 -45
  212. package/src/providers/integrations/grok/grok-ratelimit-store.ts +10 -17
  213. package/src/providers/integrations/grok/grok-turn-index.ts +11 -16
  214. package/src/providers/integrations/{hermes.ts → hermes/hermes.ts} +9 -9
  215. package/src/providers/integrations/hermes/index.ts +2 -0
  216. package/src/providers/integrations/kiro/kiro-catalog.ts +17 -1
  217. package/src/providers/integrations/kiro/kiro-discovery.ts +10 -12
  218. package/src/providers/integrations/kiro/kiro-quota.ts +8 -4
  219. package/src/providers/integrations/kiro/kiro.ts +15 -19
  220. package/src/providers/integrations/ollamacloud/index.ts +2 -0
  221. package/src/providers/integrations/{ollamacloud-models.ts → ollamacloud/models.ts} +2 -2
  222. package/src/providers/integrations/{ollamacloud-quota.ts → ollamacloud/quota.ts} +3 -3
  223. package/src/providers/integrations/opencode/opencode-go-quota.ts +2 -8
  224. package/src/providers/integrations/perch/perch-credential.ts +25 -0
  225. package/src/providers/integrations/perch/perch-protocol.ts +0 -3
  226. package/src/providers/integrations/perch/perch-quota.ts +2 -16
  227. package/src/providers/integrations/perch/perch.ts +3 -13
  228. package/src/providers/integrations/qoder/claim-qoder-credits.ts +18 -59
  229. package/src/providers/integrations/qoder/qoder-adapter.ts +1 -12
  230. package/src/providers/integrations/qoder/qoder-auth.ts +101 -29
  231. package/src/providers/integrations/qoder/qoder-catalog.ts +20 -5
  232. package/src/providers/integrations/qoder/qoder-chat.ts +7 -21
  233. package/src/providers/integrations/qoder/qoder-checkin.ts +4 -4
  234. package/src/providers/integrations/qoder/qoder-grpc.ts +0 -1
  235. package/src/providers/integrations/qoder/qoder-ide.ts +0 -107
  236. package/src/providers/integrations/qoder/qoder.ts +0 -1
  237. package/src/providers/integrations/trae/trae-protocol.ts +9 -5
  238. package/src/providers/integrations/trae/trae-quota.ts +6 -28
  239. package/src/providers/integrations/trae/trae-realm.ts +38 -0
  240. package/src/providers/integrations/trae/trae.ts +7 -24
  241. package/src/providers/integrations/zcode-start-plan/provider.ts +226 -23
  242. package/src/providers/integrations/zcode-start-plan/quota.ts +19 -2
  243. package/src/providers/integrations/zro/zro.ts +204 -0
  244. package/src/providers/operations/account-error-classification.ts +199 -307
  245. package/src/providers/operations/account-health-service.ts +82 -1
  246. package/src/providers/operations/account-refresh-status.ts +22 -37
  247. package/src/providers/operations/account-tier.ts +15 -16
  248. package/src/providers/operations/client-version-resolver.ts +1 -1
  249. package/src/providers/operations/client-versions.ts +5 -51
  250. package/src/providers/operations/cooldown-rules.ts +11 -16
  251. package/src/providers/operations/{model-catalog-cache.ts → provider-caches.ts} +68 -16
  252. package/src/providers/operations/provider-catalog-service.ts +1 -1
  253. package/src/providers/operations/provider-credential-service.ts +102 -32
  254. package/src/providers/operations/provider-rules/github.ts +48 -0
  255. package/src/providers/operations/provider-rules/grok.ts +33 -0
  256. package/src/providers/operations/provider-rules/inception.ts +49 -0
  257. package/src/providers/operations/provider-rules/index.ts +32 -0
  258. package/src/providers/operations/provider-rules/perch.ts +37 -0
  259. package/src/providers/operations/provider-rules/qoder.ts +70 -0
  260. package/src/providers/operations/provider-rules/trae.ts +81 -0
  261. package/src/providers/provider-contracts.ts +4 -2
  262. package/src/providers/{provider-credential-resolver.ts → provider-credentials.ts} +40 -3
  263. package/src/providers/provider-metadata.ts +40 -10
  264. package/src/providers/provider-probe.ts +15 -1
  265. package/src/providers/provider-registry.ts +42 -33
  266. package/src/providers/quota/quota-limit-windows.ts +16 -5
  267. package/src/providers/quota/quota-support.ts +1 -1
  268. package/src/providers/quota/quota-window-parser.ts +90 -18
  269. package/src/providers/quota/refresh.ts +75 -18
  270. package/src/security/api-key-auth-cache.ts +1 -1
  271. package/src/security/api-key-auth.ts +6 -2
  272. package/src/transport/dispatch/attempt-finalize.ts +14 -13
  273. package/src/transport/dispatch/attempt-loop.ts +71 -17
  274. package/src/transport/dispatch/generation.ts +2 -8
  275. package/src/transport/dispatch/native-shared.ts +25 -0
  276. package/src/transport/dispatch/proxy-request.ts +8 -46
  277. package/src/transport/dispatch/responses-compact.ts +2 -8
  278. package/src/transport/dispatch/streaming/finalize.ts +9 -6
  279. package/src/transport/dispatch/streaming/stream.ts +8 -36
  280. package/src/transport/dispatch/systemone.ts +2 -8
  281. package/src/transport/dispatch/token-count.ts +2 -6
  282. package/src/transport/dispatch/upstream.ts +63 -0
  283. package/src/transport/drain-endpoint.ts +31 -4
  284. package/src/transport/failure-policy.ts +16 -30
  285. package/src/transport/guards/body-policy.ts +7 -1
  286. package/src/transport/guards/error-lifecycle.ts +13 -1
  287. package/src/transport/guards/request-context.ts +22 -14
  288. package/src/transport/request/preparer.ts +81 -71
  289. package/src/transport/routing/bounded-lru.ts +14 -0
  290. package/src/transport/routing/drain-reserve.ts +129 -0
  291. package/src/transport/routing/plan-gate.ts +7 -1
  292. package/src/transport/routing/round-robin.ts +60 -0
  293. package/src/transport/routing/route-catalog.ts +123 -111
  294. package/src/transport/routing/route-model.ts +51 -9
  295. package/src/transport/routing/router.ts +393 -32
  296. package/src/transport/surface/adapters.ts +11 -2
  297. package/src/transport/surface/chat/adapter.ts +2 -2
  298. package/src/transport/surface/chat/encode.ts +47 -4
  299. package/src/transport/surface/chat/parse.ts +2 -12
  300. package/src/transport/surface/completion.ts +44 -33
  301. package/src/transport/surface/content-parts.ts +21 -0
  302. package/src/transport/surface/messages/adapter.ts +7 -1
  303. package/src/transport/surface/messages/encode.ts +11 -3
  304. package/src/transport/surface/messages/parse.ts +2 -1
  305. package/src/transport/surface/messages/stream.ts +33 -17
  306. package/src/transport/surface/responses/encode.ts +22 -5
  307. package/src/transport/surface/responses/parse.ts +3 -11
  308. package/dist/dashboard/assets/BannedPage-BxoG1MrL.js.gz +0 -0
  309. package/dist/dashboard/assets/CliToolDetailPage-ixwgKk53.js.gz +0 -0
  310. package/dist/dashboard/assets/CliToolsPage-BmttNV3U.js.gz +0 -0
  311. package/dist/dashboard/assets/CombosPage-BE3qwwfC.js.gz +0 -0
  312. package/dist/dashboard/assets/ConsoleLogPage-CRZdmMIX.js.gz +0 -0
  313. package/dist/dashboard/assets/CustomizationPage-Dv_FJ6Er.js.gz +0 -0
  314. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js +0 -1
  315. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js.gz +0 -0
  316. package/dist/dashboard/assets/InjectSystemPromptPanel-BOoO8rpz.js.gz +0 -0
  317. package/dist/dashboard/assets/LoginPage-ChJuNNVN.js.gz +0 -0
  318. package/dist/dashboard/assets/ModelPicker-BZAJtPg1.js.gz +0 -0
  319. package/dist/dashboard/assets/OverviewPage-BE40s3u5.js.gz +0 -0
  320. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js +0 -8
  321. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js.gz +0 -0
  322. package/dist/dashboard/assets/ProviderIcon-CQ6bPUf_.js.gz +0 -0
  323. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js +0 -1
  324. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js.gz +0 -0
  325. package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js.gz +0 -0
  326. package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js +0 -1
  327. package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js.gz +0 -0
  328. package/dist/dashboard/assets/SettingsPage-C5HPWqEv.js.gz +0 -0
  329. package/dist/dashboard/assets/SetupPage-CFNcATJJ.js.gz +0 -0
  330. package/dist/dashboard/assets/StudioPage-YoD4tY5l.js.gz +0 -0
  331. package/dist/dashboard/assets/UsagePage-DxjaAhb_.js.gz +0 -0
  332. package/dist/dashboard/assets/api-keys-BTlkq_Uk.js.gz +0 -0
  333. package/dist/dashboard/assets/app-BX2nnKqs.css +0 -2
  334. package/dist/dashboard/assets/app-BX2nnKqs.css.gz +0 -0
  335. package/dist/dashboard/assets/assert-CndthXFQ.js +0 -1
  336. package/dist/dashboard/assets/assert-CndthXFQ.js.gz +0 -0
  337. package/dist/dashboard/assets/badge-DdbT_7Am.js.gz +0 -0
  338. package/dist/dashboard/assets/banner-CsGN-nbm.js.gz +0 -0
  339. package/dist/dashboard/assets/card-mgZ_TLLZ.js.gz +0 -0
  340. package/dist/dashboard/assets/cli-tools-A1-v8-03.js.gz +0 -0
  341. package/dist/dashboard/assets/clipboard-button-CJ4Zty89.js.gz +0 -0
  342. package/dist/dashboard/assets/dialog-B5L2jPQ_.js.gz +0 -0
  343. package/dist/dashboard/assets/drawer-DkjUouAj.js.gz +0 -0
  344. package/dist/dashboard/assets/entry-B-LVLV6o.js.gz +0 -0
  345. package/dist/dashboard/assets/entry-CAhdYl3B.js.gz +0 -0
  346. package/dist/dashboard/assets/entry-D2xD0Tmd.js +0 -4
  347. package/dist/dashboard/assets/entry-D2xD0Tmd.js.gz +0 -0
  348. package/dist/dashboard/assets/formatters-CrsdLVsn.js.gz +0 -0
  349. package/dist/dashboard/assets/index-BU64Lqvp.js.gz +0 -0
  350. package/dist/dashboard/assets/inline-B2aDLyyg.js.gz +0 -0
  351. package/dist/dashboard/assets/input-BCVkdR9d.js.gz +0 -0
  352. package/dist/dashboard/assets/layout-DNcAdMK-.js.gz +0 -0
  353. package/dist/dashboard/assets/list-row-DgrIH0hw.js.gz +0 -0
  354. package/dist/dashboard/assets/live-Cae01iu8.js.gz +0 -0
  355. package/dist/dashboard/assets/network-CRib7H_2.js.gz +0 -0
  356. package/dist/dashboard/assets/numbers-BKQE8Xpz.js.gz +0 -0
  357. package/dist/dashboard/assets/pagination-ecPvG_8g.js.gz +0 -0
  358. package/dist/dashboard/assets/quota-DJ1QlAOq.js +0 -1
  359. package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
  360. package/dist/dashboard/assets/routing-Ckm1UXuC.js.gz +0 -0
  361. package/dist/dashboard/assets/select-ChoFWyKz.js.gz +0 -0
  362. package/dist/dashboard/assets/settings-CKjFPlKW.js.gz +0 -0
  363. package/dist/dashboard/assets/stack-DWCwU3fh.js.gz +0 -0
  364. package/dist/dashboard/assets/state-Cg3VL5J_.js.gz +0 -0
  365. package/dist/dashboard/assets/switch-Db70BMix.js.gz +0 -0
  366. package/dist/dashboard/assets/useMutation-BqdE0LuM.js.gz +0 -0
  367. package/src/providers/bundled-discovery-providers.ts +0 -98
  368. package/src/providers/configured-openai-providers.ts +0 -120
  369. package/src/providers/operations/model-discovery-cache.ts +0 -34
  370. package/src/providers/operations/probe-fetch.ts +0 -15
  371. package/src/providers/operations/provider-version-cache.ts +0 -30
  372. package/src/providers/provider-capabilities.ts +0 -33
  373. package/src/providers/provider-credential-envelope.ts +0 -43
  374. /package/dist/dashboard/assets/{app-B7kIWFm5.js → app-DIXLjojg.js} +0 -0
  375. /package/dist/dashboard/assets/{app-B7kIWFm5.js.gz → app-DIXLjojg.js.gz} +0 -0
@@ -1,77 +1,77 @@
1
1
  {
2
- "count": 121,
2
+ "count": 122,
3
3
  "assets": [
4
- "assets/BannedPage-BxoG1MrL.js",
5
- "assets/CliToolDetailPage-ixwgKk53.js",
6
- "assets/CliToolsPage-BmttNV3U.js",
7
- "assets/CombosPage-BE3qwwfC.js",
8
- "assets/ConsoleLogPage-CRZdmMIX.js",
9
- "assets/CustomizationPage-Dv_FJ6Er.js",
10
- "assets/HealthEventsModal-CoVrw2sM.js",
11
- "assets/InjectSystemPromptPanel-BOoO8rpz.js",
12
- "assets/LoginPage-ChJuNNVN.js",
13
- "assets/ModelPicker-BZAJtPg1.js",
14
- "assets/OverviewPage-BE40s3u5.js",
15
- "assets/ProviderDetailPage-sfLoQtPV.js",
16
- "assets/ProviderIcon-CQ6bPUf_.js",
17
- "assets/ProvidersPage-DcepmPWo.js",
18
- "assets/ProxyPage-BJ87Pp0U.js",
19
- "assets/QuotaPage-gkSyRAKa.js",
20
- "assets/SettingsPage-C5HPWqEv.js",
21
- "assets/SetupPage-CFNcATJJ.js",
22
- "assets/StudioPage-YoD4tY5l.js",
4
+ "assets/BannedPage-Cc0vKpMG.js",
5
+ "assets/CliToolDetailPage-Bqj2aa1T.js",
6
+ "assets/CliToolsPage-CXsaOAO2.js",
7
+ "assets/CombosPage-BeLpJ4ma.js",
8
+ "assets/ConsoleLogPage-DnV0uZ3m.js",
9
+ "assets/CustomizationPage-CBxAjo63.js",
10
+ "assets/HealthEventsModal-B5yEE0k6.js",
11
+ "assets/InjectSystemPromptPanel-BWFLECmz.js",
12
+ "assets/LoginPage-CgoPc5B9.js",
13
+ "assets/ModelPicker-D9bFQern.js",
14
+ "assets/OverviewPage-B_p4tL8k.js",
15
+ "assets/ProviderDetailPage-rs5E2rxv.js",
16
+ "assets/ProviderIcon-D_0PiZtg.js",
17
+ "assets/ProvidersPage-CLI2bJTK.js",
18
+ "assets/ProxyPage-BwNC_-Cx.js",
19
+ "assets/QuotaPage-DA-a1iSN.js",
20
+ "assets/SettingsPage-CctTga2C.js",
21
+ "assets/SetupPage-febLU4r1.js",
22
+ "assets/StudioPage-M6fvR5Wz.js",
23
23
  "assets/ToolIcon-CfeLA-Z_.js",
24
- "assets/UsagePage-DxjaAhb_.js",
25
- "assets/api-keys-BTlkq_Uk.js",
26
- "assets/app-B7kIWFm5.js",
27
- "assets/app-BX2nnKqs.css",
24
+ "assets/UsagePage-nyeuRqz9.js",
25
+ "assets/api-keys-BFOlvrJC.js",
26
+ "assets/app-DIXLjojg.js",
27
+ "assets/app-Dx2QgsNj.css",
28
28
  "assets/arrow-left-CpBxkoxI.js",
29
29
  "assets/arrow-right-Dl5yKnp8.js",
30
30
  "assets/arrow-up-59ms1iiT.js",
31
- "assets/assert-CndthXFQ.js",
31
+ "assets/assert-CqTLw5e7.js",
32
32
  "assets/backup-file-gdKIlg8U.js",
33
- "assets/badge-DdbT_7Am.js",
33
+ "assets/badge-C83EqLB0.js",
34
34
  "assets/ban-durations-B58FyRxC.js",
35
- "assets/banner-CsGN-nbm.js",
35
+ "assets/banner-D1xn7pIe.js",
36
36
  "assets/brain-Cwed_72z.js",
37
- "assets/card-mgZ_TLLZ.js",
37
+ "assets/card-DU3Lq5Lm.js",
38
38
  "assets/chevron-left-Bf6CaTSJ.js",
39
39
  "assets/chevron-right-BXIy5l3h.js",
40
40
  "assets/circle-alert-CntYHV5v.js",
41
41
  "assets/circle-check-BU3v_mFW.js",
42
42
  "assets/circle-question-mark-Cz4un8sH.js",
43
- "assets/cli-tools-A1-v8-03.js",
44
- "assets/clipboard-button-CJ4Zty89.js",
43
+ "assets/cli-tools-Dw1U_drK.js",
44
+ "assets/clipboard-button-CEL5Sg_J.js",
45
45
  "assets/database-Dz9N3zGd.js",
46
- "assets/dialog-B5L2jPQ_.js",
46
+ "assets/dialog-BjFR5UHy.js",
47
47
  "assets/dialog-actions-B1axpEg7.js",
48
48
  "assets/download-dRFo5_Wu.js",
49
- "assets/drawer-DkjUouAj.js",
50
- "assets/entry-B-LVLV6o.js",
51
- "assets/entry-CAhdYl3B.js",
52
- "assets/entry-D2xD0Tmd.js",
49
+ "assets/drawer-HvBW0AjI.js",
50
+ "assets/entry-BSu3gFlt.js",
51
+ "assets/entry-CECVELyy.js",
52
+ "assets/entry-DYYofQ7W.js",
53
53
  "assets/eye-C_hawfH4.js",
54
54
  "assets/eye-off-Bk5g2luF.js",
55
55
  "assets/fingerprint-pattern-BWuHtbW4.js",
56
- "assets/formatters-CrsdLVsn.js",
56
+ "assets/formatters-DLW4i8zK.js",
57
57
  "assets/gauge-BbIMy7MB.js",
58
58
  "assets/house-h9AZlJuT.js",
59
59
  "assets/image-BPaouViG.js",
60
- "assets/index-BU64Lqvp.js",
61
- "assets/inline-B2aDLyyg.js",
62
- "assets/input-BCVkdR9d.js",
60
+ "assets/index-1-fCqe64.js",
61
+ "assets/inline-DGvU2oLK.js",
62
+ "assets/input-iNnO1X2K.js",
63
63
  "assets/jsx-runtime-CXUMngI_.js",
64
64
  "assets/key-round-UkavsGxp.js",
65
65
  "assets/layers-2-glBbjl.js",
66
- "assets/layout-DNcAdMK-.js",
67
- "assets/list-row-DgrIH0hw.js",
68
- "assets/live-Cae01iu8.js",
66
+ "assets/layout-0q8UGypb.js",
67
+ "assets/list-row-C0w2bQjJ.js",
68
+ "assets/live-M5aJ_xCa.js",
69
69
  "assets/log-in-BmRjy60c.js",
70
70
  "assets/message-circle-CCxGwRvv.js",
71
71
  "assets/mutation-BQ0ccH_A.js",
72
- "assets/network-CRib7H_2.js",
73
- "assets/numbers-BKQE8Xpz.js",
74
- "assets/pagination-ecPvG_8g.js",
72
+ "assets/network-BKeQGT3K.js",
73
+ "assets/numbers-CivAk3da.js",
74
+ "assets/pagination-CpzvI5ep.js",
75
75
  "assets/pencil-768ysRAr.js",
76
76
  "assets/plus-DIdX_g-x.js",
77
77
  "assets/power-off-Yjw9GMJh.js",
@@ -95,22 +95,23 @@
95
95
  "assets/providers/parallel.svg",
96
96
  "assets/providers/tokenharbor.svg",
97
97
  "assets/providers/windsurf.svg",
98
+ "assets/providers/zro.svg",
98
99
  "assets/query-keys-IKLh0YFd.js",
99
- "assets/quota-DJ1QlAOq.js",
100
+ "assets/quota-DiPD6UKH.js",
100
101
  "assets/refresh-cw-BqJFk4sU.js",
101
102
  "assets/rotate-ccw-CufcqITi.js",
102
103
  "assets/route-CNSNj9Zm.js",
103
- "assets/routing-Ckm1UXuC.js",
104
+ "assets/routing-BFQX2VBc.js",
104
105
  "assets/scaling-nWE4Cx8f.js",
105
106
  "assets/search-JOKus9rb.js",
106
- "assets/select-ChoFWyKz.js",
107
+ "assets/select-CXbck0jG.js",
107
108
  "assets/settings-2-Ds9DQp0t.js",
108
- "assets/settings-CKjFPlKW.js",
109
+ "assets/settings-C_XvMHh9.js",
109
110
  "assets/shield-BMQNxORh.js",
110
111
  "assets/shield-check-DVS0Foqh.js",
111
- "assets/stack-DWCwU3fh.js",
112
- "assets/state-Cg3VL5J_.js",
113
- "assets/switch-Db70BMix.js",
112
+ "assets/stack-DggRiCxE.js",
113
+ "assets/state-ClFzfbYp.js",
114
+ "assets/switch-CTIFxKqS.js",
114
115
  "assets/terminal-0_IjPWTk.js",
115
116
  "assets/tokens-COzario9.js",
116
117
  "assets/trash-Cppn5c3R.js",
@@ -118,7 +119,7 @@
118
119
  "assets/use-modal-focus-By7oJoGd.js",
119
120
  "assets/use-reduced-motion-C4B0R6Sz.js",
120
121
  "assets/use-timeout-BwcBBRrT.js",
121
- "assets/useMutation-BqdE0LuM.js",
122
+ "assets/useMutation-BClG3yWj.js",
122
123
  "assets/wrench-B6E1oEuY.js",
123
124
  "assets/zap-C4EeVW4q.js",
124
125
  "index.html"
@@ -0,0 +1,7 @@
1
+ -- Freshness stamp for `quota_plan`: the plan column is a bare string, so
2
+ -- routing cannot tell a fresh read from a stale one. This timestamp is written
3
+ -- together with `quota_plan` on a successful quota fetch and lets the routing
4
+ -- engine downgrade a plan older than the freshness horizon to `unknown`
5
+ -- instead of trusting it blindly.
6
+ ALTER TABLE "provider_accounts"
7
+ ADD COLUMN IF NOT EXISTS "quota_plan_fetched_at" timestamptz;
@@ -0,0 +1,4 @@
1
+ -- SQLite mirror of `full-postgres/0046_add_accounts_quota_plan_fetched_at.sql`:
2
+ -- the timestamp column is stored as an ISO-8601 text string.
3
+ ALTER TABLE "provider_accounts"
4
+ ADD COLUMN "quota_plan_fetched_at" text;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cartethyia",
3
- "version": "2.7.9",
3
+ "version": "2.8.0",
4
4
  "description": "A powerful, self-hosted reverse proxy for OpenAI- and Anthropic-compatible AI clients, powered by native Bun and Elysia. It serves both protocols from a single server, routes every model across any configured provider, and translates requests and responses in real time — streaming and prompt caching included. Underneath it runs a full delivery engine: per-account health tracking, quota and cooldown enforcement, automatic failover across pools and providers, usage accounting, and telemetry. Point OpenCode, Cline, Claude Code, Codex, or any compatible client at Cartethyia and it keeps its native protocol while the gateway absorbs every provider-specific difference behind it. Runs on embedded SQLite or external PostgreSQL with optional Redis coordination, ships as a global CLI with a system-tray console, and needs only Bun 1.4+ alongside Node.js/npm.",
5
5
  "license": "GPL-3.0-only",
6
6
  "type": "module",
package/src/boot/app.ts CHANGED
@@ -10,7 +10,7 @@ import { chatAdapter } from "../transport/surface/chat/adapter";
10
10
  import { responsesAdapter } from "../transport/surface/responses/adapter";
11
11
  import { messagesAdapter } from "../transport/surface/messages/adapter";
12
12
  import { completionAdapter } from "../transport/surface/completion";
13
- import { SurfaceAdapterRegistry } from "../transport/surface/adapters";
13
+ import { SurfaceAdapterRegistry, type SurfaceAdapter } from "../transport/surface/adapters";
14
14
  import { GatewayError, publicGatewayErrorBody } from "../protocol/gateway-error";
15
15
  import { shutdownError, shutdownNotice } from "../transport/shutdown-notice";
16
16
  import type { CanonicalAdapter } from "../transport/contracts";
@@ -47,7 +47,7 @@ import {
47
47
  import { createTokenCountHandler } from "../transport/dispatch/token-count";
48
48
 
49
49
  import { createTransportPipeline } from "../transport/guards/pipeline";
50
- import { createDrainHandler, isLoopbackPeer, tokenMatches } from "../transport/drain-endpoint";
50
+ import { createDrainHandler, requireLoopbackAdmin } from "../transport/drain-endpoint";
51
51
  import { createConsoleAccountResetHandler } from "../console/auth/account-reset";
52
52
  import { resetConsoleAuthState } from "../console/auth/account-reset-store";
53
53
  import { createBackupExportHandler } from "../console/backup/export-endpoint";
@@ -210,7 +210,7 @@ export function createGatewayApp(deps: GatewayAppDeps) {
210
210
  deps.mode === "route-only"
211
211
  ? (deps.checkRedis ?? checkRedisConnection)
212
212
  : checkRedisConnection;
213
- const registry = new SurfaceAdapterRegistry([
213
+ const registry = new SurfaceAdapterRegistry<SurfaceAdapter & CanonicalAdapter>([
214
214
  chatAdapter,
215
215
  responsesAdapter,
216
216
  messagesAdapter,
@@ -424,12 +424,7 @@ export function createGatewayApp(deps: GatewayAppDeps) {
424
424
  db: deps.db,
425
425
  stateStore: requestStateStore,
426
426
  surfaceRegistry: registry,
427
- adapters: new Map<string, CanonicalAdapter>([
428
- ["chat", chatAdapter],
429
- ["responses", responsesAdapter],
430
- ["messages", messagesAdapter],
431
- ["completion", completionAdapter],
432
- ]),
427
+ adapters: new Map<string, CanonicalAdapter>(registry.bySurface()),
433
428
  preparer: deps.proxyPreparer,
434
429
  readiness: deps.readiness,
435
430
  trustedProxyBoundary: deps.trustedProxyBoundary,
@@ -652,10 +647,12 @@ export function createGatewayApp(deps: GatewayAppDeps) {
652
647
  // well-formed. What refused it is where it came from — the client asked
653
648
  // for something only the local host may ask. `invalid_request` told an
654
649
  // operator to fix the body, when the body was never the problem.
655
- if (!isLoopbackPeer(peerAddresses.get(request) ?? null))
656
- throw new GatewayError("policy_rejected", 403, "task trigger is only available from loopback (request {requestId}). This operation is only allowed from a trusted loopback connection — no hard feelings 🫂");
657
- if (!tokenMatches(request.headers.get("x-drain-token"), token))
658
- throw new GatewayError("authentication_failed", 401, "invalid drain token (request {requestId}). Wrong key, right door — check the token 🔑");
650
+ requireLoopbackAdmin(request, {
651
+ peer: peerAddresses.get(request) ?? null,
652
+ token,
653
+ policyRejectedMessage: "task trigger is only available from loopback (request {requestId}). This operation is only allowed from a trusted loopback connection — no hard feelings 🫂",
654
+ authenticationFailedMessage: "invalid drain token (request {requestId}). Wrong key, right door — check the token 🔑",
655
+ });
659
656
  const name = params.name;
660
657
  if (!tasks.names().includes(name))
661
658
  throw new GatewayError("not_found", 404, `no such task: ${name} (request {requestId}). Nothing here but dust — check the task name 🌙`);
@@ -62,7 +62,7 @@ import {
62
62
  } from "../workers/buddy-growth-tasks";
63
63
  import { createAccountSecretResolver } from "../providers/operations/provider-credential-service";
64
64
  import { quotaCacheSize } from "../providers/quota/cache";
65
- import { modelCatalogCacheSize } from "../providers/operations/model-catalog-cache";
65
+ import { modelCatalogCacheSize } from "../providers/operations/provider-caches";
66
66
  import { preferencesReaderFor } from "../persistence/tenant-preferences";
67
67
  import { sweepExpiredCooldowns } from "../providers/operations/account-health-service";
68
68
  import { DrizzleTelemetryStore } from "../persistence/stores/telemetry-store";
@@ -0,0 +1,20 @@
1
+ // Shared drain sequence for the process's queued writers. The console's manual
2
+ // "flush now" and the shutdown coordinator's telemetry flush must drain exactly
3
+ // the same buffers in the same order — a fourth writer added to one path but
4
+ // not the other would silently stop reaching the disk on one of them.
5
+ //
6
+ // The deps are the minimal write surface each buffer exposes: a `flush()` that
7
+ // resolves when the buffer is drained. The concrete buffers satisfy it, and a
8
+ // test can substitute stubs without casting.
9
+ export interface FlushAllBuffersDeps {
10
+ readonly telemetryBuffer: { flush(): Promise<void> };
11
+ readonly usageFlush: { flush(): Promise<void> };
12
+ readonly healthEventFlush: { flush(): Promise<void> };
13
+ }
14
+
15
+ /** Drains every buffered writer, in the canonical order. */
16
+ export async function flushAllBuffers(deps: FlushAllBuffersDeps): Promise<void> {
17
+ await deps.telemetryBuffer.flush();
18
+ await deps.usageFlush.flush();
19
+ await deps.healthEventFlush.flush();
20
+ }
@@ -2,6 +2,7 @@ import { closeDb } from "../persistence/boot";
2
2
  import { closeRedis } from "../persistence/redis";
3
3
  import { buildProductionDeps } from "./dependencies";
4
4
  import type { ProductionDeps } from "./dependencies";
5
+ import { flushAllBuffers } from "./flush";
5
6
  import { withTimeout } from "./timeout";
6
7
  import { resolveShutdownDrainWindowMs } from "../config/runtime";
7
8
  import { log } from "../observability/logger";
@@ -35,8 +36,11 @@ export class ShutdownCoordinator {
35
36
  ) {
36
37
  this.hooks = hooks ?? {};
37
38
  this.drainWindowMs = opts?.drainWindowMs ?? 20_000;
38
- this.drainTimeoutMs = opts?.drainTimeoutMs ?? 5_000;
39
- this.flushTimeoutMs = opts?.flushTimeoutMs ?? 2_000;
39
+ // Defaults mirror what `bootstrap()` passes (8_000 / 1_000; drainWindowMs
40
+ // 20_000 is the spec default `resolveShutdownDrainWindowMs()` returns), so
41
+ // reasoning about drain timing from the class alone matches production.
42
+ this.drainTimeoutMs = opts?.drainTimeoutMs ?? 8_000;
43
+ this.flushTimeoutMs = opts?.flushTimeoutMs ?? 1_000;
40
44
  }
41
45
 
42
46
  /**
@@ -217,9 +221,7 @@ export async function bootstrap(): Promise<CartethyiaBoot> {
217
221
  shutdownCoordinator: new ShutdownCoordinator(
218
222
  {
219
223
  flushTelemetry: async () => {
220
- await deps.telemetryBuffer.flush();
221
- await deps.usageFlush.flush();
222
- await deps.healthEventFlush.flush();
224
+ await flushAllBuffers(deps);
223
225
  },
224
226
  closePools: async () => {
225
227
  await boot.server?.stop();
@@ -0,0 +1,26 @@
1
+ // Routing configuration knobs.
2
+ import { CONFIG_SPEC, readInt } from "./spec";
3
+
4
+ /** Routing reserve percent (0–100). Accounts at/below this remaining-quota % are demoted. */
5
+ export function accountReservePct(): number {
6
+ return readInt(
7
+ "CARTETHYIA_ACCOUNT_RESERVE_PCT",
8
+ CONFIG_SPEC.CARTETHYIA_ACCOUNT_RESERVE_PCT,
9
+ );
10
+ }
11
+
12
+ /** Session-sticky warm window in ms. 0 disables stickiness. */
13
+ export function routingStickyWarmMs(): number {
14
+ return readInt(
15
+ "CARTETHYIA_ROUTING_STICKY_WARM_MS",
16
+ CONFIG_SPEC.CARTETHYIA_ROUTING_STICKY_WARM_MS,
17
+ );
18
+ }
19
+
20
+ /** LRU cap on remembered session→account sticky state per provider. */
21
+ export function routingStickyMaxSessions(): number {
22
+ return readInt(
23
+ "CARTETHYIA_ROUTING_STICKY_MAX_SESSIONS",
24
+ CONFIG_SPEC.CARTETHYIA_ROUTING_STICKY_MAX_SESSIONS,
25
+ );
26
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Shared metadata for config docs and tests.
3
+ *
4
+ * `CONFIG_SPEC` declares the gateway's spec-driven variables. These constants
5
+ * name the deliberate asymmetries around it so `.env.example`, the generated
6
+ * reference, and the drift tests all project the same exception lists.
7
+ */
8
+
9
+ /** Keys in `CONFIG_SPEC` that are intentionally absent from `.env.example`. */
10
+ export const SPEC_ONLY_ENV_EXAMPLE_KEYS: Readonly<Record<string, true>> = {
11
+ // Setup wizard writes this directly; a sample would mislead copy/paste.
12
+ CARTETHYIA_API_KEY: true,
13
+ // Opt-in diagnostics; sampling them would nudge operators to enable them.
14
+ CARTETHYIA_FORCED_GC: true,
15
+ CARTETHYIA_RUNTIME_METRICS_INTERVAL_MS: true,
16
+ // Advanced knobs documented in the generated configuration reference.
17
+ CARTETHYIA_DISABLED_TASKS: true,
18
+ // Install-identity directory; only operators who need a stable identity
19
+ // across restarts touch it, and the reference covers it.
20
+ CARTETHYIA_INSTALL_ID_DIR: true,
21
+ CARTETHYIA_OAUTH_CALLBACK_PORT: true,
22
+ // Provider-specific overrides. Readers live in `src/config/providers.ts`;
23
+ // the majority of installs do not need them in the starter sample.
24
+ CARTETHYIA_KIMI_OAUTH_HOST: true,
25
+ CARTETHYIA_KIMI_QUOTA_BASE_URL: true,
26
+ CARTETHYIA_GITHUB_ENTERPRISE_DOMAIN: true,
27
+ CARTETHYIA_OPENCODE_UNAVAILABLE_FREE: true,
28
+ // Telemetry retention is deployment-level and documented in the reference.
29
+ CARTETHYIA_TELEMETRY_RETENTION_DAYS: true,
30
+ CARTETHYIA_TELEMETRY_PAYLOAD_MAX_BYTES: true,
31
+ };
32
+
33
+ /** Keys in `.env.example` that `CONFIG_SPEC` does not declare. */
34
+ export const ENV_EXAMPLE_ONLY_KEYS: Readonly<Record<string, true>> = {
35
+ // Read outside `src/config/` by their owning modules.
36
+ CARTETHYIA_DB_MODE: true,
37
+ NODE_ENV: true,
38
+ LOG_LEVEL: true,
39
+ DATABASE_URL: true,
40
+ DATABASE_POOL_MAX: true,
41
+ DATABASE_POOL_IDLE_FLOOR: true,
42
+ REDIS_URL: true,
43
+ CARTETHYIA_DATA_DIR: true,
44
+ CARTETHYIA_TELEMETRY_PAYLOAD_DIR: true,
45
+ CARTETHYIA_CONFIG_DIR: true,
46
+ DASHBOARD_PORT: true,
47
+ VITE_BACKEND_URL: true,
48
+ // Test-runner-only knob, not read by the gateway runtime.
49
+ CARTETHYIA_TEST_JOBS: true,
50
+ };
51
+
52
+ /**
53
+ * Operator-visible variables that are intentionally outside `CONFIG_SPEC` and
54
+ * therefore omitted from the generated configuration reference.
55
+ */
56
+ export const CONFIG_REFERENCE_EXTERNAL_VARIABLES = [
57
+ "CARTETHYIA_DB_MODE",
58
+ "DATABASE_URL",
59
+ "REDIS_URL",
60
+ "NODE_ENV",
61
+ "LOG_LEVEL",
62
+ "DATABASE_POOL_MAX",
63
+ "DATABASE_POOL_IDLE_FLOOR",
64
+ "CARTETHYIA_DATA_DIR",
65
+ "CARTETHYIA_TELEMETRY_PAYLOAD_DIR",
66
+ "CARTETHYIA_CONFIG_DIR",
67
+ "DASHBOARD_PORT",
68
+ "VITE_BACKEND_URL",
69
+ "CARTETHYIA_TEST_DATABASE_URL",
70
+ "CARTETHYIA_TEST_JOBS",
71
+ "CARTETHYIA_REDIS_URL",
72
+ ] as const;
@@ -264,6 +264,26 @@ export const CONFIG_SPEC = {
264
264
  CARTETHYIA_ACCOUNT_AUTH_REJECTED_COOLDOWN_MS: { kind: "int", default: 300_000, min: 0, max: 86_400_000 },
265
265
  CARTETHYIA_ACCOUNT_POLICY_BLOCK_COOLDOWN_MS: { kind: "int", default: 300_000, min: 0, max: 86_400_000 },
266
266
  CARTETHYIA_ACCOUNT_DISABLED_RECOVERY_MS: { kind: "int", default: 86_400_000, min: 0, max: 604_800_000 },
267
+ /**
268
+ * Routing reserve: an account whose remaining quota fraction falls at or
269
+ * below this percent is demoted behind non-reserve siblings during ordering
270
+ * (it is still eligible and still routed — a reserve is a preference, never
271
+ * an exclusion). 0 disables reserve demotion for every account.
272
+ */
273
+ CARTETHYIA_ACCOUNT_RESERVE_PCT: { kind: "int", default: 20, min: 0, max: 100 },
274
+ /**
275
+ * Session stickiness warm window (milliseconds). Within this window after an
276
+ * account last served a session id, routing prefers that account when it is
277
+ * still eligible — prompt-cache warmth is worth more than perfect load
278
+ * balance. Past the window, the sticky account is only an ordering hint.
279
+ * 0 disables stickiness.
280
+ */
281
+ CARTETHYIA_ROUTING_STICKY_WARM_MS: { kind: "int", default: 300_000, min: 0, max: 86_400_000 },
282
+ /**
283
+ * Hard ceiling on distinct session ids whose sticky state is retained in
284
+ * memory per provider (LRU). Evicted sessions simply lose their preference.
285
+ */
286
+ CARTETHYIA_ROUTING_STICKY_MAX_SESSIONS: { kind: "int", default: 256, min: 1, max: 10_000 },
267
287
 
268
288
  // Proxy pool agents
269
289
  CARTETHYIA_PROXY_MAX_SOCKETS: { kind: "int", default: 100, min: 1, max: 100_000 },
@@ -11,7 +11,7 @@ import { type ShareLinkStore, type ShareLinkSummary } from "../../persistence/st
11
11
  import { API_KEY_MODES, API_KEY_CONCURRENCY_SCOPES, type ApiKeyMode, type ApiKeyModelAccessMode, type ShareLinkKind } from "../../persistence/schema";
12
12
  import { isValidTenantKeyScope, type AccessScope } from "../../security/access-control";
13
13
  import { normalizeClientRouterId } from "../../security/client-router-fingerprint";
14
- import type { ShareActivityPort } from "../share/share-usage";
14
+ import type { ShareActivityPort, SharedKeySummary } from "../share/share-usage";
15
15
  import { ConsoleDomainError } from "../shared/errors";
16
16
  import { pickDefined } from "../shared/object";
17
17
  import type { ConsoleAccessResolver } from "../auth/access";
@@ -35,6 +35,17 @@ export type {
35
35
  SharedKeySummary,
36
36
  SharedKeyActivityDetail,
37
37
  } from "../share/share-usage";
38
+
39
+ /** Paged response of the share-child list endpoint. */
40
+ export interface SharedKeyPage {
41
+ readonly rows: readonly SharedKeySummary[];
42
+ readonly total: number;
43
+ readonly active: number;
44
+ readonly revoked: number;
45
+ /** Family-wide lifetime tokens consumed, independent of the page. */
46
+ readonly consumed: number;
47
+ }
48
+
38
49
  /** How a share template's concurrency cap applies to its recipients. */
39
50
  export type ConcurrencyScope = (typeof API_KEY_CONCURRENCY_SCOPES)[number];
40
51
 
@@ -78,6 +78,38 @@ function parseSharedChildrenQuery(query: URLSearchParams): SharedChildrenPageOpt
78
78
  return { limit, offset, sortBy, order, status };
79
79
  }
80
80
 
81
+ /**
82
+ * Drops the auth-cache snapshot for a key family: the parent first, then
83
+ * every child.
84
+ *
85
+ * Parent policy is live-inherited by children at auth time, so their cached
86
+ * snapshots must die with the parent — otherwise a denylist / budget edit
87
+ * looks like a no-op for up to AUTH_CACHE_TTL_MS per child.
88
+ */
89
+ function invalidateFamilyCaches(keyId: string, children: readonly ApiKeyRecord[]): void {
90
+ invalidateApiKeyCache(keyId);
91
+ for (const child of children) invalidateApiKeyCache(child.id);
92
+ }
93
+
94
+ /**
95
+ * Purges admission counters for a key family: the parent first, then every
96
+ * child.
97
+ *
98
+ * Share children admit under the parent id, so the parent's counter must die
99
+ * with a revoked/regenerated parent too — otherwise a family budget can be
100
+ * bypassed by a counter seeded under the old credential.
101
+ */
102
+ async function purgeFamilyAdmission(
103
+ admissionService: { purgeKey(apiKeyId: string): Promise<void> },
104
+ keyId: string,
105
+ children: readonly ApiKeyRecord[],
106
+ ): Promise<void> {
107
+ await Promise.all([
108
+ admissionService.purgeKey(keyId),
109
+ ...children.map((child) => admissionService.purgeKey(child.id)),
110
+ ]);
111
+ }
112
+
81
113
  export function createApiKeyOperations(config: ApiKeyConfig) {
82
114
  const operations = {
83
115
  async listKeys(access: AccessDecision | undefined, includeChildren = false): Promise<ApiKeyResponse[]> {
@@ -92,17 +124,7 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
92
124
  const visible = includeChildren
93
125
  ? records.filter((record) => record.revokedAt === undefined)
94
126
  : topLevel;
95
- // A share template's own counter is the live family accumulator now:
96
- // every recipient commit adds to it (see ApiKeyAdmissionService
97
- // commitUsage), so reporting it raw gives the enforced family total.
98
- // No telemetry add-on is needed, and adding it here would double-count.
99
- return Promise.all(
100
- visible.map(async (record) => {
101
- const response = sanitizeApiKeyResponse(record);
102
- if (record.keyMode !== "share") return response;
103
- return response;
104
- }),
105
- );
127
+ return visible.map(sanitizeApiKeyResponse);
106
128
  },
107
129
  async listActiveBannedKeys(access: AccessDecision | undefined): Promise<ApiKeyResponse[]> {
108
130
  const authorized = requireTenantScope(access, "dashboard:read");
@@ -343,11 +365,7 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
343
365
  });
344
366
  if (!updated) throw new ConsoleDomainError("key_not_found", 404, "Key not found");
345
367
  const children = await config.store.listChildren(authorized.tenantId, keyId);
346
- // Parent policy is live-inherited by children at auth time, so their
347
- // cached snapshots must die with the parent — otherwise a denylist /
348
- // budget edit looks like a no-op for up to AUTH_CACHE_TTL_MS per child.
349
- invalidateApiKeyCache(keyId);
350
- for (const child of children) invalidateApiKeyCache(child.id);
368
+ invalidateFamilyCaches(keyId, children);
351
369
  // The allow/deny list and prefix decide which models `/v1/models`
352
370
  // returns, which is a different cache from the auth snapshot: the
353
371
  // listing is cached per (tenant, key, mode, list, prefix), so editing
@@ -397,10 +415,7 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
397
415
  // Drop stale admission counters so a newly lowered one-time/recurring
398
416
  // budget cannot be bypassed by a counter seeded under the looser limit.
399
417
  // Share children admit under the parent id, so purge the parent too.
400
- await Promise.all([
401
- config.admissionService.purgeKey(keyId),
402
- ...children.map((child) => config.admissionService.purgeKey(child.id)),
403
- ]);
418
+ await purgeFamilyAdmission(config.admissionService, keyId, children);
404
419
  }
405
420
  // Adding a daily/monthly ceiling to a key that already spent in the
406
421
  // current bucket must not hand it a full fresh budget: seed the counter
@@ -447,12 +462,8 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
447
462
  const children = await config.store.listChildren(authorized.tenantId, keyId);
448
463
  const removed = await config.store.remove(authorized.tenantId, keyId);
449
464
  if (!removed) throw new ConsoleDomainError("key_not_found", 404, "Key not found");
450
- invalidateApiKeyCache(keyId);
451
- for (const child of children) invalidateApiKeyCache(child.id);
452
- await Promise.all([
453
- config.admissionService.purgeKey(keyId),
454
- ...children.map((child) => config.admissionService.purgeKey(child.id)),
455
- ]);
465
+ invalidateFamilyCaches(keyId, children);
466
+ await purgeFamilyAdmission(config.admissionService, keyId, children);
456
467
  await config.auditSink?.record({
457
468
  access: authorized,
458
469
  action: "api_key.deleted",
@@ -626,8 +637,9 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
626
637
  keyPrefix: generated.prefix,
627
638
  });
628
639
  if (!updated) throw new ConsoleDomainError("key_not_found", 404, "Key not found");
629
- invalidateApiKeyCache(keyId);
630
- await config.admissionService.purgeKey(keyId);
640
+ // Regeneration is personal-key only, so the family is the key itself.
641
+ invalidateFamilyCaches(keyId, []);
642
+ await purgeFamilyAdmission(config.admissionService, keyId, []);
631
643
  const shareStore = config.shareStore;
632
644
  let share: ShareKeyResponse | null = null;
633
645
  if (shareStore) {
@@ -23,7 +23,7 @@
23
23
  * the operator to type it rather than press Enter on a menu row.
24
24
  */
25
25
  import { GatewayError } from "../../protocol/gateway-error";
26
- import { isLoopbackPeer, tokenMatches } from "../../transport/drain-endpoint";
26
+ import { requireLoopbackAdmin } from "../../transport/drain-endpoint";
27
27
 
28
28
  interface ConsoleAccountResetDeps {
29
29
  /** Shared secret; the route is registered only when this is set. */
@@ -40,14 +40,12 @@ export function createConsoleAccountResetHandler(
40
40
  deps: ConsoleAccountResetDeps,
41
41
  ): (context: { request: Request; body: unknown }) => Promise<Response> {
42
42
  return async ({ request, body }) => {
43
- if (!isLoopbackPeer(deps.resolvePeerAddress(request)))
44
- throw new GatewayError(
45
- "policy_rejected",
46
- 403,
47
- "console account reset is only available from loopback (request {requestId}). This operation is only allowed from a trusted loopback connection — no hard feelings 🫂",
48
- );
49
- if (!tokenMatches(request.headers.get("x-drain-token"), deps.token))
50
- throw new GatewayError("authentication_failed", 401, "invalid reset token (request {requestId}). Wrong key, right door — check the token 🔑");
43
+ requireLoopbackAdmin(request, {
44
+ peer: deps.resolvePeerAddress(request),
45
+ token: deps.token,
46
+ policyRejectedMessage: "console account reset is only available from loopback (request {requestId}). This operation is only allowed from a trusted loopback connection — no hard feelings 🫂",
47
+ authenticationFailedMessage: "invalid reset token (request {requestId}). Wrong key, right door — check the token 🔑",
48
+ });
51
49
  const confirm =
52
50
  body !== null && typeof body === "object" && "confirm" in body
53
51
  ? body.confirm
@@ -20,6 +20,8 @@
20
20
  * as sensitive as the database.
21
21
  */
22
22
  import { getTableColumns, getTableName, type Column, type Table } from "drizzle-orm";
23
+ import type { BackupInspection } from "./service";
24
+ import type { DeleteAllScope } from "./store";
23
25
  import { resolveMaxBodyBytes } from "../../config/runtime";
24
26
  import {
25
27
  apiKeys,
@@ -393,6 +395,8 @@ export type BackupSection = "config" | "telemetry";
393
395
 
394
396
  export const BACKUP_SECTIONS: readonly BackupSection[] = ["config", "telemetry"];
395
397
 
398
+ export type { BackupInspection, DeleteAllScope };
399
+
396
400
  export interface BackupPayload {
397
401
  readonly app: typeof BACKUP_APP;
398
402
  readonly version: number;