cartethyia 2.7.0 → 2.7.1

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 (320) hide show
  1. package/.env.example +107 -106
  2. package/README.md +25 -2
  3. package/dist/dashboard/assets/{BannedPage-P76QLGJA.js → BannedPage-D-J75hBM.js} +1 -1
  4. package/dist/dashboard/assets/BannedPage-D-J75hBM.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolDetailPage-DdBggJS9.js → CliToolDetailPage-CVe6RaBH.js} +1 -1
  6. package/dist/dashboard/assets/CliToolDetailPage-CVe6RaBH.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-aN1iXhhP.js → CliToolsPage-Dd8ZSamp.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-Dd8ZSamp.js.gz +0 -0
  9. package/dist/dashboard/assets/{CombosPage-DE-F1nxZ.js → CombosPage-QoJ6foxo.js} +1 -1
  10. package/dist/dashboard/assets/CombosPage-QoJ6foxo.js.gz +0 -0
  11. package/dist/dashboard/assets/ConsoleLogPage-DpNRay4C.js +1 -0
  12. package/dist/dashboard/assets/ConsoleLogPage-DpNRay4C.js.gz +0 -0
  13. package/dist/dashboard/assets/{CustomizationPage-3CUxMy2X.js → CustomizationPage-CKUFTcW4.js} +1 -1
  14. package/dist/dashboard/assets/CustomizationPage-CKUFTcW4.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-CzSLiDzr.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-CzSLiDzr.js.gz +0 -0
  17. package/dist/dashboard/assets/LoginPage-DubdQcvX.js +1 -0
  18. package/dist/dashboard/assets/LoginPage-DubdQcvX.js.gz +0 -0
  19. package/dist/dashboard/assets/{ModelPicker-DKVslqG3.js → ModelPicker-BH8Tehdi.js} +1 -1
  20. package/dist/dashboard/assets/ModelPicker-BH8Tehdi.js.gz +0 -0
  21. package/dist/dashboard/assets/OverviewPage-DuWzAQFL.js +1 -0
  22. package/dist/dashboard/assets/OverviewPage-DuWzAQFL.js.gz +0 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-BKEKe2Fm.js +4 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-BKEKe2Fm.js.gz +0 -0
  25. package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js +1 -0
  26. package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js.gz +0 -0
  27. package/dist/dashboard/assets/ProvidersPage-Bwotu8UJ.js +1 -0
  28. package/dist/dashboard/assets/ProvidersPage-Bwotu8UJ.js.gz +0 -0
  29. package/dist/dashboard/assets/ProxyPage-D7hpWbg9.js +9 -0
  30. package/dist/dashboard/assets/ProxyPage-D7hpWbg9.js.gz +0 -0
  31. package/dist/dashboard/assets/{QuotaPage-BjldQwIU.js → QuotaPage-ATpaxrbl.js} +1 -1
  32. package/dist/dashboard/assets/QuotaPage-ATpaxrbl.js.gz +0 -0
  33. package/dist/dashboard/assets/{SettingsPage-DUIoLRxk.js → SettingsPage-CETophpx.js} +1 -1
  34. package/dist/dashboard/assets/SettingsPage-CETophpx.js.gz +0 -0
  35. package/dist/dashboard/assets/SetupPage-BdgECjqq.js +1 -0
  36. package/dist/dashboard/assets/SetupPage-BdgECjqq.js.gz +0 -0
  37. package/dist/dashboard/assets/{SortableList-fI7JkdIB.js → SortableList-CAK-EN9z.js} +1 -1
  38. package/dist/dashboard/assets/SortableList-CAK-EN9z.js.gz +0 -0
  39. package/dist/dashboard/assets/{StudioPage-CHVZqskZ.js → StudioPage-B7W2_RhT.js} +3 -3
  40. package/dist/dashboard/assets/StudioPage-B7W2_RhT.js.gz +0 -0
  41. package/dist/dashboard/assets/UsagePage-6T4EFDjW.js +56 -0
  42. package/dist/dashboard/assets/UsagePage-6T4EFDjW.js.gz +0 -0
  43. package/dist/dashboard/assets/{api-keys-DFY_fZU4.js → api-keys-CUi6UXsR.js} +1 -1
  44. package/dist/dashboard/assets/api-keys-CUi6UXsR.js.gz +0 -0
  45. package/dist/dashboard/assets/app-KvsUJUMF.css +2 -0
  46. package/dist/dashboard/assets/app-KvsUJUMF.css.gz +0 -0
  47. package/dist/dashboard/assets/{assert-CwdnZARt.js → assert-67xXH5rf.js} +1 -1
  48. package/dist/dashboard/assets/assert-67xXH5rf.js.gz +0 -0
  49. package/dist/dashboard/assets/{badge-Dr-uf0Au.js → badge-DC2L43Mj.js} +1 -1
  50. package/dist/dashboard/assets/badge-DC2L43Mj.js.gz +0 -0
  51. package/dist/dashboard/assets/{banner-BddmMdoS.js → banner-BBVcb0J3.js} +1 -1
  52. package/dist/dashboard/assets/banner-BBVcb0J3.js.gz +0 -0
  53. package/dist/dashboard/assets/{card-CK0PXWlX.js → card-Cca1XC6G.js} +1 -1
  54. package/dist/dashboard/assets/card-Cca1XC6G.js.gz +0 -0
  55. package/dist/dashboard/assets/{cli-tools-CcB6mGh2.js → cli-tools-KvsWa1Jl.js} +1 -1
  56. package/dist/dashboard/assets/cli-tools-KvsWa1Jl.js.gz +0 -0
  57. package/dist/dashboard/assets/{clipboard-button-DGb2P9Yx.js → clipboard-button-DUzEbynq.js} +1 -1
  58. package/dist/dashboard/assets/clipboard-button-DUzEbynq.js.gz +0 -0
  59. package/dist/dashboard/assets/{dialog-LkfuL50u.js → dialog-DFvWhwTa.js} +1 -1
  60. package/dist/dashboard/assets/dialog-DFvWhwTa.js.gz +0 -0
  61. package/dist/dashboard/assets/{drawer-ZbEZ_84c.js → drawer-CFM2sMX1.js} +1 -1
  62. package/dist/dashboard/assets/drawer-CFM2sMX1.js.gz +0 -0
  63. package/dist/dashboard/assets/{entry-CWBY5A0f.js → entry-BLBLxX0h.js} +3 -3
  64. package/dist/dashboard/assets/entry-BLBLxX0h.js.gz +0 -0
  65. package/dist/dashboard/assets/{entry-BJfsVMvA.js → entry-Bqy8RFvM.js} +1 -1
  66. package/dist/dashboard/assets/entry-Bqy8RFvM.js.gz +0 -0
  67. package/dist/dashboard/assets/{entry-CvC1-Zqx.js → entry-_xHo5x6r.js} +1 -1
  68. package/dist/dashboard/assets/entry-_xHo5x6r.js.gz +0 -0
  69. package/dist/dashboard/assets/formatters-mpBPqAQ8.js +1 -0
  70. package/dist/dashboard/assets/formatters-mpBPqAQ8.js.gz +0 -0
  71. package/dist/dashboard/assets/{github-badge-BxpznFLt.js → github-badge-C9e8Ts7e.js} +1 -1
  72. package/dist/dashboard/assets/github-badge-C9e8Ts7e.js.gz +0 -0
  73. package/dist/dashboard/assets/{index-CDzP3ovJ.js → index-D6QzEC_n.js} +2 -2
  74. package/dist/dashboard/assets/index-D6QzEC_n.js.gz +0 -0
  75. package/dist/dashboard/assets/{inline-Ae13MQ5w.js → inline-BFUcvllj.js} +1 -1
  76. package/dist/dashboard/assets/inline-BFUcvllj.js.gz +0 -0
  77. package/dist/dashboard/assets/{input-D23fo7cp.js → input-C_ILAvBb.js} +1 -1
  78. package/dist/dashboard/assets/input-C_ILAvBb.js.gz +0 -0
  79. package/dist/dashboard/assets/{layout-CSsxjj5P.js → layout-CBIyT6Rt.js} +1 -1
  80. package/dist/dashboard/assets/layout-CBIyT6Rt.js.gz +0 -0
  81. package/dist/dashboard/assets/{list-row-dmbwZMDY.js → list-row-ebJ_-Nsb.js} +1 -1
  82. package/dist/dashboard/assets/list-row-ebJ_-Nsb.js.gz +0 -0
  83. package/dist/dashboard/assets/{live-BG1oUsZZ.js → live-D0mm--ME.js} +1 -1
  84. package/dist/dashboard/assets/live-D0mm--ME.js.gz +0 -0
  85. package/dist/dashboard/assets/{mutation-DSF-e4ho.js → mutation-sMD965oO.js} +1 -1
  86. package/dist/dashboard/assets/mutation-sMD965oO.js.gz +0 -0
  87. package/dist/dashboard/assets/{numbers-Bm_aECY7.js → numbers-BO9dKaFr.js} +1 -1
  88. package/dist/dashboard/assets/numbers-BO9dKaFr.js.gz +0 -0
  89. package/dist/dashboard/assets/providers/inception.svg +5 -0
  90. package/dist/dashboard/assets/providers/inception.svg.gz +0 -0
  91. package/dist/dashboard/assets/{query-keys-CF56nYIV.js → query-keys-CfL_Zdoh.js} +1 -1
  92. package/dist/dashboard/assets/query-keys-CfL_Zdoh.js.gz +0 -0
  93. package/dist/dashboard/assets/{routing-CeA71n7G.js → routing-BnIpi1IJ.js} +1 -1
  94. package/dist/dashboard/assets/routing-BnIpi1IJ.js.gz +0 -0
  95. package/dist/dashboard/assets/{select-KIT0rC6G.js → select-Cj5Nw43g.js} +1 -1
  96. package/dist/dashboard/assets/select-Cj5Nw43g.js.gz +0 -0
  97. package/dist/dashboard/assets/{settings-B_2gkePf.js → settings-BAFhDgX8.js} +1 -1
  98. package/dist/dashboard/assets/settings-BAFhDgX8.js.gz +0 -0
  99. package/dist/dashboard/assets/{stack-D7WOSzcX.js → stack-EMrOMt4I.js} +1 -1
  100. package/dist/dashboard/assets/stack-EMrOMt4I.js.gz +0 -0
  101. package/dist/dashboard/assets/{state-BBxtKZOQ.js → state-CWoo7ty5.js} +1 -1
  102. package/dist/dashboard/assets/state-CWoo7ty5.js.gz +0 -0
  103. package/dist/dashboard/assets/{switch-DNS_D5Ea.js → switch-CCjT6IkF.js} +1 -1
  104. package/dist/dashboard/assets/switch-CCjT6IkF.js.gz +0 -0
  105. package/dist/dashboard/assets/{useMutation-BgoxdoMk.js → useMutation-DjkLHUcE.js} +1 -1
  106. package/dist/dashboard/assets/useMutation-DjkLHUcE.js.gz +0 -0
  107. package/dist/dashboard/index.html +1 -1
  108. package/dist/dashboard/index.html.gz +0 -0
  109. package/dist/dashboard/precompressed-assets.json +54 -53
  110. package/migrations/full-postgres/0024_add_pool_provider_cooldowns.sql +12 -0
  111. package/migrations/full-postgres/0025_add_pool_random_strategy.sql +3 -0
  112. package/migrations/lite-sqlite3/0024_add_pool_provider_cooldowns.sql +12 -0
  113. package/migrations/lite-sqlite3/0025_add_pool_random_strategy.sql +20 -0
  114. package/package.json +96 -96
  115. package/scripts/cli/console-menu.ts +5 -2
  116. package/scripts/cli/tray.ts +21 -6
  117. package/src/boot/app.ts +684 -714
  118. package/src/boot/dependencies.ts +65 -62
  119. package/src/config/cooldowns.ts +16 -0
  120. package/src/config/providers.ts +13 -1
  121. package/src/config/resources.ts +70 -70
  122. package/src/config/security.ts +37 -37
  123. package/src/config/spec.ts +371 -358
  124. package/src/console/console-router.ts +233 -225
  125. package/src/console/domain-registration.ts +398 -381
  126. package/src/console/providers/catalog/catalog-projections.ts +2 -6
  127. package/src/console/providers/catalog/contracts.ts +19 -0
  128. package/src/console/providers/catalog/provider-operations.ts +38 -0
  129. package/src/console/providers/catalog/routes.ts +14 -0
  130. package/src/console/providers/catalog/store-helpers.ts +326 -0
  131. package/src/console/providers/catalog/store.ts +1292 -1563
  132. package/src/console/quota/account-quota-global.ts +2 -2
  133. package/src/console/quota/account-quota-shared.ts +1 -1
  134. package/src/console/quota/account-quota-tenant.ts +35 -12
  135. package/src/console/quota/quota-view.ts +2 -2
  136. package/src/console/routing/pools/contracts.ts +41 -1
  137. package/src/console/routing/pools/operations.ts +183 -4
  138. package/src/console/routing/pools/relay-deploy.ts +17 -4
  139. package/src/console/routing/pools/routes.ts +30 -0
  140. package/src/console/routing/pools/store.ts +98 -8
  141. package/src/main.ts +183 -167
  142. package/src/network/outbound-fetch.ts +226 -365
  143. package/src/network/pool/agent.ts +8 -0
  144. package/src/network/pool/pool-state.ts +7 -0
  145. package/src/network/pool/selector.ts +110 -2
  146. package/src/network/pool-cooldown-store.ts +252 -0
  147. package/src/network/pool-health-machine.ts +5 -3
  148. package/src/network/pool-health.ts +36 -9
  149. package/src/observability/event-query.ts +57 -0
  150. package/src/observability/metrics.ts +467 -467
  151. package/src/observability/runtime-metrics.ts +269 -269
  152. package/src/persistence/health-event-flush.ts +2 -0
  153. package/src/persistence/schema.ts +19 -0
  154. package/src/persistence/usage-flush.ts +8 -3
  155. package/src/protocol/canonical-model.ts +1 -1
  156. package/src/protocol/dsml.ts +377 -0
  157. package/src/protocol/gateway-error.ts +232 -232
  158. package/src/{providers/provider-reasoning.ts → protocol/reasoning.ts} +10 -9
  159. package/src/protocol/request/chat.ts +7 -7
  160. package/src/protocol/request/codex.ts +2 -2
  161. package/src/protocol/request/gemini.ts +1 -1
  162. package/src/protocol/request/messages.ts +6 -6
  163. package/src/protocol/request/responses.ts +7 -7
  164. package/src/protocol/response/dsml.ts +12 -133
  165. package/src/{transport → protocol}/translation/attachment-integrity.ts +2 -2
  166. package/src/{transport → protocol}/translation/cache-controls.ts +1 -1
  167. package/src/{transport → protocol}/translation/capabilities.ts +2 -2
  168. package/src/{transport → protocol}/translation/normalize.ts +1 -1
  169. package/src/{transport → protocol}/translation/quirks.ts +1 -1
  170. package/src/{transport → protocol}/translation/search.ts +1 -1
  171. package/src/{transport → protocol}/translation/thinking.ts +23 -1
  172. package/src/{transport → protocol}/translation/tool-id.ts +1 -1
  173. package/src/{transport → protocol}/translation/tool-repair.ts +2 -2
  174. package/src/protocol/upstream-parsing.ts +176 -0
  175. package/src/providers/adapters/compatible-adapter.ts +11 -2
  176. package/src/providers/authentication/oauth-refresh-service.ts +9 -5
  177. package/src/providers/default-registry.ts +21 -5
  178. package/src/providers/discovery/probe-phases.ts +16 -3
  179. package/src/providers/discovery/probing-service.ts +4 -1
  180. package/src/providers/integrations/anthropic.ts +1 -1
  181. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -4
  182. package/src/providers/integrations/buddy/buddy-chat-shared.ts +141 -8
  183. package/src/providers/integrations/buddy/buddy-checkin.ts +5 -1
  184. package/src/providers/integrations/buddy/buddy-growth-shared.ts +0 -82
  185. package/src/providers/integrations/buddy/buddy-identity.ts +25 -0
  186. package/src/providers/integrations/buddy/buddy-server-tools.ts +240 -0
  187. package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +36 -0
  188. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +6 -31
  189. package/src/providers/integrations/buddy/codebuddy/provider.ts +2 -0
  190. package/src/providers/integrations/buddy/codebuddy/shared.ts +19 -4
  191. package/src/providers/integrations/buddy/turing-device-token.ts +262 -0
  192. package/src/providers/integrations/buddy/workbuddy/provider.ts +2 -0
  193. package/src/providers/integrations/buddy/workbuddy/shared.ts +56 -3
  194. package/src/providers/integrations/claude/claude.ts +1 -1
  195. package/src/providers/integrations/cloudflareai/cloudflareai.ts +1 -1
  196. package/src/providers/integrations/codex/codex-errors.ts +1 -1
  197. package/src/providers/integrations/codex/codex.ts +1 -1
  198. package/src/providers/integrations/factory/factory.ts +1 -1
  199. package/src/providers/integrations/github/github.ts +1 -1
  200. package/src/providers/integrations/grok/grok.ts +1 -1
  201. package/src/providers/integrations/inception/provider.ts +91 -0
  202. package/src/providers/integrations/inferhub.ts +1 -1
  203. package/src/providers/integrations/kiro/kiro-request.ts +1 -1
  204. package/src/providers/integrations/mistral.ts +193 -19
  205. package/src/providers/integrations/opencode/fingerprint.ts +19 -2
  206. package/src/providers/integrations/opencode/prompt.ts +111 -0
  207. package/src/providers/integrations/opencode/provider.ts +101 -23
  208. package/src/providers/integrations/opencode/tool-definitions.ts +4 -4
  209. package/src/providers/integrations/qoder/qoder-checkin.ts +303 -0
  210. package/src/providers/integrations/qoder/qoder.ts +1641 -1458
  211. package/src/providers/integrations/shared/install-id.ts +2 -1
  212. package/src/providers/integrations/trae/trae-cache.ts +110 -0
  213. package/src/providers/integrations/trae/trae-oauth.ts +29 -4
  214. package/src/providers/integrations/trae/trae-protocol.ts +2 -1
  215. package/src/providers/integrations/trae/trae.ts +106 -23
  216. package/src/providers/operations/account-error-classification.ts +1660 -0
  217. package/src/providers/operations/account-health-service.ts +621 -2097
  218. package/src/providers/operations/account-reset-service.ts +2 -0
  219. package/src/providers/provider-contracts.ts +0 -2
  220. package/src/providers/provider-metadata.ts +25 -4
  221. package/src/providers/provider-probe.ts +2 -2
  222. package/src/{console → providers}/quota/cache.ts +1 -1
  223. package/src/{console → providers}/quota/refresh.ts +5 -5
  224. package/src/transport/dispatch/attempt-finalize.ts +114 -12
  225. package/src/transport/dispatch/attempt-loop.ts +3 -0
  226. package/src/transport/dispatch/empty-stream.ts +224 -217
  227. package/src/transport/dispatch/generation.ts +37 -55
  228. package/src/transport/dispatch/handler-deps.ts +32 -0
  229. package/src/transport/dispatch/leases.ts +9 -4
  230. package/src/transport/dispatch/proxy-request.ts +6 -30
  231. package/src/transport/dispatch/responses-compact.ts +22 -43
  232. package/src/transport/dispatch/streaming/finalize.ts +3 -0
  233. package/src/transport/dispatch/streaming/stream.ts +0 -8
  234. package/src/transport/dispatch/systemone.ts +22 -42
  235. package/src/transport/dispatch/token-count.ts +23 -39
  236. package/src/transport/dispatch/upstream.ts +1 -3
  237. package/src/transport/dispatch/websearch.ts +22 -41
  238. package/src/transport/failure-policy.ts +59 -178
  239. package/src/transport/guards/abuse-guard.ts +10 -0
  240. package/src/transport/guards/auth-guard.ts +109 -109
  241. package/src/transport/guards/error-lifecycle.ts +1 -1
  242. package/src/transport/guards/pipeline.ts +220 -220
  243. package/src/transport/guards/request-context.ts +245 -238
  244. package/src/transport/request/degradation.ts +20 -1
  245. package/src/transport/request/inflight.ts +4 -114
  246. package/src/transport/request/preparer.ts +6 -6
  247. package/src/transport/request/state.ts +66 -38
  248. package/src/transport/request/tenant-preferences.ts +1 -1
  249. package/src/transport/routing/route-catalog.ts +41 -31
  250. package/src/transport/routing/router.ts +2 -2
  251. package/src/transport/surface/messages/parse.ts +1 -1
  252. package/src/workers/buddy-growth-tasks.ts +346 -425
  253. package/src/workers/daily-checkin.ts +17 -6
  254. package/src/workers/quota-refresh-sweep.ts +2 -2
  255. package/src/workers/scheduler.ts +17 -1
  256. package/dist/dashboard/assets/BannedPage-P76QLGJA.js.gz +0 -0
  257. package/dist/dashboard/assets/CliToolDetailPage-DdBggJS9.js.gz +0 -0
  258. package/dist/dashboard/assets/CliToolsPage-aN1iXhhP.js.gz +0 -0
  259. package/dist/dashboard/assets/CombosPage-DE-F1nxZ.js.gz +0 -0
  260. package/dist/dashboard/assets/ConsoleLogPage-5SpmfLyw.js +0 -1
  261. package/dist/dashboard/assets/ConsoleLogPage-5SpmfLyw.js.gz +0 -0
  262. package/dist/dashboard/assets/CustomizationPage-3CUxMy2X.js.gz +0 -0
  263. package/dist/dashboard/assets/HealthEventsModal-BkmJQKC0.js +0 -1
  264. package/dist/dashboard/assets/HealthEventsModal-BkmJQKC0.js.gz +0 -0
  265. package/dist/dashboard/assets/LoginPage-B_0FUKWc.js +0 -1
  266. package/dist/dashboard/assets/LoginPage-B_0FUKWc.js.gz +0 -0
  267. package/dist/dashboard/assets/ModelPicker-DKVslqG3.js.gz +0 -0
  268. package/dist/dashboard/assets/OverviewPage-D-H4MEiT.js +0 -1
  269. package/dist/dashboard/assets/OverviewPage-D-H4MEiT.js.gz +0 -0
  270. package/dist/dashboard/assets/ProviderDetailPage-CEJw6Guo.js +0 -3
  271. package/dist/dashboard/assets/ProviderDetailPage-CEJw6Guo.js.gz +0 -0
  272. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js +0 -1
  273. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js.gz +0 -0
  274. package/dist/dashboard/assets/ProvidersPage-CEplAR-i.js +0 -1
  275. package/dist/dashboard/assets/ProvidersPage-CEplAR-i.js.gz +0 -0
  276. package/dist/dashboard/assets/ProxyPage-CAN7hy0f.js +0 -7
  277. package/dist/dashboard/assets/ProxyPage-CAN7hy0f.js.gz +0 -0
  278. package/dist/dashboard/assets/QuotaPage-BjldQwIU.js.gz +0 -0
  279. package/dist/dashboard/assets/SettingsPage-DUIoLRxk.js.gz +0 -0
  280. package/dist/dashboard/assets/SetupPage-_bCaTM0v.js +0 -1
  281. package/dist/dashboard/assets/SetupPage-_bCaTM0v.js.gz +0 -0
  282. package/dist/dashboard/assets/SortableList-fI7JkdIB.js.gz +0 -0
  283. package/dist/dashboard/assets/StudioPage-CHVZqskZ.js.gz +0 -0
  284. package/dist/dashboard/assets/UsagePage-DiFBymHj.js +0 -56
  285. package/dist/dashboard/assets/UsagePage-DiFBymHj.js.gz +0 -0
  286. package/dist/dashboard/assets/api-keys-DFY_fZU4.js.gz +0 -0
  287. package/dist/dashboard/assets/app-BOJ1_xUB.css +0 -2
  288. package/dist/dashboard/assets/app-BOJ1_xUB.css.gz +0 -0
  289. package/dist/dashboard/assets/assert-CwdnZARt.js.gz +0 -0
  290. package/dist/dashboard/assets/badge-Dr-uf0Au.js.gz +0 -0
  291. package/dist/dashboard/assets/banner-BddmMdoS.js.gz +0 -0
  292. package/dist/dashboard/assets/card-CK0PXWlX.js.gz +0 -0
  293. package/dist/dashboard/assets/cli-tools-CcB6mGh2.js.gz +0 -0
  294. package/dist/dashboard/assets/clipboard-button-DGb2P9Yx.js.gz +0 -0
  295. package/dist/dashboard/assets/cog-CdWhsajs.js +0 -1
  296. package/dist/dashboard/assets/cog-CdWhsajs.js.gz +0 -0
  297. package/dist/dashboard/assets/dialog-LkfuL50u.js.gz +0 -0
  298. package/dist/dashboard/assets/drawer-ZbEZ_84c.js.gz +0 -0
  299. package/dist/dashboard/assets/entry-BJfsVMvA.js.gz +0 -0
  300. package/dist/dashboard/assets/entry-CWBY5A0f.js.gz +0 -0
  301. package/dist/dashboard/assets/entry-CvC1-Zqx.js.gz +0 -0
  302. package/dist/dashboard/assets/github-badge-BxpznFLt.js.gz +0 -0
  303. package/dist/dashboard/assets/index-CDzP3ovJ.js.gz +0 -0
  304. package/dist/dashboard/assets/inline-Ae13MQ5w.js.gz +0 -0
  305. package/dist/dashboard/assets/input-D23fo7cp.js.gz +0 -0
  306. package/dist/dashboard/assets/layout-CSsxjj5P.js.gz +0 -0
  307. package/dist/dashboard/assets/list-row-dmbwZMDY.js.gz +0 -0
  308. package/dist/dashboard/assets/live-BG1oUsZZ.js.gz +0 -0
  309. package/dist/dashboard/assets/mutation-DSF-e4ho.js.gz +0 -0
  310. package/dist/dashboard/assets/numbers-Bm_aECY7.js.gz +0 -0
  311. package/dist/dashboard/assets/query-keys-CF56nYIV.js.gz +0 -0
  312. package/dist/dashboard/assets/routing-CeA71n7G.js.gz +0 -0
  313. package/dist/dashboard/assets/select-KIT0rC6G.js.gz +0 -0
  314. package/dist/dashboard/assets/settings-B_2gkePf.js.gz +0 -0
  315. package/dist/dashboard/assets/stack-D7WOSzcX.js.gz +0 -0
  316. package/dist/dashboard/assets/state-BBxtKZOQ.js.gz +0 -0
  317. package/dist/dashboard/assets/switch-DNS_D5Ea.js.gz +0 -0
  318. package/dist/dashboard/assets/useMutation-BgoxdoMk.js.gz +0 -0
  319. /package/dist/dashboard/assets/{app-CaXZ9SCn.js → app-BaWd0puM.js} +0 -0
  320. /package/dist/dashboard/assets/{app-CaXZ9SCn.js.gz → app-BaWd0puM.js.gz} +0 -0
@@ -1,358 +1,371 @@
1
- /**
2
- * Spec-driven configuration reader shared by the `src/config/` domain modules.
3
- *
4
- * `CONFIG_SPEC` is the canonical declaration of every environment variable
5
- * `src/config/` reads: name, kind, default, and bounds in one place. The
6
- * resolvers in the sibling domain modules are thin readers over it, so a new
7
- * knob cannot be added without declaring a row here plus a `.env.example`
8
- * line. Bounds are enforced on read so a malformed operator value throws at
9
- * the boundary instead of silently coercing.
10
- *
11
- * Module-specific environment reads that only affect a single subsystem (e.g.
12
- * logger level, telemetry payload storage, Postgres pool tuning) still live
13
- * next to their consumer. No I/O, no caching — readers are pure with respect
14
- * to `process.env` at call time.
15
- */
16
-
17
- /** Default directory for the built dashboard static assets. */
18
- const DEFAULT_DASHBOARD_DIST = "./dist/dashboard";
19
- /** Default upstream client version host for Kimi's OAuth origin. */
20
- const DEFAULT_KIMI_OAUTH_HOST = "https://auth.kimi.com";
21
- /** Base for Kimi Code's quota/billing surface; the usage path joins on this. */
22
- const DEFAULT_KIMI_QUOTA_BASE_URL = "https://api.kimi.com/coding/v1";
23
-
24
- // ─── Config spec ────────────────────────────────────────────────────────────
25
-
26
- interface IntConfigEntry {
27
- readonly kind: "int";
28
- readonly default: number;
29
- readonly min: number;
30
- readonly max: number;
31
- }
32
-
33
- interface TextConfigEntry {
34
- readonly kind: "text";
35
- readonly default: string;
36
- }
37
-
38
- /** Absent means "no value"; an empty/whitespace value also reads as absent. */
39
- interface OptionalTextConfigEntry {
40
- readonly kind: "optional-text";
41
- }
42
-
43
- /**
44
- * Absent or empty means "unset" — distinct from an explicit value, because
45
- * consumers apply their own fallback (cgroup detection, egress default) rather
46
- * than a fixed default here.
47
- */
48
- interface OptionalIntConfigEntry {
49
- readonly kind: "optional-int";
50
- readonly min: number;
51
- readonly max: number;
52
- }
53
-
54
- interface RequiredTextConfigEntry {
55
- readonly kind: "required";
56
- readonly error: string;
57
- }
58
-
59
- /** Enabled unless the literal string `"false"` is set. */
60
- interface FlagConfigEntry {
61
- readonly kind: "flag";
62
- }
63
-
64
- /** Comma-separated list; an empty result reads as absent. */
65
- interface ListConfigEntry {
66
- readonly kind: "list";
67
- }
68
-
69
- export type ConfigEntry =
70
- | IntConfigEntry
71
- | TextConfigEntry
72
- | OptionalTextConfigEntry
73
- | OptionalIntConfigEntry
74
- | RequiredTextConfigEntry
75
- | FlagConfigEntry
76
- | ListConfigEntry;
77
-
78
- /**
79
- * The single declaration of every environment variable `src/config/` reads.
80
- *
81
- * Adding a knob means adding a row here plus a `.env.example` line. Bounds are
82
- * enforced on read so a malformed operator value throws at the boundary
83
- * instead of silently coercing.
84
- */
85
- export const CONFIG_SPEC = {
86
- // HTTP listener + dashboard runtime
87
- PORT: { kind: "int", default: 12_800, min: 1, max: 65_535 },
88
- DASHBOARD_DIST: { kind: "text", default: DEFAULT_DASHBOARD_DIST },
89
- CARTETHYIA_API_KEY: { kind: "optional-text" },
90
- CARTETHYIA_SERVER_MAX_BODY_BYTES: {
91
- kind: "int",
92
- // A 500K-token context is roughly 2 MB of text, so 4 MB admits every real
93
- // request while halving the worst-case hold. How many may be in flight is
94
- // admission's job, so an operator with larger payloads raises this and
95
- // sets the ceiling in Settings.
96
- default: 4 * 1024 * 1024,
97
- min: 1024,
98
- max: Number.MAX_SAFE_INTEGER,
99
- },
100
- CARTETHYIA_SERVER_IDLE_TIMEOUT: { kind: "int", default: 60, min: 0, max: 86_400 },
101
-
102
- // Graceful-shutdown drain: how long in-flight requests may finish naturally
103
- // before the drain aborts the stragglers. Sized above the common case (a
104
- // typical response) but below the stream stall bound so a wedged stream is
105
- // still cut within the process budget.
106
- CARTETHYIA_SHUTDOWN_DRAIN_WINDOW_MS: {
107
- kind: "int",
108
- default: 20_000,
109
- min: 0,
110
- max: 600_000,
111
- },
112
- // Optional ceiling on in-flight requests per process. Left unset, the limit
113
- // is derived from the memory limit and the body ceiling (see
114
- // config/resources.ts): 2 GiB/4 MiB → 64, 512 MiB/4 MiB → 16, 256 MiB/16 MiB
115
- // → 2. Set it to admit more than memory alone would allow — which is the
116
- // operator's call, and is reported rather than corrected — or less, to keep
117
- // a slow upstream from being handed more concurrency than it can serve.
118
- // Optional-int, not int-with-a-zero-default: "unset" and "explicitly zero"
119
- // must not be the same thing. Zero is not a valid ceiling, so a default of 0
120
- // would need a second sentinel to mean "derive".
121
- // Slack above each key's concurrency limit. Effective ceiling is
122
- // limit + tolerance; only requests past it are refused.
123
- CARTETHYIA_CONCURRENCY_TOLERANCE: { kind: "optional-int", min: 0, max: 1024 },
124
- // How many gateway processes serve this deployment, and which one this is.
125
- // Count > 1 requires Linux, CARTETHYIA_DB_MODE=full, and a REACHABLE Redis —
126
- // checked at boot, because a half-configured multi-process deploy starts
127
- // fine and then enforces every per-key limit N times over. Lite stays
128
- // single-process: two processes on one SQLite file corrupt it.
129
- CARTETHYIA_PROCESS_COUNT: { kind: "int", default: 1, min: 1, max: 16 },
130
- // Which process this is (0 – count-1). Mandatory when count > 1: a process
131
- // that does not know its index cannot own a shard of anything.
132
- CARTETHYIA_PROCESS_INDEX: { kind: "int", default: 0, min: 0, max: 15 },
133
- // Optional secret for the operator drain endpoint (`POST /admin/drain`). When
134
- // set, a process can be stopped gracefully on platforms where a catchable
135
- // signal cannot be delivered to it (notably Windows, where `process.kill` of
136
- // a console-less process runs no JS handler). The endpoint additionally
137
- // requires a loopback peer and a timing-safe token match; unset = the route
138
- // does not exist.
139
- CARTETHYIA_DRAIN_TOKEN: { kind: "optional-text" },
140
-
141
- // Network policy (trusted proxy + SSRF)
142
- TRUSTED_PROXY_CIDRS: { kind: "list" },
143
- CARTETHYIA_ALLOW_PRIVATE_UPSTREAMS: { kind: "flag" },
144
- CARTETHYIA_ALLOWED_NETWORKS: { kind: "list" },
145
- CARTETHYIA_MAX_REDIRECTS: { kind: "optional-int", min: 0, max: 10 },
146
-
147
- // Runtime memory
148
- CARTETHYIA_MEMORY_LIMIT_BYTES: {
149
- kind: "optional-int",
150
- min: 1,
151
- max: Number.MAX_SAFE_INTEGER,
152
- },
153
- // Opt-in forced garbage collection under memory pressure. Off by default:
154
- // see `observability/runtime-metrics.ts` — a forced collection is not
155
- // reclamation, and a collection that runs while requests are in flight adds
156
- // a latency spike. This gates the pressure path only; idle reclaim runs
157
- // unconditionally, because reclaiming when nothing is in flight stalls
158
- // nothing.
159
- CARTETHYIA_FORCED_GC: { kind: "flag" },
160
- // How often runtime gauges (memory + bounded-collection sizes) are sampled.
161
- CARTETHYIA_RUNTIME_METRICS_INTERVAL_MS: {
162
- kind: "int",
163
- default: 30_000,
164
- min: 1_000,
165
- max: 300_000,
166
- },
167
- /**
168
- * Periodic maintenance tasks switched off entirely, by name. A small
169
- * deployment that does not need live quota or version polling can cut the
170
- * heaviest sweeps here instead of carrying one flag per task.
171
- */
172
- CARTETHYIA_DISABLED_TASKS: { kind: "list" },
173
-
174
- /**
175
- * How long a day's telemetry is kept, in days.
176
- *
177
- * Whole raw event files past this age are removed. No summary or compacted
178
- * form replaces the individual requests.
179
- *
180
- * This is the knob that decides how long the request list, per-request
181
- * filters, and the detail drawer stay complete.
182
- */
183
- CARTETHYIA_TELEMETRY_RETENTION_DAYS: { kind: "int", default: 30, min: 3, max: 365 },
184
- CARTETHYIA_TELEMETRY_PAYLOAD_MAX_BYTES: {
185
- kind: "int",
186
- default: 16 * 1024 * 1024,
187
- min: 1 * 1024 * 1024,
188
- max: 16 * 1024 * 1024,
189
- },
190
-
191
- // Upstream request timeout + retry backoff
192
- //
193
- // Strictly under 90s: a client whose own deadline is 90s must never be
194
- // waiting on a gateway that is still holding a request open past it, and a
195
- // provider that has not answered within 75s is not answering — the request
196
- // is queued upstream or the egress is stuck, and the honest move is to fail
197
- // over to another account rather than burn the caller's whole budget on one.
198
- // 120s outlived the clients that call this, which cap lower.
199
- CARTETHYIA_UPSTREAM_TIMEOUT_MS: { kind: "int", default: 75_000, min: 5_000, max: 600_000 },
200
- CARTETHYIA_STREAM_STALL_TIMEOUT_MS: {
201
- kind: "int",
202
- default: 360_000,
203
- min: 60_000,
204
- max: 600_000,
205
- },
206
- CARTETHYIA_STREAM_FIRST_CHUNK_TIMEOUT_MS: {
207
- kind: "int",
208
- default: 200_000,
209
- min: 30_000,
210
- max: 300_000,
211
- },
212
- CARTETHYIA_FALLBACK_RETRY_BASE_MS: { kind: "int", default: 100, min: 0, max: 60_000 },
213
- CARTETHYIA_FALLBACK_RETRY_CAP_MS: { kind: "int", default: 2_000, min: 0, max: 300_000 },
214
- CARTETHYIA_ROUTE_MAX_ATTEMPTS: { kind: "int", default: 8, min: 1, max: 64 },
215
-
216
- // Account/pool health cooldown delays. Each is the fallback used when the
217
- // upstream error states no explicit reset window; an upstream
218
- // `Retry-After`/reset header or message always wins over these.
219
- CARTETHYIA_ACCOUNT_RATE_LIMIT_COOLDOWN_MS: { kind: "int", default: 900_000, min: 0, max: 86_400_000 },
220
- CARTETHYIA_ACCOUNT_QUOTA_COOLDOWN_MS: { kind: "int", default: 3_600_000, min: 0, max: 86_400_000 },
221
- CARTETHYIA_ACCOUNT_MODEL_CAPACITY_COOLDOWN_MS: { kind: "int", default: 120_000, min: 0, max: 86_400_000 },
222
- CARTETHYIA_ACCOUNT_TRANSIENT_COOLDOWN_MS: { kind: "int", default: 30_000, min: 0, max: 86_400_000 },
223
- /**
224
- * An unmapped upstream failure cools the account down for 15 minutes.
225
- *
226
- * A shorter window puts the account straight back into rotation and re-dials
227
- * an upstream that has just answered something this gateway does not
228
- * understand — so the same unknown error is retried every minute, and an
229
- * account that is actually broken contributes a steady stream of failed
230
- * attempts instead of one. Fifteen minutes is long enough for a transient
231
- * provider condition to clear on its own and short enough that no operator
232
- * action is needed to recover it. It is still a cooldown with a deadline,
233
- * never `disabled`, so nothing here can strand an account.
234
- */
235
- CARTETHYIA_ACCOUNT_UNCLASSIFIED_COOLDOWN_MS: {
236
- kind: "int",
237
- default: 900_000,
238
- min: 0,
239
- max: 86_400_000,
240
- },
241
- CARTETHYIA_ACCOUNT_COOLDOWN_ESCALATION_MAX_MS: { kind: "int", default: 3_600_000, min: 0, max: 86_400_000 },
242
- CARTETHYIA_POOL_COOLDOWN_MS: { kind: "int", default: 120_000, min: 0, max: 86_400_000 },
243
- CARTETHYIA_ACCOUNT_AUTH_REJECTED_COOLDOWN_MS: { kind: "int", default: 300_000, min: 0, max: 86_400_000 },
244
- CARTETHYIA_ACCOUNT_POLICY_BLOCK_COOLDOWN_MS: { kind: "int", default: 300_000, min: 0, max: 86_400_000 },
245
- CARTETHYIA_ACCOUNT_DISABLED_RECOVERY_MS: { kind: "int", default: 86_400_000, min: 0, max: 604_800_000 },
246
-
247
- // Proxy pool agents
248
- CARTETHYIA_PROXY_MAX_SOCKETS: { kind: "int", default: 100, min: 1, max: 100_000 },
249
- CARTETHYIA_PROXY_MAX_FREE_SOCKETS: { kind: "int", default: 20, min: 0, max: 100_000 },
250
-
251
- // Security (encryption key, public origin, abuse ceiling)
252
- IP_RATE_MAX_PER_WINDOW: { kind: "int", default: 240, min: 1, max: Number.MAX_SAFE_INTEGER },
253
- CARTETHYIA_ENCRYPTION_KEY: {
254
- kind: "required",
255
- error:
256
- "CARTETHYIA_ENCRYPTION_KEY is required to encrypt credentials and hash API keys",
257
- },
258
- CARTETHYIA_PUBLIC_ORIGIN: {
259
- kind: "required",
260
- error:
261
- "CARTETHYIA_PUBLIC_ORIGIN is required to build OAuth redirect URIs (e.g. https://cartethyia.example.com)",
262
- },
263
-
264
- // Provider OAuth hosts
265
- CARTETHYIA_KIMI_OAUTH_HOST: { kind: "text", default: DEFAULT_KIMI_OAUTH_HOST },
266
- /**
267
- * Loopback port the browser-OAuth callback listener binds.
268
- *
269
- * Configurable because the default is a fixed number: a busy port would strand
270
- * the operator, since the login could not start and there would be no way to
271
- * move it. Codex's authorization server still expects 59653, so changing this
272
- * only helps the providers whose redirect URI is not allowlisted upstream.
273
- */
274
- CARTETHYIA_OAUTH_CALLBACK_PORT: { kind: "int", default: 59_653, min: 1_024, max: 65_535 },
275
- /**
276
- * Kimi Code quota/billing base. The `/usages` path joins on this, so the
277
- * default includes the `/v1` segment the bundled provider metadata implies.
278
- */
279
- CARTETHYIA_KIMI_QUOTA_BASE_URL: { kind: "text", default: DEFAULT_KIMI_QUOTA_BASE_URL },
280
- /**
281
- * GitHub Enterprise domain for the Copilot device login, e.g.
282
- * `company.ghe.com`. Empty (the default) means github.com. GitHub Enterprise
283
- * serves the device flow and the token-mint endpoint from its own domain, so
284
- * an operator on one has no other way to point the login at it.
285
- */
286
- CARTETHYIA_GITHUB_ENTERPRISE_DOMAIN: { kind: "text", default: "" },
287
- /**
288
- * Free-tier OpenCode ids the listing still advertises but upstream refuses.
289
- * Replaces the bundled list outright; the single token `none` trusts the
290
- * listing completely.
291
- *
292
- * The bundled ids were measured, not declared — upstream can start serving
293
- * a refused id or start refusing a served one at any time, and either way
294
- * the correct answer stopped being the one compiled in. This is the escape
295
- * hatch that needs no deploy.
296
- */
297
- CARTETHYIA_OPENCODE_UNAVAILABLE_FREE: { kind: "list" },
298
- } as const satisfies Readonly<Record<string, ConfigEntry>>;
299
-
300
- // ─── Spec readers ───────────────────────────────────────────────────────────
301
-
302
- /** Reads a bounded integer, throwing on a malformed or out-of-range value. */
303
- export function readInt(name: string, entry: IntConfigEntry): number {
304
- const raw = process.env[name];
305
- if (raw === undefined || raw.trim() === "") return entry.default;
306
- const value = Number(raw);
307
- if (!Number.isInteger(value) || value < entry.min || value > entry.max) {
308
- throw new Error(`${name} must be an integer between ${entry.min} and ${entry.max}`);
309
- }
310
- return value;
311
- }
312
-
313
- /** Reads a string with a fallback, treating an empty value as absent. */
314
- export function readText(name: string, entry: TextConfigEntry): string {
315
- const raw = process.env[name]?.trim();
316
- return raw && raw.length > 0 ? raw : entry.default;
317
- }
318
-
319
- /** Reads an optional string; empty or whitespace reads as absent. */
320
- export function readOptionalText(name: string, entry: OptionalTextConfigEntry): string | undefined {
321
- void entry;
322
- const raw = process.env[name]?.trim();
323
- return raw && raw.length > 0 ? raw : undefined;
324
- }
325
-
326
- /** Reads an optional bounded integer; absent or empty reads as `undefined`. */
327
- export function readOptionalInt(name: string, entry: OptionalIntConfigEntry): number | undefined {
328
- const raw = process.env[name];
329
- if (raw === undefined || raw.trim() === "") return undefined;
330
- const value = Number(raw);
331
- if (!Number.isInteger(value) || value < entry.min || value > entry.max) {
332
- throw new Error(`${name} must be an integer between ${entry.min} and ${entry.max}`);
333
- }
334
- return value;
335
- }
336
-
337
- /** Reads a required string, throwing with the entry's documented message. */
338
- export function readRequired(name: string, entry: RequiredTextConfigEntry): string {
339
- const raw = process.env[name];
340
- if (!raw) throw new Error(entry.error);
341
- return raw;
342
- }
343
-
344
- /** Reads a flag: enabled unless the literal `"false"` is set. */
345
- export function readFlag(name: string, entry: FlagConfigEntry): boolean {
346
- void entry;
347
- return process.env[name] !== "false";
348
- }
349
-
350
- /** Reads a comma-separated list, dropping empty entries. */
351
- export function readList(name: string, entry: ListConfigEntry): readonly string[] | undefined {
352
- void entry;
353
- const entries = process.env[name]
354
- ?.split(",")
355
- .map((value) => value.trim())
356
- .filter(Boolean);
357
- return entries && entries.length > 0 ? entries : undefined;
358
- }
1
+ /**
2
+ * Spec-driven configuration reader shared by the `src/config/` domain modules.
3
+ *
4
+ * `CONFIG_SPEC` is the canonical declaration of every environment variable
5
+ * `src/config/` reads: name, kind, default, and bounds in one place. The
6
+ * resolvers in the sibling domain modules are thin readers over it, so a new
7
+ * knob cannot be added without declaring a row here plus a `.env.example`
8
+ * line. Bounds are enforced on read so a malformed operator value throws at
9
+ * the boundary instead of silently coercing.
10
+ *
11
+ * Module-specific environment reads that only affect a single subsystem (e.g.
12
+ * logger level, telemetry payload storage, Postgres pool tuning) still live
13
+ * next to their consumer. No I/O, no caching — readers are pure with respect
14
+ * to `process.env` at call time.
15
+ */
16
+
17
+ /** Default directory for the built dashboard static assets. */
18
+ const DEFAULT_DASHBOARD_DIST = "./dist/dashboard";
19
+ /** Default upstream client version host for Kimi's OAuth origin. */
20
+ const DEFAULT_KIMI_OAUTH_HOST = "https://auth.kimi.com";
21
+ /** Base for Kimi Code's quota/billing surface; the usage path joins on this. */
22
+ const DEFAULT_KIMI_QUOTA_BASE_URL = "https://api.kimi.com/coding/v1";
23
+
24
+ // ─── Config spec ────────────────────────────────────────────────────────────
25
+
26
+ interface IntConfigEntry {
27
+ readonly kind: "int";
28
+ readonly default: number;
29
+ readonly min: number;
30
+ readonly max: number;
31
+ }
32
+
33
+ interface TextConfigEntry {
34
+ readonly kind: "text";
35
+ readonly default: string;
36
+ }
37
+
38
+ /** Absent means "no value"; an empty/whitespace value also reads as absent. */
39
+ interface OptionalTextConfigEntry {
40
+ readonly kind: "optional-text";
41
+ }
42
+
43
+ /**
44
+ * Absent or empty means "unset" — distinct from an explicit value, because
45
+ * consumers apply their own fallback (cgroup detection, egress default) rather
46
+ * than a fixed default here.
47
+ */
48
+ interface OptionalIntConfigEntry {
49
+ readonly kind: "optional-int";
50
+ readonly min: number;
51
+ readonly max: number;
52
+ }
53
+
54
+ interface RequiredTextConfigEntry {
55
+ readonly kind: "required";
56
+ readonly error: string;
57
+ }
58
+
59
+ /** Enabled unless the literal string `"false"` is set. */
60
+ interface FlagConfigEntry {
61
+ readonly kind: "flag";
62
+ }
63
+
64
+ /** Comma-separated list; an empty result reads as absent. */
65
+ interface ListConfigEntry {
66
+ readonly kind: "list";
67
+ }
68
+
69
+ export type ConfigEntry =
70
+ | IntConfigEntry
71
+ | TextConfigEntry
72
+ | OptionalTextConfigEntry
73
+ | OptionalIntConfigEntry
74
+ | RequiredTextConfigEntry
75
+ | FlagConfigEntry
76
+ | ListConfigEntry;
77
+
78
+ /**
79
+ * The single declaration of every environment variable `src/config/` reads.
80
+ *
81
+ * Adding a knob means adding a row here plus a `.env.example` line. Bounds are
82
+ * enforced on read so a malformed operator value throws at the boundary
83
+ * instead of silently coercing.
84
+ */
85
+ export const CONFIG_SPEC = {
86
+ // HTTP listener + dashboard runtime
87
+ PORT: { kind: "int", default: 12_800, min: 1, max: 65_535 },
88
+ DASHBOARD_DIST: { kind: "text", default: DEFAULT_DASHBOARD_DIST },
89
+ CARTETHYIA_API_KEY: { kind: "optional-text" },
90
+ CARTETHYIA_SERVER_MAX_BODY_BYTES: {
91
+ kind: "int",
92
+ // A 500K-token context is roughly 2 MB of text, so 4 MB admits every real
93
+ // request while halving the worst-case hold. How many may be in flight is
94
+ // admission's job, so an operator with larger payloads raises this and
95
+ // sets the ceiling in Settings.
96
+ default: 4 * 1024 * 1024,
97
+ min: 1024,
98
+ max: Number.MAX_SAFE_INTEGER,
99
+ },
100
+ CARTETHYIA_SERVER_IDLE_TIMEOUT: { kind: "int", default: 60, min: 0, max: 86_400 },
101
+
102
+ // Graceful-shutdown drain: how long in-flight requests may finish naturally
103
+ // before the drain aborts the stragglers. Sized above the common case (a
104
+ // typical response) but below the stream stall bound so a wedged stream is
105
+ // still cut within the process budget.
106
+ CARTETHYIA_SHUTDOWN_DRAIN_WINDOW_MS: {
107
+ kind: "int",
108
+ default: 20_000,
109
+ min: 0,
110
+ max: 600_000,
111
+ },
112
+ // Optional ceiling on in-flight requests per process. Left unset, the limit
113
+ // is derived from the memory limit and the body ceiling (see
114
+ // config/resources.ts): 2 GiB/4 MiB → 64, 512 MiB/4 MiB → 16, 256 MiB/16 MiB
115
+ // → 2. Set it to admit more than memory alone would allow — which is the
116
+ // operator's call, and is reported rather than corrected — or less, to keep
117
+ // a slow upstream from being handed more concurrency than it can serve.
118
+ // Optional-int, not int-with-a-zero-default: "unset" and "explicitly zero"
119
+ // must not be the same thing. Zero is not a valid ceiling, so a default of 0
120
+ // would need a second sentinel to mean "derive".
121
+ // Slack above each key's concurrency limit. Effective ceiling is
122
+ // limit + tolerance; only requests past it are refused.
123
+ CARTETHYIA_CONCURRENCY_TOLERANCE: { kind: "optional-int", min: 0, max: 1024 },
124
+ // How many gateway processes serve this deployment, and which one this is.
125
+ // Count > 1 requires Linux, CARTETHYIA_DB_MODE=full, and a REACHABLE Redis —
126
+ // checked at boot, because a half-configured multi-process deploy starts
127
+ // fine and then enforces every per-key limit N times over. Lite stays
128
+ // single-process: two processes on one SQLite file corrupt it.
129
+ CARTETHYIA_PROCESS_COUNT: { kind: "int", default: 1, min: 1, max: 16 },
130
+ // Which process this is (0 – count-1). Mandatory when count > 1: a process
131
+ // that does not know its index cannot own a shard of anything.
132
+ CARTETHYIA_PROCESS_INDEX: { kind: "int", default: 0, min: 0, max: 15 },
133
+ // Optional secret for the operator drain endpoint (`POST /admin/drain`). When
134
+ // set, a process can be stopped gracefully on platforms where a catchable
135
+ // signal cannot be delivered to it (notably Windows, where `process.kill` of
136
+ // a console-less process runs no JS handler). The endpoint additionally
137
+ // requires a loopback peer and a timing-safe token match; unset = the route
138
+ // does not exist.
139
+ CARTETHYIA_DRAIN_TOKEN: { kind: "optional-text" },
140
+ CARTETHYIA_INSTALL_ID_DIR: { kind: "optional-text" },
141
+
142
+ // Network policy (trusted proxy + SSRF)
143
+ TRUSTED_PROXY_CIDRS: { kind: "list" },
144
+ CARTETHYIA_ALLOW_PRIVATE_UPSTREAMS: { kind: "flag" },
145
+ CARTETHYIA_ALLOWED_NETWORKS: { kind: "list" },
146
+ CARTETHYIA_MAX_REDIRECTS: { kind: "optional-int", min: 0, max: 10 },
147
+
148
+ // Runtime memory
149
+ CARTETHYIA_MEMORY_LIMIT_BYTES: {
150
+ kind: "optional-int",
151
+ min: 1,
152
+ max: Number.MAX_SAFE_INTEGER,
153
+ },
154
+ // Opt-in forced garbage collection under memory pressure. Off by default:
155
+ // see `observability/runtime-metrics.ts` — a forced collection is not
156
+ // reclamation, and a collection that runs while requests are in flight adds
157
+ // a latency spike. This gates the pressure path only; idle reclaim runs
158
+ // unconditionally, because reclaiming when nothing is in flight stalls
159
+ // nothing.
160
+ CARTETHYIA_FORCED_GC: { kind: "flag" },
161
+ // How often runtime gauges (memory + bounded-collection sizes) are sampled.
162
+ CARTETHYIA_RUNTIME_METRICS_INTERVAL_MS: {
163
+ kind: "int",
164
+ default: 30_000,
165
+ min: 1_000,
166
+ max: 300_000,
167
+ },
168
+ /**
169
+ * Periodic maintenance tasks switched off entirely, by name. A small
170
+ * deployment that does not need live quota or version polling can cut the
171
+ * heaviest sweeps here instead of carrying one flag per task.
172
+ */
173
+ CARTETHYIA_DISABLED_TASKS: { kind: "list" },
174
+
175
+ /**
176
+ * How long a day's telemetry is kept, in days.
177
+ *
178
+ * Whole raw event files past this age are removed. No summary or compacted
179
+ * form replaces the individual requests.
180
+ *
181
+ * This is the knob that decides how long the request list, per-request
182
+ * filters, and the detail drawer stay complete.
183
+ */
184
+ CARTETHYIA_TELEMETRY_RETENTION_DAYS: { kind: "int", default: 30, min: 3, max: 365 },
185
+ CARTETHYIA_TELEMETRY_PAYLOAD_MAX_BYTES: {
186
+ kind: "int",
187
+ default: 16 * 1024 * 1024,
188
+ min: 1 * 1024 * 1024,
189
+ max: 16 * 1024 * 1024,
190
+ },
191
+
192
+ // Upstream request timeout + retry backoff
193
+ //
194
+ // Strictly under 90s: a client whose own deadline is 90s must never be
195
+ // waiting on a gateway that is still holding a request open past it, and a
196
+ // provider that has not answered within 75s is not answering — the request
197
+ // is queued upstream or the egress is stuck, and the honest move is to fail
198
+ // over to another account rather than burn the caller's whole budget on one.
199
+ // 120s outlived the clients that call this, which cap lower.
200
+ CARTETHYIA_UPSTREAM_TIMEOUT_MS: { kind: "int", default: 75_000, min: 5_000, max: 600_000 },
201
+ CARTETHYIA_STREAM_STALL_TIMEOUT_MS: {
202
+ kind: "int",
203
+ default: 360_000,
204
+ min: 60_000,
205
+ max: 600_000,
206
+ },
207
+ CARTETHYIA_STREAM_FIRST_CHUNK_TIMEOUT_MS: {
208
+ kind: "int",
209
+ default: 200_000,
210
+ min: 30_000,
211
+ max: 300_000,
212
+ },
213
+ CARTETHYIA_FALLBACK_RETRY_BASE_MS: { kind: "int", default: 100, min: 0, max: 60_000 },
214
+ CARTETHYIA_FALLBACK_RETRY_CAP_MS: { kind: "int", default: 2_000, min: 0, max: 300_000 },
215
+ CARTETHYIA_ROUTE_MAX_ATTEMPTS: { kind: "int", default: 8, min: 1, max: 64 },
216
+
217
+ // Account/pool health cooldown delays. Each is the fallback used when the
218
+ // upstream error states no explicit reset window; an upstream
219
+ // `Retry-After`/reset header or message always wins over these.
220
+ CARTETHYIA_ACCOUNT_RATE_LIMIT_COOLDOWN_MS: { kind: "int", default: 900_000, min: 0, max: 86_400_000 },
221
+ CARTETHYIA_ACCOUNT_QUOTA_COOLDOWN_MS: { kind: "int", default: 3_600_000, min: 0, max: 86_400_000 },
222
+ CARTETHYIA_ACCOUNT_MODEL_CAPACITY_COOLDOWN_MS: { kind: "int", default: 120_000, min: 0, max: 86_400_000 },
223
+ CARTETHYIA_ACCOUNT_TRANSIENT_COOLDOWN_MS: { kind: "int", default: 30_000, min: 0, max: 86_400_000 },
224
+ /**
225
+ * An unmapped upstream failure cools the account down for 15 minutes.
226
+ *
227
+ * A shorter window puts the account straight back into rotation and re-dials
228
+ * an upstream that has just answered something this gateway does not
229
+ * understand — so the same unknown error is retried every minute, and an
230
+ * account that is actually broken contributes a steady stream of failed
231
+ * attempts instead of one. Fifteen minutes is long enough for a transient
232
+ * provider condition to clear on its own and short enough that no operator
233
+ * action is needed to recover it. It is still a cooldown with a deadline,
234
+ * never `disabled`, so nothing here can strand an account.
235
+ */
236
+ CARTETHYIA_ACCOUNT_UNCLASSIFIED_COOLDOWN_MS: {
237
+ kind: "int",
238
+ default: 900_000,
239
+ min: 0,
240
+ max: 86_400_000,
241
+ },
242
+ CARTETHYIA_ACCOUNT_COOLDOWN_ESCALATION_MAX_MS: { kind: "int", default: 3_600_000, min: 0, max: 86_400_000 },
243
+ CARTETHYIA_POOL_COOLDOWN_MS: { kind: "int", default: 120_000, min: 0, max: 86_400_000 },
244
+ /**
245
+ * A provider-scoped egress cooldown (a 429 the pool itself earned) when the
246
+ * upstream named no window. Default 1h: an egress that exhausted a
247
+ * request-counted allowance stays refused long past minute 15, and a short
248
+ * default sent the pool back into rotation to be refused again.
249
+ */
250
+ CARTETHYIA_POOL_PROVIDER_RATE_LIMIT_COOLDOWN_MS: {
251
+ kind: "int",
252
+ default: 3_600_000,
253
+ min: 0,
254
+ max: 86_400_000,
255
+ },
256
+ CARTETHYIA_ACCOUNT_AUTH_REJECTED_COOLDOWN_MS: { kind: "int", default: 300_000, min: 0, max: 86_400_000 },
257
+ CARTETHYIA_ACCOUNT_POLICY_BLOCK_COOLDOWN_MS: { kind: "int", default: 300_000, min: 0, max: 86_400_000 },
258
+ CARTETHYIA_ACCOUNT_DISABLED_RECOVERY_MS: { kind: "int", default: 86_400_000, min: 0, max: 604_800_000 },
259
+
260
+ // Proxy pool agents
261
+ CARTETHYIA_PROXY_MAX_SOCKETS: { kind: "int", default: 100, min: 1, max: 100_000 },
262
+ CARTETHYIA_PROXY_MAX_FREE_SOCKETS: { kind: "int", default: 20, min: 0, max: 100_000 },
263
+
264
+ // Security (encryption key, public origin, abuse ceiling)
265
+ IP_RATE_MAX_PER_WINDOW: { kind: "int", default: 240, min: 1, max: Number.MAX_SAFE_INTEGER },
266
+ CARTETHYIA_ENCRYPTION_KEY: {
267
+ kind: "required",
268
+ error:
269
+ "CARTETHYIA_ENCRYPTION_KEY is required to encrypt credentials and hash API keys",
270
+ },
271
+ CARTETHYIA_PUBLIC_ORIGIN: {
272
+ kind: "required",
273
+ error:
274
+ "CARTETHYIA_PUBLIC_ORIGIN is required to build OAuth redirect URIs (e.g. https://cartethyia.example.com)",
275
+ },
276
+
277
+ // Provider OAuth hosts
278
+ CARTETHYIA_KIMI_OAUTH_HOST: { kind: "text", default: DEFAULT_KIMI_OAUTH_HOST },
279
+ /**
280
+ * Loopback port the browser-OAuth callback listener binds.
281
+ *
282
+ * Configurable because the default is a fixed number: a busy port would strand
283
+ * the operator, since the login could not start and there would be no way to
284
+ * move it. Codex's authorization server still expects 59653, so changing this
285
+ * only helps the providers whose redirect URI is not allowlisted upstream.
286
+ */
287
+ CARTETHYIA_OAUTH_CALLBACK_PORT: { kind: "int", default: 59_653, min: 1_024, max: 65_535 },
288
+ /**
289
+ * Kimi Code quota/billing base. The `/usages` path joins on this, so the
290
+ * default includes the `/v1` segment the bundled provider metadata implies.
291
+ */
292
+ CARTETHYIA_KIMI_QUOTA_BASE_URL: { kind: "text", default: DEFAULT_KIMI_QUOTA_BASE_URL },
293
+ /**
294
+ * GitHub Enterprise domain for the Copilot device login, e.g.
295
+ * `company.ghe.com`. Empty (the default) means github.com. GitHub Enterprise
296
+ * serves the device flow and the token-mint endpoint from its own domain, so
297
+ * an operator on one has no other way to point the login at it.
298
+ */
299
+ CARTETHYIA_GITHUB_ENTERPRISE_DOMAIN: { kind: "text", default: "" },
300
+ /**
301
+ * Free-tier OpenCode ids the listing still advertises but upstream refuses.
302
+ * Replaces the bundled list outright; the single token `none` trusts the
303
+ * listing completely.
304
+ *
305
+ * The bundled ids were measured, not declared — upstream can start serving
306
+ * a refused id or start refusing a served one at any time, and either way
307
+ * the correct answer stopped being the one compiled in. This is the escape
308
+ * hatch that needs no deploy.
309
+ */
310
+ CARTETHYIA_OPENCODE_UNAVAILABLE_FREE: { kind: "list" },
311
+ } as const satisfies Readonly<Record<string, ConfigEntry>>;
312
+
313
+ // ─── Spec readers ───────────────────────────────────────────────────────────
314
+
315
+ /** Reads a bounded integer, throwing on a malformed or out-of-range value. */
316
+ export function readInt(name: string, entry: IntConfigEntry): number {
317
+ const raw = process.env[name];
318
+ if (raw === undefined || raw.trim() === "") return entry.default;
319
+ const value = Number(raw);
320
+ if (!Number.isInteger(value) || value < entry.min || value > entry.max) {
321
+ throw new Error(`${name} must be an integer between ${entry.min} and ${entry.max}`);
322
+ }
323
+ return value;
324
+ }
325
+
326
+ /** Reads a string with a fallback, treating an empty value as absent. */
327
+ export function readText(name: string, entry: TextConfigEntry): string {
328
+ const raw = process.env[name]?.trim();
329
+ return raw && raw.length > 0 ? raw : entry.default;
330
+ }
331
+
332
+ /** Reads an optional string; empty or whitespace reads as absent. */
333
+ export function readOptionalText(name: string, entry: OptionalTextConfigEntry): string | undefined {
334
+ void entry;
335
+ const raw = process.env[name]?.trim();
336
+ return raw && raw.length > 0 ? raw : undefined;
337
+ }
338
+
339
+ /** Reads an optional bounded integer; absent or empty reads as `undefined`. */
340
+ export function readOptionalInt(name: string, entry: OptionalIntConfigEntry): number | undefined {
341
+ const raw = process.env[name];
342
+ if (raw === undefined || raw.trim() === "") return undefined;
343
+ const value = Number(raw);
344
+ if (!Number.isInteger(value) || value < entry.min || value > entry.max) {
345
+ throw new Error(`${name} must be an integer between ${entry.min} and ${entry.max}`);
346
+ }
347
+ return value;
348
+ }
349
+
350
+ /** Reads a required string, throwing with the entry's documented message. */
351
+ export function readRequired(name: string, entry: RequiredTextConfigEntry): string {
352
+ const raw = process.env[name];
353
+ if (!raw) throw new Error(entry.error);
354
+ return raw;
355
+ }
356
+
357
+ /** Reads a flag: enabled unless the literal `"false"` is set. */
358
+ export function readFlag(name: string, entry: FlagConfigEntry): boolean {
359
+ void entry;
360
+ return process.env[name] !== "false";
361
+ }
362
+
363
+ /** Reads a comma-separated list, dropping empty entries. */
364
+ export function readList(name: string, entry: ListConfigEntry): readonly string[] | undefined {
365
+ void entry;
366
+ const entries = process.env[name]
367
+ ?.split(",")
368
+ .map((value) => value.trim())
369
+ .filter(Boolean);
370
+ return entries && entries.length > 0 ? entries : undefined;
371
+ }