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
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The durable mirror of the pool selector's provider cooldown state.
|
|
3
|
+
*
|
|
4
|
+
* The selector's own map is process-local, mirrored to Redis only when Redis
|
|
5
|
+
* exists — so the usual installation ran with the flags surviving just a
|
|
6
|
+
* restart, while the upstream's refusal of the egress did not. The result was
|
|
7
|
+
* the report that motivated this: no cooldown shown on the proxy page, and
|
|
8
|
+
* still-429 requests, because dispatch re-picked a spent egress it no longer
|
|
9
|
+
* remembered. `network_pools.provider_cooldowns` is that state's home between
|
|
10
|
+
* boots: dispatch flaggers write each marker, boot reads them all back.
|
|
11
|
+
*
|
|
12
|
+
* One entry per cooling provider, keyed by the lowercased provider id — the
|
|
13
|
+
* same normalization the selector applies to its in-memory key.
|
|
14
|
+
*/
|
|
15
|
+
import { randomUUID } from "node:crypto";
|
|
16
|
+
import { eq } from "drizzle-orm";
|
|
17
|
+
import type { CartethyiaDatabase } from "../persistence/db-handle";
|
|
18
|
+
import { networkPools, healthEvents } from "../persistence/schema";
|
|
19
|
+
import { log } from "../observability/logger";
|
|
20
|
+
import { providerRateLimitIsIpScoped } from "../providers/provider-metadata";
|
|
21
|
+
import { PROVIDER_BLOCK_MS, type ProxyCooldownEntry } from "./pool/selector";
|
|
22
|
+
|
|
23
|
+
type PoolProviderCooldownMap = NonNullable<
|
|
24
|
+
(typeof networkPools.$inferSelect)["providerCooldowns"]
|
|
25
|
+
>;
|
|
26
|
+
|
|
27
|
+
function readMarkerMap(
|
|
28
|
+
db: CartethyiaDatabase,
|
|
29
|
+
poolId: string,
|
|
30
|
+
): Promise<PoolProviderCooldownMap> {
|
|
31
|
+
return db
|
|
32
|
+
.select({ providerCooldowns: networkPools.providerCooldowns })
|
|
33
|
+
.from(networkPools)
|
|
34
|
+
.where(eq(networkPools.id, poolId))
|
|
35
|
+
.limit(1)
|
|
36
|
+
.then((rows) => rows[0]?.providerCooldowns ?? {});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function writeMarkerMap(
|
|
40
|
+
db: CartethyiaDatabase,
|
|
41
|
+
poolId: string,
|
|
42
|
+
map: PoolProviderCooldownMap,
|
|
43
|
+
): Promise<unknown> {
|
|
44
|
+
return db.update(networkPools).set({ providerCooldowns: map }).where(eq(networkPools.id, poolId));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function entryOf(marker: Omit<ProxyCooldownEntry, "poolId" | "providerId">): {
|
|
48
|
+
until: number;
|
|
49
|
+
reason: string;
|
|
50
|
+
blocked?: boolean;
|
|
51
|
+
} {
|
|
52
|
+
return marker.blocked === true
|
|
53
|
+
? { until: marker.until, reason: marker.reason, blocked: true }
|
|
54
|
+
: { until: marker.until, reason: marker.reason };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Writes up (or replaces) one provider's marker on the pool row.
|
|
59
|
+
*
|
|
60
|
+
* Fill-in, not replace: an entry for another cooling provider must survive.
|
|
61
|
+
* Failures are logged and swallowed — the in-memory/Redis state stays
|
|
62
|
+
* conservative for the running process, and only the *next* restart would miss
|
|
63
|
+
* a flag this write failed to land.
|
|
64
|
+
*/
|
|
65
|
+
export async function upsertProviderCooldownMarker(
|
|
66
|
+
db: CartethyiaDatabase,
|
|
67
|
+
poolId: string,
|
|
68
|
+
providerId: string,
|
|
69
|
+
marker: { until: number; reason: string; blocked?: boolean },
|
|
70
|
+
): Promise<void> {
|
|
71
|
+
try {
|
|
72
|
+
const key = providerId.toLowerCase();
|
|
73
|
+
const map = await readMarkerMap(db, poolId);
|
|
74
|
+
map[key] = entryOf(marker);
|
|
75
|
+
await writeMarkerMap(db, poolId, map);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
log.warn("pool provider cooldown mirror write failed", { networkPoolId: poolId, providerId, error: String(error) });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Removes one provider's marker, optionally only when it is a *block*.
|
|
83
|
+
*
|
|
84
|
+
* `onlyBlocked` mirrors `clearProviderBlockedOnly`: a probe that answered 429
|
|
85
|
+
* may correct a retired claim but must not release a live cooldown, and the
|
|
86
|
+
* same rule applies to the durable copy.
|
|
87
|
+
*/
|
|
88
|
+
export async function dropProviderCooldownMarker(
|
|
89
|
+
db: CartethyiaDatabase,
|
|
90
|
+
poolId: string,
|
|
91
|
+
providerId: string,
|
|
92
|
+
onlyBlocked = false,
|
|
93
|
+
): Promise<void> {
|
|
94
|
+
try {
|
|
95
|
+
const key = providerId.toLowerCase();
|
|
96
|
+
const map = await readMarkerMap(db, poolId);
|
|
97
|
+
const current = map[key];
|
|
98
|
+
if (current === undefined) return;
|
|
99
|
+
if (onlyBlocked && current.blocked !== true) return;
|
|
100
|
+
delete map[key];
|
|
101
|
+
await writeMarkerMap(db, poolId, map);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
log.warn("pool provider cooldown mirror clear failed", { networkPoolId: poolId, providerId, error: String(error) });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Removes every provider's marker from one pool row (disable/replace paths).
|
|
109
|
+
*/
|
|
110
|
+
export async function dropAllProviderCooldownMarkers(
|
|
111
|
+
db: CartethyiaDatabase,
|
|
112
|
+
poolId: string,
|
|
113
|
+
): Promise<void> {
|
|
114
|
+
try {
|
|
115
|
+
const map = await readMarkerMap(db, poolId);
|
|
116
|
+
if (Object.keys(map).length === 0) return;
|
|
117
|
+
await writeMarkerMap(db, poolId, {});
|
|
118
|
+
} catch (error) {
|
|
119
|
+
log.warn("pool provider cooldown mirror purge failed", { networkPoolId: poolId, error: String(error) });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Writes a probe-measured verdict the same way a dispatch-side event would be
|
|
125
|
+
* recorded: one mirror marker and one audit row.
|
|
126
|
+
*
|
|
127
|
+
* The probe is the only writer that can flag a cooldown from the control
|
|
128
|
+
* plane — its 429 is a fresh measurement of "this egress is out of allowance"
|
|
129
|
+
* (a cooldown marker), and its 403 is a fresh measurement of "this egress was
|
|
130
|
+
* retired" (a block marker). Without this, an operator who tested a
|
|
131
|
+
* rate-limited or retired pool saw the verdict once, refreshing lost it, and
|
|
132
|
+
* the activity panel showed nothing: the fact existed only in the remembered
|
|
133
|
+
* HTTP response.
|
|
134
|
+
*
|
|
135
|
+
* Not coalesced: a probe is operator-initiated and one row per click is the
|
|
136
|
+
* visible history the panel exists for.
|
|
137
|
+
*/
|
|
138
|
+
export async function recordProbeVerdict(
|
|
139
|
+
db: CartethyiaDatabase,
|
|
140
|
+
poolId: string,
|
|
141
|
+
providerId: string,
|
|
142
|
+
marker: { until: number; reason: string; blocked?: boolean },
|
|
143
|
+
): Promise<void> {
|
|
144
|
+
await upsertProviderCooldownMarker(db, poolId, providerId, marker);
|
|
145
|
+
try {
|
|
146
|
+
await db.insert(healthEvents).values({
|
|
147
|
+
id: randomUUID(),
|
|
148
|
+
entityKind: "pool",
|
|
149
|
+
networkPoolId: poolId,
|
|
150
|
+
fromStatus: null,
|
|
151
|
+
toStatus: "cooldown",
|
|
152
|
+
reason: `provider ${providerId}: ${marker.reason}`,
|
|
153
|
+
errorCategory: marker.blocked === true ? "egress_blocked" : "rate_limit_transient",
|
|
154
|
+
createdAt: new Date(),
|
|
155
|
+
});
|
|
156
|
+
} catch (error) {
|
|
157
|
+
log.warn("pool probe event failed", { networkPoolId: poolId, providerId, error: String(error) });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Reads every live marker back into the selector at boot.
|
|
163
|
+
*
|
|
164
|
+
* Only future-dated entries are restored: an expired marker describes an
|
|
165
|
+
* allowance that refilled while the process was down, and resurrecting it
|
|
166
|
+
* would sideline a working address. Repeat 429s re-insert markers, so nothing
|
|
167
|
+
* is lost by dropping the expired ones.
|
|
168
|
+
*/
|
|
169
|
+
export async function restoreProviderCooldownMarkers(
|
|
170
|
+
selector: { hydrateProviderCooldown(entry: ProxyCooldownEntry): void },
|
|
171
|
+
db: CartethyiaDatabase,
|
|
172
|
+
): Promise<number> {
|
|
173
|
+
const rows = await db
|
|
174
|
+
.select({ id: networkPools.id, providerCooldowns: networkPools.providerCooldowns })
|
|
175
|
+
.from(networkPools);
|
|
176
|
+
let restored = 0;
|
|
177
|
+
for (const row of rows) {
|
|
178
|
+
const map = row.providerCooldowns;
|
|
179
|
+
if (map === null || map === undefined) continue;
|
|
180
|
+
for (const [providerId, marker] of Object.entries(map)) {
|
|
181
|
+
if (Date.now() >= marker.until) continue;
|
|
182
|
+
selector.hydrateProviderCooldown({
|
|
183
|
+
poolId: row.id,
|
|
184
|
+
providerId,
|
|
185
|
+
until: marker.until,
|
|
186
|
+
reason: marker.reason,
|
|
187
|
+
...(marker.blocked === true ? { blocked: true } : {}),
|
|
188
|
+
});
|
|
189
|
+
restored += 1;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return restored;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** The columns the probe-verdict reconciliation reads, mirroring the row's own read path. */
|
|
196
|
+
function probeEndpointOf(endpointConfig: unknown): string {
|
|
197
|
+
const raw = (endpointConfig ?? {}) as Record<string, unknown>;
|
|
198
|
+
return typeof raw["endpoint"] === "string" ? raw["endpoint"] : "";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Re-enforces the stored probe verdicts at boot (new: replay, read-ahead).
|
|
203
|
+
*
|
|
204
|
+
* A stored `blocked` verdict is a *state*, not a decoration. If the marker the
|
|
205
|
+
* probe flagged failed to persist, or the verdict predated the marker column,
|
|
206
|
+
* the egress would return to rotation after restart and get refused again —
|
|
207
|
+
* dispatch spending a real request to rediscover a refusal the row already
|
|
208
|
+
* knew. So the boot read reconciles: a fresh (endpoint still matches) `blocked`
|
|
209
|
+
* verdict for an address-keyed provider becomes a block flag, even when the
|
|
210
|
+
* mirror has nothing (or has an older, cooler entry — the retirement claim is
|
|
211
|
+
* the sharper fact and wins).
|
|
212
|
+
*
|
|
213
|
+
* `throttled` is deliberately **not** replayed into a cooldown: the cooldown
|
|
214
|
+
* the probe flagged already carries the measured window, and blind-replaying
|
|
215
|
+
* one from a verdict that predates the restart could outlive the upstream's
|
|
216
|
+
* actual window.
|
|
217
|
+
*
|
|
218
|
+
* Returns how many verdicts were replayed.
|
|
219
|
+
*/
|
|
220
|
+
export async function restorePoolProbeVerdicts(
|
|
221
|
+
selector: { hydrateProviderCooldown(entry: ProxyCooldownEntry): void },
|
|
222
|
+
db: CartethyiaDatabase,
|
|
223
|
+
): Promise<number> {
|
|
224
|
+
const rows = await db
|
|
225
|
+
.select({
|
|
226
|
+
id: networkPools.id,
|
|
227
|
+
status: networkPools.status,
|
|
228
|
+
endpointConfig: networkPools.endpointConfig,
|
|
229
|
+
providerProbeProviderId: networkPools.providerProbeProviderId,
|
|
230
|
+
providerProbeStatus: networkPools.providerProbeStatus,
|
|
231
|
+
providerProbeReason: networkPools.providerProbeReason,
|
|
232
|
+
providerProbeEndpoint: networkPools.providerProbeEndpoint,
|
|
233
|
+
})
|
|
234
|
+
.from(networkPools);
|
|
235
|
+
let replayed = 0;
|
|
236
|
+
for (const row of rows) {
|
|
237
|
+
if (row.status !== "active") continue;
|
|
238
|
+
if (row.providerProbeStatus !== "blocked") continue;
|
|
239
|
+
if (row.providerProbeEndpoint !== probeEndpointOf(row.endpointConfig)) continue;
|
|
240
|
+
const providerId = row.providerProbeProviderId;
|
|
241
|
+
if (typeof providerId !== "string" || !providerRateLimitIsIpScoped(providerId)) continue;
|
|
242
|
+
selector.hydrateProviderCooldown({
|
|
243
|
+
poolId: row.id,
|
|
244
|
+
providerId: providerId.toLowerCase(),
|
|
245
|
+
until: Date.now() + PROVIDER_BLOCK_MS,
|
|
246
|
+
reason: row.providerProbeReason ?? "Upstream refused this egress address",
|
|
247
|
+
blocked: true,
|
|
248
|
+
});
|
|
249
|
+
replayed += 1;
|
|
250
|
+
}
|
|
251
|
+
return replayed;
|
|
252
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { healthEventBuffer } from "../persistence/health-event-flush";
|
|
2
3
|
import { and, desc, eq, inArray, isNotNull, lte, sql } from "drizzle-orm";
|
|
3
4
|
import type { CartethyiaDatabase } from "../persistence/db-handle";
|
|
@@ -140,7 +141,7 @@ export async function recordPoolDispatchOutcome(
|
|
|
140
141
|
createdAt: now,
|
|
141
142
|
};
|
|
142
143
|
if (buffer !== undefined) buffer.enqueue(row);
|
|
143
|
-
else await db.insert(healthEvents).values(row as never);
|
|
144
|
+
else await db.insert(healthEvents).values({ id: randomUUID(), ...row } as never);
|
|
144
145
|
}
|
|
145
146
|
if (healthChanged) {
|
|
146
147
|
publishPoolHealth({
|
|
@@ -198,6 +199,7 @@ export async function disablePoolForProxyHttpStatus(
|
|
|
198
199
|
if (!updated[0]) return false;
|
|
199
200
|
|
|
200
201
|
await db.insert(healthEvents).values({
|
|
202
|
+
id: randomUUID(),
|
|
201
203
|
entityKind: "pool",
|
|
202
204
|
networkPoolId: poolId,
|
|
203
205
|
fromStatus: pool.status,
|
|
@@ -226,7 +228,7 @@ export async function recoverNetworkPool(db: CartethyiaDatabase, tenantId: strin
|
|
|
226
228
|
if (!pool) return false;
|
|
227
229
|
const now = new Date();
|
|
228
230
|
await db.update(networkPools).set({ status: "active", consecutiveFailures: 0, cooldownUntil: null, lastRecoveredAt: now }).where(and(eq(networkPools.tenantId, tenantId), eq(networkPools.id, poolId)));
|
|
229
|
-
await db.insert(healthEvents).values({ entityKind: "pool", networkPoolId: poolId, fromStatus: pool.status, toStatus: "active", reason: "Manual operator recovery", errorCategory: null, createdAt: now });
|
|
231
|
+
await db.insert(healthEvents).values({ id: randomUUID(), entityKind: "pool", networkPoolId: poolId, fromStatus: pool.status, toStatus: "active", reason: "Manual operator recovery", errorCategory: null, createdAt: now });
|
|
230
232
|
publishPoolHealth({ poolId, tenantId: pool.tenantId, status: "active", consecutiveFailures: 0 });
|
|
231
233
|
return true;
|
|
232
234
|
}
|
|
@@ -248,7 +250,7 @@ export async function sweepExpiredPoolCooldowns(db: CartethyiaDatabase): Promise
|
|
|
248
250
|
const expired = await db.select({ id: networkPools.id, tenantId: networkPools.tenantId, status: networkPools.status }).from(networkPools).where(and(inArray(networkPools.status, ["cooldown"]), isNotNull(networkPools.cooldownUntil), lte(networkPools.cooldownUntil, now))).limit(POOL_COOLDOWN_RECOVERY_BATCH);
|
|
249
251
|
if (expired.length === 0) return 0;
|
|
250
252
|
await db.update(networkPools).set({ status: "active", consecutiveFailures: 0, cooldownUntil: null, lastRecoveredAt: now }).where(inArray(networkPools.id, expired.map((row) => row.id)));
|
|
251
|
-
await db.insert(healthEvents).values(expired.map((row) => ({ entityKind: "pool" as const, networkPoolId: row.id, fromStatus: row.status, toStatus: "active" as const, reason: "Cooldown elapsed — auto-recovered", errorCategory: null, createdAt: now })));
|
|
253
|
+
await db.insert(healthEvents).values(expired.map((row) => ({ id: randomUUID(), entityKind: "pool" as const, networkPoolId: row.id, fromStatus: row.status, toStatus: "active" as const, reason: "Cooldown elapsed — auto-recovered", errorCategory: null, createdAt: now })));
|
|
252
254
|
for (const row of expired) publishPoolHealth({ poolId: row.id, tenantId: row.tenantId, status: "active", consecutiveFailures: 0 });
|
|
253
255
|
return expired.length;
|
|
254
256
|
}
|
|
@@ -1,21 +1,26 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import type { CartethyiaDatabase } from "../persistence/db-handle";
|
|
2
3
|
import { healthEvents } from "../persistence/schema";
|
|
3
4
|
import type { NetworkPoolSelector } from "./pool/selector";
|
|
4
5
|
import type { GatewayError } from "../protocol/gateway-error";
|
|
5
6
|
import { metrics } from "../observability/metrics";
|
|
6
7
|
import { log } from "../observability/logger";
|
|
8
|
+
import { upsertProviderCooldownMarker } from "./pool-cooldown-store";
|
|
9
|
+
import { resolvePoolProviderRateLimitCooldownMs } from "../config/cooldowns";
|
|
7
10
|
|
|
8
11
|
/**
|
|
9
12
|
* Durable pool rate-limit history.
|
|
10
13
|
*
|
|
11
14
|
* Per-(pool, provider) 429 cooldowns live in `NetworkPoolSelector` (fast,
|
|
12
|
-
* volatile, consulted on every dispatch). This recorder mirrors each flag
|
|
13
|
-
*
|
|
14
|
-
* same merged log the account recorder writes to
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* volatile, consulted on every dispatch). This recorder mirrors each flag into
|
|
16
|
+
* two durable homes: the append-only `health_events` table
|
|
17
|
+
* (`entity_kind: "pool"`, the same merged log the account recorder writes to
|
|
18
|
+
* for accounts) for audit, and the `provider_cooldowns` column on the pool row
|
|
19
|
+
* as the state boot restores — a restart must not make dispatch forget the
|
|
20
|
+
* egresses the upstream is still refusing. It never touches
|
|
21
|
+
* `network_pools.status`: a single provider's 429 must not flip the whole pool
|
|
22
|
+
* to `cooldown` (that would head-of-line-block every other provider on the
|
|
23
|
+
* pool).
|
|
19
24
|
*/
|
|
20
25
|
|
|
21
26
|
/**
|
|
@@ -41,12 +46,14 @@ async function recordPoolCooldownEvent(
|
|
|
41
46
|
if (last !== undefined && now - last < POOL_COOLDOWN_EVENT_WINDOW_MS) return;
|
|
42
47
|
try {
|
|
43
48
|
await db.insert(healthEvents).values({
|
|
49
|
+
id: randomUUID(),
|
|
44
50
|
entityKind: "pool",
|
|
45
51
|
networkPoolId: poolId,
|
|
46
52
|
fromStatus: null,
|
|
47
53
|
toStatus: "cooldown",
|
|
48
54
|
reason: `provider ${providerId}: ${reason}`,
|
|
49
55
|
errorCategory: "rate_limit_transient",
|
|
56
|
+
createdAt: new Date(),
|
|
50
57
|
});
|
|
51
58
|
} catch {
|
|
52
59
|
// Non-fatal: observability writes must never fail dispatch. The
|
|
@@ -85,6 +92,12 @@ export function flagPoolBlocked(
|
|
|
85
92
|
const reason = `Provider ${providerId} refused this egress`;
|
|
86
93
|
void poolSelector
|
|
87
94
|
.flagProviderBlocked(poolId, providerId, reason)
|
|
95
|
+
.then((entry) =>
|
|
96
|
+
// Persisted even when the flag was refused (a credential-keyed
|
|
97
|
+
// provider): the selector returned an inert entry, so the mirror
|
|
98
|
+
// records nothing a reader would act on.
|
|
99
|
+
upsertProviderCooldownMarker(db, poolId, providerId, entry),
|
|
100
|
+
)
|
|
88
101
|
.catch((cause: unknown) => {
|
|
89
102
|
metrics.pool_cooldown_record_failed.inc(1);
|
|
90
103
|
log.warn("pool block flag failed", {
|
|
@@ -119,12 +132,14 @@ async function recordPoolBlockEvent(
|
|
|
119
132
|
error: GatewayError,
|
|
120
133
|
): Promise<void> {
|
|
121
134
|
await db.insert(healthEvents).values({
|
|
135
|
+
id: randomUUID(),
|
|
122
136
|
entityKind: "pool",
|
|
123
137
|
networkPoolId: poolId,
|
|
124
138
|
fromStatus: null,
|
|
125
139
|
toStatus: "cooldown",
|
|
126
140
|
reason: `provider ${providerId}: ${reason}${providerCodeSuffix(error)}`,
|
|
127
141
|
errorCategory: "egress_blocked",
|
|
142
|
+
createdAt: new Date(),
|
|
128
143
|
});
|
|
129
144
|
}
|
|
130
145
|
|
|
@@ -137,9 +152,20 @@ function providerCodeSuffix(error: GatewayError): string {
|
|
|
137
152
|
/** Cooldown applied when an upstream 429 carries no `retryAfterMs`. */
|
|
138
153
|
const DEFAULT_PROVIDER_COOLDOWN_MS = 15 * 60 * 1000;
|
|
139
154
|
|
|
155
|
+
/**
|
|
156
|
+
* The fallback window for a provider-scoped egress cooldown. Upstream `Retry-After`
|
|
157
|
+
* always wins; the configured default exists because a request-counted free
|
|
158
|
+
* tier — the one provider that flags at all — stays refused for hours after
|
|
159
|
+
* the 15-minute guess this function used before.
|
|
160
|
+
*/
|
|
161
|
+
function providerCooldownFallbackMs(): number {
|
|
162
|
+
return Math.max(resolvePoolProviderRateLimitCooldownMs(), DEFAULT_PROVIDER_COOLDOWN_MS);
|
|
163
|
+
}
|
|
164
|
+
|
|
140
165
|
/**
|
|
141
166
|
* Records a provider-scoped 429 cooldown: the volatile per-(pool, provider)
|
|
142
|
-
* flag consulted on every dispatch,
|
|
167
|
+
* flag consulted on every dispatch, its durable audit row, and the durable
|
|
168
|
+
* state boot restores from.
|
|
143
169
|
*
|
|
144
170
|
* Both paths that reach this decision — the non-streaming attempt loop and
|
|
145
171
|
* the streaming failure path — call this one function, because each write
|
|
@@ -161,9 +187,10 @@ export function flagPoolCooldown(
|
|
|
161
187
|
const retryAfterMs =
|
|
162
188
|
typeof error.details.retryAfterMs === "number"
|
|
163
189
|
? error.details.retryAfterMs
|
|
164
|
-
:
|
|
190
|
+
: providerCooldownFallbackMs();
|
|
165
191
|
void poolSelector
|
|
166
192
|
.flagProviderCooldown(poolId, providerId, retryAfterMs, reason)
|
|
193
|
+
.then((entry) => upsertProviderCooldownMarker(db, poolId, providerId, entry))
|
|
167
194
|
.catch((cause: unknown) => {
|
|
168
195
|
metrics.pool_cooldown_record_failed.inc(1);
|
|
169
196
|
log.warn("pool cooldown flag failed", {
|
|
@@ -537,6 +537,63 @@ export async function chartTelemetry(
|
|
|
537
537
|
}));
|
|
538
538
|
}
|
|
539
539
|
|
|
540
|
+
/** One day × model usage cell for a single provider — the Activity Matrix feed. */
|
|
541
|
+
export interface ProviderUsageDayRow {
|
|
542
|
+
/** UTC day, `YYYY-MM-DD` (epoch-day buckets, so the same key on both backends). */
|
|
543
|
+
readonly day: string;
|
|
544
|
+
/** What the client asked for; `null` when the event carried no model. */
|
|
545
|
+
readonly model: string | null;
|
|
546
|
+
readonly requests: number;
|
|
547
|
+
readonly inputTokens: number;
|
|
548
|
+
readonly cachedInputTokens: number;
|
|
549
|
+
readonly outputTokens: number;
|
|
550
|
+
readonly costUsd: number;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Per-provider daily usage grouped by `requested_model`, day-bucketed like
|
|
555
|
+
* {@link chartTelemetry} but with a second grouping dimension (requested
|
|
556
|
+
* model) and the totals the tooltip/summary strip need. Provider rows carry
|
|
557
|
+
* no catalog foreign keys, so an unknown or deleted provider id returns empty
|
|
558
|
+
* rather than erroring.
|
|
559
|
+
*/
|
|
560
|
+
export async function providerUsageMatrix(
|
|
561
|
+
db: CartethyiaDatabase,
|
|
562
|
+
tenantId: string,
|
|
563
|
+
providerId: string,
|
|
564
|
+
since: Date,
|
|
565
|
+
): Promise<ProviderUsageDayRow[]> {
|
|
566
|
+
// Same inlining rule as chartTelemetry: SELECT and GROUP BY must spell the
|
|
567
|
+
// identical expression or Postgres 42803-rejects the grouped reference.
|
|
568
|
+
const w = sql.raw(String(86400));
|
|
569
|
+
const dayExpression = isPostgres(db)
|
|
570
|
+
? sql<number>`(cast(extract(epoch from ${telemetryEvents.createdAt}) as bigint) / ${w}) * ${w}`
|
|
571
|
+
: sql<number>`(cast(strftime('%s', ${telemetryEvents.createdAt}) as integer) / ${w}) * ${w}`;
|
|
572
|
+
const rows = await db
|
|
573
|
+
.select({
|
|
574
|
+
day: dayExpression,
|
|
575
|
+
model: telemetryEvents.requestedModel,
|
|
576
|
+
requests: sql<number>`count(*)`,
|
|
577
|
+
inputTokens: sql<number>`coalesce(sum(${telemetryEvents.inputTokens}), 0)`,
|
|
578
|
+
cachedInputTokens: sql<number>`coalesce(sum(${telemetryEvents.cachedInputTokens}), 0)`,
|
|
579
|
+
outputTokens: sql<number>`coalesce(sum(${telemetryEvents.outputTokens}), 0)`,
|
|
580
|
+
costUsd: sql<number>`coalesce(sum(${telemetryEvents.estimatedCostUsd}), 0)`,
|
|
581
|
+
})
|
|
582
|
+
.from(telemetryEvents)
|
|
583
|
+
.where(and(scoped(tenantId, since), eq(telemetryEvents.providerId, providerId)))
|
|
584
|
+
.groupBy(dayExpression, telemetryEvents.requestedModel)
|
|
585
|
+
.orderBy(dayExpression);
|
|
586
|
+
return rows.map((row) => ({
|
|
587
|
+
day: new Date(numeric(row.day) * 1000).toISOString().slice(0, 10),
|
|
588
|
+
model: row.model,
|
|
589
|
+
requests: numeric(row.requests),
|
|
590
|
+
inputTokens: numeric(row.inputTokens),
|
|
591
|
+
cachedInputTokens: numeric(row.cachedInputTokens),
|
|
592
|
+
outputTokens: numeric(row.outputTokens),
|
|
593
|
+
costUsd: numeric(row.costUsd),
|
|
594
|
+
}));
|
|
595
|
+
}
|
|
596
|
+
|
|
540
597
|
/** Overview/health totals for one tenant window. */
|
|
541
598
|
export async function healthTelemetry(
|
|
542
599
|
db: CartethyiaDatabase,
|