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
|
@@ -33,7 +33,13 @@ import type { NetworkPoolSelector } from "../../network/pool/selector";
|
|
|
33
33
|
import type { ProxyRequestState } from "../request/state";
|
|
34
34
|
import type { TelemetryBatchBuffer } from "../../observability/telemetry-buffer";
|
|
35
35
|
import { flagPoolBlocked, flagPoolCooldown } from "../../network/pool-health";
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
completeAttempt,
|
|
38
|
+
errorClientResponseBody,
|
|
39
|
+
estimatedUsage,
|
|
40
|
+
type AttemptCompletion,
|
|
41
|
+
type ProviderExchangeCapture,
|
|
42
|
+
} from "./attempt-finalize";
|
|
37
43
|
import { repriceUsage } from "../../providers/provider-usage";
|
|
38
44
|
import { isFreeTierIpBlock, isOAuthCredentialInvalidated, shouldCooldownPool } from "./retry-policy";
|
|
39
45
|
import { drainAbortReason } from "../shutdown-notice";
|
|
@@ -92,6 +98,50 @@ function buildMemberOrdinals(
|
|
|
92
98
|
return ordinals;
|
|
93
99
|
}
|
|
94
100
|
|
|
101
|
+
/** Build the common failure payload; the call site supplies only attempt-specific decisions. */
|
|
102
|
+
function failureCompletion(input: {
|
|
103
|
+
readonly state: ProxyRequestState;
|
|
104
|
+
readonly deps: AttemptLoopDeps;
|
|
105
|
+
readonly candidate?: RouteCandidate | undefined;
|
|
106
|
+
readonly status: AttemptCompletion["status"];
|
|
107
|
+
readonly errorCategory?: string;
|
|
108
|
+
readonly errorOrigin?: string;
|
|
109
|
+
readonly error: unknown;
|
|
110
|
+
readonly tenantId: string | null;
|
|
111
|
+
readonly terminal: boolean;
|
|
112
|
+
readonly networkPoolId?: string | undefined;
|
|
113
|
+
readonly lease?: AdmissionLease | undefined;
|
|
114
|
+
readonly commitUsage?: AttemptCompletion["commitUsage"];
|
|
115
|
+
readonly providerCapture?: ProviderExchangeCapture | undefined;
|
|
116
|
+
readonly includeClientResponse: boolean;
|
|
117
|
+
}): AttemptCompletion {
|
|
118
|
+
const { state, deps, candidate } = input;
|
|
119
|
+
return {
|
|
120
|
+
status: input.status,
|
|
121
|
+
providerId: candidate?.provider_id ?? "",
|
|
122
|
+
...(candidate?.provider_account_id ? { accountId: candidate.provider_account_id } : {}),
|
|
123
|
+
...(candidate?.provider_account_label ? { accountLabel: candidate.provider_account_label } : {}),
|
|
124
|
+
...(candidate?.model_id === undefined ? {} : { modelId: candidate.model_id }),
|
|
125
|
+
...(input.networkPoolId === undefined ? {} : { networkPoolId: input.networkPoolId }),
|
|
126
|
+
...(input.errorCategory === undefined ? {} : { errorCategory: input.errorCategory }),
|
|
127
|
+
...(input.errorOrigin === undefined ? {} : { errorOrigin: input.errorOrigin }),
|
|
128
|
+
error: input.error,
|
|
129
|
+
...(input.lease === undefined ? {} : { lease: input.lease }),
|
|
130
|
+
...(input.commitUsage === undefined ? {} : { commitUsage: input.commitUsage }),
|
|
131
|
+
terminal: input.terminal,
|
|
132
|
+
tenantId: input.tenantId,
|
|
133
|
+
ingressBody: state.ingressBody,
|
|
134
|
+
responseBody: null,
|
|
135
|
+
...(input.includeClientResponse && input.status !== "cancelled"
|
|
136
|
+
? { clientResponseText: errorClientResponseBody(input.error) }
|
|
137
|
+
: {}),
|
|
138
|
+
...(input.providerCapture === undefined ? {} : { providerCapture: input.providerCapture }),
|
|
139
|
+
db: deps.db,
|
|
140
|
+
...(deps.telemetryBuffer === undefined ? {} : { telemetryBuffer: deps.telemetryBuffer }),
|
|
141
|
+
...(deps.snapshotService === undefined ? {} : { snapshotService: deps.snapshotService }),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
95
145
|
/** Route-specific preconditions resolved for one candidate before its leases are taken. */
|
|
96
146
|
interface PreparedAttempt<TAdapter> {
|
|
97
147
|
readonly credential: ResolvedCredential;
|
|
@@ -170,9 +220,14 @@ export async function runAttemptLoop<TResult, TAdapter>(
|
|
|
170
220
|
// sink reaches the payload store. When the terminal attempt fails before it
|
|
171
221
|
// fires an outbound request (lease/acquire/prepare threw, or failover
|
|
172
222
|
// exhausted), its sink is empty and the drawer reads "—" for a request that
|
|
173
|
-
// DID reach an upstream on an earlier attempt.
|
|
174
|
-
//
|
|
175
|
-
|
|
223
|
+
// DID reach an upstream on an earlier attempt. Only the LAST fired exchange
|
|
224
|
+
// is ever consulted — every consumer below falls back to the most recent
|
|
225
|
+
// capture that has a request — so this holds one reference, not the whole
|
|
226
|
+
// history: a request walking N attempts used to pin N provider exchanges
|
|
227
|
+
// (each holding its bounded provider tee) alive until the loop returned.
|
|
228
|
+
// Earlier attempts' captures are released as each new attempt fires; the
|
|
229
|
+
// payload store's own bounded writer is what bounds anything persisted.
|
|
230
|
+
let lastFiredCapture: ProviderExchangeCapture | undefined;
|
|
176
231
|
const maxAttempts = resolveRouteMaxAttempts();
|
|
177
232
|
// One combo member must not be able to spend the whole budget.
|
|
178
233
|
//
|
|
@@ -222,36 +277,23 @@ export async function runAttemptLoop<TResult, TAdapter>(
|
|
|
222
277
|
if (remainingMs < MIN_RETRY_BUDGET_MS) {
|
|
223
278
|
if (lastError !== undefined && !state.completed) {
|
|
224
279
|
const terminal = classifyTerminalOutcome(lastError, state.abortController.signal);
|
|
225
|
-
const lastFired =
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
280
|
+
const lastFired =
|
|
281
|
+
lastFiredCapture?.request !== null && lastFiredCapture !== undefined
|
|
282
|
+
? lastFiredCapture
|
|
283
|
+
: undefined;
|
|
284
|
+
await completeAttempt(state, failureCompletion({
|
|
285
|
+
state,
|
|
286
|
+
deps,
|
|
287
|
+
candidate,
|
|
229
288
|
status: terminal.status,
|
|
230
|
-
providerId: candidate.provider_id,
|
|
231
|
-
...(candidate.provider_account_id
|
|
232
|
-
? { accountId: candidate.provider_account_id }
|
|
233
|
-
: {}),
|
|
234
|
-
...(candidate.provider_account_label
|
|
235
|
-
? { accountLabel: candidate.provider_account_label }
|
|
236
|
-
: {}),
|
|
237
289
|
errorCategory: terminal.errorCategory,
|
|
238
290
|
errorOrigin: terminal.errorOrigin,
|
|
239
|
-
modelId: candidate.model_id,
|
|
240
291
|
error: lastError,
|
|
241
|
-
terminal: true,
|
|
242
292
|
tenantId: input.tenantId,
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
: {}),
|
|
248
|
-
// Same fallback the terminal catch uses: a request that did fire
|
|
249
|
-
// upstream keeps that exchange in Request Detail.
|
|
250
|
-
...(lastFired ? { providerCapture: lastFired } : {}),
|
|
251
|
-
db: deps.db,
|
|
252
|
-
...(deps.telemetryBuffer ? { telemetryBuffer: deps.telemetryBuffer } : {}),
|
|
253
|
-
...(deps.snapshotService ? { snapshotService: deps.snapshotService } : {}),
|
|
254
|
-
});
|
|
293
|
+
terminal: true,
|
|
294
|
+
providerCapture: lastFired,
|
|
295
|
+
includeClientResponse: true,
|
|
296
|
+
}));
|
|
255
297
|
metrics.proxy_requests_total.inc(1, {
|
|
256
298
|
status: terminal.status === "cancelled" ? "cancelled" : "failed",
|
|
257
299
|
});
|
|
@@ -358,7 +400,9 @@ export async function runAttemptLoop<TResult, TAdapter>(
|
|
|
358
400
|
cancelled ||
|
|
359
401
|
!isRetryableFailure(error) ||
|
|
360
402
|
(index === candidates.length - 1 && !poolScopedFailure);
|
|
361
|
-
|
|
403
|
+
// Track only the most recent fired exchange; the array this replaced
|
|
404
|
+
// accumulated every attempt's capture until the loop returned.
|
|
405
|
+
if (providerCapture.request !== null) lastFiredCapture = providerCapture;
|
|
362
406
|
// The payload store only keeps the terminal attempt's exchange. When the
|
|
363
407
|
// terminal attempt never fired (failover walked past the attempt that
|
|
364
408
|
// did, or prepare/lease threw first), fall back to the last fired
|
|
@@ -367,50 +411,39 @@ export async function runAttemptLoop<TResult, TAdapter>(
|
|
|
367
411
|
const terminalCapture =
|
|
368
412
|
providerCapture.request !== null
|
|
369
413
|
? providerCapture
|
|
370
|
-
:
|
|
371
|
-
|
|
372
|
-
|
|
414
|
+
: lastFiredCapture ?? providerCapture;
|
|
415
|
+
await completeAttempt(state, failureCompletion({
|
|
416
|
+
state,
|
|
417
|
+
deps,
|
|
418
|
+
candidate,
|
|
373
419
|
status: terminal.status,
|
|
374
|
-
providerId: candidate.provider_id,
|
|
375
|
-
...(candidate.provider_account_id ? { accountId: candidate.provider_account_id } : {}),
|
|
376
|
-
...(candidate.provider_account_label ? { accountLabel: candidate.provider_account_label } : {}),
|
|
377
420
|
errorCategory: terminal.errorCategory,
|
|
378
421
|
errorOrigin: terminal.errorOrigin,
|
|
379
|
-
modelId: candidate.model_id,
|
|
380
|
-
...(networkPoolId ? { networkPoolId } : {}),
|
|
381
422
|
error,
|
|
382
|
-
|
|
423
|
+
tenantId: input.tenantId,
|
|
424
|
+
terminal: terminalAttempt,
|
|
425
|
+
networkPoolId,
|
|
426
|
+
lease,
|
|
383
427
|
// A failed attempt may have sent bytes upstream, so it is not free —
|
|
384
428
|
// but it must not be charged the full input estimate plus a full
|
|
385
429
|
// output reserve either, because a request that walks N candidates
|
|
386
430
|
// would then leave N estimates on the counters for one client request.
|
|
387
431
|
// Commit the input estimate only: nothing was generated, and the
|
|
388
432
|
// `finally` release then refunds the (uncommitted) output reserve.
|
|
389
|
-
|
|
390
|
-
?
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
}
|
|
397
|
-
: {}),
|
|
398
|
-
terminal: terminalAttempt,
|
|
399
|
-
tenantId: input.tenantId,
|
|
400
|
-
ingressBody: state.ingressBody,
|
|
401
|
-
responseBody: null,
|
|
433
|
+
commitUsage: !cancelled && bindingEstablished
|
|
434
|
+
? repriceUsage(
|
|
435
|
+
estimatedUsage(leaseSource.estimatedInputTokens, 0),
|
|
436
|
+
candidate.provider_id,
|
|
437
|
+
candidate.model_id,
|
|
438
|
+
)
|
|
439
|
+
: undefined,
|
|
402
440
|
// The client still receives the public error envelope on the wire —
|
|
403
441
|
// store it so Request Detail's Client Response panel traces failures
|
|
404
442
|
// exactly as the client saw them. Cancelled requests have no client
|
|
405
443
|
// response (the caller is gone).
|
|
406
|
-
...(terminalAttempt && !cancelled
|
|
407
|
-
? { clientResponseText: errorClientResponseBody(error) }
|
|
408
|
-
: {}),
|
|
409
444
|
providerCapture: terminalCapture,
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
...(deps.snapshotService ? { snapshotService: deps.snapshotService } : {}),
|
|
413
|
-
});
|
|
445
|
+
includeClientResponse: terminalAttempt,
|
|
446
|
+
}));
|
|
414
447
|
// A 403 can be a deterministic provider policy rejection, not an
|
|
415
448
|
// invalid OAuth credential. Only the evidence-based classifier above
|
|
416
449
|
// may trigger a refresh.
|
|
@@ -495,36 +528,25 @@ export async function runAttemptLoop<TResult, TAdapter>(
|
|
|
495
528
|
if (!state.completed) {
|
|
496
529
|
const terminal = classifyTerminalOutcome(lastError, state.abortController.signal);
|
|
497
530
|
const lastCandidate = candidates[Math.min(index, candidates.length - 1)];
|
|
498
|
-
const terminalCapture =
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
531
|
+
const terminalCapture =
|
|
532
|
+
lastFiredCapture?.request !== null && lastFiredCapture !== undefined
|
|
533
|
+
? lastFiredCapture
|
|
534
|
+
: undefined;
|
|
535
|
+
await completeAttempt(state, failureCompletion({
|
|
536
|
+
state,
|
|
537
|
+
deps,
|
|
538
|
+
candidate: lastCandidate,
|
|
502
539
|
status: terminal.status,
|
|
503
|
-
providerId: lastCandidate?.provider_id ?? "",
|
|
504
|
-
...(lastCandidate?.provider_account_id
|
|
505
|
-
? { accountId: lastCandidate.provider_account_id }
|
|
506
|
-
: {}),
|
|
507
|
-
...(lastCandidate?.provider_account_label
|
|
508
|
-
? { accountLabel: lastCandidate.provider_account_label }
|
|
509
|
-
: {}),
|
|
510
540
|
errorCategory: terminal.errorCategory,
|
|
511
541
|
errorOrigin: terminal.errorOrigin,
|
|
512
|
-
...(lastCandidate?.model_id === undefined ? {} : { modelId: lastCandidate.model_id }),
|
|
513
542
|
error: lastError,
|
|
514
543
|
terminal: true,
|
|
515
544
|
tenantId: input.tenantId,
|
|
516
|
-
ingressBody: state.ingressBody,
|
|
517
|
-
responseBody: null,
|
|
518
545
|
// The client receives the public error envelope on the wire — store it
|
|
519
546
|
// so the Client Response panel traces the failure as the client saw it.
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
...(terminalCapture === undefined ? {} : { providerCapture: terminalCapture }),
|
|
524
|
-
db: deps.db,
|
|
525
|
-
...(deps.telemetryBuffer ? { telemetryBuffer: deps.telemetryBuffer } : {}),
|
|
526
|
-
...(deps.snapshotService ? { snapshotService: deps.snapshotService } : {}),
|
|
527
|
-
});
|
|
547
|
+
providerCapture: terminalCapture,
|
|
548
|
+
includeClientResponse: true,
|
|
549
|
+
}));
|
|
528
550
|
}
|
|
529
551
|
metrics.proxy_requests_total.inc(1, { status: "failed" });
|
|
530
552
|
throw lastError;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Tenant payload-capture policy resolved once and shared by dispatch attempts.
|
|
2
|
+
import type { CartethyiaDatabase } from "../../persistence/db-handle";
|
|
3
|
+
import { payloadRetentionMs, resolvePayloadCapture } from "../../observability/payload-capture";
|
|
4
|
+
import type { TelemetryPayloadDepth, TelemetryPayloadMode } from "../../console/settings/contracts";
|
|
5
|
+
|
|
6
|
+
/** Per-stage body cap for the provider request/response tee. */
|
|
7
|
+
export function providerCaptureMaxBytes(depth?: TelemetryPayloadDepth): number {
|
|
8
|
+
if (depth === "moderate") return 2 * 1024 * 1024;
|
|
9
|
+
if (depth === "maximum") return 4 * 1024 * 1024;
|
|
10
|
+
return 1024 * 1024;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Resolve whether provider bodies should be retained, memoized on this request. */
|
|
14
|
+
export async function shouldCaptureProviderBodyForRequest(
|
|
15
|
+
db: CartethyiaDatabase,
|
|
16
|
+
tenantId: string | null,
|
|
17
|
+
state: { payloadCapturePreference?: unknown },
|
|
18
|
+
): Promise<boolean> {
|
|
19
|
+
if (state.payloadCapturePreference === undefined) {
|
|
20
|
+
state.payloadCapturePreference = await resolvePayloadCapture(db, tenantId);
|
|
21
|
+
}
|
|
22
|
+
const resolved = state.payloadCapturePreference as
|
|
23
|
+
| { readonly ok: true; readonly mode: string }
|
|
24
|
+
| { readonly ok: false };
|
|
25
|
+
return resolved.ok === true && resolved.mode === "full";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Resolve the tenant's capture mode once per request; failed reads fail closed. */
|
|
29
|
+
export async function resolveCaptureModeForRequest(
|
|
30
|
+
db: CartethyiaDatabase,
|
|
31
|
+
tenantId: string | null,
|
|
32
|
+
state: { payloadCapturePreference?: unknown },
|
|
33
|
+
): Promise<TelemetryPayloadMode> {
|
|
34
|
+
if (state.payloadCapturePreference === undefined) {
|
|
35
|
+
state.payloadCapturePreference = await resolvePayloadCapture(db, tenantId);
|
|
36
|
+
}
|
|
37
|
+
const resolved = state.payloadCapturePreference as
|
|
38
|
+
| { readonly ok: true; readonly mode: TelemetryPayloadMode }
|
|
39
|
+
| { readonly ok: false };
|
|
40
|
+
return resolved.ok === true ? resolved.mode : "none";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Resolve the tenant's capture depth once per request. */
|
|
44
|
+
export async function resolveCaptureDepthForRequest(
|
|
45
|
+
db: CartethyiaDatabase,
|
|
46
|
+
tenantId: string | null,
|
|
47
|
+
state: { payloadCapturePreference?: unknown },
|
|
48
|
+
): Promise<TelemetryPayloadDepth> {
|
|
49
|
+
if (state.payloadCapturePreference === undefined) {
|
|
50
|
+
state.payloadCapturePreference = await resolvePayloadCapture(db, tenantId);
|
|
51
|
+
}
|
|
52
|
+
const resolved = state.payloadCapturePreference as
|
|
53
|
+
| { readonly ok: true; readonly depth: TelemetryPayloadDepth }
|
|
54
|
+
| { readonly ok: false };
|
|
55
|
+
return resolved.ok === true ? resolved.depth : "minimum";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Resolve the tenant's payload retention once per request. */
|
|
59
|
+
export async function resolveCaptureRetentionForRequest(
|
|
60
|
+
db: CartethyiaDatabase,
|
|
61
|
+
tenantId: string | null,
|
|
62
|
+
state: { payloadCapturePreference?: unknown },
|
|
63
|
+
): Promise<number> {
|
|
64
|
+
if (state.payloadCapturePreference === undefined) {
|
|
65
|
+
state.payloadCapturePreference = await resolvePayloadCapture(db, tenantId);
|
|
66
|
+
}
|
|
67
|
+
const resolved = state.payloadCapturePreference as
|
|
68
|
+
| { readonly ok: true; readonly retentionMs: number }
|
|
69
|
+
| { readonly ok: false };
|
|
70
|
+
return resolved.ok === true ? resolved.retentionMs : payloadRetentionMs();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Combined body cap for the four payload panels at one configured depth. */
|
|
74
|
+
export function captureDepthMaxBytes(depth: TelemetryPayloadDepth): number {
|
|
75
|
+
if (depth === "moderate") return 8 * 1024 * 1024;
|
|
76
|
+
if (depth === "maximum") return 16 * 1024 * 1024;
|
|
77
|
+
return 1 * 1024 * 1024;
|
|
78
|
+
}
|
|
@@ -32,11 +32,13 @@ import {
|
|
|
32
32
|
completionContext,
|
|
33
33
|
estimatedUsage,
|
|
34
34
|
parseCapturedBody,
|
|
35
|
+
terminalFailure,
|
|
36
|
+
} from "./attempt-finalize";
|
|
37
|
+
import {
|
|
35
38
|
providerCaptureMaxBytes,
|
|
36
39
|
resolveCaptureDepthForRequest,
|
|
37
40
|
shouldCaptureProviderBodyForRequest,
|
|
38
|
-
|
|
39
|
-
} from "./attempt-finalize";
|
|
41
|
+
} from "./capture-policy";
|
|
40
42
|
import { runAttemptLoop } from "./attempt-loop";
|
|
41
43
|
import { projectForRoute, routeCapabilitiesFor } from "../translation/capabilities";
|
|
42
44
|
import { dispatchStreamingAttempt } from "./streaming/stream";
|
|
@@ -10,8 +10,11 @@
|
|
|
10
10
|
* client-visible tracking → encode + enqueue) is preserved exactly.
|
|
11
11
|
*/
|
|
12
12
|
import type { CanonicalEvent, SourceSurface, UsageRecord } from "../../../protocol/canonical-model";
|
|
13
|
+
import { canonicalTerminal } from "../../../protocol/codecs";
|
|
14
|
+
import { reasoningTextOf } from "../../../protocol/reasoning-text";
|
|
13
15
|
import { MimoThinkSplitter } from "../../../providers/integrations/xiaomi-mimo/think-stream";
|
|
14
16
|
import { createDispatchStreamEncoder, type DispatchStreamEncoder } from "../stream-bridge";
|
|
17
|
+
import { THINKING_LOOP_MARKER, ThinkingLoopDetector } from "./thinking-loop";
|
|
15
18
|
|
|
16
19
|
/** The terminal event shape every stream settles (or fails to settle) on. */
|
|
17
20
|
export type StreamTerminalEvent = Extract<CanonicalEvent, { type: "terminal" }>;
|
|
@@ -78,20 +81,41 @@ export class StreamEventPipeline {
|
|
|
78
81
|
// One decoder per stream: `new TextDecoder()` per chunk would allocate
|
|
79
82
|
// on every upstream event for the telemetry transcript copy.
|
|
80
83
|
private readonly clientResponseDecoder = new TextDecoder();
|
|
84
|
+
/**
|
|
85
|
+
* Resolved tenant capture mode. `none`/`metadata` gate the diagnostics the
|
|
86
|
+
* tenant will never be able to see: the JSON-serialized event transcript
|
|
87
|
+
* copy and the client-response accumulation are skipped entirely, so a
|
|
88
|
+
* privacy-lean tenant's streams cost neither the per-event `JSON.stringify`
|
|
89
|
+
* nor the up-to-512 KiB buffered client-response copy. The fields the
|
|
90
|
+
* completion path always needs (terminal, usage, timing, reported model,
|
|
91
|
+
* event count, client-visible flag) are recorded regardless — the Responses
|
|
92
|
+
* prelude retry depends on the flag, and the request row depends on the rest.
|
|
93
|
+
*/
|
|
94
|
+
private readonly diagnosticsCaptureMode: "none" | "metadata" | "full";
|
|
81
95
|
private readonly streamEncoder: DispatchStreamEncoder;
|
|
82
96
|
// One splitter per stream: a thinking tag can straddle two provider chunks,
|
|
83
97
|
// and the text/idle bookkeeping below must see the split result, not the raw
|
|
84
98
|
// tagged chunk.
|
|
85
99
|
private readonly thinkSplitter = new MimoThinkSplitter();
|
|
100
|
+
/**
|
|
101
|
+
* Content-level loop guard. A model that degenerates into a repeated
|
|
102
|
+
* chain-of-thought keeps sending bytes, so the stall timeout — which fires
|
|
103
|
+
* only on silence — never trips and the runaway burns the whole output
|
|
104
|
+
* budget. This detector watches the reasoning text itself and terminates the
|
|
105
|
+
* stream early when a loop shape is recognized.
|
|
106
|
+
*/
|
|
107
|
+
private readonly loopDetector = new ThinkingLoopDetector();
|
|
86
108
|
private lastSequenceNumber = -1;
|
|
87
109
|
|
|
88
110
|
constructor(
|
|
89
111
|
surface: SourceSurface,
|
|
90
112
|
encoderOptions: Record<string, unknown>,
|
|
91
113
|
recoverableResponsesPrelude: boolean,
|
|
114
|
+
diagnostics: { captureMode: "none" | "metadata" | "full" } = { captureMode: "full" },
|
|
92
115
|
) {
|
|
93
116
|
this.streamEncoder = createDispatchStreamEncoder(surface, encoderOptions);
|
|
94
117
|
this.recoverableResponsesPrelude = recoverableResponsesPrelude;
|
|
118
|
+
this.diagnosticsCaptureMode = diagnostics.captureMode;
|
|
95
119
|
}
|
|
96
120
|
|
|
97
121
|
/** Number of canonical events processed, including those omitted from diagnostics. */
|
|
@@ -104,6 +128,13 @@ export class StreamEventPipeline {
|
|
|
104
128
|
return this.clientVisibleEvent;
|
|
105
129
|
}
|
|
106
130
|
|
|
131
|
+
/** Whether the loop guard has terminated this stream early. The pull loop
|
|
132
|
+
* stops consuming the upstream once it has: continuing would keep billing
|
|
133
|
+
* and emitting a runaway the client was already told is over. */
|
|
134
|
+
get loopDetected(): boolean {
|
|
135
|
+
return this.loopDetector.detected;
|
|
136
|
+
}
|
|
137
|
+
|
|
107
138
|
/** The client-facing response transcript (decoded SSE/JSON) — distinct from the provider-side canonical events. */
|
|
108
139
|
get clientResponseText(): string {
|
|
109
140
|
const text = this.clientResponseChunks.join("");
|
|
@@ -114,6 +145,9 @@ export class StreamEventPipeline {
|
|
|
114
145
|
get telemetryEventTranscript():
|
|
115
146
|
| readonly (CanonicalEvent & { timestamp: number })[]
|
|
116
147
|
| TruncatedEventTranscript {
|
|
148
|
+
// Gated modes recorded nothing, so the transcript is the plain empty
|
|
149
|
+
// array — no truncated-marker object for content that was never kept.
|
|
150
|
+
if (this.diagnosticsCaptureMode !== "full") return [];
|
|
117
151
|
const tail = this.eventTranscriptTail.slice(this.eventTranscriptTailStart).map((entry) => entry.event);
|
|
118
152
|
const events = [...this.eventTranscriptHead, ...tail];
|
|
119
153
|
const omittedEvents = this.totalEventCount - events.length;
|
|
@@ -140,6 +174,9 @@ export class StreamEventPipeline {
|
|
|
140
174
|
|
|
141
175
|
private recordTelemetryEvent(event: CanonicalEvent & { timestamp: number }): void {
|
|
142
176
|
this.totalEventCount += 1;
|
|
177
|
+
// Gated diagnostics: the transcript is only ever read by the terminal
|
|
178
|
+
// capture path in `full` mode, so `none`/`metadata` pay only the count.
|
|
179
|
+
if (this.diagnosticsCaptureMode !== "full") return;
|
|
143
180
|
// Single serialization per event: the byte size is stored with the entry
|
|
144
181
|
// so eviction subtracts without re-serializing the removed event.
|
|
145
182
|
const eventBytes = Buffer.byteLength(JSON.stringify(event), "utf8");
|
|
@@ -179,6 +216,9 @@ export class StreamEventPipeline {
|
|
|
179
216
|
}
|
|
180
217
|
|
|
181
218
|
private appendClientResponse(bytes: Uint8Array): void {
|
|
219
|
+
// Gated diagnostics: the client-response copy feeds only the terminal
|
|
220
|
+
// capture's client panel, which exists only in `full` mode.
|
|
221
|
+
if (this.diagnosticsCaptureMode !== "full") return;
|
|
182
222
|
if (this.clientResponseTruncated) return;
|
|
183
223
|
const chunk = this.clientResponseDecoder.decode(bytes, { stream: true });
|
|
184
224
|
if (this.clientResponseLength + chunk.length > CLIENT_RESPONSE_CAP) {
|
|
@@ -213,6 +253,12 @@ export class StreamEventPipeline {
|
|
|
213
253
|
event: CanonicalEvent,
|
|
214
254
|
controller: ReadableStreamDefaultController<Uint8Array>,
|
|
215
255
|
): boolean {
|
|
256
|
+
// Once the guard has terminated the stream, the surface encoders reject
|
|
257
|
+
// any further event — content *and* a second terminal both throw
|
|
258
|
+
// (`canonical event arrived after terminal`). The pull loop stops on
|
|
259
|
+
// `loopDetected`, so this only matters for an event already in flight;
|
|
260
|
+
// dropping it is the alternative to throwing mid-stream.
|
|
261
|
+
if (this.loopDetector.detected) return false;
|
|
216
262
|
// The one place every surface shares. A provider that writes its reasoning
|
|
217
263
|
// into the answer as `<think>…</think>` had that text forwarded verbatim:
|
|
218
264
|
// the chat wire happened to split it in its own decoder, so chat looked
|
|
@@ -222,11 +268,12 @@ export class StreamEventPipeline {
|
|
|
222
268
|
const split = this.thinkSplitter.push(event.content.text);
|
|
223
269
|
let enqueued = false;
|
|
224
270
|
if (split.reasoning.length > 0) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
271
|
+
const reasoningEvent: CanonicalEvent = {
|
|
272
|
+
...event,
|
|
273
|
+
content: { kind: "reasoning", payload: null, summary: split.reasoning },
|
|
274
|
+
};
|
|
275
|
+
enqueued = this.enqueueOne(reasoningEvent, controller) || enqueued;
|
|
276
|
+
if (this.guardReasoning(split.reasoning, controller)) return enqueued;
|
|
230
277
|
}
|
|
231
278
|
if (split.text.length > 0) {
|
|
232
279
|
enqueued =
|
|
@@ -235,6 +282,14 @@ export class StreamEventPipeline {
|
|
|
235
282
|
}
|
|
236
283
|
return enqueued;
|
|
237
284
|
}
|
|
285
|
+
// Reasoning a provider already classified: feed it to the guard too, so a
|
|
286
|
+
// loop is caught whether the reasoning arrived pre-classified or was
|
|
287
|
+
// recovered from a `<think>` span in the answer.
|
|
288
|
+
if (event.type === "content_delta" && event.content.kind === "reasoning") {
|
|
289
|
+
const enqueued = this.enqueueOne(event, controller);
|
|
290
|
+
this.guardReasoning(reasoningTextOf(event.content) ?? "", controller);
|
|
291
|
+
return enqueued;
|
|
292
|
+
}
|
|
238
293
|
// Text held back for a tag that never completed must be released *before*
|
|
239
294
|
// the terminal: a content event after it is rejected by the surface
|
|
240
295
|
// encoders, and the tail of the answer would be lost instead.
|
|
@@ -264,6 +319,57 @@ export class StreamEventPipeline {
|
|
|
264
319
|
return this.enqueueOne(event, controller);
|
|
265
320
|
}
|
|
266
321
|
|
|
322
|
+
/**
|
|
323
|
+
* Feeds one reasoning chunk to the loop guard and, on the first hit,
|
|
324
|
+
* terminates the stream early. Returns whether it terminated.
|
|
325
|
+
*
|
|
326
|
+
* The terminal is a real canonical terminal rather than an error frame: the
|
|
327
|
+
* client already received valid reasoning, and the runaway is cut off
|
|
328
|
+
* mid-generation — which is exactly what `stop_reason: "length"` means on
|
|
329
|
+
* every surface (`length` / `max_tokens`). `provider_stop_reason` carries the
|
|
330
|
+
* marker so the cut is distinguishable from hitting the real token cap.
|
|
331
|
+
*/
|
|
332
|
+
private guardReasoning(
|
|
333
|
+
text: string,
|
|
334
|
+
controller: ReadableStreamDefaultController<Uint8Array>,
|
|
335
|
+
): boolean {
|
|
336
|
+
const reason = text.length > 0 ? this.loopDetector.push(text) : undefined;
|
|
337
|
+
if (reason === undefined) return false;
|
|
338
|
+
// Flush before the terminal: content after it is rejected by the surface
|
|
339
|
+
// encoders, and the held-back text would be lost.
|
|
340
|
+
const rest = this.thinkSplitter.flush();
|
|
341
|
+
if (rest.reasoning.length > 0) {
|
|
342
|
+
this.enqueueOne(
|
|
343
|
+
{
|
|
344
|
+
type: "content_delta",
|
|
345
|
+
sequence_number: this.lastSequenceNumber + 1,
|
|
346
|
+
content: { kind: "reasoning", payload: null, summary: rest.reasoning },
|
|
347
|
+
},
|
|
348
|
+
controller,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
if (rest.text.length > 0) {
|
|
352
|
+
this.enqueueOne(
|
|
353
|
+
{
|
|
354
|
+
type: "content_delta",
|
|
355
|
+
sequence_number: this.lastSequenceNumber + 1,
|
|
356
|
+
content: { kind: "text", text: rest.text },
|
|
357
|
+
},
|
|
358
|
+
controller,
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
this.enqueueOne(
|
|
362
|
+
canonicalTerminal({
|
|
363
|
+
sequenceNumber: this.lastSequenceNumber + 1,
|
|
364
|
+
state: "complete",
|
|
365
|
+
stopReason: "length",
|
|
366
|
+
providerStopReason: THINKING_LOOP_MARKER,
|
|
367
|
+
}),
|
|
368
|
+
controller,
|
|
369
|
+
);
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
|
|
267
373
|
private enqueueOne(
|
|
268
374
|
event: CanonicalEvent,
|
|
269
375
|
controller: ReadableStreamDefaultController<Uint8Array>,
|
|
@@ -43,12 +43,13 @@ import type { AdmissionLease } from "../../../security/admission/contracts";
|
|
|
43
43
|
import type { NetworkPoolSelector } from "../../../network/pool/selector";
|
|
44
44
|
import type { TelemetryBatchBuffer } from "../../../observability/telemetry-buffer";
|
|
45
45
|
import type { ProviderExchangeCapture } from "../attempt-finalize";
|
|
46
|
+
import { captureProviderExchange } from "../attempt-finalize";
|
|
46
47
|
import {
|
|
47
|
-
captureProviderExchange,
|
|
48
48
|
providerCaptureMaxBytes,
|
|
49
49
|
resolveCaptureDepthForRequest,
|
|
50
|
+
resolveCaptureModeForRequest,
|
|
50
51
|
shouldCaptureProviderBodyForRequest,
|
|
51
|
-
} from "../
|
|
52
|
+
} from "../capture-policy";
|
|
52
53
|
import { resolveTenantImageClampMaxBytes } from "../../request/tenant-preferences";
|
|
53
54
|
import { proxySuccessHeaders, buildUpstreamDispatchContext } from "../upstream";
|
|
54
55
|
import type { AttemptLeaseOwnership } from "../leases";
|
|
@@ -179,6 +180,14 @@ export async function dispatchStreamingAttempt(input: StreamingDispatchInput): P
|
|
|
179
180
|
prepared.authorization.snapshot.tenant_id,
|
|
180
181
|
state,
|
|
181
182
|
);
|
|
183
|
+
// Resolved once, before the pipeline exists: the pipeline's diagnostics are
|
|
184
|
+
// gated on it from construction, so `none`/`metadata` tenants never pay the
|
|
185
|
+
// transcript serialization or the client-response copy for any event.
|
|
186
|
+
const captureMode = await resolveCaptureModeForRequest(
|
|
187
|
+
deps.db,
|
|
188
|
+
prepared.authorization.snapshot.tenant_id,
|
|
189
|
+
state,
|
|
190
|
+
);
|
|
182
191
|
const wrappedOutboundFetch = outboundFetch
|
|
183
192
|
? captureProviderExchange(
|
|
184
193
|
outboundFetch,
|
|
@@ -276,6 +285,7 @@ export async function dispatchStreamingAttempt(input: StreamingDispatchInput): P
|
|
|
276
285
|
? { response_id: `msg-${crypto.randomUUID()}`, model: candidate.model_id }
|
|
277
286
|
: streamOptions,
|
|
278
287
|
recoverableResponsesPrelude,
|
|
288
|
+
{ captureMode },
|
|
279
289
|
);
|
|
280
290
|
const finalizer = new StreamFinalizer(
|
|
281
291
|
{
|
|
@@ -400,6 +410,13 @@ export async function dispatchStreamingAttempt(input: StreamingDispatchInput): P
|
|
|
400
410
|
}
|
|
401
411
|
sawUpstreamActivity = true;
|
|
402
412
|
if (pipeline.enqueueEvent(result.value, controller)) return;
|
|
413
|
+
// The guard terminated the stream early: stop consuming. Pulling
|
|
414
|
+
// on would keep the runaway upstream alive and bill tokens the
|
|
415
|
+
// client was already told are not coming.
|
|
416
|
+
if (pipeline.loopDetected) {
|
|
417
|
+
await finalizer.finalizeStream(controller);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
403
420
|
}
|
|
404
421
|
} catch (err) {
|
|
405
422
|
finalizer.clearStallWatchdog();
|