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
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical resource policy: how much memory this process may use, and how
|
|
3
|
+
* much work it may hold at once.
|
|
4
|
+
*
|
|
5
|
+
* These two numbers were previously decided in three unrelated places — a
|
|
6
|
+
* hardcoded cap in `concurrency-guard.ts`, a host-wide `os.totalmem()` in the
|
|
7
|
+
* headroom guard, and a fallback constant in `runtime-metrics.ts` — which
|
|
8
|
+
* meant a container limited to 512 MiB admitted the same concurrency as a 4 GiB
|
|
9
|
+
* one and only started shedding once the *host* was nearly full. On a 1-core /
|
|
10
|
+
* 2 GiB VPS that is the difference between shedding early and being
|
|
11
|
+
* OOM-killed, and the OOM kills the dashboard too — the one surface that would
|
|
12
|
+
* have explained what happened.
|
|
13
|
+
*
|
|
14
|
+
* Everything here is derived from one limit and one body ceiling, so the
|
|
15
|
+
* numbers cannot disagree with each other:
|
|
16
|
+
*
|
|
17
|
+
* concurrency = floor(limit × BUDGET_FRACTION / (TRANSIENT_COPIES × body))
|
|
18
|
+
*
|
|
19
|
+
* The transient-copies factor is why this is not simply "limit / body": a
|
|
20
|
+
* request body is held more than once between the socket read, the JSON parse,
|
|
21
|
+
* and the upstream re-serialization, so budgeting one copy per request
|
|
22
|
+
* understates the real hold by a factor of about two.
|
|
23
|
+
*/
|
|
24
|
+
import { CONFIG_SPEC, readOptionalInt } from "./spec";
|
|
25
|
+
import { resolveMaxBodyBytes } from "./runtime";
|
|
26
|
+
|
|
27
|
+
/** Used when no limit is discoverable at all (native dev, macOS, Windows). */
|
|
28
|
+
export const FALLBACK_MEMORY_LIMIT_BYTES = 512 * 1024 * 1024;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Ceiling on concurrent in-flight requests per process.
|
|
32
|
+
*
|
|
33
|
+
* A hard ceiling even when memory would allow more: this process is one core,
|
|
34
|
+
* and past this point queueing delay, not memory, is what degrades. It also
|
|
35
|
+
* bounds every per-request structure that is sized by concurrency.
|
|
36
|
+
*/
|
|
37
|
+
export const MAX_PROCESS_CONCURRENCY = 64;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* How many copies of a request body are resident at once.
|
|
41
|
+
*
|
|
42
|
+
* Measured, not assumed: the socket read buffer, the parsed object graph, and
|
|
43
|
+
* the upstream re-serialization each hold a body-sized allocation, and garbage
|
|
44
|
+
* collection lags rather than keeping them to one. Budgeting one copy halves
|
|
45
|
+
* the real peak and lets the process admit twice the concurrency it can hold.
|
|
46
|
+
*/
|
|
47
|
+
export const REQUEST_BODY_TRANSIENT_COPIES = 2;
|
|
48
|
+
|
|
49
|
+
/** Share of the memory limit all in-flight request bodies together may hold. */
|
|
50
|
+
export const REQUEST_BODY_BUDGET_FRACTION = 0.25;
|
|
51
|
+
|
|
52
|
+
/** Share of the memory limit kept free for allocation spikes and GC slack. */
|
|
53
|
+
export const MEMORY_HEADROOM_FRACTION = 0.2;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Concurrency this process should admit, derived from the memory limit and the
|
|
57
|
+
* body ceiling rather than guessed.
|
|
58
|
+
*
|
|
59
|
+
* Worked examples, which are the point of having one formula:
|
|
60
|
+
* - 2 GiB limit, 4 MiB bodies → 64 (the ceiling; memory is not the binder)
|
|
61
|
+
* - 512 MiB limit, 4 MiB bodies → 16
|
|
62
|
+
* - 256 MiB limit, 16 MiB bodies → 2
|
|
63
|
+
*
|
|
64
|
+
* An explicit override is honoured, clamped to 1..MAX_PROCESS_CONCURRENCY. It
|
|
65
|
+
* is the operator's call and is never silently raised back to the derived
|
|
66
|
+
* value; exceeding the derived figure is a deliberate choice, so it is
|
|
67
|
+
* reported at the call site rather than corrected here.
|
|
68
|
+
*/
|
|
69
|
+
export function deriveProcessConcurrency(options: {
|
|
70
|
+
readonly memoryLimitBytes: number;
|
|
71
|
+
readonly maxBodyBytes: number;
|
|
72
|
+
readonly override?: number | undefined;
|
|
73
|
+
}): number {
|
|
74
|
+
if (options.override !== undefined) {
|
|
75
|
+
if (!Number.isInteger(options.override)) return 1;
|
|
76
|
+
return Math.min(MAX_PROCESS_CONCURRENCY, Math.max(1, options.override));
|
|
77
|
+
}
|
|
78
|
+
const body = Math.max(1, options.maxBodyBytes);
|
|
79
|
+
const perRequest = REQUEST_BODY_TRANSIENT_COPIES * body;
|
|
80
|
+
const budget = Math.max(1, Math.floor(options.memoryLimitBytes * REQUEST_BODY_BUDGET_FRACTION));
|
|
81
|
+
const derived = Math.floor(budget / perRequest);
|
|
82
|
+
return Math.min(MAX_PROCESS_CONCURRENCY, Math.max(1, derived));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Bytes kept free below the memory limit.
|
|
87
|
+
*
|
|
88
|
+
* A guard that only reacts once RSS equals the limit has already lost: the
|
|
89
|
+
* allocation that breaches it happens between two samples, and the spike that
|
|
90
|
+
* matters — an upstream response landing all at once — arrives in far less
|
|
91
|
+
* than one sample interval.
|
|
92
|
+
*/
|
|
93
|
+
export function deriveMemoryHeadroomBytes(memoryLimitBytes: number): number {
|
|
94
|
+
return Math.ceil(memoryLimitBytes * MEMORY_HEADROOM_FRACTION);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The optional operator override, read through the config spec so "unset" and
|
|
99
|
+
* "out of range" are treated the way every other knob treats them.
|
|
100
|
+
*/
|
|
101
|
+
export function resolveProcessConcurrencyOverride(): number | undefined {
|
|
102
|
+
return readOptionalInt(
|
|
103
|
+
"CARTETHYIA_PROCESS_CONCURRENCY_LIMIT",
|
|
104
|
+
CONFIG_SPEC.CARTETHYIA_PROCESS_CONCURRENCY_LIMIT,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Slack added on top of every key's concurrency limit.
|
|
110
|
+
*
|
|
111
|
+
* Operator configuration, not something a key carries about itself: it is
|
|
112
|
+
* deliberate hidden allowance so a run that lands a few requests over the
|
|
113
|
+
* number an operator chose is not refused at that number. The effective
|
|
114
|
+
* ceiling is `limit + tolerance`, and only requests past it are refused.
|
|
115
|
+
*/
|
|
116
|
+
export function resolveConcurrencyTolerance(): number {
|
|
117
|
+
const value = readOptionalInt(
|
|
118
|
+
"CARTETHYIA_CONCURRENCY_TOLERANCE",
|
|
119
|
+
CONFIG_SPEC.CARTETHYIA_CONCURRENCY_TOLERANCE,
|
|
120
|
+
);
|
|
121
|
+
return value === undefined ? 0 : Math.max(0, value);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The effective concurrency for this process.
|
|
126
|
+
*
|
|
127
|
+
* Takes the limit as an argument rather than resolving it here: the limit
|
|
128
|
+
* owner is `resolveMemoryLimitBytes()` in `observability/runtime-metrics.ts`,
|
|
129
|
+
* and duplicating that resolution would let the two drift.
|
|
130
|
+
*/
|
|
131
|
+
export function resolveProcessConcurrency(memoryLimitBytes: number): number {
|
|
132
|
+
return deriveProcessConcurrency({
|
|
133
|
+
memoryLimitBytes,
|
|
134
|
+
maxBodyBytes: resolveMaxBodyBytes(),
|
|
135
|
+
override: resolveProcessConcurrencyOverride(),
|
|
136
|
+
});
|
|
137
|
+
}
|
package/src/config/runtime.ts
CHANGED
|
@@ -1,76 +1,133 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HTTP listener, dashboard runtime, and graceful-shutdown drain resolvers.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { CONFIG_SPEC, readInt, readOptionalText } from "./spec";
|
|
6
|
-
import { resolvePackagePath } from "./package-paths";
|
|
7
|
-
|
|
8
|
-
/** Resolves the HTTP listener port from the process environment. */
|
|
9
|
-
export function resolvePort(): number {
|
|
10
|
-
return readInt("PORT", CONFIG_SPEC.PORT);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/** Resolves the dashboard static asset directory. */
|
|
14
|
-
export function resolveDashboardDist(): string {
|
|
15
|
-
const raw = process.env.DASHBOARD_DIST?.trim();
|
|
16
|
-
if (!raw) return resolvePackagePath("dist", "dashboard");
|
|
17
|
-
return raw;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Resolves the configured default gateway API key (`CARTETHYIA_API_KEY`), if
|
|
22
|
-
* one is set. Consumers that need the raw secret — first-boot seeding and the
|
|
23
|
-
* console's key recovery from existing installations — read it here instead of
|
|
24
|
-
* probing `process.env` directly.
|
|
25
|
-
*/
|
|
26
|
-
export function resolveDefaultGatewayApiKey(): string | undefined {
|
|
27
|
-
return readOptionalText("CARTETHYIA_API_KEY", CONFIG_SPEC.CARTETHYIA_API_KEY);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Resolves the request-body ceiling shared by the Bun listener and ingress middleware. */
|
|
31
|
-
export function resolveMaxBodyBytes(): number {
|
|
32
|
-
return readInt("CARTETHYIA_SERVER_MAX_BODY_BYTES", CONFIG_SPEC.CARTETHYIA_SERVER_MAX_BODY_BYTES);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** Resolves the listener idle-socket timeout (seconds). */
|
|
36
|
-
export function resolveIdleTimeout(): number {
|
|
37
|
-
return readInt("CARTETHYIA_SERVER_IDLE_TIMEOUT", CONFIG_SPEC.CARTETHYIA_SERVER_IDLE_TIMEOUT);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Resolves the graceful-shutdown drain window: time for in-flight requests to finish before abort. */
|
|
41
|
-
export function resolveShutdownDrainWindowMs(): number {
|
|
42
|
-
return readInt(
|
|
43
|
-
"CARTETHYIA_SHUTDOWN_DRAIN_WINDOW_MS",
|
|
44
|
-
CONFIG_SPEC.CARTETHYIA_SHUTDOWN_DRAIN_WINDOW_MS,
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Resolves the operator drain token, or `undefined` when the drain route is disabled. */
|
|
49
|
-
export function resolveDrainToken(): string | undefined {
|
|
50
|
-
return readOptionalText("CARTETHYIA_DRAIN_TOKEN", CONFIG_SPEC.CARTETHYIA_DRAIN_TOKEN);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Whether Elysia precompiles route handlers at startup (AOT).
|
|
55
|
-
*
|
|
56
|
-
* Precompilation takes the eager validator branch, which calls TypeBox's
|
|
57
|
-
* `SchemaCompile(schema)` and then reads `.buildResult.external` off the
|
|
58
|
-
* result. In a source-run process that field is absent — the lazy
|
|
59
|
-
* `require("typebox/schema")` resolves to a namespace that does not carry it —
|
|
60
|
-
* and the process dies at the first schema-bearing route with
|
|
61
|
-
* `undefined is not an object (evaluating 'this.tb.buildResult.external')`.
|
|
62
|
-
* `scripts/build/aot.ts` avoids this by rewriting the call site and bundling
|
|
63
|
-
* the namespaces statically, so only the built artifact can actually take the
|
|
64
|
-
* eager path.
|
|
65
|
-
*
|
|
66
|
-
* So: precompilation is on for production *builds*, and off for a source run
|
|
67
|
-
* even when `NODE_ENV=production`. A deployment that runs
|
|
68
|
-
* `bun run src/main.ts` must not be pushed onto the eager branch.
|
|
69
|
-
*
|
|
70
|
-
* `CARTETHYIA_SOURCE_RUN` marks a source run explicitly. It is set by
|
|
71
|
-
* `scripts/deploy/*.sh`, which are the only supported way to run from source.
|
|
72
|
-
*/
|
|
73
|
-
export function resolveElysiaPrecompile(): boolean {
|
|
74
|
-
if (process.env.CARTETHYIA_SOURCE_RUN === "1") return false;
|
|
75
|
-
return process.env.NODE_ENV === "production";
|
|
76
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* HTTP listener, dashboard runtime, and graceful-shutdown drain resolvers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { CONFIG_SPEC, readInt, readOptionalText } from "./spec";
|
|
6
|
+
import { resolvePackagePath } from "./package-paths";
|
|
7
|
+
|
|
8
|
+
/** Resolves the HTTP listener port from the process environment. */
|
|
9
|
+
export function resolvePort(): number {
|
|
10
|
+
return readInt("PORT", CONFIG_SPEC.PORT);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Resolves the dashboard static asset directory. */
|
|
14
|
+
export function resolveDashboardDist(): string {
|
|
15
|
+
const raw = process.env.DASHBOARD_DIST?.trim();
|
|
16
|
+
if (!raw) return resolvePackagePath("dist", "dashboard");
|
|
17
|
+
return raw;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolves the configured default gateway API key (`CARTETHYIA_API_KEY`), if
|
|
22
|
+
* one is set. Consumers that need the raw secret — first-boot seeding and the
|
|
23
|
+
* console's key recovery from existing installations — read it here instead of
|
|
24
|
+
* probing `process.env` directly.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveDefaultGatewayApiKey(): string | undefined {
|
|
27
|
+
return readOptionalText("CARTETHYIA_API_KEY", CONFIG_SPEC.CARTETHYIA_API_KEY);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Resolves the request-body ceiling shared by the Bun listener and ingress middleware. */
|
|
31
|
+
export function resolveMaxBodyBytes(): number {
|
|
32
|
+
return readInt("CARTETHYIA_SERVER_MAX_BODY_BYTES", CONFIG_SPEC.CARTETHYIA_SERVER_MAX_BODY_BYTES);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Resolves the listener idle-socket timeout (seconds). */
|
|
36
|
+
export function resolveIdleTimeout(): number {
|
|
37
|
+
return readInt("CARTETHYIA_SERVER_IDLE_TIMEOUT", CONFIG_SPEC.CARTETHYIA_SERVER_IDLE_TIMEOUT);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Resolves the graceful-shutdown drain window: time for in-flight requests to finish before abort. */
|
|
41
|
+
export function resolveShutdownDrainWindowMs(): number {
|
|
42
|
+
return readInt(
|
|
43
|
+
"CARTETHYIA_SHUTDOWN_DRAIN_WINDOW_MS",
|
|
44
|
+
CONFIG_SPEC.CARTETHYIA_SHUTDOWN_DRAIN_WINDOW_MS,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Resolves the operator drain token, or `undefined` when the drain route is disabled. */
|
|
49
|
+
export function resolveDrainToken(): string | undefined {
|
|
50
|
+
return readOptionalText("CARTETHYIA_DRAIN_TOKEN", CONFIG_SPEC.CARTETHYIA_DRAIN_TOKEN);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Whether Elysia precompiles route handlers at startup (AOT).
|
|
55
|
+
*
|
|
56
|
+
* Precompilation takes the eager validator branch, which calls TypeBox's
|
|
57
|
+
* `SchemaCompile(schema)` and then reads `.buildResult.external` off the
|
|
58
|
+
* result. In a source-run process that field is absent — the lazy
|
|
59
|
+
* `require("typebox/schema")` resolves to a namespace that does not carry it —
|
|
60
|
+
* and the process dies at the first schema-bearing route with
|
|
61
|
+
* `undefined is not an object (evaluating 'this.tb.buildResult.external')`.
|
|
62
|
+
* `scripts/build/aot.ts` avoids this by rewriting the call site and bundling
|
|
63
|
+
* the namespaces statically, so only the built artifact can actually take the
|
|
64
|
+
* eager path.
|
|
65
|
+
*
|
|
66
|
+
* So: precompilation is on for production *builds*, and off for a source run
|
|
67
|
+
* even when `NODE_ENV=production`. A deployment that runs
|
|
68
|
+
* `bun run src/main.ts` must not be pushed onto the eager branch.
|
|
69
|
+
*
|
|
70
|
+
* `CARTETHYIA_SOURCE_RUN` marks a source run explicitly. It is set by
|
|
71
|
+
* `scripts/deploy/*.sh`, which are the only supported way to run from source.
|
|
72
|
+
*/
|
|
73
|
+
export function resolveElysiaPrecompile(): boolean {
|
|
74
|
+
if (process.env.CARTETHYIA_SOURCE_RUN === "1") return false;
|
|
75
|
+
return process.env.NODE_ENV === "production";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* How many gateway processes this deployment runs, and which one this is.
|
|
80
|
+
*
|
|
81
|
+
* Multi-process is opt-in and deliberately narrow. Every process shares the
|
|
82
|
+
* port through `SO_REUSEPORT`, so the kernel load-balances across them — which
|
|
83
|
+
* means state that used to be process-local is now per-process unless it lives
|
|
84
|
+
* somewhere shared. A second process without that sharing is not "twice the
|
|
85
|
+
* capacity", it is two processes disagreeing:
|
|
86
|
+
*
|
|
87
|
+
* - **PostgreSQL, not SQLite.** Two processes on one SQLite file corrupt it;
|
|
88
|
+
* there is no cross-process lock in the driver.
|
|
89
|
+
* - **Redis configured AND reachable.** Admission counters, quotas, and
|
|
90
|
+
* cooldowns are per-process without it, so a per-key RPM limit silently
|
|
91
|
+
* becomes N× the configured value.
|
|
92
|
+
* - **Linux.** `SO_REUSEPORT` is the mechanism; elsewhere it is a silent no-op
|
|
93
|
+
* and the second process simply fails to bind.
|
|
94
|
+
*
|
|
95
|
+
* These are checked at resolution time rather than left to fail at bind time,
|
|
96
|
+
* because a half-configured multi-process deploy that starts anyway looks fine
|
|
97
|
+
* until a limit is enforced twice.
|
|
98
|
+
*/
|
|
99
|
+
export interface ProcessTopology {
|
|
100
|
+
readonly count: number;
|
|
101
|
+
readonly index: number;
|
|
102
|
+
/** True when more than one process serves this deployment. */
|
|
103
|
+
readonly multiProcess: boolean;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Resolves the topology from the environment.
|
|
108
|
+
*
|
|
109
|
+
* `count > 1` requires an explicit index: a process that does not know which
|
|
110
|
+
* one it is cannot own a shard of anything, and a guessed index would have two
|
|
111
|
+
* processes claiming the same one.
|
|
112
|
+
*/
|
|
113
|
+
export function resolveProcessTopology(): ProcessTopology {
|
|
114
|
+
const count = readInt("CARTETHYIA_PROCESS_COUNT", CONFIG_SPEC.CARTETHYIA_PROCESS_COUNT);
|
|
115
|
+
if (count === 1) return { count: 1, index: 0, multiProcess: false };
|
|
116
|
+
// Mandatory when the count is above one: a process that does not know which
|
|
117
|
+
// one it is cannot own a shard of anything, and a defaulted index would have
|
|
118
|
+
// two processes claiming the same number. The spec's default of 0 cannot
|
|
119
|
+
// express "absent", so the raw value is checked here before it is read.
|
|
120
|
+
const rawIndex = process.env.CARTETHYIA_PROCESS_INDEX?.trim();
|
|
121
|
+
if (rawIndex === undefined || rawIndex.length === 0) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`CARTETHYIA_PROCESS_INDEX is required when CARTETHYIA_PROCESS_COUNT is ${count} (0 – ${count - 1})`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
const index = readInt("CARTETHYIA_PROCESS_INDEX", CONFIG_SPEC.CARTETHYIA_PROCESS_INDEX);
|
|
127
|
+
if (index >= count) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`CARTETHYIA_PROCESS_INDEX must be an integer between 0 and ${count - 1} (got "${rawIndex}")`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return { count, index, multiProcess: true };
|
|
133
|
+
}
|
package/src/config/spec.ts
CHANGED
|
@@ -110,6 +110,28 @@ export const CONFIG_SPEC = {
|
|
|
110
110
|
min: 0,
|
|
111
111
|
max: 600_000,
|
|
112
112
|
},
|
|
113
|
+
// Optional ceiling on in-flight requests per process. Left unset, the limit
|
|
114
|
+
// is derived from the memory limit and the body ceiling (see
|
|
115
|
+
// config/resources.ts): 2 GiB/4 MiB → 64, 512 MiB/4 MiB → 16, 256 MiB/16 MiB
|
|
116
|
+
// → 2. Set it to admit more than memory alone would allow — which is the
|
|
117
|
+
// operator's call, and is reported rather than corrected — or less, to keep
|
|
118
|
+
// a slow upstream from being handed more concurrency than it can serve.
|
|
119
|
+
// Optional-int, not int-with-a-zero-default: "unset" and "explicitly zero"
|
|
120
|
+
// must not be the same thing. Zero is not a valid ceiling, so a default of 0
|
|
121
|
+
// would need a second sentinel to mean "derive".
|
|
122
|
+
CARTETHYIA_PROCESS_CONCURRENCY_LIMIT: { kind: "optional-int", min: 1, max: 64 },
|
|
123
|
+
// Slack above each key's concurrency limit. Effective ceiling is
|
|
124
|
+
// limit + tolerance; only requests past it are refused.
|
|
125
|
+
CARTETHYIA_CONCURRENCY_TOLERANCE: { kind: "optional-int", min: 0, max: 1024 },
|
|
126
|
+
// How many gateway processes serve this deployment, and which one this is.
|
|
127
|
+
// Count > 1 requires Linux, CARTETHYIA_DB_MODE=full, and a REACHABLE Redis —
|
|
128
|
+
// checked at boot, because a half-configured multi-process deploy starts
|
|
129
|
+
// fine and then enforces every per-key limit N times over. Lite stays
|
|
130
|
+
// single-process: two processes on one SQLite file corrupt it.
|
|
131
|
+
CARTETHYIA_PROCESS_COUNT: { kind: "int", default: 1, min: 1, max: 16 },
|
|
132
|
+
// Which process this is (0 – count-1). Mandatory when count > 1: a process
|
|
133
|
+
// that does not know its index cannot own a shard of anything.
|
|
134
|
+
CARTETHYIA_PROCESS_INDEX: { kind: "int", default: 0, min: 0, max: 15 },
|
|
113
135
|
// Optional secret for the operator drain endpoint (`POST /admin/drain`). When
|
|
114
136
|
// set, a process can be stopped gracefully on platforms where a catchable
|
|
115
137
|
// signal cannot be delivered to it (notably Windows, where `process.kill` of
|
|
@@ -130,9 +152,12 @@ export const CONFIG_SPEC = {
|
|
|
130
152
|
min: 1,
|
|
131
153
|
max: Number.MAX_SAFE_INTEGER,
|
|
132
154
|
},
|
|
133
|
-
// Opt-in forced garbage collection. Off by default:
|
|
134
|
-
// `observability/runtime-metrics.ts` — a forced collection is not
|
|
135
|
-
// reclamation, and
|
|
155
|
+
// Opt-in forced garbage collection under memory pressure. Off by default:
|
|
156
|
+
// see `observability/runtime-metrics.ts` — a forced collection is not
|
|
157
|
+
// reclamation, and a collection that runs while requests are in flight adds
|
|
158
|
+
// a latency spike. This gates the pressure path only; idle reclaim runs
|
|
159
|
+
// unconditionally, because reclaiming when nothing is in flight stalls
|
|
160
|
+
// nothing.
|
|
136
161
|
CARTETHYIA_FORCED_GC: { kind: "flag" },
|
|
137
162
|
// How often runtime gauges (memory + bounded-collection sizes) are sampled.
|
|
138
163
|
CARTETHYIA_RUNTIME_METRICS_INTERVAL_MS: {
|
|
@@ -151,37 +176,13 @@ export const CONFIG_SPEC = {
|
|
|
151
176
|
/**
|
|
152
177
|
* How long a day's telemetry is kept, in days.
|
|
153
178
|
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
* individual request no longer exists anywhere, so its detail is gone —
|
|
157
|
-
* totals, breakdowns and spend are unaffected, because the index keeps
|
|
158
|
-
* running sums rather than the rows.
|
|
179
|
+
* Whole raw event files past this age are removed. No summary or compacted
|
|
180
|
+
* form replaces the individual requests.
|
|
159
181
|
*
|
|
160
182
|
* This is the knob that decides how long the request list, per-request
|
|
161
183
|
* filters, and the detail drawer stay complete.
|
|
162
184
|
*/
|
|
163
185
|
CARTETHYIA_TELEMETRY_RETENTION_DAYS: { kind: "int", default: 30, min: 3, max: 365 },
|
|
164
|
-
/**
|
|
165
|
-
* Age at which a request's detail is described as no longer stored.
|
|
166
|
-
*
|
|
167
|
-
* Retention itself is `CARTETHYIA_TELEMETRY_RETENTION_DAYS`; this only words
|
|
168
|
-
* the operator-facing message shown when a request that old is asked for.
|
|
169
|
-
*/
|
|
170
|
-
CARTETHYIA_TELEMETRY_COMPACT_AFTER_DAYS: { kind: "int", default: 7, min: 1, max: 365 },
|
|
171
|
-
/**
|
|
172
|
-
* How often each day's index is brought up to date, in minutes.
|
|
173
|
-
*
|
|
174
|
-
* Bringing an index up to date means folding the bytes appended since the
|
|
175
|
-
* last pass — not re-reading the day. Reads fold before they answer too, so
|
|
176
|
-
* raising this does not make the numbers stale; it only leaves the on-disk
|
|
177
|
-
* index further behind, and the next read pays for the remainder.
|
|
178
|
-
*/
|
|
179
|
-
CARTETHYIA_TELEMETRY_COMPACT_INTERVAL_MINUTES: {
|
|
180
|
-
kind: "int",
|
|
181
|
-
default: 60,
|
|
182
|
-
min: 5,
|
|
183
|
-
max: 1440,
|
|
184
|
-
},
|
|
185
186
|
CARTETHYIA_TELEMETRY_PAYLOAD_MAX_BYTES: {
|
|
186
187
|
kind: "int",
|
|
187
188
|
default: 16 * 1024 * 1024,
|
package/src/config/telemetry.ts
CHANGED
|
@@ -20,28 +20,3 @@ export function resolveTelemetryPayloadMaxBytes(): number {
|
|
|
20
20
|
);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
/**
|
|
24
|
-
* Age at which hourly telemetry files are compacted into a per-day summary.
|
|
25
|
-
*
|
|
26
|
-
* Clamped below the retention window: compacting at or past the delete age
|
|
27
|
-
* would fold a file and throw it away in the same pass, spending the read for
|
|
28
|
-
* totals that were about to be discarded.
|
|
29
|
-
*/
|
|
30
|
-
export function resolveTelemetryCompactAfterDays(): number {
|
|
31
|
-
const compacted = readInt(
|
|
32
|
-
"CARTETHYIA_TELEMETRY_COMPACT_AFTER_DAYS",
|
|
33
|
-
CONFIG_SPEC.CARTETHYIA_TELEMETRY_COMPACT_AFTER_DAYS,
|
|
34
|
-
);
|
|
35
|
-
return Math.min(compacted, resolveTelemetryRetentionDays());
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Minutes between compaction passes. */
|
|
39
|
-
export function resolveTelemetryCompactIntervalMs(): number {
|
|
40
|
-
const minutes = readInt(
|
|
41
|
-
"CARTETHYIA_TELEMETRY_COMPACT_INTERVAL_MINUTES",
|
|
42
|
-
CONFIG_SPEC.CARTETHYIA_TELEMETRY_COMPACT_INTERVAL_MINUTES,
|
|
43
|
-
);
|
|
44
|
-
return minutes * 60_000;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
@@ -2,7 +2,7 @@ import { eq } from "drizzle-orm";
|
|
|
2
2
|
|
|
3
3
|
import type { CartethyiaDatabase } from "../../persistence/db-handle";
|
|
4
4
|
import { apiKeys } from "../../persistence/schema";
|
|
5
|
-
import {
|
|
5
|
+
import { sumTelemetryTokens } from "../../observability/event-query";
|
|
6
6
|
import { utcDayStart, utcMonthStart } from "../shared/day";
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -27,27 +27,24 @@ export async function familyBucketSpend(
|
|
|
27
27
|
keyId: string,
|
|
28
28
|
now: Date,
|
|
29
29
|
): Promise<{ daily: number; monthly: number }> {
|
|
30
|
+
// The tenant comes from the key row itself: the caller passes only the key
|
|
31
|
+
// id, and the SQL sums are tenant-scoped so scoping must not be guessed.
|
|
32
|
+
const [parent] = await db
|
|
33
|
+
.select({ tenantId: apiKeys.tenantId })
|
|
34
|
+
.from(apiKeys)
|
|
35
|
+
.where(eq(apiKeys.id, keyId))
|
|
36
|
+
.limit(1);
|
|
37
|
+
const tenantId = parent?.tenantId;
|
|
38
|
+
if (tenantId === undefined) return { daily: 0, monthly: 0 };
|
|
30
39
|
const children = await db
|
|
31
40
|
.select({ id: apiKeys.id })
|
|
32
41
|
.from(apiKeys)
|
|
33
42
|
.where(eq(apiKeys.parentKeyId, keyId));
|
|
34
43
|
const keyIds = [keyId, ...children.map((child) => child.id)];
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
// Deliberately not index *plus* events: they hold the same requests, and
|
|
40
|
-
// adding them doubled every bucket. Folding first is what makes the index
|
|
41
|
-
// sufficient on its own; the watermark is the point up to which it is
|
|
42
|
-
// complete.
|
|
43
|
-
const allowed = new Set(keyIds);
|
|
44
|
-
const indexed = await queryIndexedBuckets({ since: start, foldFirst: true }).catch(
|
|
45
|
-
() => [],
|
|
46
|
-
);
|
|
47
|
-
return indexed
|
|
48
|
-
.filter((bucket) => bucket.apiKeyId !== null && allowed.has(bucket.apiKeyId))
|
|
49
|
-
.reduce((sum, bucket) => sum + bucket.inputTokens + bucket.outputTokens, 0);
|
|
50
|
-
};
|
|
44
|
+
// Two SQL sums instead of reading every event of the family: the bucket
|
|
45
|
+
// spend only needs a total, and this runs on the key-creation path.
|
|
46
|
+
const spentSince = async (start: Date): Promise<number> =>
|
|
47
|
+
sumTelemetryTokens(db, tenantId, { apiKeyIds: keyIds, since: start });
|
|
51
48
|
const [daily, monthly] = await Promise.all([
|
|
52
49
|
spentSince(utcDayStart(now)),
|
|
53
50
|
spentSince(utcMonthStart(now)),
|