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.
- package/.env.example +107 -106
- package/README.md +25 -2
- package/dist/dashboard/assets/{BannedPage-P76QLGJA.js → BannedPage-D-J75hBM.js} +1 -1
- package/dist/dashboard/assets/BannedPage-D-J75hBM.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-DdBggJS9.js → CliToolDetailPage-CVe6RaBH.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-CVe6RaBH.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-aN1iXhhP.js → CliToolsPage-Dd8ZSamp.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-Dd8ZSamp.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-DE-F1nxZ.js → CombosPage-QoJ6foxo.js} +1 -1
- package/dist/dashboard/assets/CombosPage-QoJ6foxo.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-DpNRay4C.js +1 -0
- package/dist/dashboard/assets/ConsoleLogPage-DpNRay4C.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-3CUxMy2X.js → CustomizationPage-CKUFTcW4.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-CKUFTcW4.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-CzSLiDzr.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-CzSLiDzr.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-DubdQcvX.js +1 -0
- package/dist/dashboard/assets/LoginPage-DubdQcvX.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-DKVslqG3.js → ModelPicker-BH8Tehdi.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-BH8Tehdi.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-DuWzAQFL.js +1 -0
- package/dist/dashboard/assets/OverviewPage-DuWzAQFL.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-BKEKe2Fm.js +4 -0
- package/dist/dashboard/assets/ProviderDetailPage-BKEKe2Fm.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js +1 -0
- package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-Bwotu8UJ.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-Bwotu8UJ.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-D7hpWbg9.js +9 -0
- package/dist/dashboard/assets/ProxyPage-D7hpWbg9.js.gz +0 -0
- package/dist/dashboard/assets/{QuotaPage-BjldQwIU.js → QuotaPage-ATpaxrbl.js} +1 -1
- package/dist/dashboard/assets/QuotaPage-ATpaxrbl.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-DUIoLRxk.js → SettingsPage-CETophpx.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-CETophpx.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-BdgECjqq.js +1 -0
- package/dist/dashboard/assets/SetupPage-BdgECjqq.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-fI7JkdIB.js → SortableList-CAK-EN9z.js} +1 -1
- package/dist/dashboard/assets/SortableList-CAK-EN9z.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-CHVZqskZ.js → StudioPage-B7W2_RhT.js} +3 -3
- package/dist/dashboard/assets/StudioPage-B7W2_RhT.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-6T4EFDjW.js +56 -0
- package/dist/dashboard/assets/UsagePage-6T4EFDjW.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-DFY_fZU4.js → api-keys-CUi6UXsR.js} +1 -1
- package/dist/dashboard/assets/api-keys-CUi6UXsR.js.gz +0 -0
- package/dist/dashboard/assets/app-KvsUJUMF.css +2 -0
- package/dist/dashboard/assets/app-KvsUJUMF.css.gz +0 -0
- package/dist/dashboard/assets/{assert-CwdnZARt.js → assert-67xXH5rf.js} +1 -1
- package/dist/dashboard/assets/assert-67xXH5rf.js.gz +0 -0
- package/dist/dashboard/assets/{badge-Dr-uf0Au.js → badge-DC2L43Mj.js} +1 -1
- package/dist/dashboard/assets/badge-DC2L43Mj.js.gz +0 -0
- package/dist/dashboard/assets/{banner-BddmMdoS.js → banner-BBVcb0J3.js} +1 -1
- package/dist/dashboard/assets/banner-BBVcb0J3.js.gz +0 -0
- package/dist/dashboard/assets/{card-CK0PXWlX.js → card-Cca1XC6G.js} +1 -1
- package/dist/dashboard/assets/card-Cca1XC6G.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-CcB6mGh2.js → cli-tools-KvsWa1Jl.js} +1 -1
- package/dist/dashboard/assets/cli-tools-KvsWa1Jl.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-DGb2P9Yx.js → clipboard-button-DUzEbynq.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-DUzEbynq.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-LkfuL50u.js → dialog-DFvWhwTa.js} +1 -1
- package/dist/dashboard/assets/dialog-DFvWhwTa.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-ZbEZ_84c.js → drawer-CFM2sMX1.js} +1 -1
- package/dist/dashboard/assets/drawer-CFM2sMX1.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CWBY5A0f.js → entry-BLBLxX0h.js} +3 -3
- package/dist/dashboard/assets/entry-BLBLxX0h.js.gz +0 -0
- package/dist/dashboard/assets/{entry-BJfsVMvA.js → entry-Bqy8RFvM.js} +1 -1
- package/dist/dashboard/assets/entry-Bqy8RFvM.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CvC1-Zqx.js → entry-_xHo5x6r.js} +1 -1
- package/dist/dashboard/assets/entry-_xHo5x6r.js.gz +0 -0
- package/dist/dashboard/assets/formatters-mpBPqAQ8.js +1 -0
- package/dist/dashboard/assets/formatters-mpBPqAQ8.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-BxpznFLt.js → github-badge-C9e8Ts7e.js} +1 -1
- package/dist/dashboard/assets/github-badge-C9e8Ts7e.js.gz +0 -0
- package/dist/dashboard/assets/{index-CDzP3ovJ.js → index-D6QzEC_n.js} +2 -2
- package/dist/dashboard/assets/index-D6QzEC_n.js.gz +0 -0
- package/dist/dashboard/assets/{inline-Ae13MQ5w.js → inline-BFUcvllj.js} +1 -1
- package/dist/dashboard/assets/inline-BFUcvllj.js.gz +0 -0
- package/dist/dashboard/assets/{input-D23fo7cp.js → input-C_ILAvBb.js} +1 -1
- package/dist/dashboard/assets/input-C_ILAvBb.js.gz +0 -0
- package/dist/dashboard/assets/{layout-CSsxjj5P.js → layout-CBIyT6Rt.js} +1 -1
- package/dist/dashboard/assets/layout-CBIyT6Rt.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-dmbwZMDY.js → list-row-ebJ_-Nsb.js} +1 -1
- package/dist/dashboard/assets/list-row-ebJ_-Nsb.js.gz +0 -0
- package/dist/dashboard/assets/{live-BG1oUsZZ.js → live-D0mm--ME.js} +1 -1
- package/dist/dashboard/assets/live-D0mm--ME.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-DSF-e4ho.js → mutation-sMD965oO.js} +1 -1
- package/dist/dashboard/assets/mutation-sMD965oO.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-Bm_aECY7.js → numbers-BO9dKaFr.js} +1 -1
- package/dist/dashboard/assets/numbers-BO9dKaFr.js.gz +0 -0
- package/dist/dashboard/assets/providers/inception.svg +5 -0
- package/dist/dashboard/assets/providers/inception.svg.gz +0 -0
- package/dist/dashboard/assets/{query-keys-CF56nYIV.js → query-keys-CfL_Zdoh.js} +1 -1
- package/dist/dashboard/assets/query-keys-CfL_Zdoh.js.gz +0 -0
- package/dist/dashboard/assets/{routing-CeA71n7G.js → routing-BnIpi1IJ.js} +1 -1
- package/dist/dashboard/assets/routing-BnIpi1IJ.js.gz +0 -0
- package/dist/dashboard/assets/{select-KIT0rC6G.js → select-Cj5Nw43g.js} +1 -1
- package/dist/dashboard/assets/select-Cj5Nw43g.js.gz +0 -0
- package/dist/dashboard/assets/{settings-B_2gkePf.js → settings-BAFhDgX8.js} +1 -1
- package/dist/dashboard/assets/settings-BAFhDgX8.js.gz +0 -0
- package/dist/dashboard/assets/{stack-D7WOSzcX.js → stack-EMrOMt4I.js} +1 -1
- package/dist/dashboard/assets/stack-EMrOMt4I.js.gz +0 -0
- package/dist/dashboard/assets/{state-BBxtKZOQ.js → state-CWoo7ty5.js} +1 -1
- package/dist/dashboard/assets/state-CWoo7ty5.js.gz +0 -0
- package/dist/dashboard/assets/{switch-DNS_D5Ea.js → switch-CCjT6IkF.js} +1 -1
- package/dist/dashboard/assets/switch-CCjT6IkF.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-BgoxdoMk.js → useMutation-DjkLHUcE.js} +1 -1
- package/dist/dashboard/assets/useMutation-DjkLHUcE.js.gz +0 -0
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +54 -53
- package/migrations/full-postgres/0024_add_pool_provider_cooldowns.sql +12 -0
- package/migrations/full-postgres/0025_add_pool_random_strategy.sql +3 -0
- package/migrations/lite-sqlite3/0024_add_pool_provider_cooldowns.sql +12 -0
- package/migrations/lite-sqlite3/0025_add_pool_random_strategy.sql +20 -0
- package/package.json +96 -96
- package/scripts/cli/console-menu.ts +5 -2
- package/scripts/cli/tray.ts +21 -6
- package/src/boot/app.ts +684 -714
- package/src/boot/dependencies.ts +65 -62
- package/src/config/cooldowns.ts +16 -0
- package/src/config/providers.ts +13 -1
- package/src/config/resources.ts +70 -70
- package/src/config/security.ts +37 -37
- package/src/config/spec.ts +371 -358
- package/src/console/console-router.ts +233 -225
- package/src/console/domain-registration.ts +398 -381
- package/src/console/providers/catalog/catalog-projections.ts +2 -6
- package/src/console/providers/catalog/contracts.ts +19 -0
- package/src/console/providers/catalog/provider-operations.ts +38 -0
- package/src/console/providers/catalog/routes.ts +14 -0
- package/src/console/providers/catalog/store-helpers.ts +326 -0
- package/src/console/providers/catalog/store.ts +1292 -1563
- package/src/console/quota/account-quota-global.ts +2 -2
- package/src/console/quota/account-quota-shared.ts +1 -1
- package/src/console/quota/account-quota-tenant.ts +35 -12
- package/src/console/quota/quota-view.ts +2 -2
- package/src/console/routing/pools/contracts.ts +41 -1
- package/src/console/routing/pools/operations.ts +183 -4
- package/src/console/routing/pools/relay-deploy.ts +17 -4
- package/src/console/routing/pools/routes.ts +30 -0
- package/src/console/routing/pools/store.ts +98 -8
- package/src/main.ts +183 -167
- package/src/network/outbound-fetch.ts +226 -365
- package/src/network/pool/agent.ts +8 -0
- package/src/network/pool/pool-state.ts +7 -0
- package/src/network/pool/selector.ts +110 -2
- package/src/network/pool-cooldown-store.ts +252 -0
- package/src/network/pool-health-machine.ts +5 -3
- package/src/network/pool-health.ts +36 -9
- package/src/observability/event-query.ts +57 -0
- package/src/observability/metrics.ts +467 -467
- package/src/observability/runtime-metrics.ts +269 -269
- package/src/persistence/health-event-flush.ts +2 -0
- package/src/persistence/schema.ts +19 -0
- package/src/persistence/usage-flush.ts +8 -3
- package/src/protocol/canonical-model.ts +1 -1
- package/src/protocol/dsml.ts +377 -0
- package/src/protocol/gateway-error.ts +232 -232
- package/src/{providers/provider-reasoning.ts → protocol/reasoning.ts} +10 -9
- package/src/protocol/request/chat.ts +7 -7
- package/src/protocol/request/codex.ts +2 -2
- package/src/protocol/request/gemini.ts +1 -1
- package/src/protocol/request/messages.ts +6 -6
- package/src/protocol/request/responses.ts +7 -7
- package/src/protocol/response/dsml.ts +12 -133
- package/src/{transport → protocol}/translation/attachment-integrity.ts +2 -2
- package/src/{transport → protocol}/translation/cache-controls.ts +1 -1
- package/src/{transport → protocol}/translation/capabilities.ts +2 -2
- package/src/{transport → protocol}/translation/normalize.ts +1 -1
- package/src/{transport → protocol}/translation/quirks.ts +1 -1
- package/src/{transport → protocol}/translation/search.ts +1 -1
- package/src/{transport → protocol}/translation/thinking.ts +23 -1
- package/src/{transport → protocol}/translation/tool-id.ts +1 -1
- package/src/{transport → protocol}/translation/tool-repair.ts +2 -2
- package/src/protocol/upstream-parsing.ts +176 -0
- package/src/providers/adapters/compatible-adapter.ts +11 -2
- package/src/providers/authentication/oauth-refresh-service.ts +9 -5
- package/src/providers/default-registry.ts +21 -5
- package/src/providers/discovery/probe-phases.ts +16 -3
- package/src/providers/discovery/probing-service.ts +4 -1
- package/src/providers/integrations/anthropic.ts +1 -1
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -4
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +141 -8
- package/src/providers/integrations/buddy/buddy-checkin.ts +5 -1
- package/src/providers/integrations/buddy/buddy-growth-shared.ts +0 -82
- package/src/providers/integrations/buddy/buddy-identity.ts +25 -0
- package/src/providers/integrations/buddy/buddy-server-tools.ts +240 -0
- package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +36 -0
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +6 -31
- package/src/providers/integrations/buddy/codebuddy/provider.ts +2 -0
- package/src/providers/integrations/buddy/codebuddy/shared.ts +19 -4
- package/src/providers/integrations/buddy/turing-device-token.ts +262 -0
- package/src/providers/integrations/buddy/workbuddy/provider.ts +2 -0
- package/src/providers/integrations/buddy/workbuddy/shared.ts +56 -3
- package/src/providers/integrations/claude/claude.ts +1 -1
- package/src/providers/integrations/cloudflareai/cloudflareai.ts +1 -1
- package/src/providers/integrations/codex/codex-errors.ts +1 -1
- package/src/providers/integrations/codex/codex.ts +1 -1
- package/src/providers/integrations/factory/factory.ts +1 -1
- package/src/providers/integrations/github/github.ts +1 -1
- package/src/providers/integrations/grok/grok.ts +1 -1
- package/src/providers/integrations/inception/provider.ts +91 -0
- package/src/providers/integrations/inferhub.ts +1 -1
- package/src/providers/integrations/kiro/kiro-request.ts +1 -1
- package/src/providers/integrations/mistral.ts +193 -19
- package/src/providers/integrations/opencode/fingerprint.ts +19 -2
- package/src/providers/integrations/opencode/prompt.ts +111 -0
- package/src/providers/integrations/opencode/provider.ts +101 -23
- package/src/providers/integrations/opencode/tool-definitions.ts +4 -4
- package/src/providers/integrations/qoder/qoder-checkin.ts +303 -0
- package/src/providers/integrations/qoder/qoder.ts +1641 -1458
- package/src/providers/integrations/shared/install-id.ts +2 -1
- package/src/providers/integrations/trae/trae-cache.ts +110 -0
- package/src/providers/integrations/trae/trae-oauth.ts +29 -4
- package/src/providers/integrations/trae/trae-protocol.ts +2 -1
- package/src/providers/integrations/trae/trae.ts +106 -23
- package/src/providers/operations/account-error-classification.ts +1660 -0
- package/src/providers/operations/account-health-service.ts +621 -2097
- package/src/providers/operations/account-reset-service.ts +2 -0
- package/src/providers/provider-contracts.ts +0 -2
- package/src/providers/provider-metadata.ts +25 -4
- package/src/providers/provider-probe.ts +2 -2
- package/src/{console → providers}/quota/cache.ts +1 -1
- package/src/{console → providers}/quota/refresh.ts +5 -5
- package/src/transport/dispatch/attempt-finalize.ts +114 -12
- package/src/transport/dispatch/attempt-loop.ts +3 -0
- package/src/transport/dispatch/empty-stream.ts +224 -217
- package/src/transport/dispatch/generation.ts +37 -55
- package/src/transport/dispatch/handler-deps.ts +32 -0
- package/src/transport/dispatch/leases.ts +9 -4
- package/src/transport/dispatch/proxy-request.ts +6 -30
- package/src/transport/dispatch/responses-compact.ts +22 -43
- package/src/transport/dispatch/streaming/finalize.ts +3 -0
- package/src/transport/dispatch/streaming/stream.ts +0 -8
- package/src/transport/dispatch/systemone.ts +22 -42
- package/src/transport/dispatch/token-count.ts +23 -39
- package/src/transport/dispatch/upstream.ts +1 -3
- package/src/transport/dispatch/websearch.ts +22 -41
- package/src/transport/failure-policy.ts +59 -178
- package/src/transport/guards/abuse-guard.ts +10 -0
- package/src/transport/guards/auth-guard.ts +109 -109
- package/src/transport/guards/error-lifecycle.ts +1 -1
- package/src/transport/guards/pipeline.ts +220 -220
- package/src/transport/guards/request-context.ts +245 -238
- package/src/transport/request/degradation.ts +20 -1
- package/src/transport/request/inflight.ts +4 -114
- package/src/transport/request/preparer.ts +6 -6
- package/src/transport/request/state.ts +66 -38
- package/src/transport/request/tenant-preferences.ts +1 -1
- package/src/transport/routing/route-catalog.ts +41 -31
- package/src/transport/routing/router.ts +2 -2
- package/src/transport/surface/messages/parse.ts +1 -1
- package/src/workers/buddy-growth-tasks.ts +346 -425
- package/src/workers/daily-checkin.ts +17 -6
- package/src/workers/quota-refresh-sweep.ts +2 -2
- package/src/workers/scheduler.ts +17 -1
- package/dist/dashboard/assets/BannedPage-P76QLGJA.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-DdBggJS9.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-aN1iXhhP.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-DE-F1nxZ.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-5SpmfLyw.js +0 -1
- package/dist/dashboard/assets/ConsoleLogPage-5SpmfLyw.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-3CUxMy2X.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-BkmJQKC0.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-BkmJQKC0.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-B_0FUKWc.js +0 -1
- package/dist/dashboard/assets/LoginPage-B_0FUKWc.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-DKVslqG3.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-D-H4MEiT.js +0 -1
- package/dist/dashboard/assets/OverviewPage-D-H4MEiT.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CEJw6Guo.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage-CEJw6Guo.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js +0 -1
- package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-CEplAR-i.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-CEplAR-i.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-CAN7hy0f.js +0 -7
- package/dist/dashboard/assets/ProxyPage-CAN7hy0f.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BjldQwIU.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-DUIoLRxk.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-_bCaTM0v.js +0 -1
- package/dist/dashboard/assets/SetupPage-_bCaTM0v.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-fI7JkdIB.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-CHVZqskZ.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-DiFBymHj.js +0 -56
- package/dist/dashboard/assets/UsagePage-DiFBymHj.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-DFY_fZU4.js.gz +0 -0
- package/dist/dashboard/assets/app-BOJ1_xUB.css +0 -2
- package/dist/dashboard/assets/app-BOJ1_xUB.css.gz +0 -0
- package/dist/dashboard/assets/assert-CwdnZARt.js.gz +0 -0
- package/dist/dashboard/assets/badge-Dr-uf0Au.js.gz +0 -0
- package/dist/dashboard/assets/banner-BddmMdoS.js.gz +0 -0
- package/dist/dashboard/assets/card-CK0PXWlX.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-CcB6mGh2.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-DGb2P9Yx.js.gz +0 -0
- package/dist/dashboard/assets/cog-CdWhsajs.js +0 -1
- package/dist/dashboard/assets/cog-CdWhsajs.js.gz +0 -0
- package/dist/dashboard/assets/dialog-LkfuL50u.js.gz +0 -0
- package/dist/dashboard/assets/drawer-ZbEZ_84c.js.gz +0 -0
- package/dist/dashboard/assets/entry-BJfsVMvA.js.gz +0 -0
- package/dist/dashboard/assets/entry-CWBY5A0f.js.gz +0 -0
- package/dist/dashboard/assets/entry-CvC1-Zqx.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-BxpznFLt.js.gz +0 -0
- package/dist/dashboard/assets/index-CDzP3ovJ.js.gz +0 -0
- package/dist/dashboard/assets/inline-Ae13MQ5w.js.gz +0 -0
- package/dist/dashboard/assets/input-D23fo7cp.js.gz +0 -0
- package/dist/dashboard/assets/layout-CSsxjj5P.js.gz +0 -0
- package/dist/dashboard/assets/list-row-dmbwZMDY.js.gz +0 -0
- package/dist/dashboard/assets/live-BG1oUsZZ.js.gz +0 -0
- package/dist/dashboard/assets/mutation-DSF-e4ho.js.gz +0 -0
- package/dist/dashboard/assets/numbers-Bm_aECY7.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-CF56nYIV.js.gz +0 -0
- package/dist/dashboard/assets/routing-CeA71n7G.js.gz +0 -0
- package/dist/dashboard/assets/select-KIT0rC6G.js.gz +0 -0
- package/dist/dashboard/assets/settings-B_2gkePf.js.gz +0 -0
- package/dist/dashboard/assets/stack-D7WOSzcX.js.gz +0 -0
- package/dist/dashboard/assets/state-BBxtKZOQ.js.gz +0 -0
- package/dist/dashboard/assets/switch-DNS_D5Ea.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-BgoxdoMk.js.gz +0 -0
- /package/dist/dashboard/assets/{app-CaXZ9SCn.js → app-BaWd0puM.js} +0 -0
- /package/dist/dashboard/assets/{app-CaXZ9SCn.js.gz → app-BaWd0puM.js.gz} +0 -0
package/src/boot/dependencies.ts
CHANGED
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
ValidatedNetworkBindingFactory,
|
|
49
49
|
} from "../network/pool/resolver";
|
|
50
50
|
import { NetworkPoolSelector } from "../network/pool/selector";
|
|
51
|
+
import { restorePoolProbeVerdicts, restoreProviderCooldownMarkers } from "../network/pool-cooldown-store";
|
|
51
52
|
import { ScheduledTaskRegistry } from "../workers/scheduler";
|
|
52
53
|
import { quotaRefreshSweep } from "../workers/quota-refresh-sweep";
|
|
53
54
|
import { markProviderApiKeyCredential } from "../console/quota/account-quota-shared";
|
|
@@ -55,15 +56,11 @@ import { dailyCheckinEgressForPass } from "../workers/daily-checkin";
|
|
|
55
56
|
import {
|
|
56
57
|
buddyAdoptTask,
|
|
57
58
|
buddyActivityReportTask,
|
|
58
|
-
buddyLotteryTask,
|
|
59
|
-
buddyRedeemTask,
|
|
60
59
|
buddyStreakTask,
|
|
61
|
-
buddyTravelTask,
|
|
62
|
-
buddyTrialTask,
|
|
63
60
|
syncBuddyUsage,
|
|
64
61
|
} from "../workers/buddy-growth-tasks";
|
|
65
62
|
import { createAccountSecretResolver } from "../providers/operations/provider-credential-service";
|
|
66
|
-
import { quotaCacheSize } from "../
|
|
63
|
+
import { quotaCacheSize } from "../providers/quota/cache";
|
|
67
64
|
import { modelCatalogCacheSize } from "../providers/operations/model-catalog-cache";
|
|
68
65
|
import { preferencesReaderFor } from "../persistence/tenant-preferences";
|
|
69
66
|
import { sweepExpiredCooldowns } from "../providers/operations/account-health-service";
|
|
@@ -260,39 +257,6 @@ export async function buildProductionDeps(): Promise<ProductionDeps> {
|
|
|
260
257
|
redis ? new RedisAdmissionController(redis) : undefined,
|
|
261
258
|
redis,
|
|
262
259
|
);
|
|
263
|
-
const readRoutingAccountInflight = async (
|
|
264
|
-
providerId: string,
|
|
265
|
-
tenantId: string | null,
|
|
266
|
-
): Promise<readonly { accountId: string; inflight: number }[]> => {
|
|
267
|
-
if (providerId === "" || tenantId === null) return [];
|
|
268
|
-
const accounts = await new DrizzleProviderCatalogStore(db, {
|
|
269
|
-
telemetryBuffer,
|
|
270
|
-
bundledModelCatalog: bundledCatalog,
|
|
271
|
-
providerRegistry: registry,
|
|
272
|
-
outboundFetchFor: async () => ({ fetch: networkBindingFactory.fetch(undefined, tenantId) }),
|
|
273
|
-
snapshotInvalidator: snapshotService,
|
|
274
|
-
}).listAccounts(tenantId, providerId);
|
|
275
|
-
const owned = new Set(accounts.map((account) => account.id));
|
|
276
|
-
// Scoped read: only the provider's own candidates for this tenant are
|
|
277
|
-
// queried, so the console read costs one exact MGET over the accounts it
|
|
278
|
-
// is about to report on — not a fleet-wide `KEYS` scan whose cost is the
|
|
279
|
-
// size of every bucket every process ever wrote.
|
|
280
|
-
const routeSnapshot = await snapshotService.getSnapshot();
|
|
281
|
-
const ownedCandidates = routeSnapshot.candidates.filter(
|
|
282
|
-
(candidate) =>
|
|
283
|
-
candidate.provider_id === providerId &&
|
|
284
|
-
candidate.provider_account_id !== undefined &&
|
|
285
|
-
owned.has(candidate.provider_account_id),
|
|
286
|
-
);
|
|
287
|
-
const snapshot = await routingEngine.accountInflightSnapshot(ownedCandidates);
|
|
288
|
-
const totals = new Map<string, number>();
|
|
289
|
-
for (const [bucket, count] of snapshot) {
|
|
290
|
-
const accountId = bucket.slice(bucket.lastIndexOf(":") + 1);
|
|
291
|
-
if (!owned.has(accountId)) continue;
|
|
292
|
-
totals.set(accountId, (totals.get(accountId) ?? 0) + count);
|
|
293
|
-
}
|
|
294
|
-
return [...totals].map(([accountId, inflight]) => ({ accountId, inflight }));
|
|
295
|
-
};
|
|
296
260
|
// Peer processes' settings mutations arrive here. Subscribed once for the
|
|
297
261
|
// process: the revision counter is process-global, and the handler only
|
|
298
262
|
// moves it forward.
|
|
@@ -379,6 +343,10 @@ export async function buildProductionDeps(): Promise<ProductionDeps> {
|
|
|
379
343
|
scheduledTasks.register({
|
|
380
344
|
name: "account-health-sweep",
|
|
381
345
|
intervalMs: 30_000,
|
|
346
|
+
// Offsets the pass from lease-sweep and runtime-metrics, which share the
|
|
347
|
+
// same cadence: under the registry's single-flight gate, aligned ticks
|
|
348
|
+
// make the 30s tasks skip each other when one runs long.
|
|
349
|
+
initialDelayMs: 5_000,
|
|
382
350
|
// Recovers cooldowns deployment-wide and invalidates the shared snapshot.
|
|
383
351
|
shared: true,
|
|
384
352
|
run: async () => {
|
|
@@ -420,6 +388,22 @@ export async function buildProductionDeps(): Promise<ProductionDeps> {
|
|
|
420
388
|
});
|
|
421
389
|
const telemetryBuffer = new TelemetryBatchBuffer(db);
|
|
422
390
|
const poolSelector = new NetworkPoolSelector(redis);
|
|
391
|
+
// Replay the provider cooldown markers the previous process recorded
|
|
392
|
+
// (`network_pools.provider_cooldowns`). Without this a restart made dispatch
|
|
393
|
+
// forget every egress the upstream was still refusing: the flags are
|
|
394
|
+
// process-local otherwise, so the pool re-entered rotation and took 429s the
|
|
395
|
+
// first request after boot re-discovered. Expired entries are dropped by the
|
|
396
|
+
// restore itself. Best-effort: a failed read leaves selection matching what
|
|
397
|
+
// a fresh deployment would do.
|
|
398
|
+
try {
|
|
399
|
+
const restored = await restoreProviderCooldownMarkers(poolSelector, db);
|
|
400
|
+
const replayed = await restorePoolProbeVerdicts(poolSelector, db);
|
|
401
|
+
if (restored > 0 || replayed > 0) {
|
|
402
|
+
log.info("[pool-selector] restored provider cooldown state", { mirrored: restored, probeVerdictsApplied: replayed });
|
|
403
|
+
}
|
|
404
|
+
} catch (error) {
|
|
405
|
+
log.warn("[pool-selector] provider cooldown restore failed", { error: String(error) });
|
|
406
|
+
}
|
|
423
407
|
const poolAgentResolver = new PoolAgentResolver(
|
|
424
408
|
new DrizzleNetworkPoolLoader(db),
|
|
425
409
|
{ ssrfPolicy, db },
|
|
@@ -429,6 +413,49 @@ export async function buildProductionDeps(): Promise<ProductionDeps> {
|
|
|
429
413
|
undefined,
|
|
430
414
|
poolAgentResolver,
|
|
431
415
|
);
|
|
416
|
+
// The account-inflight reader the console queries. It lives after the deps
|
|
417
|
+
// it closes over (telemetry buffer, binding factory), so the closure never
|
|
418
|
+
// touches a TDZ binding; it also reuses one catalog store per tenant instead
|
|
419
|
+
// of constructing a fresh store object on every console read.
|
|
420
|
+
let lastInflightTenant = "";
|
|
421
|
+
let lastInflightStore: DrizzleProviderCatalogStore | undefined;
|
|
422
|
+
const readRoutingAccountInflight = async (
|
|
423
|
+
providerId: string,
|
|
424
|
+
tenantId: string | null,
|
|
425
|
+
): Promise<readonly { accountId: string; inflight: number }[]> => {
|
|
426
|
+
if (providerId === "" || tenantId === null) return [];
|
|
427
|
+
if (tenantId !== lastInflightTenant || lastInflightStore === undefined) {
|
|
428
|
+
lastInflightStore = new DrizzleProviderCatalogStore(db, {
|
|
429
|
+
telemetryBuffer,
|
|
430
|
+
bundledModelCatalog: bundledCatalog,
|
|
431
|
+
providerRegistry: registry,
|
|
432
|
+
outboundFetchFor: async () => ({ fetch: networkBindingFactory.fetch(undefined, tenantId) }),
|
|
433
|
+
snapshotInvalidator: snapshotService,
|
|
434
|
+
});
|
|
435
|
+
lastInflightTenant = tenantId;
|
|
436
|
+
}
|
|
437
|
+
const accounts = await lastInflightStore.listAccounts(tenantId, providerId);
|
|
438
|
+
const owned = new Set(accounts.map((account) => account.id));
|
|
439
|
+
// Scoped read: only the provider's own candidates for this tenant are
|
|
440
|
+
// queried, so the console read costs one exact MGET over the accounts it
|
|
441
|
+
// is about to report on — not a fleet-wide `KEYS` scan whose cost is the
|
|
442
|
+
// size of every bucket every process ever wrote.
|
|
443
|
+
const routeSnapshot = await snapshotService.getSnapshot();
|
|
444
|
+
const ownedCandidates = routeSnapshot.candidates.filter(
|
|
445
|
+
(candidate) =>
|
|
446
|
+
candidate.provider_id === providerId &&
|
|
447
|
+
candidate.provider_account_id !== undefined &&
|
|
448
|
+
owned.has(candidate.provider_account_id),
|
|
449
|
+
);
|
|
450
|
+
const snapshot = await routingEngine.accountInflightSnapshot(ownedCandidates);
|
|
451
|
+
const totals = new Map<string, number>();
|
|
452
|
+
for (const [bucket, count] of snapshot) {
|
|
453
|
+
const accountId = bucket.slice(bucket.lastIndexOf(":") + 1);
|
|
454
|
+
if (!owned.has(accountId)) continue;
|
|
455
|
+
totals.set(accountId, (totals.get(accountId) ?? 0) + count);
|
|
456
|
+
}
|
|
457
|
+
return [...totals].map(([accountId, inflight]) => ({ accountId, inflight }));
|
|
458
|
+
};
|
|
432
459
|
// Runtime gauges (memory + bounded-collection sizes) sampled on the shared
|
|
433
460
|
// maintenance cadence. The sampler reads live sizes through closures so the
|
|
434
461
|
// observability layer never holds direct references to routing/pool state.
|
|
@@ -606,36 +633,12 @@ export async function buildProductionDeps(): Promise<ProductionDeps> {
|
|
|
606
633
|
initialDelayMs: 5 * 60_000,
|
|
607
634
|
run: () => buddyStreakTask(buddyTaskDeps),
|
|
608
635
|
});
|
|
609
|
-
scheduledTasks.register({
|
|
610
|
-
name: "buddy-redeem",
|
|
611
|
-
intervalMs: 24 * 60 * 60_000,
|
|
612
|
-
initialDelayMs: 8 * 60_000,
|
|
613
|
-
run: () => buddyRedeemTask(buddyTaskDeps),
|
|
614
|
-
});
|
|
615
|
-
scheduledTasks.register({
|
|
616
|
-
name: "buddy-lottery",
|
|
617
|
-
intervalMs: 24 * 60 * 60_000,
|
|
618
|
-
initialDelayMs: 11 * 60_000,
|
|
619
|
-
run: () => buddyLotteryTask(buddyTaskDeps),
|
|
620
|
-
});
|
|
621
|
-
scheduledTasks.register({
|
|
622
|
-
name: "buddy-travel",
|
|
623
|
-
intervalMs: 24 * 60 * 60_000,
|
|
624
|
-
initialDelayMs: 14 * 60_000,
|
|
625
|
-
run: () => buddyTravelTask(buddyTaskDeps),
|
|
626
|
-
});
|
|
627
636
|
scheduledTasks.register({
|
|
628
637
|
name: "buddy-adopt",
|
|
629
638
|
intervalMs: 24 * 60 * 60_000,
|
|
630
639
|
initialDelayMs: 17 * 60_000,
|
|
631
640
|
run: () => buddyAdoptTask(buddyTaskDeps),
|
|
632
641
|
});
|
|
633
|
-
scheduledTasks.register({
|
|
634
|
-
name: "buddy-trial",
|
|
635
|
-
intervalMs: 24 * 60 * 60_000,
|
|
636
|
-
initialDelayMs: 20 * 60_000,
|
|
637
|
-
run: () => buddyTrialTask(buddyTaskDeps),
|
|
638
|
-
});
|
|
639
642
|
// Growth-activity report: the `/v2/report` batch that lights the login streak
|
|
640
643
|
// and scores the daily growth event. Once a day with its own ledger — it used
|
|
641
644
|
// to ride the quota sweep with no marker of its own, so its eight POSTs ran
|
package/src/config/cooldowns.ts
CHANGED
|
@@ -168,6 +168,21 @@ export function resolveAccountDisabledRecoveryMs(): number {
|
|
|
168
168
|
return _disabledRecovery;
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
let _poolProviderRateLimitCooldown: number | undefined;
|
|
172
|
+
/**
|
|
173
|
+
* Fallback cooldown for a provider-scoped egress 429 when the upstream named no
|
|
174
|
+
* window (default 1h). See `CARTETHYIA_POOL_PROVIDER_RATE_LIMIT_COOLDOWN_MS`.
|
|
175
|
+
* An upstream `retryAfterMs` always wins over this.
|
|
176
|
+
*/
|
|
177
|
+
export function resolvePoolProviderRateLimitCooldownMs(): number {
|
|
178
|
+
if (_poolProviderRateLimitCooldown === undefined)
|
|
179
|
+
_poolProviderRateLimitCooldown = readInt(
|
|
180
|
+
"CARTETHYIA_POOL_PROVIDER_RATE_LIMIT_COOLDOWN_MS",
|
|
181
|
+
CONFIG_SPEC.CARTETHYIA_POOL_PROVIDER_RATE_LIMIT_COOLDOWN_MS,
|
|
182
|
+
);
|
|
183
|
+
return _poolProviderRateLimitCooldown;
|
|
184
|
+
}
|
|
185
|
+
|
|
171
186
|
/**
|
|
172
187
|
* Resets all cached cooldown values. Call this in tests that override
|
|
173
188
|
* cooldown env-vars between test cases.
|
|
@@ -180,6 +195,7 @@ export function resetCooldownCacheForTests(): void {
|
|
|
180
195
|
_unclassifiedCooldown = undefined;
|
|
181
196
|
_escalationMaxCooldown = undefined;
|
|
182
197
|
_poolCooldown = undefined;
|
|
198
|
+
_poolProviderRateLimitCooldown = undefined;
|
|
183
199
|
_authRejectedCooldown = undefined;
|
|
184
200
|
_policyBlockCooldown = undefined;
|
|
185
201
|
_disabledRecovery = undefined;
|
package/src/config/providers.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* public origin.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { CONFIG_SPEC, readInt, readRequired, readText } from "./spec";
|
|
7
|
+
import { CONFIG_SPEC, readInt, readOptionalText, readRequired, readText } from "./spec";
|
|
8
8
|
|
|
9
9
|
/** Reads and normalizes the public OAuth origin. */
|
|
10
10
|
export function requirePublicOrigin(): string {
|
|
@@ -48,6 +48,18 @@ export function resolveKimiQuotaBaseUrl(): string {
|
|
|
48
48
|
);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Operator override for the directory installation identities are persisted in.
|
|
53
|
+
* Read here so the only literal `process.env` for this value stays in config;
|
|
54
|
+
* the providers that stamp an install id consume the resolver instead.
|
|
55
|
+
*/
|
|
56
|
+
export function resolveInstallIdDir(): string | undefined {
|
|
57
|
+
return readOptionalText(
|
|
58
|
+
"CARTETHYIA_INSTALL_ID_DIR",
|
|
59
|
+
CONFIG_SPEC.CARTETHYIA_INSTALL_ID_DIR,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
51
63
|
/**
|
|
52
64
|
* Operator override for the OpenCode free-tier ids upstream refuses, or
|
|
53
65
|
* `undefined` when unset — meaning the bundled, measured list stands.
|
package/src/config/resources.ts
CHANGED
|
@@ -1,70 +1,70 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Canonical resource policy: how much memory this process may use, and how
|
|
3
|
-
* much room it must keep free.
|
|
4
|
-
*
|
|
5
|
-
* The memory figure used to be decided in unrelated places — a host-wide
|
|
6
|
-
* `os.totalmem()` in the headroom guard, a fallback constant in
|
|
7
|
-
* `runtime-metrics.ts` — which meant a container limited to 512 MiB shed on
|
|
8
|
-
* the same threshold as a 4 GiB one and only reacted once the *host* was
|
|
9
|
-
* nearly full. On a 1-core / 2 GiB VPS that is the difference between
|
|
10
|
-
* shedding early and being OOM-killed, and the OOM kills the dashboard too —
|
|
11
|
-
* the one surface that would have explained what happened.
|
|
12
|
-
*
|
|
13
|
-
* How many requests may run at once is NOT decided here. That is admission's
|
|
14
|
-
* job: the tenant ceiling and each key's own limit, both set in Settings. A
|
|
15
|
-
* second, process-wide ceiling derived from memory sat below the number an
|
|
16
|
-
* operator could configure and refused requests that every configured limit
|
|
17
|
-
* still allowed, which is why it is gone.
|
|
18
|
-
*/
|
|
19
|
-
import { CONFIG_SPEC, readOptionalInt } from "./spec";
|
|
20
|
-
|
|
21
|
-
/** Used when no limit is discoverable at all (native dev, macOS, Windows). */
|
|
22
|
-
export const FALLBACK_MEMORY_LIMIT_BYTES = 512 * 1024 * 1024;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* How many copies of a request body are resident at once.
|
|
27
|
-
*
|
|
28
|
-
* Measured, not assumed: the socket read buffer, the parsed object graph, and
|
|
29
|
-
* the upstream re-serialization each hold a body-sized allocation, and garbage
|
|
30
|
-
* collection lags rather than keeping them to one. Budgeting one copy halves
|
|
31
|
-
* the real peak and lets the process admit twice the concurrency it can hold.
|
|
32
|
-
*/
|
|
33
|
-
export const REQUEST_BODY_TRANSIENT_COPIES = 2;
|
|
34
|
-
|
|
35
|
-
/** Share of the memory limit all in-flight request bodies together may hold. */
|
|
36
|
-
export const REQUEST_BODY_BUDGET_FRACTION = 0.25;
|
|
37
|
-
|
|
38
|
-
/** Share of the memory limit kept free for allocation spikes and GC slack. */
|
|
39
|
-
export const MEMORY_HEADROOM_FRACTION = 0.2;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Bytes kept free below the memory limit.
|
|
44
|
-
*
|
|
45
|
-
* A guard that only reacts once RSS equals the limit has already lost: the
|
|
46
|
-
* allocation that breaches it happens between two samples, and the spike that
|
|
47
|
-
* matters — an upstream response landing all at once — arrives in far less
|
|
48
|
-
* than one sample interval.
|
|
49
|
-
*/
|
|
50
|
-
export function deriveMemoryHeadroomBytes(memoryLimitBytes: number): number {
|
|
51
|
-
return Math.ceil(memoryLimitBytes * MEMORY_HEADROOM_FRACTION);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Slack added on top of every key's concurrency limit.
|
|
57
|
-
*
|
|
58
|
-
* Operator configuration, not something a key carries about itself: it is
|
|
59
|
-
* deliberate hidden allowance so a run that lands a few requests over the
|
|
60
|
-
* number an operator chose is not refused at that number. The effective
|
|
61
|
-
* ceiling is `limit + tolerance`, and only requests past it are refused.
|
|
62
|
-
*/
|
|
63
|
-
export function resolveConcurrencyTolerance(): number {
|
|
64
|
-
const value = readOptionalInt(
|
|
65
|
-
"CARTETHYIA_CONCURRENCY_TOLERANCE",
|
|
66
|
-
CONFIG_SPEC.CARTETHYIA_CONCURRENCY_TOLERANCE,
|
|
67
|
-
);
|
|
68
|
-
return value === undefined ? 0 : Math.max(0, value);
|
|
69
|
-
}
|
|
70
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Canonical resource policy: how much memory this process may use, and how
|
|
3
|
+
* much room it must keep free.
|
|
4
|
+
*
|
|
5
|
+
* The memory figure used to be decided in unrelated places — a host-wide
|
|
6
|
+
* `os.totalmem()` in the headroom guard, a fallback constant in
|
|
7
|
+
* `runtime-metrics.ts` — which meant a container limited to 512 MiB shed on
|
|
8
|
+
* the same threshold as a 4 GiB one and only reacted once the *host* was
|
|
9
|
+
* nearly full. On a 1-core / 2 GiB VPS that is the difference between
|
|
10
|
+
* shedding early and being OOM-killed, and the OOM kills the dashboard too —
|
|
11
|
+
* the one surface that would have explained what happened.
|
|
12
|
+
*
|
|
13
|
+
* How many requests may run at once is NOT decided here. That is admission's
|
|
14
|
+
* job: the tenant ceiling and each key's own limit, both set in Settings. A
|
|
15
|
+
* second, process-wide ceiling derived from memory sat below the number an
|
|
16
|
+
* operator could configure and refused requests that every configured limit
|
|
17
|
+
* still allowed, which is why it is gone.
|
|
18
|
+
*/
|
|
19
|
+
import { CONFIG_SPEC, readOptionalInt } from "./spec";
|
|
20
|
+
|
|
21
|
+
/** Used when no limit is discoverable at all (native dev, macOS, Windows). */
|
|
22
|
+
export const FALLBACK_MEMORY_LIMIT_BYTES = 512 * 1024 * 1024;
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* How many copies of a request body are resident at once.
|
|
27
|
+
*
|
|
28
|
+
* Measured, not assumed: the socket read buffer, the parsed object graph, and
|
|
29
|
+
* the upstream re-serialization each hold a body-sized allocation, and garbage
|
|
30
|
+
* collection lags rather than keeping them to one. Budgeting one copy halves
|
|
31
|
+
* the real peak and lets the process admit twice the concurrency it can hold.
|
|
32
|
+
*/
|
|
33
|
+
export const REQUEST_BODY_TRANSIENT_COPIES = 2;
|
|
34
|
+
|
|
35
|
+
/** Share of the memory limit all in-flight request bodies together may hold. */
|
|
36
|
+
export const REQUEST_BODY_BUDGET_FRACTION = 0.25;
|
|
37
|
+
|
|
38
|
+
/** Share of the memory limit kept free for allocation spikes and GC slack. */
|
|
39
|
+
export const MEMORY_HEADROOM_FRACTION = 0.2;
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Bytes kept free below the memory limit.
|
|
44
|
+
*
|
|
45
|
+
* A guard that only reacts once RSS equals the limit has already lost: the
|
|
46
|
+
* allocation that breaches it happens between two samples, and the spike that
|
|
47
|
+
* matters — an upstream response landing all at once — arrives in far less
|
|
48
|
+
* than one sample interval.
|
|
49
|
+
*/
|
|
50
|
+
export function deriveMemoryHeadroomBytes(memoryLimitBytes: number): number {
|
|
51
|
+
return Math.ceil(memoryLimitBytes * MEMORY_HEADROOM_FRACTION);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Slack added on top of every key's concurrency limit.
|
|
57
|
+
*
|
|
58
|
+
* Operator configuration, not something a key carries about itself: it is
|
|
59
|
+
* deliberate hidden allowance so a run that lands a few requests over the
|
|
60
|
+
* number an operator chose is not refused at that number. The effective
|
|
61
|
+
* ceiling is `limit + tolerance`, and only requests past it are refused.
|
|
62
|
+
*/
|
|
63
|
+
export function resolveConcurrencyTolerance(): number {
|
|
64
|
+
const value = readOptionalInt(
|
|
65
|
+
"CARTETHYIA_CONCURRENCY_TOLERANCE",
|
|
66
|
+
CONFIG_SPEC.CARTETHYIA_CONCURRENCY_TOLERANCE,
|
|
67
|
+
);
|
|
68
|
+
return value === undefined ? 0 : Math.max(0, value);
|
|
69
|
+
}
|
|
70
|
+
|
package/src/config/security.ts
CHANGED
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Security resolvers: the application encryption key and the abuse-protection
|
|
3
|
-
* ceiling.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { CONFIG_SPEC, readInt, readRequired } from "./spec";
|
|
7
|
-
|
|
8
|
-
const ENCRYPTION_KEY_LENGTH = 32;
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Per-IP request ceiling for the unauthenticated abuse-protection layer.
|
|
12
|
-
* Defaults to 240 requests per window so an unset env does not mean unlimited;
|
|
13
|
-
* operators tune it via `IP_RATE_MAX_PER_WINDOW`.
|
|
14
|
-
*/
|
|
15
|
-
export function resolveIpRateLimit(): number {
|
|
16
|
-
return readInt("IP_RATE_MAX_PER_WINDOW", CONFIG_SPEC.IP_RATE_MAX_PER_WINDOW);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** Reads the application encryption key without caching it. */
|
|
20
|
-
export function requireEncryptionKeyEnv(): string {
|
|
21
|
-
return readRequired("CARTETHYIA_ENCRYPTION_KEY", CONFIG_SPEC.CARTETHYIA_ENCRYPTION_KEY);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Decodes and validates a base64 or hexadecimal 256-bit encryption key. */
|
|
25
|
-
export function decodeEncryptionKey(raw: string): Buffer {
|
|
26
|
-
const candidate = raw.trim();
|
|
27
|
-
const buf = /^[0-9a-fA-F]{64}$/.test(candidate)
|
|
28
|
-
? Buffer.from(candidate, "hex")
|
|
29
|
-
: Buffer.from(candidate, "base64");
|
|
30
|
-
if (buf.length !== ENCRYPTION_KEY_LENGTH) {
|
|
31
|
-
throw new Error(
|
|
32
|
-
`CARTETHYIA_ENCRYPTION_KEY must decode to exactly ${ENCRYPTION_KEY_LENGTH} bytes (got ${buf.length}); ` +
|
|
33
|
-
"provide a base64 or hex encoded 256-bit key",
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
return buf;
|
|
37
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Security resolvers: the application encryption key and the abuse-protection
|
|
3
|
+
* ceiling.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { CONFIG_SPEC, readInt, readRequired } from "./spec";
|
|
7
|
+
|
|
8
|
+
const ENCRYPTION_KEY_LENGTH = 32;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Per-IP request ceiling for the unauthenticated abuse-protection layer.
|
|
12
|
+
* Defaults to 240 requests per window so an unset env does not mean unlimited;
|
|
13
|
+
* operators tune it via `IP_RATE_MAX_PER_WINDOW`.
|
|
14
|
+
*/
|
|
15
|
+
export function resolveIpRateLimit(): number {
|
|
16
|
+
return readInt("IP_RATE_MAX_PER_WINDOW", CONFIG_SPEC.IP_RATE_MAX_PER_WINDOW);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Reads the application encryption key without caching it. */
|
|
20
|
+
export function requireEncryptionKeyEnv(): string {
|
|
21
|
+
return readRequired("CARTETHYIA_ENCRYPTION_KEY", CONFIG_SPEC.CARTETHYIA_ENCRYPTION_KEY);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Decodes and validates a base64 or hexadecimal 256-bit encryption key. */
|
|
25
|
+
export function decodeEncryptionKey(raw: string): Buffer {
|
|
26
|
+
const candidate = raw.trim();
|
|
27
|
+
const buf = /^[0-9a-fA-F]{64}$/.test(candidate)
|
|
28
|
+
? Buffer.from(candidate, "hex")
|
|
29
|
+
: Buffer.from(candidate, "base64");
|
|
30
|
+
if (buf.length !== ENCRYPTION_KEY_LENGTH) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`CARTETHYIA_ENCRYPTION_KEY must decode to exactly ${ENCRYPTION_KEY_LENGTH} bytes (got ${buf.length}); ` +
|
|
33
|
+
"provide a base64 or hex encoded 256-bit key",
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return buf;
|
|
37
|
+
}
|