cartethyia 2.5.9 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +15 -42
- package/README.md +11 -10
- package/dist/dashboard/assets/{BannedPage-Brvuj_Eb.js → BannedPage-l6XMnfr9.js} +1 -1
- package/dist/dashboard/assets/BannedPage-l6XMnfr9.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-CqfXtI3y.js → CliToolDetailPage-DwOzAW45.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-DwOzAW45.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-GQeS91bg.js → CliToolsPage-BT3Up3QA.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-BT3Up3QA.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-BDRsTXBB.js → CombosPage-6kYIUpzR.js} +1 -1
- package/dist/dashboard/assets/CombosPage-6kYIUpzR.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-zP8AFHX0.js → ConsoleLogPage-zRnj1mlR.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-zRnj1mlR.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-BmfrNZ14.js → CustomizationPage-C7m-H1Rz.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-C7m-H1Rz.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-DoreIgm8.js → LoginPage-CbphLvjg.js} +1 -1
- package/dist/dashboard/assets/LoginPage-CbphLvjg.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-C7pDb41R.js → ModelPicker-DVny4S5_.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-DVny4S5_.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js +1 -0
- package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js.gz +0 -0
- package/dist/dashboard/assets/{ProvidersPage-BnoPgA7w.js → ProvidersPage-Dfb2896L.js} +1 -1
- package/dist/dashboard/assets/ProvidersPage-Dfb2896L.js.gz +0 -0
- package/dist/dashboard/assets/{ProxyPage-Bq_TGgdb.js → ProxyPage-BaCIdAUt.js} +1 -1
- package/dist/dashboard/assets/ProxyPage-BaCIdAUt.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js +1 -0
- package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js +1 -0
- package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-Day3mCOn.js → SetupPage-D4g6vOGc.js} +1 -1
- package/dist/dashboard/assets/SetupPage-D4g6vOGc.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-DQxllnB5.js → SortableList-C93i14BD.js} +1 -1
- package/dist/dashboard/assets/SortableList-C93i14BD.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-uMExoWu1.js → StudioPage-SgQpymjm.js} +1 -1
- package/dist/dashboard/assets/{StudioPage-uMExoWu1.js.gz → StudioPage-SgQpymjm.js.gz} +0 -0
- package/dist/dashboard/assets/{UsagePage-DXa1oR-4.js → UsagePage-BFME-eba.js} +2 -2
- package/dist/dashboard/assets/UsagePage-BFME-eba.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-DQYLQxpW.js → api-keys-C23S9sgp.js} +1 -1
- package/dist/dashboard/assets/api-keys-C23S9sgp.js.gz +0 -0
- package/dist/dashboard/assets/{app-BIt446yE.js → app-2KhtHCv9.js} +1 -1
- package/dist/dashboard/assets/{app-BIt446yE.js.gz → app-2KhtHCv9.js.gz} +0 -0
- package/dist/dashboard/assets/app-CEeaMHOy.css +2 -0
- package/dist/dashboard/assets/app-CEeaMHOy.css.gz +0 -0
- package/dist/dashboard/assets/{assert-C9FD_JwJ.js → assert-DU6qrMA_.js} +1 -1
- package/dist/dashboard/assets/assert-DU6qrMA_.js.gz +0 -0
- package/dist/dashboard/assets/{badge-CR72dasT.js → badge-CKsF123d.js} +1 -1
- package/dist/dashboard/assets/badge-CKsF123d.js.gz +0 -0
- package/dist/dashboard/assets/{banner-CI9_eSfO.js → banner-8j7uChA9.js} +1 -1
- package/dist/dashboard/assets/banner-8j7uChA9.js.gz +0 -0
- package/dist/dashboard/assets/{card-DS7CQEsO.js → card-CdsSlWA3.js} +1 -1
- package/dist/dashboard/assets/card-CdsSlWA3.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-CNqzrfdb.js → cli-tools-BaFUuFFF.js} +1 -1
- package/dist/dashboard/assets/cli-tools-BaFUuFFF.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-611pSsaX.js → clipboard-button-uL5aYLfW.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-uL5aYLfW.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-k37exjNt.js → dialog-C07hr5_G.js} +1 -1
- package/dist/dashboard/assets/dialog-C07hr5_G.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-CX7N8ucR.js → drawer-BPO1TcKH.js} +1 -1
- package/dist/dashboard/assets/drawer-BPO1TcKH.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CTP8AyiS.js → entry-Bz_CtDfC.js} +1 -1
- package/dist/dashboard/assets/entry-Bz_CtDfC.js.gz +0 -0
- package/dist/dashboard/assets/entry-CwPRKk0d.js +4 -0
- package/dist/dashboard/assets/entry-CwPRKk0d.js.gz +0 -0
- package/dist/dashboard/assets/entry-GJRbmrpA.js +1 -0
- package/dist/dashboard/assets/entry-GJRbmrpA.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-sltk1qUi.js → github-badge-DmQreSDd.js} +1 -1
- package/dist/dashboard/assets/github-badge-DmQreSDd.js.gz +0 -0
- package/dist/dashboard/assets/{index-z3zDOSMM.js → index-Bblu6erR.js} +2 -2
- package/dist/dashboard/assets/index-Bblu6erR.js.gz +0 -0
- package/dist/dashboard/assets/{inline-BhY2sSnO.js → inline-C9dCYaBZ.js} +1 -1
- package/dist/dashboard/assets/inline-C9dCYaBZ.js.gz +0 -0
- package/dist/dashboard/assets/{input-V1umFiBk.js → input-CjCnXweJ.js} +1 -1
- package/dist/dashboard/assets/input-CjCnXweJ.js.gz +0 -0
- package/dist/dashboard/assets/{layout-BBOYycyS.js → layout-V6y8DAkl.js} +1 -1
- package/dist/dashboard/assets/layout-V6y8DAkl.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-1zzZYM0B.js → list-row-CM1AaG_y.js} +1 -1
- package/dist/dashboard/assets/list-row-CM1AaG_y.js.gz +0 -0
- package/dist/dashboard/assets/{live-BivGfuUs.js → live-ta6GMb-8.js} +1 -1
- package/dist/dashboard/assets/live-ta6GMb-8.js.gz +0 -0
- package/dist/dashboard/assets/{model-bans-CmffjLuG.js → model-bans-BmYTmbAk.js} +1 -1
- package/dist/dashboard/assets/model-bans-BmYTmbAk.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-46P0oWMe.js → mutation-g_fduiNw.js} +1 -1
- package/dist/dashboard/assets/mutation-g_fduiNw.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-DkfMR5Qs.js → numbers-BOevJzdz.js} +1 -1
- package/dist/dashboard/assets/numbers-BOevJzdz.js.gz +0 -0
- package/dist/dashboard/assets/{query-keys-BFhGpwR1.js → query-keys-UETqfytp.js} +1 -1
- package/dist/dashboard/assets/query-keys-UETqfytp.js.gz +0 -0
- package/dist/dashboard/assets/{routing-CAb4F37R.js → routing-BPUo-W8d.js} +1 -1
- package/dist/dashboard/assets/routing-BPUo-W8d.js.gz +0 -0
- package/dist/dashboard/assets/{select-C6MMiaio.js → select-C980c4cz.js} +1 -1
- package/dist/dashboard/assets/select-C980c4cz.js.gz +0 -0
- package/dist/dashboard/assets/{settings-DNX9sydi.js → settings--3hxSyry.js} +1 -1
- package/dist/dashboard/assets/settings--3hxSyry.js.gz +0 -0
- package/dist/dashboard/assets/{stack-BHTfXdz_.js → stack-_gfTdLVB.js} +1 -1
- package/dist/dashboard/assets/stack-_gfTdLVB.js.gz +0 -0
- package/dist/dashboard/assets/{state-mPDRlQkl.js → state-2c5V4zo7.js} +1 -1
- package/dist/dashboard/assets/state-2c5V4zo7.js.gz +0 -0
- package/dist/dashboard/assets/{switch-DOmJxg7H.js → switch-Dkke7Naf.js} +1 -1
- package/dist/dashboard/assets/switch-Dkke7Naf.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-Dem0lXQj.js → useMutation-EDFRkn1i.js} +1 -1
- package/dist/dashboard/assets/useMutation-EDFRkn1i.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 +51 -51
- package/migrations/README.md +52 -52
- package/migrations/full-postgres/0020_add_telemetry_event_analytics.sql +77 -0
- package/migrations/full-postgres/0021_add_api_key_concurrency_tolerance.sql +13 -0
- package/migrations/full-postgres/0022_add_inferhub_config_key.sql +17 -0
- package/migrations/full-postgres/0023_drop_api_key_concurrency_tolerance.sql +13 -0
- package/migrations/lite-sqlite3/0020_add_telemetry_event_analytics.sql +69 -0
- package/migrations/lite-sqlite3/0021_add_api_key_concurrency_tolerance.sql +13 -0
- package/migrations/lite-sqlite3/0022_add_inferhub_config_key.sql +17 -0
- package/migrations/lite-sqlite3/0023_drop_api_key_concurrency_tolerance.sql +13 -0
- package/package.json +96 -97
- package/scripts/cli/admin.ts +126 -125
- package/scripts/cli/autostart.ts +237 -237
- package/scripts/cli/background.ts +120 -110
- package/scripts/cli/bin/carte.cjs +8 -8
- package/scripts/cli/bin/cartethyia.cjs +167 -167
- package/scripts/cli/console-menu.ts +308 -275
- package/scripts/cli/controller.ts +145 -0
- package/scripts/cli/doctor.ts +1 -1
- package/scripts/cli/env.ts +99 -99
- package/scripts/cli/gateway.ts +264 -261
- package/scripts/{health.ts → cli/health.ts} +4 -4
- package/scripts/cli/instance.ts +234 -224
- package/scripts/cli/main.ts +188 -164
- package/scripts/cli/platform.ts +64 -64
- package/scripts/cli/prompt.ts +275 -107
- package/scripts/cli/services.ts +88 -88
- package/scripts/cli/setup.ts +445 -445
- package/scripts/cli/start.ts +85 -82
- package/scripts/cli/startup-signal.ts +23 -23
- package/scripts/cli/telemetry.ts +70 -0
- package/scripts/cli/tray/index.ts +263 -263
- package/scripts/cli/tray/windows.ps1 +107 -107
- package/scripts/cli/tray.ts +192 -208
- package/scripts/cli/update.ts +39 -39
- package/scripts/cli/version.ts +67 -67
- package/scripts/cli/wordmark.ts +58 -58
- package/src/boot/app.ts +16 -1
- package/src/boot/dependencies.ts +128 -25
- package/src/config/package-paths.ts +24 -0
- package/src/config/resources.ts +137 -0
- package/src/config/runtime.ts +133 -76
- package/src/config/spec.ts +30 -29
- package/src/config/telemetry.ts +0 -25
- package/src/console/api-keys/bucket-spend.ts +14 -17
- package/src/console/api-keys/contracts.ts +410 -410
- package/src/console/api-keys/routes.ts +12 -0
- package/src/console/audit/store.ts +2 -3
- package/src/console/auth/setup-service.ts +8 -1
- package/src/console/backup/routes.ts +7 -0
- package/src/console/backup/service.ts +463 -463
- package/src/console/backup/store.ts +16 -11
- package/src/console/backup/validate.ts +1 -4
- package/src/console/cli-tools/fs-ops.ts +18 -2
- package/src/console/data-management/contracts.ts +1 -8
- package/src/console/data-management/operations.ts +61 -48
- package/src/console/data-management/routes.ts +4 -2
- package/src/console/domain-registration.ts +25 -3
- package/src/console/inferhub/client.ts +139 -0
- package/src/console/inferhub/key-store.ts +129 -0
- package/src/console/inferhub/normalize.ts +347 -0
- package/src/console/inferhub/routes.ts +411 -0
- package/src/console/inferhub/store.ts +109 -0
- package/src/console/inferhub/types.ts +183 -0
- package/src/console/observability/contracts.ts +2 -41
- package/src/console/observability/store.ts +176 -371
- package/src/console/providers/catalog/public-model-store.ts +0 -0
- package/src/console/providers/catalog/store.ts +23 -36
- package/src/console/quota/account-quota-shared.ts +52 -52
- package/src/console/quota/account-quota-tenant.ts +16 -24
- package/src/console/quota/refresh.ts +441 -441
- package/src/console/remote-support/service.ts +270 -114
- package/src/console/routing/pools/import-parse.ts +206 -206
- package/src/console/routing/pools/operations.ts +543 -543
- package/src/console/routing/pools/probe-verdict.ts +132 -132
- package/src/console/routing/pools/routes.ts +181 -181
- package/src/console/routing/pools/store.ts +1 -1
- package/src/console/settings/contracts.ts +42 -40
- package/src/console/settings/store.ts +25 -25
- package/src/console/share/share-stats.ts +22 -4
- package/src/console/share/share-usage.ts +30 -4
- package/src/console/shared/cursor.ts +15 -0
- package/src/console/updates/repository-updates.ts +40 -39
- package/src/console/updates/routes.ts +12 -0
- package/src/main.ts +13 -5
- package/src/network/pool/resolver.ts +1104 -1104
- package/src/observability/client-name.ts +22 -0
- package/src/observability/event-log.ts +5 -29
- package/src/observability/event-query.ts +690 -0
- package/src/observability/log-ring.ts +26 -2
- package/src/observability/log-sanitize.ts +118 -0
- package/src/observability/logger.ts +17 -13
- package/src/observability/metrics.ts +35 -0
- package/src/observability/payload-capture.ts +114 -41
- package/src/observability/payload-store.ts +249 -24
- package/src/observability/raw-event-writer.ts +118 -0
- package/src/observability/runtime-metrics.ts +71 -1
- package/src/observability/telemetry-buffer.ts +100 -17
- package/src/observability/telemetry-reconciliation.ts +417 -0
- package/src/persistence/boot.ts +20 -9
- package/src/persistence/db-pg.ts +13 -0
- package/src/persistence/db-sqlite.ts +569 -497
- package/src/persistence/dialect.ts +3 -2
- package/src/persistence/migrate.ts +181 -181
- package/src/persistence/schema.ts +1496 -1406
- package/src/persistence/shared-cache.ts +5 -1
- package/src/persistence/stores/api-key-store.ts +581 -581
- package/src/persistence/stores/telemetry-store.ts +124 -63
- package/src/protocol/reasoning-text.ts +30 -0
- package/src/protocol/registry.ts +2 -0
- package/src/protocol/response/chat.ts +33 -7
- package/src/protocol/response/codex.ts +14 -2
- package/src/protocol/response/messages.ts +5 -2
- package/src/providers/authentication/oauth-flow-store.ts +3 -10
- package/src/providers/bundled-discovery-providers.ts +122 -122
- package/src/providers/default-registry.ts +14 -0
- package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
- package/src/providers/discovery/models-dev-catalog.ts +354 -354
- package/src/providers/discovery/probe-phases.ts +43 -7
- package/src/providers/discovery/probing-service.ts +70 -32
- package/src/providers/integrations/antigravity/antigravity.ts +9 -2
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +85 -17
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +934 -934
- package/src/providers/integrations/buddy/buddy-checkin.ts +5 -9
- package/src/providers/integrations/buddy/buddy-discovery-shared.ts +3 -7
- package/src/providers/integrations/buddy/buddy-quota-shared.ts +3 -1
- package/src/providers/integrations/buddy/capabilities.ts +4 -0
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +57 -49
- package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -180
- package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -206
- package/src/providers/integrations/buddy/workbuddy/quota.ts +161 -161
- package/src/providers/integrations/claude/claude-quota.ts +2 -2
- package/src/providers/integrations/claude/claude-user-profiles.ts +3 -2
- package/src/providers/integrations/claude/claude.ts +3 -4
- package/src/providers/integrations/cline/cline.ts +94 -8
- package/src/providers/integrations/codex/codex.ts +61 -37
- package/src/providers/integrations/cursor/cursor-catalog.ts +368 -368
- package/src/providers/integrations/cursor/cursor-http2.ts +300 -300
- package/src/providers/integrations/cursor/cursor-identity.ts +2 -4
- package/src/providers/integrations/cursor/cursor-request.ts +11 -0
- package/src/providers/integrations/cursor/cursor.ts +29 -17
- package/src/providers/integrations/cursor/protocol/framing.ts +129 -129
- package/src/providers/integrations/cursor/protocol/messages.ts +2380 -2380
- package/src/providers/integrations/cursor/protocol/struct.ts +305 -305
- package/src/providers/integrations/cursor/protocol/tool-canonical.ts +275 -0
- package/src/providers/integrations/github/github.ts +3 -1
- package/src/providers/integrations/grok/grok-quota.ts +279 -279
- package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -73
- package/src/providers/integrations/grok/grok-ratelimit.ts +218 -218
- package/src/providers/integrations/hermes.ts +7 -1
- package/src/providers/integrations/inferhub.ts +201 -73
- package/src/providers/integrations/kilo/kilo.ts +152 -152
- package/src/providers/integrations/kiro/kiro-machine-id.ts +2 -4
- package/src/providers/integrations/opencode/provider.ts +2 -0
- package/src/providers/integrations/openrouter/provider.ts +129 -129
- package/src/providers/integrations/qoder/qoder-quota.ts +193 -192
- package/src/providers/integrations/qoder/qoder-realm.ts +106 -106
- package/src/providers/integrations/trae/trae-headers.ts +17 -48
- package/src/providers/integrations/trae/trae-oauth.ts +4 -26
- package/src/providers/integrations/trae/trae-quota.ts +389 -384
- package/src/providers/integrations/trae/trae-realm.ts +15 -2
- package/src/providers/integrations/trae/trae.ts +22 -24
- package/src/providers/integrations/xiaomi-mimo/think-stream.ts +100 -24
- package/src/providers/integrations/zcode/oauth.ts +1 -1
- package/src/providers/operations/account-health-service.ts +605 -279
- package/src/providers/provider-metadata.ts +12 -1
- package/src/providers/provider-model-definition.ts +373 -373
- package/src/providers/provider-probe.ts +3 -0
- package/src/providers/quota/quota-contracts.ts +3 -7
- package/src/providers/search/search-provider.ts +2 -6
- package/src/security/admission/contracts.ts +10 -0
- package/src/security/admission/in-memory-store.ts +89 -14
- package/src/security/admission/lua.ts +191 -182
- package/src/security/admission/redis-store.ts +30 -0
- package/src/security/admission/service.ts +18 -0
- package/src/security/api-key-auth-cache.ts +96 -0
- package/src/security/api-key-auth.ts +11 -100
- package/src/security/client-router-fingerprint.ts +3 -7
- package/src/security/ip-boundary.ts +7 -3
- package/src/shared/coerce.ts +67 -0
- package/src/shared/hash.ts +6 -0
- package/src/shared/time.ts +7 -0
- package/src/transport/dispatch/attempt-finalize.ts +26 -138
- package/src/transport/dispatch/attempt-loop.ts +103 -81
- package/src/transport/dispatch/capture-policy.ts +78 -0
- package/src/transport/dispatch/proxy-request.ts +4 -2
- package/src/transport/dispatch/streaming/encoder.ts +111 -5
- package/src/transport/dispatch/streaming/stream.ts +19 -2
- package/src/transport/dispatch/streaming/thinking-loop.ts +300 -0
- package/src/transport/failure-policy.ts +27 -0
- package/src/transport/guards/abuse-guard.ts +2 -2
- package/src/transport/guards/auth-guard.ts +17 -0
- package/src/transport/guards/body-policy.ts +3 -2
- package/src/transport/guards/concurrency-guard.ts +89 -13
- package/src/transport/guards/early-rejection-capture.ts +43 -3
- package/src/transport/guards/error-lifecycle.ts +10 -2
- package/src/transport/guards/guard-errors.ts +23 -0
- package/src/transport/guards/headroom-guard.ts +41 -7
- package/src/transport/guards/pipeline.ts +24 -0
- package/src/transport/guards/request-context.ts +2 -1
- package/src/transport/routing/admission.ts +51 -9
- package/src/transport/routing/bounded-lru.ts +49 -0
- package/src/transport/routing/candidate-index.ts +88 -0
- package/src/transport/routing/reservations.ts +121 -7
- package/src/transport/routing/round-robin.ts +10 -22
- package/src/transport/routing/route-catalog.ts +26 -13
- package/src/transport/routing/route-model.ts +22 -2
- package/src/transport/routing/router.ts +53 -31
- package/src/transport/surface/chat/encode.ts +2 -3
- package/src/transport/surface/chat/parse.ts +2 -1
- package/src/transport/surface/dialects.ts +11 -3
- package/src/transport/surface/messages/encode.ts +2 -1
- package/src/transport/surface/messages/parse.ts +3 -1
- package/src/transport/surface/messages/stream.ts +2 -6
- package/src/workers/maintenance-lease.ts +200 -0
- package/src/workers/scheduler.ts +101 -1
- package/dist/dashboard/assets/BannedPage-Brvuj_Eb.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-CqfXtI3y.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-GQeS91bg.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-BDRsTXBB.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-zP8AFHX0.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-BmfrNZ14.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-Dn3M6WZc.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-Dn3M6WZc.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-DoreIgm8.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-C7pDb41R.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-CvvD1Ugk.js +0 -1
- package/dist/dashboard/assets/OverviewPage-CvvD1Ugk.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-Clbn1gsW.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage-Clbn1gsW.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-BnoPgA7w.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-Bq_TGgdb.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-D9DHXvom.js +0 -1
- package/dist/dashboard/assets/QuotaPage-D9DHXvom.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-7xV8WkWL.js +0 -1
- package/dist/dashboard/assets/SettingsPage-7xV8WkWL.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-Day3mCOn.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-DQxllnB5.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-DXa1oR-4.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
- package/dist/dashboard/assets/app-CX_Z8cMs.css +0 -2
- package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
- package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
- package/dist/dashboard/assets/badge-CR72dasT.js.gz +0 -0
- package/dist/dashboard/assets/banner-CI9_eSfO.js.gz +0 -0
- package/dist/dashboard/assets/card-DS7CQEsO.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-611pSsaX.js.gz +0 -0
- package/dist/dashboard/assets/dialog-k37exjNt.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CX7N8ucR.js.gz +0 -0
- package/dist/dashboard/assets/entry-B9Xvtnfj.js +0 -1
- package/dist/dashboard/assets/entry-B9Xvtnfj.js.gz +0 -0
- package/dist/dashboard/assets/entry-ByMGkR3C.js +0 -4
- package/dist/dashboard/assets/entry-ByMGkR3C.js.gz +0 -0
- package/dist/dashboard/assets/entry-CTP8AyiS.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-sltk1qUi.js.gz +0 -0
- package/dist/dashboard/assets/index-z3zDOSMM.js.gz +0 -0
- package/dist/dashboard/assets/inline-BhY2sSnO.js.gz +0 -0
- package/dist/dashboard/assets/input-V1umFiBk.js.gz +0 -0
- package/dist/dashboard/assets/layout-BBOYycyS.js.gz +0 -0
- package/dist/dashboard/assets/list-row-1zzZYM0B.js.gz +0 -0
- package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
- package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
- package/dist/dashboard/assets/numbers-DkfMR5Qs.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
- package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
- package/dist/dashboard/assets/select-C6MMiaio.js.gz +0 -0
- package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
- package/dist/dashboard/assets/stack-BHTfXdz_.js.gz +0 -0
- package/dist/dashboard/assets/state-mPDRlQkl.js.gz +0 -0
- package/dist/dashboard/assets/switch-DOmJxg7H.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
- package/scripts/build/aot.ts +0 -143
- package/scripts/build/build.ts +0 -24
- package/scripts/build/compile.ts +0 -88
- package/scripts/build/restart.ts +0 -120
- package/scripts/build/start.ts +0 -25
- package/src/observability/event-compaction.ts +0 -317
- package/src/observability/event-day-index.ts +0 -440
package/scripts/cli/update.ts
CHANGED
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The update path.
|
|
3
|
-
*
|
|
4
|
-
* npm cannot replace this package's directory while a process is executing out
|
|
5
|
-
* of it (EBUSY on Windows). So we spawn the installer as a detached process,
|
|
6
|
-
* print a short notice, and exit — the installer runs after this process is gone.
|
|
7
|
-
* After install, it attempts to restart the gateway (systemd if available, else
|
|
8
|
-
* `cartethyia start`), so the new version comes up automatically.
|
|
9
|
-
* Cross-OS: uses npm.cmd on Windows.
|
|
10
|
-
*/
|
|
11
|
-
import { spawn } from "node:child_process";
|
|
12
|
-
|
|
13
|
-
export function updateCommand(): string {
|
|
14
|
-
return "npm install -g cartethyia@latest --prefer-online";
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function runDetachedUpdate(): void {
|
|
18
|
-
const isWin = process.platform === "win32";
|
|
19
|
-
const shell = isWin ? "cmd.exe" : "/bin/sh";
|
|
20
|
-
const install = "npm install -g cartethyia@latest --prefer-online";
|
|
21
|
-
const restart = isWin
|
|
22
|
-
? "cartethyia start"
|
|
23
|
-
: "systemctl restart cartethyia 2>/dev/null || cartethyia start";
|
|
24
|
-
const script = `${install} && ${restart}`;
|
|
25
|
-
const args = isWin ? ["/c", script] : ["-c", script];
|
|
26
|
-
const child = spawn(shell, args, {
|
|
27
|
-
detached: true,
|
|
28
|
-
stdio: "ignore",
|
|
29
|
-
});
|
|
30
|
-
child.unref();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function printUpdateInstructions(from: string, to: string | null): void {
|
|
34
|
-
console.log("");
|
|
35
|
-
console.log(`Update available: ${from} \u2192 ${to ?? "latest"}`);
|
|
36
|
-
console.log("");
|
|
37
|
-
console.log("Starting detached update; this process will exit now.");
|
|
38
|
-
console.log("The gateway will restart automatically after the install.");
|
|
39
|
-
console.log("");
|
|
1
|
+
/**
|
|
2
|
+
* The update path.
|
|
3
|
+
*
|
|
4
|
+
* npm cannot replace this package's directory while a process is executing out
|
|
5
|
+
* of it (EBUSY on Windows). So we spawn the installer as a detached process,
|
|
6
|
+
* print a short notice, and exit — the installer runs after this process is gone.
|
|
7
|
+
* After install, it attempts to restart the gateway (systemd if available, else
|
|
8
|
+
* `cartethyia start`), so the new version comes up automatically.
|
|
9
|
+
* Cross-OS: uses npm.cmd on Windows.
|
|
10
|
+
*/
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
12
|
+
|
|
13
|
+
export function updateCommand(): string {
|
|
14
|
+
return "npm install -g cartethyia@latest --prefer-online";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function runDetachedUpdate(): void {
|
|
18
|
+
const isWin = process.platform === "win32";
|
|
19
|
+
const shell = isWin ? "cmd.exe" : "/bin/sh";
|
|
20
|
+
const install = "npm install -g cartethyia@latest --prefer-online";
|
|
21
|
+
const restart = isWin
|
|
22
|
+
? "cartethyia start"
|
|
23
|
+
: "systemctl restart cartethyia 2>/dev/null || cartethyia start";
|
|
24
|
+
const script = `${install} && ${restart}`;
|
|
25
|
+
const args = isWin ? ["/c", script] : ["-c", script];
|
|
26
|
+
const child = spawn(shell, args, {
|
|
27
|
+
detached: true,
|
|
28
|
+
stdio: "ignore",
|
|
29
|
+
});
|
|
30
|
+
child.unref();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function printUpdateInstructions(from: string, to: string | null): void {
|
|
34
|
+
console.log("");
|
|
35
|
+
console.log(`Update available: ${from} \u2192 ${to ?? "latest"}`);
|
|
36
|
+
console.log("");
|
|
37
|
+
console.log("Starting detached update; this process will exit now.");
|
|
38
|
+
console.log("The gateway will restart automatically after the install.");
|
|
39
|
+
console.log("");
|
|
40
40
|
}
|
package/scripts/cli/version.ts
CHANGED
|
@@ -1,67 +1,67 @@
|
|
|
1
|
-
import { resolvePackagePath } from "../../src/config/package-paths";
|
|
2
|
-
|
|
3
|
-
export async function readRequiredBunVersion(): Promise<string> {
|
|
4
|
-
const metadata: unknown = await Bun.file(resolvePackagePath("package.json")).json();
|
|
5
|
-
if (metadata === null || typeof metadata !== "object" || !("engines" in metadata) ||
|
|
6
|
-
metadata.engines === null || typeof metadata.engines !== "object" ||
|
|
7
|
-
!("bun" in metadata.engines) || typeof metadata.engines.bun !== "string") {
|
|
8
|
-
throw new Error("Cartethyia's required Bun version is unavailable in package.json");
|
|
9
|
-
}
|
|
10
|
-
const minimum = metadata.engines.bun.trim().replace(/^>=\s*/, "");
|
|
11
|
-
if (!/^\d+(?:\.\d+){0,2}$/.test(minimum)) throw new Error(`Unsupported Bun engine requirement: ${metadata.engines.bun}`);
|
|
12
|
-
return minimum;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function bunVersionAtLeast(actual: string, minimum: string): boolean {
|
|
16
|
-
const left = actual.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
17
|
-
const right = minimum.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
18
|
-
const length = Math.max(left.length, right.length);
|
|
19
|
-
for (let i = 0; i < length; i += 1) {
|
|
20
|
-
const a = left[i] ?? 0;
|
|
21
|
-
const b = right[i] ?? 0;
|
|
22
|
-
if (a !== b) return a > b;
|
|
23
|
-
}
|
|
24
|
-
return true;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export async function assertBunVersion(): Promise<void> {
|
|
28
|
-
const minimum = await readRequiredBunVersion();
|
|
29
|
-
if (bunVersionAtLeast(Bun.version, minimum)) return;
|
|
30
|
-
throw new Error(`Bun ${minimum} or newer is required (found ${Bun.version}). Install it from https://bun.sh and run this command again.`);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Reads the version from the installed package manifest. */
|
|
34
|
-
export async function readInstalledVersion(): Promise<string> {
|
|
35
|
-
const metadata: unknown = await Bun.file(resolvePackagePath("package.json")).json();
|
|
36
|
-
if (
|
|
37
|
-
metadata === null ||
|
|
38
|
-
typeof metadata !== "object" ||
|
|
39
|
-
!("version" in metadata) ||
|
|
40
|
-
typeof metadata.version !== "string"
|
|
41
|
-
) {
|
|
42
|
-
throw new Error("Cartethyia package version is unavailable");
|
|
43
|
-
}
|
|
44
|
-
return metadata.version;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Asks the npm registry for the newest published version, or null on failure. */
|
|
48
|
-
export async function readLatestPublishedVersion(): Promise<string | null> {
|
|
49
|
-
try {
|
|
50
|
-
const response = await fetch("https://registry.npmjs.org/cartethyia/latest", {
|
|
51
|
-
signal: AbortSignal.timeout(4_000),
|
|
52
|
-
});
|
|
53
|
-
if (!response.ok) return null;
|
|
54
|
-
const metadata: unknown = await response.json();
|
|
55
|
-
if (
|
|
56
|
-
metadata !== null &&
|
|
57
|
-
typeof metadata === "object" &&
|
|
58
|
-
"version" in metadata &&
|
|
59
|
-
typeof metadata.version === "string"
|
|
60
|
-
) {
|
|
61
|
-
return metadata.version;
|
|
62
|
-
}
|
|
63
|
-
return null;
|
|
64
|
-
} catch {
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
1
|
+
import { resolvePackagePath } from "../../src/config/package-paths";
|
|
2
|
+
|
|
3
|
+
export async function readRequiredBunVersion(): Promise<string> {
|
|
4
|
+
const metadata: unknown = await Bun.file(resolvePackagePath("package.json")).json();
|
|
5
|
+
if (metadata === null || typeof metadata !== "object" || !("engines" in metadata) ||
|
|
6
|
+
metadata.engines === null || typeof metadata.engines !== "object" ||
|
|
7
|
+
!("bun" in metadata.engines) || typeof metadata.engines.bun !== "string") {
|
|
8
|
+
throw new Error("Cartethyia's required Bun version is unavailable in package.json");
|
|
9
|
+
}
|
|
10
|
+
const minimum = metadata.engines.bun.trim().replace(/^>=\s*/, "");
|
|
11
|
+
if (!/^\d+(?:\.\d+){0,2}$/.test(minimum)) throw new Error(`Unsupported Bun engine requirement: ${metadata.engines.bun}`);
|
|
12
|
+
return minimum;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function bunVersionAtLeast(actual: string, minimum: string): boolean {
|
|
16
|
+
const left = actual.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
17
|
+
const right = minimum.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
18
|
+
const length = Math.max(left.length, right.length);
|
|
19
|
+
for (let i = 0; i < length; i += 1) {
|
|
20
|
+
const a = left[i] ?? 0;
|
|
21
|
+
const b = right[i] ?? 0;
|
|
22
|
+
if (a !== b) return a > b;
|
|
23
|
+
}
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function assertBunVersion(): Promise<void> {
|
|
28
|
+
const minimum = await readRequiredBunVersion();
|
|
29
|
+
if (bunVersionAtLeast(Bun.version, minimum)) return;
|
|
30
|
+
throw new Error(`Bun ${minimum} or newer is required (found ${Bun.version}). Install it from https://bun.sh and run this command again.`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Reads the version from the installed package manifest. */
|
|
34
|
+
export async function readInstalledVersion(): Promise<string> {
|
|
35
|
+
const metadata: unknown = await Bun.file(resolvePackagePath("package.json")).json();
|
|
36
|
+
if (
|
|
37
|
+
metadata === null ||
|
|
38
|
+
typeof metadata !== "object" ||
|
|
39
|
+
!("version" in metadata) ||
|
|
40
|
+
typeof metadata.version !== "string"
|
|
41
|
+
) {
|
|
42
|
+
throw new Error("Cartethyia package version is unavailable");
|
|
43
|
+
}
|
|
44
|
+
return metadata.version;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Asks the npm registry for the newest published version, or null on failure. */
|
|
48
|
+
export async function readLatestPublishedVersion(): Promise<string | null> {
|
|
49
|
+
try {
|
|
50
|
+
const response = await fetch("https://registry.npmjs.org/cartethyia/latest", {
|
|
51
|
+
signal: AbortSignal.timeout(4_000),
|
|
52
|
+
});
|
|
53
|
+
if (!response.ok) return null;
|
|
54
|
+
const metadata: unknown = await response.json();
|
|
55
|
+
if (
|
|
56
|
+
metadata !== null &&
|
|
57
|
+
typeof metadata === "object" &&
|
|
58
|
+
"version" in metadata &&
|
|
59
|
+
typeof metadata.version === "string"
|
|
60
|
+
) {
|
|
61
|
+
return metadata.version;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
package/scripts/cli/wordmark.ts
CHANGED
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The console wordmark.
|
|
3
|
-
*
|
|
4
|
-
* Rendered with half-block characters so each letter is two pixel rows tall in
|
|
5
|
-
* one character cell, and coloured with a horizontal gradient across the
|
|
6
|
-
* Cartethyia blue-grey range. The caller decides whether to print it: a
|
|
7
|
-
* non-colour terminal, or one narrower than the wordmark, should skip it.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
const WORDMARK: readonly string[] = [
|
|
11
|
-
" ██████╗ █████╗ ██████╗ ████████╗███████╗████████╗██╗ ██╗██╗ ██╗██╗ █████╗ ",
|
|
12
|
-
"██╔════╝██╔══██╗██╔══██╗╚══██╔══╝██╔════╝╚══██╔══╝██║ ██║╚██╗ ██╔╝██║██╔══██╗",
|
|
13
|
-
"██║ ███████║██████╔╝ ██║ █████╗ ██║ ███████║ ╚████╔╝ ██║███████║",
|
|
14
|
-
"██║ ██╔══██║██╔══██╗ ██║ ██╔══╝ ██║ ██╔══██║ ╚██╔╝ ██║██╔══██║",
|
|
15
|
-
"╚██████╗██║ ██║██║ ██║ ██║ ███████╗ ██║ ██║ ██║ ██║ ██║██║ ██║",
|
|
16
|
-
" ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝",
|
|
17
|
-
];
|
|
18
|
-
|
|
19
|
-
/** Blue-grey gradient: dim at the edges, brighter through the middle. */
|
|
20
|
-
const GRADIENT: readonly [number, number, number][] = [
|
|
21
|
-
[90, 120, 150],
|
|
22
|
-
[120, 160, 200],
|
|
23
|
-
[150, 200, 240],
|
|
24
|
-
[120, 160, 200],
|
|
25
|
-
[90, 120, 150],
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
const RESET = "\u001b[0m";
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Returns the wordmark lines with per-column colour, or an empty array when the
|
|
32
|
-
* terminal is too narrow or colour is disabled.
|
|
33
|
-
*/
|
|
34
|
-
export function renderWordmark(columns = process.stdout.columns ?? 80): string[] {
|
|
35
|
-
if (process.env.NO_COLOR !== undefined) return [];
|
|
36
|
-
const width = WORDMARK[0]!.length;
|
|
37
|
-
if (columns < width) return [];
|
|
38
|
-
|
|
39
|
-
const coloured: string[] = [];
|
|
40
|
-
for (const row of WORDMARK) {
|
|
41
|
-
let line = "";
|
|
42
|
-
let current = -1;
|
|
43
|
-
for (let column = 0; column < row.length; column += 1) {
|
|
44
|
-
const band = Math.min(
|
|
45
|
-
GRADIENT.length - 1,
|
|
46
|
-
Math.floor((column / width) * GRADIENT.length),
|
|
47
|
-
);
|
|
48
|
-
const [r, g, b] = GRADIENT[band]!;
|
|
49
|
-
if (band !== current) {
|
|
50
|
-
line += `\u001b[38;2;${r};${g};${b}m`;
|
|
51
|
-
current = band;
|
|
52
|
-
}
|
|
53
|
-
line += row[column];
|
|
54
|
-
}
|
|
55
|
-
coloured.push(`${line}${RESET}`);
|
|
56
|
-
}
|
|
57
|
-
return coloured;
|
|
58
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* The console wordmark.
|
|
3
|
+
*
|
|
4
|
+
* Rendered with half-block characters so each letter is two pixel rows tall in
|
|
5
|
+
* one character cell, and coloured with a horizontal gradient across the
|
|
6
|
+
* Cartethyia blue-grey range. The caller decides whether to print it: a
|
|
7
|
+
* non-colour terminal, or one narrower than the wordmark, should skip it.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const WORDMARK: readonly string[] = [
|
|
11
|
+
" ██████╗ █████╗ ██████╗ ████████╗███████╗████████╗██╗ ██╗██╗ ██╗██╗ █████╗ ",
|
|
12
|
+
"██╔════╝██╔══██╗██╔══██╗╚══██╔══╝██╔════╝╚══██╔══╝██║ ██║╚██╗ ██╔╝██║██╔══██╗",
|
|
13
|
+
"██║ ███████║██████╔╝ ██║ █████╗ ██║ ███████║ ╚████╔╝ ██║███████║",
|
|
14
|
+
"██║ ██╔══██║██╔══██╗ ██║ ██╔══╝ ██║ ██╔══██║ ╚██╔╝ ██║██╔══██║",
|
|
15
|
+
"╚██████╗██║ ██║██║ ██║ ██║ ███████╗ ██║ ██║ ██║ ██║ ██║██║ ██║",
|
|
16
|
+
" ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
/** Blue-grey gradient: dim at the edges, brighter through the middle. */
|
|
20
|
+
const GRADIENT: readonly [number, number, number][] = [
|
|
21
|
+
[90, 120, 150],
|
|
22
|
+
[120, 160, 200],
|
|
23
|
+
[150, 200, 240],
|
|
24
|
+
[120, 160, 200],
|
|
25
|
+
[90, 120, 150],
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const RESET = "\u001b[0m";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Returns the wordmark lines with per-column colour, or an empty array when the
|
|
32
|
+
* terminal is too narrow or colour is disabled.
|
|
33
|
+
*/
|
|
34
|
+
export function renderWordmark(columns = process.stdout.columns ?? 80): string[] {
|
|
35
|
+
if (process.env.NO_COLOR !== undefined) return [];
|
|
36
|
+
const width = WORDMARK[0]!.length;
|
|
37
|
+
if (columns < width) return [];
|
|
38
|
+
|
|
39
|
+
const coloured: string[] = [];
|
|
40
|
+
for (const row of WORDMARK) {
|
|
41
|
+
let line = "";
|
|
42
|
+
let current = -1;
|
|
43
|
+
for (let column = 0; column < row.length; column += 1) {
|
|
44
|
+
const band = Math.min(
|
|
45
|
+
GRADIENT.length - 1,
|
|
46
|
+
Math.floor((column / width) * GRADIENT.length),
|
|
47
|
+
);
|
|
48
|
+
const [r, g, b] = GRADIENT[band]!;
|
|
49
|
+
if (band !== current) {
|
|
50
|
+
line += `\u001b[38;2;${r};${g};${b}m`;
|
|
51
|
+
current = band;
|
|
52
|
+
}
|
|
53
|
+
line += row[column];
|
|
54
|
+
}
|
|
55
|
+
coloured.push(`${line}${RESET}`);
|
|
56
|
+
}
|
|
57
|
+
return coloured;
|
|
58
|
+
}
|
package/src/boot/app.ts
CHANGED
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
import { createTokenCountHandler } from "../transport/dispatch/token-count";
|
|
46
46
|
|
|
47
47
|
import { createTransportPipeline } from "../transport/guards/pipeline";
|
|
48
|
+
import { defaultProcessConcurrencyController } from "../transport/guards/concurrency-guard";
|
|
48
49
|
import { createDrainHandler, isLoopbackPeer, tokenMatches } from "../transport/drain-endpoint";
|
|
49
50
|
import { createConsoleAccountResetHandler } from "../console/auth/account-reset";
|
|
50
51
|
import { resetConsoleAuthState } from "../console/auth/account-reset-store";
|
|
@@ -244,6 +245,10 @@ export function createGatewayApp(deps: GatewayAppDeps) {
|
|
|
244
245
|
deps.mode === "production" ? deps.shutdownCoordinator : undefined,
|
|
245
246
|
);
|
|
246
247
|
deps.runtimeMetricsSampler?.bindInFlight(() => requestStateStore.inFlightCount());
|
|
248
|
+
// The concurrency ceiling is derived (memory limit ÷ body budget), so it is
|
|
249
|
+
// published as a gauge: an operator reading "shedding at 16 in flight" needs
|
|
250
|
+
// to see that 16 IS the limit on this box, not that the process is broken.
|
|
251
|
+
deps.runtimeMetricsSampler?.bindConcurrencyLimit(defaultProcessConcurrencyController.limit());
|
|
247
252
|
// Shutdown ordering: abort in-flight proxy controllers first so the
|
|
248
253
|
// bounded drain observes cancellation and finalizers run before
|
|
249
254
|
// telemetry flush and pool close. The reason is threaded through so the
|
|
@@ -379,6 +384,16 @@ export function createGatewayApp(deps: GatewayAppDeps) {
|
|
|
379
384
|
telemetry: deps.telemetryBuffer,
|
|
380
385
|
...(deps.modelStrikes ? { modelStrikes: deps.modelStrikes } : {}),
|
|
381
386
|
...(deps.consoleApi?.redis ? { redis: deps.consoleApi.redis } : {}),
|
|
387
|
+
// Bound at construction so the guard holds the sampler's live snapshot
|
|
388
|
+
// rather than a value captured at boot.
|
|
389
|
+
...(deps.runtimeMetricsSampler
|
|
390
|
+
? { memorySnapshot: () => deps.runtimeMetricsSampler?.memorySnapshot() }
|
|
391
|
+
: {}),
|
|
392
|
+
// Reads the cached route snapshot, which the settings route invalidates
|
|
393
|
+
// when the switch is saved — so flipping it takes effect on the next
|
|
394
|
+
// request and adds no query to the proxy path.
|
|
395
|
+
isInvokeDisabled: async (tenantId) =>
|
|
396
|
+
(await deps.snapshotService.getSnapshot())?.invokeDisabledTenants?.has(tenantId),
|
|
382
397
|
});
|
|
383
398
|
transportPipeline.mountRoot(app);
|
|
384
399
|
const proxyDeps: ProviderProxyHandlerDeps = {
|
|
@@ -657,7 +672,7 @@ export function createGatewayApp(deps: GatewayAppDeps) {
|
|
|
657
672
|
createShareRouter({
|
|
658
673
|
db: deps.db,
|
|
659
674
|
shareStore: new DrizzleShareLinkStore(deps.db),
|
|
660
|
-
stats: createShareStatsPort({ redis: deps.consoleApi?.redis }),
|
|
675
|
+
stats: createShareStatsPort({ db: deps.db, redis: deps.consoleApi?.redis }),
|
|
661
676
|
resolveClientIp: (request) => {
|
|
662
677
|
const peer = peerAddresses.get(request);
|
|
663
678
|
return peer
|
package/src/boot/dependencies.ts
CHANGED
|
@@ -3,6 +3,9 @@ import { assertPoolFitsServerCapacity, bootDatabase, ensureMigrated } from "../p
|
|
|
3
3
|
import { poolMaxFromEnv } from "../persistence/db-pg";
|
|
4
4
|
import type { CartethyiaDatabase } from "../persistence/db-handle";
|
|
5
5
|
import { resolveRedisBackend, resolveRedisClient } from "../persistence/redis";
|
|
6
|
+
import { resolveProcessTopology, type ProcessTopology } from "../config/runtime";
|
|
7
|
+
import { createRedisMaintenanceLeaseStore } from "../workers/maintenance-lease";
|
|
8
|
+
import { resolveDbMode } from "../persistence/db-mode";
|
|
6
9
|
import type { RedisClient } from "../persistence/redis";
|
|
7
10
|
import {
|
|
8
11
|
bundledModelCatalog,
|
|
@@ -71,8 +74,6 @@ import { preferencesReaderFor } from "../persistence/tenant-preferences";
|
|
|
71
74
|
import { sweepExpiredCooldowns } from "../providers/operations/account-health-service";
|
|
72
75
|
import { DrizzleTelemetryStore } from "../persistence/stores/telemetry-store";
|
|
73
76
|
import { TelemetryPayloadCapture } from "../observability/payload-capture";
|
|
74
|
-
import { foldDayIndex, listEventDays } from "../observability/event-day-index";
|
|
75
|
-
import { resolveTelemetryCompactIntervalMs } from "../config/telemetry";
|
|
76
77
|
import { sweepExpiredPoolCooldowns } from "../network/pool-health-machine";
|
|
77
78
|
import { TelemetryBatchBuffer } from "../observability/telemetry-buffer";
|
|
78
79
|
import { RuntimeMetricsSampler } from "../observability/runtime-metrics";
|
|
@@ -86,6 +87,7 @@ import {
|
|
|
86
87
|
import { resolveSsrfPolicy, resolveTrustedProxyBoundary } from "../config/network";
|
|
87
88
|
import type { TrustedProxyBoundary } from "../config/network";
|
|
88
89
|
import { resolveTelemetryRetentionDays } from "../config/telemetry";
|
|
90
|
+
import { resolveConcurrencyTolerance } from "../config/resources";
|
|
89
91
|
import { CONFIG_SPEC, readInt, readList } from "../config/spec";
|
|
90
92
|
import type { ProviderAdapter } from "../providers/provider-contracts";
|
|
91
93
|
import type { ProviderRegistry } from "../providers/provider-registry";
|
|
@@ -145,9 +147,89 @@ const OAUTH_REFRESH_INTER_ITEM_DELAY_MS = 1_500;
|
|
|
145
147
|
const CLIENT_VERSION_MONITOR_INTERVAL_MS = 30 * 60_000;
|
|
146
148
|
const CLIENT_VERSION_INITIAL_DELAY_MS = 10_000;
|
|
147
149
|
|
|
150
|
+
/** Bound on the boot-time Redis probe: a start must fail, not hang. */
|
|
151
|
+
const REDIS_PROBE_TIMEOUT_MS = 2_000;
|
|
152
|
+
|
|
153
|
+
async function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
|
154
|
+
const { promise: timeout, reject } = Promise.withResolvers<never>();
|
|
155
|
+
const timer = setTimeout(() => reject(new Error("timeout")), ms);
|
|
156
|
+
try {
|
|
157
|
+
return await Promise.race([promise, timeout]);
|
|
158
|
+
} finally {
|
|
159
|
+
clearTimeout(timer);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Bounded Redis reachability probe: fails rather than hangs. */
|
|
164
|
+
async function redisIsReachable(): Promise<boolean> {
|
|
165
|
+
const client = resolveRedisClient();
|
|
166
|
+
if (!client) return false;
|
|
167
|
+
try {
|
|
168
|
+
const result = await withTimeout(client.ping(), REDIS_PROBE_TIMEOUT_MS);
|
|
169
|
+
return result === "PONG";
|
|
170
|
+
} catch {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Refuses to start a multi-process deployment whose shared state is not
|
|
177
|
+
* actually shared.
|
|
178
|
+
*
|
|
179
|
+
* Every process shares one port via `SO_REUSEPORT`, so the kernel balances
|
|
180
|
+
* across them — and each process keeps its own in-memory admission counters,
|
|
181
|
+
* quota cache, and cooldown map. Those are only correct when they live in
|
|
182
|
+
* Redis. Starting anyway does not fail loudly; it enforces every per-key limit
|
|
183
|
+
* N times over, which then reads as "the limit did not work".
|
|
184
|
+
*
|
|
185
|
+
* Redis is checked by PING with a hard bound, not merely for being configured:
|
|
186
|
+
* a configured-but-unreachable Redis is the failure mode that matters, and an
|
|
187
|
+
* unbounded connect at boot would hang the start instead of failing it.
|
|
188
|
+
*/
|
|
189
|
+
async function assertMultiProcessPreconditions(
|
|
190
|
+
topology: ProcessTopology,
|
|
191
|
+
redisReachable: () => Promise<boolean>,
|
|
192
|
+
): Promise<void> {
|
|
193
|
+
if (!topology.multiProcess) return;
|
|
194
|
+
if (process.platform !== "linux") {
|
|
195
|
+
throw Object.assign(
|
|
196
|
+
new Error(
|
|
197
|
+
`CARTETHYIA_PROCESS_COUNT=${topology.count} requires Linux (SO_REUSEPORT); ` +
|
|
198
|
+
`this process is on ${process.platform}`,
|
|
199
|
+
),
|
|
200
|
+
{ code: "multi_process_unsupported_platform" },
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
if (resolveDbMode() !== "full") {
|
|
204
|
+
throw Object.assign(
|
|
205
|
+
new Error(
|
|
206
|
+
`CARTETHYIA_PROCESS_COUNT=${topology.count} requires CARTETHYIA_DB_MODE=full (PostgreSQL); ` +
|
|
207
|
+
"two processes on one SQLite file corrupt it — there is no cross-process lock in the driver",
|
|
208
|
+
),
|
|
209
|
+
{ code: "multi_process_requires_postgres" },
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
const reachable = await redisReachable();
|
|
213
|
+
if (!reachable) {
|
|
214
|
+
throw Object.assign(
|
|
215
|
+
new Error(
|
|
216
|
+
`CARTETHYIA_PROCESS_COUNT=${topology.count} requires a REACHABLE Redis: admission counters, ` +
|
|
217
|
+
"quotas, and cooldowns are per-process without one, so every per-key limit would be " +
|
|
218
|
+
`enforced ${topology.count} times over`,
|
|
219
|
+
),
|
|
220
|
+
{ code: "multi_process_requires_redis" },
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
148
225
|
export async function buildProductionDeps(): Promise<ProductionDeps> {
|
|
149
226
|
const startupStartedAt = performance.now();
|
|
150
227
|
const startupMemoryBefore = process.memoryUsage();
|
|
228
|
+
// Resolved first and validated before any connection is opened: a
|
|
229
|
+
// half-configured multi-process deploy that starts anyway looks healthy
|
|
230
|
+
// until a per-key limit is enforced N times over.
|
|
231
|
+
const topology = resolveProcessTopology();
|
|
232
|
+
await assertMultiProcessPreconditions(topology, redisIsReachable);
|
|
151
233
|
const handle = await measureStartupPhase("database-connect", bootDatabase);
|
|
152
234
|
const db = handle.db;
|
|
153
235
|
// One rule, no mode flag: REDIS_URL set → shared client, unset → memory.
|
|
@@ -157,7 +239,7 @@ export async function buildProductionDeps(): Promise<ProductionDeps> {
|
|
|
157
239
|
await ensureMigrated();
|
|
158
240
|
// Pool sizing is an external-server concern; the embedded backend has no
|
|
159
241
|
// connection budget to fit into.
|
|
160
|
-
if (handle.kind === "pg") await assertPoolFitsServerCapacity(poolMaxFromEnv());
|
|
242
|
+
if (handle.kind === "pg") await assertPoolFitsServerCapacity(poolMaxFromEnv(), topology.count);
|
|
161
243
|
});
|
|
162
244
|
await measureStartupPhase("provider-seed", () => seedBundledProviders(db));
|
|
163
245
|
// Boot-only: retire global rows for providers the bundle has dropped, so a
|
|
@@ -193,7 +275,6 @@ export async function buildProductionDeps(): Promise<ProductionDeps> {
|
|
|
193
275
|
tenantId: string | null,
|
|
194
276
|
): Promise<readonly { accountId: string; inflight: number }[]> => {
|
|
195
277
|
if (providerId === "" || tenantId === null) return [];
|
|
196
|
-
const snapshot = await routingEngine.accountInflightSnapshot();
|
|
197
278
|
const accounts = await new DrizzleProviderCatalogStore(db, {
|
|
198
279
|
telemetryBuffer,
|
|
199
280
|
bundledModelCatalog: bundledCatalog,
|
|
@@ -202,10 +283,20 @@ export async function buildProductionDeps(): Promise<ProductionDeps> {
|
|
|
202
283
|
snapshotInvalidator: snapshotService,
|
|
203
284
|
}).listAccounts(tenantId, providerId);
|
|
204
285
|
const owned = new Set(accounts.map((account) => account.id));
|
|
286
|
+
// Scoped read: only the provider's own candidates for this tenant are
|
|
287
|
+
// queried, so the console read costs one exact MGET over the accounts it
|
|
288
|
+
// is about to report on — not a fleet-wide `KEYS` scan whose cost is the
|
|
289
|
+
// size of every bucket every process ever wrote.
|
|
290
|
+
const routeSnapshot = await snapshotService.getSnapshot();
|
|
291
|
+
const ownedCandidates = routeSnapshot.candidates.filter(
|
|
292
|
+
(candidate) =>
|
|
293
|
+
candidate.provider_id === providerId &&
|
|
294
|
+
candidate.provider_account_id !== undefined &&
|
|
295
|
+
owned.has(candidate.provider_account_id),
|
|
296
|
+
);
|
|
297
|
+
const snapshot = await routingEngine.accountInflightSnapshot(ownedCandidates);
|
|
205
298
|
const totals = new Map<string, number>();
|
|
206
299
|
for (const [bucket, count] of snapshot) {
|
|
207
|
-
const prefix = `${providerId}:`;
|
|
208
|
-
if (!bucket.startsWith(prefix)) continue;
|
|
209
300
|
const accountId = bucket.slice(bucket.lastIndexOf(":") + 1);
|
|
210
301
|
if (!owned.has(accountId)) continue;
|
|
211
302
|
totals.set(accountId, (totals.get(accountId) ?? 0) + count);
|
|
@@ -260,6 +351,13 @@ export async function buildProductionDeps(): Promise<ProductionDeps> {
|
|
|
260
351
|
const children = await store.sumChildrenConsumed(apiKeyId);
|
|
261
352
|
return (row.lifetimeTokensConsumed ?? 0) + children;
|
|
262
353
|
},
|
|
354
|
+
// Slack above every key's concurrency limit. A tenant preference an
|
|
355
|
+
// operator sets in the runtime settings modal, falling back to the
|
|
356
|
+
// deployment default so an unset row does not silently lose the allowance.
|
|
357
|
+
(tenantId) =>
|
|
358
|
+
preferencesReaderFor(db)
|
|
359
|
+
.readPreferences(tenantId)
|
|
360
|
+
.then((prefs) => prefs?.concurrencyTolerance ?? resolveConcurrencyTolerance()),
|
|
263
361
|
);
|
|
264
362
|
const ipStore = redis
|
|
265
363
|
? new RedisIpAbuseStore(redis)
|
|
@@ -282,8 +380,14 @@ export async function buildProductionDeps(): Promise<ProductionDeps> {
|
|
|
282
380
|
// One list covers every periodic task: a small deployment can cut the
|
|
283
381
|
// heaviest sweeps (quota refresh, OAuth refresh, client-version polling)
|
|
284
382
|
// without the codebase growing a flag per task.
|
|
383
|
+
// Leases coordinate the SHARED sweeps across processes. Absent Redis there is
|
|
384
|
+
// nothing to coordinate with, and the registry's local one-at-a-time rule is
|
|
385
|
+
// the correct (and only) guard.
|
|
285
386
|
const scheduledTasks = new ScheduledTaskRegistry(
|
|
286
387
|
readList("CARTETHYIA_DISABLED_TASKS", CONFIG_SPEC.CARTETHYIA_DISABLED_TASKS) ?? [],
|
|
388
|
+
{
|
|
389
|
+
...(redis ? { lease: createRedisMaintenanceLeaseStore(redis) } : {}),
|
|
390
|
+
},
|
|
287
391
|
);
|
|
288
392
|
if (redis) {
|
|
289
393
|
scheduledTasks.register({
|
|
@@ -295,6 +399,8 @@ export async function buildProductionDeps(): Promise<ProductionDeps> {
|
|
|
295
399
|
scheduledTasks.register({
|
|
296
400
|
name: "account-health-sweep",
|
|
297
401
|
intervalMs: 30_000,
|
|
402
|
+
// Recovers cooldowns deployment-wide and invalidates the shared snapshot.
|
|
403
|
+
shared: true,
|
|
298
404
|
run: async () => {
|
|
299
405
|
const recovered = await sweepExpiredCooldowns(db);
|
|
300
406
|
const poolRecovered = await sweepExpiredPoolCooldowns(db);
|
|
@@ -306,27 +412,11 @@ export async function buildProductionDeps(): Promise<ProductionDeps> {
|
|
|
306
412
|
scheduledTasks.register({
|
|
307
413
|
name: "telemetry-payload-cleanup",
|
|
308
414
|
intervalMs: 15 * 60_000,
|
|
415
|
+
// Deletes payload frames for every tenant: two passes over the same
|
|
416
|
+
// batches is duplicated work, not speedup.
|
|
417
|
+
shared: true,
|
|
309
418
|
run: () => payloadCapture.cleanupExpired(),
|
|
310
419
|
});
|
|
311
|
-
scheduledTasks.register({
|
|
312
|
-
name: "telemetry-index-fold",
|
|
313
|
-
intervalMs: resolveTelemetryCompactIntervalMs(),
|
|
314
|
-
// Brings each day's index up to date with the bytes appended since the last
|
|
315
|
-
// pass, so a usage read is a small JSON read rather than a parse of the
|
|
316
|
-
// whole period. Folding is incremental and idempotent: a crash only leaves
|
|
317
|
-
// the watermark behind, and the next pass re-reads the tail.
|
|
318
|
-
run: async () => {
|
|
319
|
-
const days = await listEventDays();
|
|
320
|
-
let folded = 0;
|
|
321
|
-
for (const day of days) {
|
|
322
|
-
const result = await foldDayIndex(day).catch(() => ({ folded: 0 }));
|
|
323
|
-
folded += result.folded;
|
|
324
|
-
}
|
|
325
|
-
if (folded > 0) {
|
|
326
|
-
log.info(`[telemetry] indexed ${folded} event(s) across ${days.length} day(s)`);
|
|
327
|
-
}
|
|
328
|
-
},
|
|
329
|
-
});
|
|
330
420
|
scheduledTasks.register({
|
|
331
421
|
name: "console-session-sweep",
|
|
332
422
|
// Sessions live 48 h, so an hourly sweep deleted nothing 47 times out of
|
|
@@ -334,11 +424,15 @@ export async function buildProductionDeps(): Promise<ProductionDeps> {
|
|
|
334
424
|
// be expired. Half the session lifetime is the point where a pass can
|
|
335
425
|
// actually find work, and still leaves a full grace period after it.
|
|
336
426
|
intervalMs: 12 * 60 * 60_000,
|
|
427
|
+
shared: true,
|
|
337
428
|
run: () => deleteExpiredSessions(db),
|
|
338
429
|
});
|
|
339
430
|
scheduledTasks.register({
|
|
340
431
|
name: "telemetry-retention",
|
|
341
432
|
intervalMs: 6 * 60 * 60_000,
|
|
433
|
+
// Retention deletes rows and files for the whole deployment; a second
|
|
434
|
+
// concurrent pass would churn the same batches.
|
|
435
|
+
shared: true,
|
|
342
436
|
run: () =>
|
|
343
437
|
telemetryStore.pruneTelemetry(
|
|
344
438
|
new Date(Date.now() - resolveTelemetryRetentionDays() * 24 * 60 * 60_000),
|
|
@@ -365,7 +459,16 @@ export async function buildProductionDeps(): Promise<ProductionDeps> {
|
|
|
365
459
|
modelCatalogEntries: () => modelCatalogCacheSize(),
|
|
366
460
|
// Late-bound via bindInFlight once the request state store exists.
|
|
367
461
|
inFlight: () => 0,
|
|
462
|
+
// The buffer is built below; the closure keeps the sampler from holding a
|
|
463
|
+
// reference during construction.
|
|
464
|
+
telemetryBacklog: () => telemetryBuffer?.snapshot(),
|
|
368
465
|
});
|
|
466
|
+
// One sample NOW, not on the first tick. The headroom guard reads
|
|
467
|
+
// `memorySnapshot()` on every gateway request and treats `undefined` as
|
|
468
|
+
// "unknown" — so without this the guard would spend its first interval
|
|
469
|
+
// (seconds, at the configured cadence) unable to shed at all, which is
|
|
470
|
+
// exactly the window a cold start is most likely to be short of memory.
|
|
471
|
+
runtimeMetricsSampler.sample();
|
|
369
472
|
scheduledTasks.register({
|
|
370
473
|
name: "runtime-metrics",
|
|
371
474
|
intervalMs: readInt(
|
|
@@ -24,6 +24,30 @@ export function resolvePackagePath(...segments: string[]): string {
|
|
|
24
24
|
return resolve(PACKAGE_ROOT, ...segments);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/** Outfile `bun build --compile` writes when no `--outfile` is given. */
|
|
28
|
+
export const DEFAULT_BINARY_OUTFILE = "dist/cartethyia";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The artifact path `bun build --compile` actually writes for `outfile`.
|
|
32
|
+
*
|
|
33
|
+
* Bun appends `.exe` when it compiles for Windows, so the extensionless
|
|
34
|
+
* `dist/cartethyia` handed to it never exists on that platform. This is the one
|
|
35
|
+
* place that knows where the compiled binary lands; `scripts/build/` resolves
|
|
36
|
+
* its target from here instead of re-deriving the name, because a launcher that
|
|
37
|
+
* probed the requested path reported "run bun run build first" immediately
|
|
38
|
+
* after a successful build.
|
|
39
|
+
*
|
|
40
|
+
* It sits in `src/config/` rather than `scripts/build/` because the launcher
|
|
41
|
+
* ships in the npm package and `scripts/build/` does not: a shipped module that
|
|
42
|
+
* imported the build script crashed every installed copy of `carte` with
|
|
43
|
+
* `Cannot find module '../build/compile'`.
|
|
44
|
+
*/
|
|
45
|
+
export function compiledBinaryPath(outfile: string = DEFAULT_BINARY_OUTFILE): string {
|
|
46
|
+
return process.platform === "win32" && !outfile.toLowerCase().endsWith(".exe")
|
|
47
|
+
? `${outfile}.exe`
|
|
48
|
+
: outfile;
|
|
49
|
+
}
|
|
50
|
+
|
|
27
51
|
/** User-writable directory for the npm-installed CLI's configuration. */
|
|
28
52
|
export function resolveUserConfigDir(env: NodeJS.ProcessEnv = process.env): string {
|
|
29
53
|
if (process.platform === "win32") {
|