cartethyia 2.7.0 → 2.7.2
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-XaUmiBrC.js} +1 -1
- package/dist/dashboard/assets/BannedPage-XaUmiBrC.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-DdBggJS9.js → CliToolDetailPage-CqD7QiLM.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-CqD7QiLM.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-aN1iXhhP.js → CliToolsPage-0XUo4UMn.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-0XUo4UMn.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-DE-F1nxZ.js → CombosPage-CXGS_Ct-.js} +1 -1
- package/dist/dashboard/assets/CombosPage-CXGS_Ct-.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-DGnGNYnM.js +1 -0
- package/dist/dashboard/assets/ConsoleLogPage-DGnGNYnM.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-3CUxMy2X.js → CustomizationPage-DAG5J9EY.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-DAG5J9EY.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-BKtAPYVF.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-BKtAPYVF.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-BdmRLnq2.js +1 -0
- package/dist/dashboard/assets/LoginPage-BdmRLnq2.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-CqMxibJB.js +1 -0
- package/dist/dashboard/assets/OverviewPage-CqMxibJB.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CqMh2NRX.js +4 -0
- package/dist/dashboard/assets/ProviderDetailPage-CqMh2NRX.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-B9uWu4Nz.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-B9uWu4Nz.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-BoWOhpDO.js +9 -0
- package/dist/dashboard/assets/ProxyPage-BoWOhpDO.js.gz +0 -0
- package/dist/dashboard/assets/{QuotaPage-BjldQwIU.js → QuotaPage-7P2n--Fv.js} +1 -1
- package/dist/dashboard/assets/QuotaPage-7P2n--Fv.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-DUIoLRxk.js → SettingsPage-CxZ1fTkN.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-CxZ1fTkN.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-Co12Zzaw.js +1 -0
- package/dist/dashboard/assets/SetupPage-Co12Zzaw.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-fI7JkdIB.js → SortableList-Dqf4zpS-.js} +1 -1
- package/dist/dashboard/assets/SortableList-Dqf4zpS-.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-CHVZqskZ.js → StudioPage-Byj2lAEm.js} +3 -3
- package/dist/dashboard/assets/StudioPage-Byj2lAEm.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-CTkLAuRc.js +56 -0
- package/dist/dashboard/assets/UsagePage-CTkLAuRc.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-BJfsVMvA.js → entry-Bqy8RFvM.js} +1 -1
- package/dist/dashboard/assets/entry-Bqy8RFvM.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CWBY5A0f.js → entry-ClCMCWWQ.js} +3 -3
- package/dist/dashboard/assets/entry-ClCMCWWQ.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-CWWZ-uCZ.js +1 -0
- package/dist/dashboard/assets/formatters-CWWZ-uCZ.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-DFVFrnWW.js} +2 -2
- package/dist/dashboard/assets/index-DFVFrnWW.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-DBNVLdUu.js} +1 -1
- package/dist/dashboard/assets/select-DBNVLdUu.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/cli-tools/contracts.ts +10 -4
- 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 +124 -208
- package/src/network/pool/agent.ts +15 -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 +22 -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/ollamacloud-models.ts +50 -0
- 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-oauth.ts +12 -1
- package/src/providers/integrations/qoder/qoder-realm.ts +20 -0
- package/src/providers/integrations/qoder/qoder-text-tools.ts +277 -0
- package/src/providers/integrations/qoder/qoder.ts +315 -400
- 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 +1684 -0
- package/src/providers/operations/account-health-service.ts +27 -1503
- package/src/providers/operations/account-reset-service.ts +2 -0
- package/src/providers/operations/provider-catalog-service.ts +11 -1
- package/src/providers/provider-contracts.ts +7 -4
- 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/security/cli-client-fingerprint.ts +9 -2
- 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 +9 -2
- 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 +92 -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 +75 -43
- package/src/transport/routing/router.ts +30 -2
- package/src/transport/surface/messages/parse.ts +1 -1
- package/src/transport/surface/responses/encode.ts +2 -3
- package/src/transport/surface/responses/parse.ts +49 -7
- package/src/types/undici.d.ts +33 -0
- 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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Drizzle-backed console persistence for network pools.
|
|
2
|
-
import { and, asc, eq, sql } from "drizzle-orm";
|
|
2
|
+
import { and, asc, eq, inArray, sql } from "drizzle-orm";
|
|
3
3
|
import type { CartethyiaDatabase } from "../../../persistence/db-handle";
|
|
4
4
|
import { networkPools, poolRoutingSettings } from "../../../persistence/schema";
|
|
5
5
|
import {
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
recoverNetworkPool,
|
|
35
35
|
} from "../../../network/pool-health-machine";
|
|
36
36
|
import { drainPoolByteDelta, recordPoolBytes } from "../../../network/pool/byte-accounting";
|
|
37
|
+
import * as poolCooldownStore from "../../../network/pool-cooldown-store";
|
|
37
38
|
// Cloudflare's trace endpoint answers with plain `key=value` lines including
|
|
38
39
|
// `ip=`, the address the request egressed from. Dialing it THROUGH the pool
|
|
39
40
|
// therefore reports the pool's public address — which is what an operator
|
|
@@ -255,7 +256,7 @@ export class DrizzleNetworkPoolStore implements NetworkPoolStore {
|
|
|
255
256
|
reason: error instanceof Error ? error.message : "Failed to build pool agent",
|
|
256
257
|
};
|
|
257
258
|
}
|
|
258
|
-
return await this.dialProviderProbe(agent, poolId, providerId, poolId);
|
|
259
|
+
return await this.dialProviderProbe(agent, poolId, providerId, poolId, rec.endpoint);
|
|
259
260
|
}
|
|
260
261
|
|
|
261
262
|
/**
|
|
@@ -295,13 +296,19 @@ export class DrizzleNetworkPoolStore implements NetworkPoolStore {
|
|
|
295
296
|
*
|
|
296
297
|
* `label` is what the result reports (a pool id, or a pasted endpoint that
|
|
297
298
|
* has no row yet). `poolId` is the row to bank the verdict on — omitted for
|
|
298
|
-
* an ad-hoc probe, because there is nothing to attach it to.
|
|
299
|
+
* an ad-hoc probe, because there is nothing to attach it to. `endpoint` is
|
|
300
|
+
* the address the verdict describes, recorded beside it so
|
|
301
|
+
* `storedProviderProbe` can verify it later — it must be the pool's *current
|
|
302
|
+
* endpoint*, never the label: a verdict banked against the pool id fails the
|
|
303
|
+
* endpoint comparison on read and is hidden as "never probed", which is
|
|
304
|
+
* exactly the disappearing-verdict report.
|
|
299
305
|
*/
|
|
300
306
|
private async dialProviderProbe(
|
|
301
307
|
agent: PoolAgent,
|
|
302
308
|
label: string,
|
|
303
309
|
providerId: string,
|
|
304
310
|
poolId?: string,
|
|
311
|
+
endpoint?: string,
|
|
305
312
|
): Promise<PoolProviderProbeResult> {
|
|
306
313
|
const spec = await buildProviderProbeRequest(providerId);
|
|
307
314
|
if (!spec) {
|
|
@@ -343,7 +350,7 @@ export class DrizzleNetworkPoolStore implements NetworkPoolStore {
|
|
|
343
350
|
latencyMs,
|
|
344
351
|
...(verdict.reason === undefined ? {} : { reason: verdict.reason }),
|
|
345
352
|
};
|
|
346
|
-
if (poolId !== undefined) await this.persistProviderProbe(poolId, label, result);
|
|
353
|
+
if (poolId !== undefined) await this.persistProviderProbe(poolId, endpoint ?? label, result);
|
|
347
354
|
return result;
|
|
348
355
|
}
|
|
349
356
|
|
|
@@ -403,12 +410,24 @@ export class DrizzleNetworkPoolStore implements NetworkPoolStore {
|
|
|
403
410
|
}
|
|
404
411
|
|
|
405
412
|
async create(record: NetworkPoolRecord): Promise<void> {
|
|
406
|
-
|
|
407
|
-
|
|
413
|
+
await this.createMany([record]).then(() => undefined);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Inserts every record in one statement.
|
|
418
|
+
*
|
|
419
|
+
* A batch import used to be one round trip per proxy, so a long paste took
|
|
420
|
+
* minutes and the dialog sat there while the operator waited. Drizzle emits a
|
|
421
|
+
* single multi-row INSERT, which makes the cost independent of the paste
|
|
422
|
+
* length.
|
|
423
|
+
*/
|
|
424
|
+
async createMany(records: readonly NetworkPoolRecord[]): Promise<number> {
|
|
425
|
+
if (records.length === 0) return 0;
|
|
426
|
+
const values = records.map((record) => ({
|
|
408
427
|
id: record.id,
|
|
409
428
|
tenantId: record.tenantId,
|
|
410
429
|
createdAt: new Date(record.createdAt),
|
|
411
|
-
kind,
|
|
430
|
+
kind: record.kind === "https" ? "http" : record.kind,
|
|
412
431
|
endpointConfig: {
|
|
413
432
|
endpoint: record.endpoint,
|
|
414
433
|
...(record.label ? { label: record.label } : {}),
|
|
@@ -423,7 +442,50 @@ export class DrizzleNetworkPoolStore implements NetworkPoolStore {
|
|
|
423
442
|
consecutiveFailures: 0,
|
|
424
443
|
...(record.quotaBytes !== undefined ? { quotaBytes: record.quotaBytes } : {}),
|
|
425
444
|
...(record.family !== undefined ? { family: record.family } : {}),
|
|
426
|
-
});
|
|
445
|
+
}));
|
|
446
|
+
await this.db.insert(networkPools).values(values);
|
|
447
|
+
return values.length;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Sets one status on every named pool in a single statement.
|
|
452
|
+
*
|
|
453
|
+
* Bulk enable/disable was a loop of per-row updates — one round trip each,
|
|
454
|
+
* and one snapshot invalidation each. A tenant-wide selection is exactly the
|
|
455
|
+
* case where that is felt.
|
|
456
|
+
*/
|
|
457
|
+
async updateManyStatus(
|
|
458
|
+
tenantId: string,
|
|
459
|
+
poolIds: readonly string[],
|
|
460
|
+
status: NetworkPoolRecord["status"],
|
|
461
|
+
): Promise<number> {
|
|
462
|
+
if (poolIds.length === 0) return 0;
|
|
463
|
+
const rows = await this.db
|
|
464
|
+
.update(networkPools)
|
|
465
|
+
.set({ status })
|
|
466
|
+
.where(
|
|
467
|
+
and(
|
|
468
|
+
eq(networkPools.tenantId, tenantId),
|
|
469
|
+
inArray(networkPools.id, [...poolIds]),
|
|
470
|
+
),
|
|
471
|
+
)
|
|
472
|
+
.returning({ id: networkPools.id });
|
|
473
|
+
return rows.length;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** Deletes every named pool in one statement. */
|
|
477
|
+
async deleteMany(tenantId: string, poolIds: readonly string[]): Promise<number> {
|
|
478
|
+
if (poolIds.length === 0) return 0;
|
|
479
|
+
const rows = await this.db
|
|
480
|
+
.delete(networkPools)
|
|
481
|
+
.where(
|
|
482
|
+
and(
|
|
483
|
+
eq(networkPools.tenantId, tenantId),
|
|
484
|
+
inArray(networkPools.id, [...poolIds]),
|
|
485
|
+
),
|
|
486
|
+
)
|
|
487
|
+
.returning({ id: networkPools.id });
|
|
488
|
+
return rows.length;
|
|
427
489
|
}
|
|
428
490
|
|
|
429
491
|
async update(
|
|
@@ -513,6 +575,34 @@ export class DrizzleNetworkPoolStore implements NetworkPoolStore {
|
|
|
513
575
|
return listNetworkPoolHealthEvents(this.db, tenantId, poolId);
|
|
514
576
|
}
|
|
515
577
|
|
|
578
|
+
async dropProviderCooldownMarker(
|
|
579
|
+
tenantId: string,
|
|
580
|
+
poolId: string,
|
|
581
|
+
providerId: string,
|
|
582
|
+
onlyBlocked: boolean,
|
|
583
|
+
): Promise<void> {
|
|
584
|
+
if ((await this.get(tenantId, poolId)) === undefined) return;
|
|
585
|
+
await poolCooldownStore.dropProviderCooldownMarker(this.db, poolId, providerId, onlyBlocked);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
async dropAllProviderCooldownMarkers(
|
|
589
|
+
tenantId: string,
|
|
590
|
+
poolId: string,
|
|
591
|
+
): Promise<void> {
|
|
592
|
+
if ((await this.get(tenantId, poolId)) === undefined) return;
|
|
593
|
+
await poolCooldownStore.dropAllProviderCooldownMarkers(this.db, poolId);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
async recordProviderProbeCooldown(
|
|
597
|
+
tenantId: string,
|
|
598
|
+
poolId: string,
|
|
599
|
+
providerId: string,
|
|
600
|
+
marker: { until: number; reason: string; blocked?: boolean },
|
|
601
|
+
): Promise<void> {
|
|
602
|
+
if ((await this.get(tenantId, poolId)) === undefined) return;
|
|
603
|
+
await poolCooldownStore.recordProbeVerdict(this.db, poolId, providerId, marker);
|
|
604
|
+
}
|
|
605
|
+
|
|
516
606
|
async recover(tenantId: string, poolId: string): Promise<boolean> {
|
|
517
607
|
return recoverNetworkPool(this.db, tenantId, poolId);
|
|
518
608
|
}
|
package/src/main.ts
CHANGED
|
@@ -1,167 +1,183 @@
|
|
|
1
|
-
// Must run before any module that registers a route: it wires Elysia's TypeBox
|
|
2
|
-
// bridge statically, which the source-run path needs under NODE_ENV=production.
|
|
3
|
-
import "./boot/elysia-typebox-bridge";
|
|
4
|
-
import { createGatewayApp, createGatewayShell } from "./boot/app";
|
|
5
|
-
import { bootstrap } from "./boot/lifecycle";
|
|
6
|
-
import type { CartethyiaBoot } from "./boot/lifecycle";
|
|
7
|
-
import {
|
|
8
|
-
resolveDrainToken,
|
|
9
|
-
resolveIdleTimeout,
|
|
10
|
-
resolveMaxBodyBytes,
|
|
11
|
-
resolvePort,
|
|
12
|
-
resolveProcessTopology,
|
|
13
|
-
} from "./config/runtime";
|
|
14
|
-
import { Manifest } from "elysia";
|
|
15
|
-
import { log } from "./observability/logger";
|
|
16
|
-
|
|
17
|
-
const port = resolvePort();
|
|
18
|
-
const entryMemory = process.memoryUsage();
|
|
19
|
-
log.info(
|
|
20
|
-
`[startup] phase=entrypoint-ready elapsed_ms=${Math.round(process.uptime() * 1_000)} ` +
|
|
21
|
-
`rss_mb=${(entryMemory.rss / 1024 / 1024).toFixed(1)} ` +
|
|
22
|
-
`heap_used_mb=${(entryMemory.heapUsed / 1024 / 1024).toFixed(1)}`,
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
declare global {
|
|
26
|
-
// eslint-disable-next-line no-var -- globalThis augmentation requires `var`
|
|
27
|
-
var __cartethyiaBoot: CartethyiaBoot | undefined;
|
|
28
|
-
// eslint-disable-next-line no-var
|
|
29
|
-
var __cartethyiaSignalsRegistered: boolean | undefined;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const boot = Manifest.isCapturing()
|
|
33
|
-
? undefined
|
|
34
|
-
: (globalThis.__cartethyiaBoot ??= await bootstrap());
|
|
35
|
-
|
|
36
|
-
const app = boot
|
|
37
|
-
? createGatewayApp({
|
|
38
|
-
mode: "production",
|
|
39
|
-
db: boot.deps.db,
|
|
40
|
-
proxyPreparer: boot.deps.proxyPreparer,
|
|
41
|
-
resolveProviderAdapter: boot.deps.resolveProviderAdapter,
|
|
42
|
-
byokUpstreamHosts: boot.deps.byokUpstreamHosts,
|
|
43
|
-
networkBindingFactory: boot.deps.networkBindingFactory,
|
|
44
|
-
ipAbuseProtection: boot.deps.ipAbuseProtection,
|
|
45
|
-
trustedProxyBoundary: boot.deps.trustedProxyBoundary,
|
|
46
|
-
poolSelector: boot.deps.poolSelector,
|
|
47
|
-
snapshotService: boot.deps.snapshotService,
|
|
48
|
-
readiness: boot.deps.readiness,
|
|
49
|
-
telemetryBuffer: boot.deps.telemetryBuffer,
|
|
50
|
-
resolveOAuthRefresher: boot.deps.resolveOAuthRefresher,
|
|
51
|
-
oauthRefreshService: boot.deps.oauthRefreshService,
|
|
52
|
-
maxBodyBytes: resolveMaxBodyBytes(),
|
|
53
|
-
scheduledTasks: boot.deps.scheduledTasks,
|
|
54
|
-
// The sampler is constructed in the dependency graph and already
|
|
55
|
-
// sampling on its own interval; without passing it here the app cannot
|
|
56
|
-
// bind its in-flight source, so `cartethyia_process_in_flight` stays
|
|
57
|
-
// pinned at the constructor's `() => 0` placeholder forever while the
|
|
58
|
-
// registry it was meant to read reports real flights.
|
|
59
|
-
runtimeMetricsSampler: boot.deps.runtimeMetricsSampler,
|
|
60
|
-
shutdownCoordinator: boot.shutdownCoordinator,
|
|
61
|
-
// Signal-free graceful stop for platforms where a catchable signal cannot
|
|
62
|
-
// be delivered (Windows). Off unless `CARTETHYIA_DRAIN_TOKEN` is set.
|
|
63
|
-
...(resolveDrainToken() !== undefined
|
|
64
|
-
? { drainToken: resolveDrainToken() as string, triggerDrain: () => shutdown("SIGTERM") }
|
|
65
|
-
: {}),
|
|
66
|
-
// The console mounts in every boot: without REDIS_URL it runs on the
|
|
67
|
-
// in-process memory backend like the rest of the gateway.
|
|
68
|
-
consoleApi: {
|
|
69
|
-
db: boot.deps.db,
|
|
70
|
-
accessResolver: () => undefined,
|
|
71
|
-
routeSnapshotService: boot.deps.snapshotService,
|
|
72
|
-
poolSelector: boot.deps.poolSelector,
|
|
73
|
-
telemetryBuffer: boot.deps.telemetryBuffer,
|
|
74
|
-
providerRegistry: boot.deps.providerRegistry,
|
|
75
|
-
bundledModelCatalog: boot.deps.bundledModelCatalog,
|
|
76
|
-
networkBindingFactory: boot.deps.networkBindingFactory,
|
|
77
|
-
redis: boot.deps.redis,
|
|
78
|
-
oauthRefreshService: boot.deps.oauthRefreshService,
|
|
79
|
-
admissionService: boot.deps.admissionService,
|
|
80
|
-
readRoutingAccountInflight: boot.deps.readRoutingAccountInflight,
|
|
81
|
-
// The same three writers the shutdown coordinator drains, so a manual
|
|
82
|
-
// flush from the console does exactly what a graceful stop would do
|
|
83
|
-
// for the queues, minus the stop.
|
|
84
|
-
flushBuffers: {
|
|
85
|
-
flush: async () => {
|
|
86
|
-
await boot.deps.telemetryBuffer.flush();
|
|
87
|
-
await boot.deps.usageFlush.flush();
|
|
88
|
-
await boot.deps.healthEventFlush.flush();
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
},
|
|
92
|
-
})
|
|
93
|
-
: createGatewayShell();
|
|
94
|
-
export { app };
|
|
95
|
-
|
|
96
|
-
function shutdown(signal: "SIGINT" | "SIGTERM", reason: "SIGINT" | "SIGTERM" | "update" = signal): void {
|
|
97
|
-
if (!boot) return;
|
|
98
|
-
log.info(`[shutdown] ${signal} received, draining...`);
|
|
99
|
-
// Derived from the coordinator's own budget, not a hand-kept literal: a
|
|
100
|
-
// force-exit shorter than the drain would hard-kill a process that was about
|
|
101
|
-
// to finish gracefully, truncating the in-flight responses the drain exists
|
|
102
|
-
// to protect.
|
|
103
|
-
const forceExit = setTimeout(() => {
|
|
104
|
-
log.error("[shutdown] forced exit after drain budget elapsed");
|
|
105
|
-
process.exit(1);
|
|
106
|
-
}, boot.shutdownCoordinator.totalShutdownBudgetMs());
|
|
107
|
-
forceExit.unref();
|
|
108
|
-
boot.shutdownCoordinator
|
|
109
|
-
.begin(reason)
|
|
110
|
-
.then(() => {
|
|
111
|
-
log.info("[shutdown] complete");
|
|
112
|
-
process.exit(0);
|
|
113
|
-
})
|
|
114
|
-
.catch((error: unknown) => {
|
|
115
|
-
log.error("[shutdown] failed", error as Error);
|
|
116
|
-
process.exit(1);
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (boot) {
|
|
121
|
-
boot
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
1
|
+
// Must run before any module that registers a route: it wires Elysia's TypeBox
|
|
2
|
+
// bridge statically, which the source-run path needs under NODE_ENV=production.
|
|
3
|
+
import "./boot/elysia-typebox-bridge";
|
|
4
|
+
import { createGatewayApp, createGatewayShell } from "./boot/app";
|
|
5
|
+
import { bootstrap } from "./boot/lifecycle";
|
|
6
|
+
import type { CartethyiaBoot } from "./boot/lifecycle";
|
|
7
|
+
import {
|
|
8
|
+
resolveDrainToken,
|
|
9
|
+
resolveIdleTimeout,
|
|
10
|
+
resolveMaxBodyBytes,
|
|
11
|
+
resolvePort,
|
|
12
|
+
resolveProcessTopology,
|
|
13
|
+
} from "./config/runtime";
|
|
14
|
+
import { Manifest } from "elysia";
|
|
15
|
+
import { log } from "./observability/logger";
|
|
16
|
+
|
|
17
|
+
const port = resolvePort();
|
|
18
|
+
const entryMemory = process.memoryUsage();
|
|
19
|
+
log.info(
|
|
20
|
+
`[startup] phase=entrypoint-ready elapsed_ms=${Math.round(process.uptime() * 1_000)} ` +
|
|
21
|
+
`rss_mb=${(entryMemory.rss / 1024 / 1024).toFixed(1)} ` +
|
|
22
|
+
`heap_used_mb=${(entryMemory.heapUsed / 1024 / 1024).toFixed(1)}`,
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
declare global {
|
|
26
|
+
// eslint-disable-next-line no-var -- globalThis augmentation requires `var`
|
|
27
|
+
var __cartethyiaBoot: CartethyiaBoot | undefined;
|
|
28
|
+
// eslint-disable-next-line no-var
|
|
29
|
+
var __cartethyiaSignalsRegistered: boolean | undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const boot = Manifest.isCapturing()
|
|
33
|
+
? undefined
|
|
34
|
+
: (globalThis.__cartethyiaBoot ??= await bootstrap());
|
|
35
|
+
|
|
36
|
+
const app = boot
|
|
37
|
+
? createGatewayApp({
|
|
38
|
+
mode: "production",
|
|
39
|
+
db: boot.deps.db,
|
|
40
|
+
proxyPreparer: boot.deps.proxyPreparer,
|
|
41
|
+
resolveProviderAdapter: boot.deps.resolveProviderAdapter,
|
|
42
|
+
byokUpstreamHosts: boot.deps.byokUpstreamHosts,
|
|
43
|
+
networkBindingFactory: boot.deps.networkBindingFactory,
|
|
44
|
+
ipAbuseProtection: boot.deps.ipAbuseProtection,
|
|
45
|
+
trustedProxyBoundary: boot.deps.trustedProxyBoundary,
|
|
46
|
+
poolSelector: boot.deps.poolSelector,
|
|
47
|
+
snapshotService: boot.deps.snapshotService,
|
|
48
|
+
readiness: boot.deps.readiness,
|
|
49
|
+
telemetryBuffer: boot.deps.telemetryBuffer,
|
|
50
|
+
resolveOAuthRefresher: boot.deps.resolveOAuthRefresher,
|
|
51
|
+
oauthRefreshService: boot.deps.oauthRefreshService,
|
|
52
|
+
maxBodyBytes: resolveMaxBodyBytes(),
|
|
53
|
+
scheduledTasks: boot.deps.scheduledTasks,
|
|
54
|
+
// The sampler is constructed in the dependency graph and already
|
|
55
|
+
// sampling on its own interval; without passing it here the app cannot
|
|
56
|
+
// bind its in-flight source, so `cartethyia_process_in_flight` stays
|
|
57
|
+
// pinned at the constructor's `() => 0` placeholder forever while the
|
|
58
|
+
// registry it was meant to read reports real flights.
|
|
59
|
+
runtimeMetricsSampler: boot.deps.runtimeMetricsSampler,
|
|
60
|
+
shutdownCoordinator: boot.shutdownCoordinator,
|
|
61
|
+
// Signal-free graceful stop for platforms where a catchable signal cannot
|
|
62
|
+
// be delivered (Windows). Off unless `CARTETHYIA_DRAIN_TOKEN` is set.
|
|
63
|
+
...(resolveDrainToken() !== undefined
|
|
64
|
+
? { drainToken: resolveDrainToken() as string, triggerDrain: () => shutdown("SIGTERM") }
|
|
65
|
+
: {}),
|
|
66
|
+
// The console mounts in every boot: without REDIS_URL it runs on the
|
|
67
|
+
// in-process memory backend like the rest of the gateway.
|
|
68
|
+
consoleApi: {
|
|
69
|
+
db: boot.deps.db,
|
|
70
|
+
accessResolver: () => undefined,
|
|
71
|
+
routeSnapshotService: boot.deps.snapshotService,
|
|
72
|
+
poolSelector: boot.deps.poolSelector,
|
|
73
|
+
telemetryBuffer: boot.deps.telemetryBuffer,
|
|
74
|
+
providerRegistry: boot.deps.providerRegistry,
|
|
75
|
+
bundledModelCatalog: boot.deps.bundledModelCatalog,
|
|
76
|
+
networkBindingFactory: boot.deps.networkBindingFactory,
|
|
77
|
+
redis: boot.deps.redis,
|
|
78
|
+
oauthRefreshService: boot.deps.oauthRefreshService,
|
|
79
|
+
admissionService: boot.deps.admissionService,
|
|
80
|
+
readRoutingAccountInflight: boot.deps.readRoutingAccountInflight,
|
|
81
|
+
// The same three writers the shutdown coordinator drains, so a manual
|
|
82
|
+
// flush from the console does exactly what a graceful stop would do
|
|
83
|
+
// for the queues, minus the stop.
|
|
84
|
+
flushBuffers: {
|
|
85
|
+
flush: async () => {
|
|
86
|
+
await boot.deps.telemetryBuffer.flush();
|
|
87
|
+
await boot.deps.usageFlush.flush();
|
|
88
|
+
await boot.deps.healthEventFlush.flush();
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
: createGatewayShell();
|
|
94
|
+
export { app };
|
|
95
|
+
|
|
96
|
+
function shutdown(signal: "SIGINT" | "SIGTERM", reason: "SIGINT" | "SIGTERM" | "update" = signal): void {
|
|
97
|
+
if (!boot) return;
|
|
98
|
+
log.info(`[shutdown] ${signal} received, draining...`);
|
|
99
|
+
// Derived from the coordinator's own budget, not a hand-kept literal: a
|
|
100
|
+
// force-exit shorter than the drain would hard-kill a process that was about
|
|
101
|
+
// to finish gracefully, truncating the in-flight responses the drain exists
|
|
102
|
+
// to protect.
|
|
103
|
+
const forceExit = setTimeout(() => {
|
|
104
|
+
log.error("[shutdown] forced exit after drain budget elapsed");
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}, boot.shutdownCoordinator.totalShutdownBudgetMs());
|
|
107
|
+
forceExit.unref();
|
|
108
|
+
boot.shutdownCoordinator
|
|
109
|
+
.begin(reason)
|
|
110
|
+
.then(() => {
|
|
111
|
+
log.info("[shutdown] complete");
|
|
112
|
+
process.exit(0);
|
|
113
|
+
})
|
|
114
|
+
.catch((error: unknown) => {
|
|
115
|
+
log.error("[shutdown] failed", error as Error);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (boot) {
|
|
121
|
+
// `bun --hot` re-runs this module against the cached boot object; stop the
|
|
122
|
+
// previous listener before rebinding so a reload does not stack two servers
|
|
123
|
+
// on the same port.
|
|
124
|
+
if (boot.server && typeof boot.server.stop === "function") {
|
|
125
|
+
try {
|
|
126
|
+
boot.server.stop();
|
|
127
|
+
} catch {
|
|
128
|
+
// Best-effort on reload; the bind below would fail loudly if it stuck.
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
boot.server = app.listen(
|
|
132
|
+
{
|
|
133
|
+
port,
|
|
134
|
+
// Cap idle sockets so a Slowloris/keep-alive-abuse client cannot
|
|
135
|
+
// pin one of the server's `maxRequestBodySize`-budgeted sockets
|
|
136
|
+
// indefinitely. 60 s aligns with the DB pool idle timeout so all
|
|
137
|
+
// long-idle resources reap together.
|
|
138
|
+
idleTimeout: resolveIdleTimeout(),
|
|
139
|
+
// SO_REUSEPORT load-balancing, so several processes can share the port
|
|
140
|
+
// without a reverse proxy. Tied to the declared topology rather than
|
|
141
|
+
// left always-on: with one process declared, enabling it lets an
|
|
142
|
+
// unrelated process on the same port silently take half the traffic —
|
|
143
|
+
// which is worse than a bind error, because nothing reports it.
|
|
144
|
+
reusePort: resolveProcessTopology().multiProcess,
|
|
145
|
+
// 8 MiB cap on request bodies keeps a rogue client from allocating
|
|
146
|
+
// gigabytes of memory in the ingress path before validation rejects.
|
|
147
|
+
maxRequestBodySize: resolveMaxBodyBytes(),
|
|
148
|
+
},
|
|
149
|
+
() => {
|
|
150
|
+
log.info(`Cartethyia listening on :${port} (Bun ${Bun.version})`);
|
|
151
|
+
const memory = process.memoryUsage();
|
|
152
|
+
log.info(
|
|
153
|
+
`[startup] phase=listener-ready rss_mb=${(memory.rss / 1024 / 1024).toFixed(1)} ` +
|
|
154
|
+
`heap_used_mb=${(memory.heapUsed / 1024 / 1024).toFixed(1)}`,
|
|
155
|
+
);
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
// Started here rather than inside the dependency builder: the first tick of
|
|
159
|
+
// the lease sweep and the health sweep should not run before the listener
|
|
160
|
+
// exists to serve traffic.
|
|
161
|
+
boot.deps.scheduledTasks.start();
|
|
162
|
+
// Warm the route snapshot before the first request lands: the first /v1/*
|
|
163
|
+
// request otherwise pays the full catalog build synchronously (ten+ SELECTs).
|
|
164
|
+
// A failure here is a soft miss — the next request rebuilds as before.
|
|
165
|
+
void boot.deps.snapshotService.getSnapshot().catch((error: unknown) => {
|
|
166
|
+
log.warn("[startup] initial route snapshot warm failed", error as Error);
|
|
167
|
+
});
|
|
168
|
+
if (!globalThis.__cartethyiaSignalsRegistered) {
|
|
169
|
+
globalThis.__cartethyiaSignalsRegistered = true;
|
|
170
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
171
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
172
|
+
// An in-place update signals SIGUSR2 before swapping the image so the old
|
|
173
|
+
// process drains with the `update` reason and its callers are told the
|
|
174
|
+
// replacement is seconds away instead of seeing a generic shutdown. Not
|
|
175
|
+
// available on Windows, where the listener is simply never registered —
|
|
176
|
+
// production runs in Linux containers, which is where an update happens.
|
|
177
|
+
try {
|
|
178
|
+
process.on("SIGUSR2", () => shutdown("SIGTERM", "update"));
|
|
179
|
+
} catch {
|
|
180
|
+
// Platform without SIGUSR2: update drains fall back to the SIGTERM notice.
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|