cartethyia 2.5.10 → 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 +8 -0
- package/dist/dashboard/assets/{BannedPage-Cd30Z0b9.js → BannedPage-l6XMnfr9.js} +1 -1
- package/dist/dashboard/assets/BannedPage-l6XMnfr9.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-wf5u-ww6.js → CliToolDetailPage-DwOzAW45.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-DwOzAW45.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-DykPvuck.js → CliToolsPage-BT3Up3QA.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-BT3Up3QA.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-DwvSD3vs.js → CombosPage-6kYIUpzR.js} +1 -1
- package/dist/dashboard/assets/CombosPage-6kYIUpzR.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-CIr7KDex.js → ConsoleLogPage-zRnj1mlR.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-zRnj1mlR.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-82X0DDdH.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-CS5mPuVF.js → LoginPage-CbphLvjg.js} +1 -1
- package/dist/dashboard/assets/LoginPage-CbphLvjg.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-C0UCUsVJ.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-vdja5ZPK.js → ProvidersPage-Dfb2896L.js} +1 -1
- package/dist/dashboard/assets/ProvidersPage-Dfb2896L.js.gz +0 -0
- package/dist/dashboard/assets/{ProxyPage-BB4CNm4-.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-FadUOl9S.js → SetupPage-D4g6vOGc.js} +1 -1
- package/dist/dashboard/assets/SetupPage-D4g6vOGc.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-Cfy6_Khv.js → SortableList-C93i14BD.js} +1 -1
- package/dist/dashboard/assets/SortableList-C93i14BD.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-CM92fgJf.js → StudioPage-SgQpymjm.js} +1 -1
- package/dist/dashboard/assets/{StudioPage-CM92fgJf.js.gz → StudioPage-SgQpymjm.js.gz} +0 -0
- package/dist/dashboard/assets/{UsagePage-BT4MyQeT.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-B7pVRhJ1.js → app-2KhtHCv9.js} +1 -1
- package/dist/dashboard/assets/{app-B7pVRhJ1.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-Bvyr7D0u.js → badge-CKsF123d.js} +1 -1
- package/dist/dashboard/assets/badge-CKsF123d.js.gz +0 -0
- package/dist/dashboard/assets/{banner-DIvKAZih.js → banner-8j7uChA9.js} +1 -1
- package/dist/dashboard/assets/banner-8j7uChA9.js.gz +0 -0
- package/dist/dashboard/assets/{card-D5_gYIS_.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-B6vDBtbm.js → clipboard-button-uL5aYLfW.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-uL5aYLfW.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-CpjrgdwJ.js → dialog-C07hr5_G.js} +1 -1
- package/dist/dashboard/assets/dialog-C07hr5_G.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-BWmt3b5y.js → drawer-BPO1TcKH.js} +1 -1
- package/dist/dashboard/assets/drawer-BPO1TcKH.js.gz +0 -0
- package/dist/dashboard/assets/{entry-C_oakV3z.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-RRHHWGxh.js → github-badge-DmQreSDd.js} +1 -1
- package/dist/dashboard/assets/github-badge-DmQreSDd.js.gz +0 -0
- package/dist/dashboard/assets/{index-Bhey_rg_.js → index-Bblu6erR.js} +2 -2
- package/dist/dashboard/assets/index-Bblu6erR.js.gz +0 -0
- package/dist/dashboard/assets/{inline-C2_EQ5Uf.js → inline-C9dCYaBZ.js} +1 -1
- package/dist/dashboard/assets/inline-C9dCYaBZ.js.gz +0 -0
- package/dist/dashboard/assets/{input-BMRlQ450.js → input-CjCnXweJ.js} +1 -1
- package/dist/dashboard/assets/input-CjCnXweJ.js.gz +0 -0
- package/dist/dashboard/assets/{layout-XR5xP8ew.js → layout-V6y8DAkl.js} +1 -1
- package/dist/dashboard/assets/layout-V6y8DAkl.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-Cw07dioc.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-DrdA9oSM.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-CxVVB__N.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-uPOwM9Hs.js → stack-_gfTdLVB.js} +1 -1
- package/dist/dashboard/assets/stack-_gfTdLVB.js.gz +0 -0
- package/dist/dashboard/assets/{state-D1Yz79iL.js → state-2c5V4zo7.js} +1 -1
- package/dist/dashboard/assets/state-2c5V4zo7.js.gz +0 -0
- package/dist/dashboard/assets/{switch-sevsRrRo.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 -95
- package/scripts/cli/admin.ts +126 -128
- 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 +39 -30
- package/scripts/cli/controller.ts +145 -0
- package/scripts/cli/env.ts +99 -99
- package/scripts/cli/gateway.ts +264 -260
- package/scripts/cli/instance.ts +234 -224
- package/scripts/cli/main.ts +188 -167
- package/scripts/cli/platform.ts +64 -64
- package/scripts/cli/prompt.ts +217 -79
- package/scripts/cli/services.ts +88 -88
- package/scripts/cli/setup.ts +445 -445
- package/scripts/cli/start.ts +85 -85
- 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 +29 -4
- 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/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/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/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/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/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 +10 -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/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/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/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-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/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/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-Cd30Z0b9.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-wf5u-ww6.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-DykPvuck.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-DwvSD3vs.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-CIr7KDex.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-82X0DDdH.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-BYPucTTn.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-BYPucTTn.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-CS5mPuVF.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-C0UCUsVJ.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-Dk_ew3YU.js +0 -1
- package/dist/dashboard/assets/OverviewPage-Dk_ew3YU.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CAiRn2Q0.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage-CAiRn2Q0.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-vdja5ZPK.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-BB4CNm4-.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-FEc4iNyq.js +0 -1
- package/dist/dashboard/assets/QuotaPage-FEc4iNyq.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-Dlfi5L4N.js +0 -1
- package/dist/dashboard/assets/SettingsPage-Dlfi5L4N.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-FadUOl9S.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-Cfy6_Khv.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-BT4MyQeT.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-Bvyr7D0u.js.gz +0 -0
- package/dist/dashboard/assets/banner-DIvKAZih.js.gz +0 -0
- package/dist/dashboard/assets/card-D5_gYIS_.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-B6vDBtbm.js.gz +0 -0
- package/dist/dashboard/assets/dialog-CpjrgdwJ.js.gz +0 -0
- package/dist/dashboard/assets/drawer-BWmt3b5y.js.gz +0 -0
- package/dist/dashboard/assets/entry-B-gvimrg.js +0 -1
- package/dist/dashboard/assets/entry-B-gvimrg.js.gz +0 -0
- package/dist/dashboard/assets/entry-C_oakV3z.js.gz +0 -0
- package/dist/dashboard/assets/entry-Dj0cHfEc.js +0 -4
- package/dist/dashboard/assets/entry-Dj0cHfEc.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-RRHHWGxh.js.gz +0 -0
- package/dist/dashboard/assets/index-Bhey_rg_.js.gz +0 -0
- package/dist/dashboard/assets/inline-C2_EQ5Uf.js.gz +0 -0
- package/dist/dashboard/assets/input-BMRlQ450.js.gz +0 -0
- package/dist/dashboard/assets/layout-XR5xP8ew.js.gz +0 -0
- package/dist/dashboard/assets/list-row-Cw07dioc.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-DrdA9oSM.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-CxVVB__N.js.gz +0 -0
- package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
- package/dist/dashboard/assets/stack-uPOwM9Hs.js.gz +0 -0
- package/dist/dashboard/assets/state-D1Yz79iL.js.gz +0 -0
- package/dist/dashboard/assets/switch-sevsRrRo.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
- package/src/observability/event-compaction.ts +0 -317
- package/src/observability/event-day-index.ts +0 -439
|
@@ -7,10 +7,9 @@ import type { UsageRecord } from "../../protocol/canonical-model";
|
|
|
7
7
|
import { classifyUpstreamFailure } from "../failure-policy";
|
|
8
8
|
import type { AdmissionLease } from "../../security/admission/contracts";
|
|
9
9
|
import type { CartethyiaDatabase } from "../../persistence/db-handle";
|
|
10
|
-
import { TelemetryPayloadCapture } from "../../observability/payload-capture";
|
|
10
|
+
import { resolvePayloadCapture, TelemetryPayloadCapture, truncateUtf8 } from "../../observability/payload-capture";
|
|
11
11
|
import { payloadDirectory } from "../../observability/payload-store";
|
|
12
12
|
import type { TelemetryBatchBuffer } from "../../observability/telemetry-buffer";
|
|
13
|
-
import type { TelemetryPayloadDepth } from "../../console/settings/contracts";
|
|
14
13
|
import type { ProxyRequestOutcome, ProxyRequestState } from "../request/state";
|
|
15
14
|
import type { PayloadCaptureState } from "../../observability/payload-capture";
|
|
16
15
|
import { finalizeRequestTelemetry } from "../guards/error-lifecycle";
|
|
@@ -22,126 +21,7 @@ import {
|
|
|
22
21
|
recordPoolDispatchOutcome,
|
|
23
22
|
} from "../../network/pool-health-machine";
|
|
24
23
|
import { ProxyConnectRefusalError } from "../../network/pool/agent";
|
|
25
|
-
import {
|
|
26
|
-
payloadCaptureKeepsBodies,
|
|
27
|
-
payloadRetentionMs,
|
|
28
|
-
resolvePayloadCapture,
|
|
29
|
-
} from "../../observability/payload-capture";
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* True when the tenant keeps response bodies (mode `full`). The dispatch
|
|
33
|
-
* path calls this once per attempt to decide whether the provider-response
|
|
34
|
-
* tee is worth its clone + bounded read; `none`/`metadata` skip the body.
|
|
35
|
-
*/
|
|
36
|
-
export async function shouldCaptureProviderBody(
|
|
37
|
-
db: CartethyiaDatabase,
|
|
38
|
-
tenantId: string | null,
|
|
39
|
-
): Promise<boolean> {
|
|
40
|
-
return payloadCaptureKeepsBodies(db, tenantId);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Per-stage cap for the provider request/response tee at one capture depth.
|
|
45
|
-
*
|
|
46
|
-
* The tee is what the drawer's "Provider Request" and "Provider Response"
|
|
47
|
-
* panels read, so its cap is the most a provider panel can ever show. It used
|
|
48
|
-
* to be a flat 256 KiB at every depth: on a large request the translated
|
|
49
|
-
* provider body was clipped to 256 KiB before the shared record budget ever
|
|
50
|
-
* ran, so a tenant who raised the depth to Maximum — 16 MiB — still saw a
|
|
51
|
-
* quarter-megabyte provider request and an empty provider response. Derived
|
|
52
|
-
* from the same depth tiers as the record budget, so raising the depth now
|
|
53
|
-
* raises both.
|
|
54
|
-
*
|
|
55
|
-
* Capped at a quarter of the depth's total budget: four panels share it, and
|
|
56
|
-
* one of them must never be able to spend the whole thing.
|
|
57
|
-
*/
|
|
58
|
-
export function providerCaptureMaxBytes(depth?: TelemetryPayloadDepth): number {
|
|
59
|
-
if (depth === "moderate") return 2 * 1024 * 1024;
|
|
60
|
-
if (depth === "maximum") return 4 * 1024 * 1024;
|
|
61
|
-
return 256 * 1024;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Same answer as {@link shouldCaptureProviderBody}, memoized on the request.
|
|
66
|
-
*
|
|
67
|
-
* The preference is a tenant setting, so it cannot change between attempts of
|
|
68
|
-
* one request. Resolving it per attempt meant a request with several attempts
|
|
69
|
-
* paid for it repeatedly, and the per-attempt answer could in principle differ
|
|
70
|
-
* from the one the terminal capture then acted on. Passing `state` lets every
|
|
71
|
-
* attempt of a request share one resolution.
|
|
72
|
-
*/
|
|
73
|
-
export async function shouldCaptureProviderBodyForRequest(
|
|
74
|
-
db: CartethyiaDatabase,
|
|
75
|
-
tenantId: string | null,
|
|
76
|
-
state: { payloadCapturePreference?: unknown },
|
|
77
|
-
): Promise<boolean> {
|
|
78
|
-
if (state.payloadCapturePreference === undefined) {
|
|
79
|
-
state.payloadCapturePreference = await resolvePayloadCapture(db, tenantId);
|
|
80
|
-
}
|
|
81
|
-
const resolved = state.payloadCapturePreference as
|
|
82
|
-
| { readonly ok: true; readonly mode: string }
|
|
83
|
-
| { readonly ok: false };
|
|
84
|
-
return resolved.ok === true && resolved.mode === "full";
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* The tenant's capture depth, memoized on the request like the mode above.
|
|
89
|
-
*
|
|
90
|
-
* Read *after* {@link shouldCaptureProviderBodyForRequest} has populated the
|
|
91
|
-
* memo, so the two answers come from one preference read. It sizes the
|
|
92
|
-
* provider tee: a tenant who raised the depth is asking for more of every
|
|
93
|
-
* panel, and a flat cap would answer with the same clipped body regardless.
|
|
94
|
-
*/
|
|
95
|
-
export async function resolveCaptureDepthForRequest(
|
|
96
|
-
db: CartethyiaDatabase,
|
|
97
|
-
tenantId: string | null,
|
|
98
|
-
state: { payloadCapturePreference?: unknown },
|
|
99
|
-
): Promise<TelemetryPayloadDepth> {
|
|
100
|
-
if (state.payloadCapturePreference === undefined) {
|
|
101
|
-
state.payloadCapturePreference = await resolvePayloadCapture(db, tenantId);
|
|
102
|
-
}
|
|
103
|
-
const resolved = state.payloadCapturePreference as
|
|
104
|
-
| { readonly ok: true; readonly depth: TelemetryPayloadDepth }
|
|
105
|
-
| { readonly ok: false };
|
|
106
|
-
return resolved.ok === true ? resolved.depth : "minimum";
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* The tenant's chosen payload retention, memoized on the request like the mode
|
|
111
|
-
* and depth above — and resolved *after* one of them has populated the memo, so
|
|
112
|
-
* all three come from one preference read.
|
|
113
|
-
*
|
|
114
|
-
* Falls back to the deployment default when the read failed, which is the same
|
|
115
|
-
* answer the capture path reaches on its own; returning it here keeps the
|
|
116
|
-
* caller from having to know the fallback exists.
|
|
117
|
-
*/
|
|
118
|
-
export async function resolveCaptureRetentionForRequest(
|
|
119
|
-
db: CartethyiaDatabase,
|
|
120
|
-
tenantId: string | null,
|
|
121
|
-
state: { payloadCapturePreference?: unknown },
|
|
122
|
-
): Promise<number> {
|
|
123
|
-
if (state.payloadCapturePreference === undefined) {
|
|
124
|
-
state.payloadCapturePreference = await resolvePayloadCapture(db, tenantId);
|
|
125
|
-
}
|
|
126
|
-
const resolved = state.payloadCapturePreference as
|
|
127
|
-
| { readonly ok: true; readonly retentionMs: number }
|
|
128
|
-
| { readonly ok: false };
|
|
129
|
-
return resolved.ok === true ? resolved.retentionMs : payloadRetentionMs();
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Combined body cap for one capture depth. Fixed size tiers: every depth
|
|
134
|
-
* stores all four drawer panels (client request, translated provider
|
|
135
|
-
* request, raw provider response, final client response) — only the cap
|
|
136
|
-
* changes, so raising the depth never changes *what* is visible, only how
|
|
137
|
-
* much of large bodies survives. Moderate and maximum spike CPU/memory while
|
|
138
|
-
* active; minimum is light enough to leave on.
|
|
139
|
-
*/
|
|
140
|
-
function captureDepthMaxBytes(depth: TelemetryPayloadDepth): number {
|
|
141
|
-
if (depth === "moderate") return 8 * 1024 * 1024;
|
|
142
|
-
if (depth === "maximum") return 16 * 1024 * 1024;
|
|
143
|
-
return 1 * 1024 * 1024;
|
|
144
|
-
}
|
|
24
|
+
import { captureDepthMaxBytes, providerCaptureMaxBytes } from "./capture-policy";
|
|
145
25
|
|
|
146
26
|
/**
|
|
147
27
|
* What the client received for a failed terminal attempt, in the same public
|
|
@@ -186,21 +66,24 @@ async function readBoundedBody(response: Response, maxBytes: number): Promise<st
|
|
|
186
66
|
const reader = response.body.getReader();
|
|
187
67
|
const decoder = new TextDecoder();
|
|
188
68
|
// Collect decoded chunks and join once: `+=` per chunk reallocates the
|
|
189
|
-
// prefix (O(n^2)).
|
|
69
|
+
// prefix (O(n^2)). The budget is UTF-8 **bytes** — the same basis the record
|
|
70
|
+
// budget and the request-line slice use — measured per decoded chunk, with
|
|
71
|
+
// the final chunk trimmed by truncateUtf8 so a code point is never split.
|
|
190
72
|
const parts: string[] = [];
|
|
191
|
-
let
|
|
73
|
+
let totalBytes = 0;
|
|
192
74
|
try {
|
|
193
|
-
while (
|
|
75
|
+
while (totalBytes < maxBytes) {
|
|
194
76
|
const { done, value } = await reader.read();
|
|
195
77
|
if (done) break;
|
|
196
78
|
const chunk = decoder.decode(value, { stream: true });
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
79
|
+
const chunkBytes = Buffer.byteLength(chunk, "utf8");
|
|
80
|
+
if (totalBytes + chunkBytes > maxBytes) {
|
|
81
|
+
parts.push(truncateUtf8(chunk, maxBytes - totalBytes));
|
|
82
|
+
totalBytes = maxBytes;
|
|
200
83
|
break;
|
|
201
84
|
}
|
|
202
85
|
parts.push(chunk);
|
|
203
|
-
|
|
86
|
+
totalBytes += chunkBytes;
|
|
204
87
|
}
|
|
205
88
|
} catch {
|
|
206
89
|
// Best-effort capture must never fail the request path.
|
|
@@ -339,11 +222,16 @@ export function captureProviderExchange(
|
|
|
339
222
|
method: init?.method ?? "GET",
|
|
340
223
|
url,
|
|
341
224
|
...(headers === undefined ? {} : { headers }),
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
|
|
225
|
+
// `captureBody=false` is the `none`/`metadata` path: record that a body
|
|
226
|
+
// was sent and how big it was, without slicing or parsing it. The old
|
|
227
|
+
// shape sliced up to `maxBytes` of the translated body and ran it through
|
|
228
|
+
// `JSON.parse` for every attempt — content a metadata-mode tenant had
|
|
229
|
+
// explicitly declined to retain, paid on every dispatch.
|
|
230
|
+
...(rawBody === null
|
|
231
|
+
? {}
|
|
232
|
+
: captureBody
|
|
233
|
+
? { body: parseCapturedBody(truncateUtf8(rawBody, maxBytes)) }
|
|
234
|
+
: { body_present: true, body_bytes: Buffer.byteLength(rawBody, "utf8") }),
|
|
347
235
|
};
|
|
348
236
|
const response = await inner(input, init);
|
|
349
237
|
sink.response =
|
|
@@ -426,7 +314,7 @@ async function captureTerminalPayload(
|
|
|
426
314
|
note("captured_metadata");
|
|
427
315
|
return;
|
|
428
316
|
}
|
|
429
|
-
await new TelemetryPayloadCapture(db).capture({
|
|
317
|
+
const metadataCapture = await new TelemetryPayloadCapture(db).capture({
|
|
430
318
|
tenantId,
|
|
431
319
|
requestId,
|
|
432
320
|
requestBody: null,
|
|
@@ -436,13 +324,13 @@ async function captureTerminalPayload(
|
|
|
436
324
|
scope: "tenant",
|
|
437
325
|
tenantOptIn: true,
|
|
438
326
|
});
|
|
439
|
-
note("captured_metadata");
|
|
327
|
+
note(metadataCapture.payloadCaptureState ?? "captured_metadata");
|
|
440
328
|
return;
|
|
441
329
|
}
|
|
442
330
|
const maxBytes = captureDepthMaxBytes(depth);
|
|
443
331
|
const providerRequest = providerCapture?.request ?? null;
|
|
444
332
|
const providerResponse = await resolvedProviderResponse(providerCapture);
|
|
445
|
-
await new TelemetryPayloadCapture(db).capture({
|
|
333
|
+
const terminalCapture = await new TelemetryPayloadCapture(db).capture({
|
|
446
334
|
tenantId,
|
|
447
335
|
requestId,
|
|
448
336
|
requestBody: requestBody ?? null,
|
|
@@ -455,7 +343,7 @@ async function captureTerminalPayload(
|
|
|
455
343
|
scope: "tenant",
|
|
456
344
|
tenantOptIn: true,
|
|
457
345
|
});
|
|
458
|
-
note("captured");
|
|
346
|
+
note(terminalCapture.payloadCaptureState ?? "captured");
|
|
459
347
|
} catch (error) {
|
|
460
348
|
note("failed_write");
|
|
461
349
|
reportCaptureFailure(error);
|
|
@@ -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";
|