cartethyia 2.5.10 → 2.6.1
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-wr370U7a.js} +1 -1
- package/dist/dashboard/assets/BannedPage-wr370U7a.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-wf5u-ww6.js → CliToolDetailPage-D1sCezWa.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-D1sCezWa.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-DykPvuck.js → CliToolsPage-DVWbJPEQ.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-DVWbJPEQ.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-DwvSD3vs.js → CombosPage-CZc7fyMe.js} +1 -1
- package/dist/dashboard/assets/CombosPage-CZc7fyMe.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-CIr7KDex.js → ConsoleLogPage-OPN3qRCa.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-OPN3qRCa.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-82X0DDdH.js → CustomizationPage-DGTSqmwm.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-DGTSqmwm.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-BujG2QP4.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-BujG2QP4.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-CS5mPuVF.js → LoginPage-C_Jd_KgP.js} +1 -1
- package/dist/dashboard/assets/LoginPage-C_Jd_KgP.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-Bpbu-VeU.js +1 -0
- package/dist/dashboard/assets/OverviewPage-Bpbu-VeU.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-BEhXOO4L.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage-BEhXOO4L.js.gz +0 -0
- package/dist/dashboard/assets/{ProvidersPage-vdja5ZPK.js → ProvidersPage-DbQnexQd.js} +1 -1
- package/dist/dashboard/assets/ProvidersPage-DbQnexQd.js.gz +0 -0
- package/dist/dashboard/assets/{ProxyPage-BB4CNm4-.js → ProxyPage-ByprzADo.js} +1 -1
- package/dist/dashboard/assets/ProxyPage-ByprzADo.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-CvSfv6tP.js +1 -0
- package/dist/dashboard/assets/QuotaPage-CvSfv6tP.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-_NzLq3C-.js +1 -0
- package/dist/dashboard/assets/SettingsPage-_NzLq3C-.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-FadUOl9S.js → SetupPage-Bg9DcG-G.js} +1 -1
- package/dist/dashboard/assets/SetupPage-Bg9DcG-G.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-Cfy6_Khv.js → SortableList-C2PWPzJm.js} +1 -1
- package/dist/dashboard/assets/SortableList-C2PWPzJm.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-CM92fgJf.js → StudioPage-Dky1zZMt.js} +1 -1
- package/dist/dashboard/assets/StudioPage-Dky1zZMt.js.gz +0 -0
- package/dist/dashboard/assets/{UsagePage-BT4MyQeT.js → UsagePage-C5Vdpd_c.js} +2 -2
- package/dist/dashboard/assets/UsagePage-C5Vdpd_c.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-BNFjUjQ0.js +4 -0
- package/dist/dashboard/assets/entry-BNFjUjQ0.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-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-DyllNVCY.js} +2 -2
- package/dist/dashboard/assets/index-DyllNVCY.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-DAhoTabv.js} +1 -1
- package/dist/dashboard/assets/select-DAhoTabv.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 +123 -110
- package/scripts/cli/bin/carte.cjs +8 -8
- package/scripts/cli/bin/cartethyia.cjs +177 -167
- package/scripts/cli/console-menu.ts +45 -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 +195 -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/hidden-window.vbs +16 -2
- 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/StudioPage-CM92fgJf.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
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Immutable per-snapshot candidate index for model resolution.
|
|
2
|
+
import type { RouteCandidate, RouteSnapshot } from "./route-model";
|
|
3
|
+
|
|
4
|
+
/** One parsed model reference: `provider/model` or a bare `model`. */
|
|
5
|
+
export interface ParsedModel {
|
|
6
|
+
readonly providerId: string | null;
|
|
7
|
+
readonly modelId: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Parse one model reference into its provider and bare model id. */
|
|
11
|
+
export function parseModelReference(model: string): ParsedModel {
|
|
12
|
+
const slash = model.indexOf("/");
|
|
13
|
+
if (slash === -1) return { providerId: null, modelId: model };
|
|
14
|
+
return { providerId: model.slice(0, slash), modelId: model.slice(slash + 1) };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* An immutable, per-snapshot index of route candidates.
|
|
19
|
+
*
|
|
20
|
+
* Snapshots are immutable by contract (rebuilt wholesale on catalog change),
|
|
21
|
+
* so the index is built once per snapshot — O(C) — and reused by every plan,
|
|
22
|
+
* instead of the previous behavior of rescanning the full candidate list per
|
|
23
|
+
* resolved model inside one plan: a combo with K unique members over C
|
|
24
|
+
* candidates cost K×C `candidateMatches` calls (each re-parsing the model
|
|
25
|
+
* string), repeated on every request.
|
|
26
|
+
*/
|
|
27
|
+
export interface RouteCandidateIndex {
|
|
28
|
+
/** Candidates keyed by `provider_id/model_id`, snapshot order preserved. */
|
|
29
|
+
readonly qualified: ReadonlyMap<string, readonly RouteCandidate[]>;
|
|
30
|
+
/** Candidates keyed by bare `model_id`, snapshot order preserved. */
|
|
31
|
+
readonly bare: ReadonlyMap<string, readonly RouteCandidate[]>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** WeakMap cache: an evicted/rebuilt snapshot releases its index automatically. */
|
|
35
|
+
const indexBySnapshot = new WeakMap<RouteSnapshot, RouteCandidateIndex>();
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Builds (once per snapshot) the lookup index for `resolveMatchingCandidates`.
|
|
39
|
+
*
|
|
40
|
+
* Each candidate is visited exactly once and filed under both its qualified and
|
|
41
|
+
* bare key, so a lookup later is one `Map.get` regardless of catalog size.
|
|
42
|
+
* Snapshot order is preserved within each bucket because `Map` iterates in
|
|
43
|
+
* insertion order and candidates are appended in one pass.
|
|
44
|
+
*/
|
|
45
|
+
export function routeCandidateIndex(snapshot: RouteSnapshot): RouteCandidateIndex {
|
|
46
|
+
const cached = indexBySnapshot.get(snapshot);
|
|
47
|
+
if (cached) return cached;
|
|
48
|
+
const qualified = new Map<string, RouteCandidate[]>();
|
|
49
|
+
const bare = new Map<string, RouteCandidate[]>();
|
|
50
|
+
for (const candidate of snapshot.candidates) {
|
|
51
|
+
const qualifiedKey = `${candidate.provider_id}/${candidate.model_id}`;
|
|
52
|
+
const qualifiedList = qualified.get(qualifiedKey);
|
|
53
|
+
if (qualifiedList) qualifiedList.push(candidate);
|
|
54
|
+
else qualified.set(qualifiedKey, [candidate]);
|
|
55
|
+
|
|
56
|
+
const bareList = bare.get(candidate.model_id);
|
|
57
|
+
if (bareList) bareList.push(candidate);
|
|
58
|
+
else bare.set(candidate.model_id, [candidate]);
|
|
59
|
+
}
|
|
60
|
+
const index: RouteCandidateIndex = { qualified, bare };
|
|
61
|
+
indexBySnapshot.set(snapshot, index);
|
|
62
|
+
return index;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Looks up the candidates a parsed model reference resolves to, filtered by
|
|
67
|
+
* tenant.
|
|
68
|
+
*
|
|
69
|
+
* A qualified reference (`provider/model`) reads the exact bucket and requires
|
|
70
|
+
* the provider to match; a bare reference reads every provider's bucket for
|
|
71
|
+
* that model. The tenant rule is the resolver's existing one, unchanged:
|
|
72
|
+
* a candidate is visible only when it is global (`tenant_id == null`) or
|
|
73
|
+
* belongs to the requesting tenant.
|
|
74
|
+
*/
|
|
75
|
+
export function lookupRouteCandidates(
|
|
76
|
+
index: RouteCandidateIndex,
|
|
77
|
+
parsedModel: ParsedModel,
|
|
78
|
+
tenantId: string | null,
|
|
79
|
+
): readonly RouteCandidate[] {
|
|
80
|
+
const group =
|
|
81
|
+
parsedModel.providerId === null
|
|
82
|
+
? index.bare.get(parsedModel.modelId)
|
|
83
|
+
: index.qualified.get(`${parsedModel.providerId}/${parsedModel.modelId}`);
|
|
84
|
+
if (group === undefined || group.length === 0) return [];
|
|
85
|
+
return group.filter(
|
|
86
|
+
(candidate) => candidate.tenant_id == null || candidate.tenant_id === tenantId,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -1,25 +1,136 @@
|
|
|
1
|
-
// In-memory reservation lease store with
|
|
1
|
+
// In-memory reservation lease store with expiry-ordered cleanup.
|
|
2
2
|
import type { Reservation, RouteCandidate, RoutingRevision } from "./route-model";
|
|
3
3
|
|
|
4
|
+
/** One heap node: when a lease expires, and which lease it names. */
|
|
5
|
+
interface ExpiryNode {
|
|
6
|
+
readonly expiresAt: number;
|
|
7
|
+
readonly leaseId: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
4
10
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
11
|
+
* Binary min-heap on `expiresAt`.
|
|
12
|
+
*
|
|
13
|
+
* The sweep only ever needs the *earliest* expiring reservations, so a heap
|
|
14
|
+
* answers "which nodes are expired right now" by examining heads only — the
|
|
15
|
+
* previous implementation walked the entire lease map per sweep interval.
|
|
16
|
+
* Stale nodes (a lease released before its expiry) are harmless: the sweep
|
|
17
|
+
* verifies against the map before deleting and drops the node.
|
|
18
|
+
*/
|
|
19
|
+
class ExpiryHeap {
|
|
20
|
+
private nodes: ExpiryNode[] = [];
|
|
21
|
+
|
|
22
|
+
get size(): number {
|
|
23
|
+
return this.nodes.length;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
insert(node: ExpiryNode): void {
|
|
27
|
+
this.nodes.push(node);
|
|
28
|
+
this.bubbleUp(this.nodes.length - 1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The earliest-expiring node, without removing it. */
|
|
32
|
+
peek(): ExpiryNode | undefined {
|
|
33
|
+
return this.nodes[0];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
pop(): ExpiryNode | undefined {
|
|
37
|
+
const top = this.nodes[0];
|
|
38
|
+
const last = this.nodes.pop();
|
|
39
|
+
if (this.nodes.length > 0 && last !== undefined) {
|
|
40
|
+
this.nodes[0] = last;
|
|
41
|
+
this.bubbleDown(0);
|
|
42
|
+
}
|
|
43
|
+
return top;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private bubbleUp(index: number): void {
|
|
47
|
+
let current = index;
|
|
48
|
+
while (current > 0) {
|
|
49
|
+
const parent = (current - 1) >> 1;
|
|
50
|
+
if (this.nodes[current]!.expiresAt >= this.nodes[parent]!.expiresAt) break;
|
|
51
|
+
[this.nodes[current], this.nodes[parent]] = [this.nodes[parent]!, this.nodes[current]!];
|
|
52
|
+
current = parent;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private bubbleDown(index: number): void {
|
|
57
|
+
let current = index;
|
|
58
|
+
for (;;) {
|
|
59
|
+
const left = current * 2 + 1;
|
|
60
|
+
const right = left + 1;
|
|
61
|
+
let smallest = current;
|
|
62
|
+
if (left < this.nodes.length && this.nodes[left]!.expiresAt < this.nodes[smallest]!.expiresAt) {
|
|
63
|
+
smallest = left;
|
|
64
|
+
}
|
|
65
|
+
if (
|
|
66
|
+
right < this.nodes.length &&
|
|
67
|
+
this.nodes[right]!.expiresAt < this.nodes[smallest]!.expiresAt
|
|
68
|
+
) {
|
|
69
|
+
smallest = right;
|
|
70
|
+
}
|
|
71
|
+
if (smallest === current) break;
|
|
72
|
+
[this.nodes[current], this.nodes[smallest]] = [this.nodes[smallest]!, this.nodes[current]!];
|
|
73
|
+
current = smallest;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* In-memory reservation lease store with expiry-ordered cleanup.
|
|
80
|
+
*
|
|
81
|
+
* Acquisitions insert into an expiry min-heap; the bounded sweep examines only
|
|
82
|
+
* expired heads and verifies the map still holds that exact reservation before
|
|
83
|
+
* deleting (a released entry leaves a harmless stale node behind, dropped on
|
|
84
|
+
* the next sweep that reaches it or by periodic compaction). Stale nodes are
|
|
85
|
+
* compacted when they dominate the heap, so a long stream of short-lived
|
|
86
|
+
* released reservations cannot grow the heap without bound.
|
|
87
|
+
*
|
|
88
|
+
* The sweep interval stays 30 s and the lease TTL stays 30 s by default. Live
|
|
89
|
+
* reservations are not capped — capacity is admission's job, not the lease
|
|
90
|
+
* bookkeeping's. `expires_at` is NOT used to skip admission release: the
|
|
91
|
+
* admission counters protect the real limits and carry their own TTL
|
|
92
|
+
* (`INFLIGHT_TTL_SECONDS`); this map is local bookkeeping only.
|
|
7
93
|
*/
|
|
8
94
|
export class ReservationManager {
|
|
9
95
|
private leases = new Map<string, Reservation>();
|
|
96
|
+
private expiry = new ExpiryHeap();
|
|
10
97
|
private lastSweepAt = 0;
|
|
11
98
|
private static readonly SWEEP_INTERVAL_MS = 30_000;
|
|
12
99
|
|
|
100
|
+
constructor(private readonly now: () => number = Date.now) {}
|
|
101
|
+
|
|
13
102
|
private sweepExpired(now: number): void {
|
|
14
103
|
if (now - this.lastSweepAt < ReservationManager.SWEEP_INTERVAL_MS) return;
|
|
15
104
|
this.lastSweepAt = now;
|
|
16
|
-
|
|
17
|
-
|
|
105
|
+
// Only expired heads are examined: once the head expires later than `now`,
|
|
106
|
+
// every remaining node does too, so the loop stops without touching them.
|
|
107
|
+
while (this.expiry.size > 0) {
|
|
108
|
+
const head = this.expiry.peek()!;
|
|
109
|
+
if (head.expiresAt >= now) break;
|
|
110
|
+
this.expiry.pop();
|
|
111
|
+
const lease = this.leases.get(head.leaseId);
|
|
112
|
+
// A released reservation is a harmless stale node: nothing to delete.
|
|
113
|
+
// (Lease identity is the unique lease id, so a reused id cannot alias —
|
|
114
|
+
// the id embeds acquisition time plus randomness.)
|
|
115
|
+
if (lease && lease.expires_at === head.expiresAt && lease.expires_at < now) {
|
|
116
|
+
this.leases.delete(head.leaseId);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Periodic compaction of stale nodes (entries whose lease was released or
|
|
120
|
+
// re-keyed before expiry): rebuild the heap only when stale nodes make up
|
|
121
|
+
// the bulk of it, so the common all-live case pays nothing.
|
|
122
|
+
if (this.expiry.size > this.leases.size * 2) {
|
|
123
|
+
const live: ExpiryNode[] = [];
|
|
124
|
+
for (const [leaseId, lease] of this.leases) {
|
|
125
|
+
live.push({ expiresAt: lease.expires_at, leaseId });
|
|
126
|
+
}
|
|
127
|
+
this.expiry = new ExpiryHeap();
|
|
128
|
+
for (const node of live) this.expiry.insert(node);
|
|
18
129
|
}
|
|
19
130
|
}
|
|
20
131
|
|
|
21
132
|
acquire(candidate: RouteCandidate, _revision: RoutingRevision, ttlMs = 30_000): Reservation {
|
|
22
|
-
const now =
|
|
133
|
+
const now = this.now();
|
|
23
134
|
this.sweepExpired(now);
|
|
24
135
|
const lease_id = `${candidate.provider_id}:${candidate.model_id}:${now}:${Math.random().toString(16).slice(2)}`;
|
|
25
136
|
const r: Reservation = {
|
|
@@ -29,17 +140,20 @@ export class ReservationManager {
|
|
|
29
140
|
expires_at: now + ttlMs,
|
|
30
141
|
};
|
|
31
142
|
this.leases.set(lease_id, r);
|
|
143
|
+
this.expiry.insert({ expiresAt: r.expires_at, leaseId: lease_id });
|
|
32
144
|
return r;
|
|
33
145
|
}
|
|
34
146
|
|
|
35
147
|
release(lease_id: string): void {
|
|
148
|
+
// The heap node stays until its expiry passes — the sweep treats it as
|
|
149
|
+
// stale and drops it. Cheaper than a heap search per release.
|
|
36
150
|
this.leases.delete(lease_id);
|
|
37
151
|
}
|
|
38
152
|
|
|
39
153
|
get(lease_id: string): Reservation | undefined {
|
|
40
154
|
const r = this.leases.get(lease_id);
|
|
41
155
|
if (!r) return undefined;
|
|
42
|
-
if (r.expires_at <
|
|
156
|
+
if (r.expires_at < this.now()) {
|
|
43
157
|
this.leases.delete(lease_id);
|
|
44
158
|
return undefined;
|
|
45
159
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Round-robin rotation state and its bounded per-key LRU.
|
|
2
2
|
import type { RouteCandidate } from "./route-model";
|
|
3
|
+
import { BoundedLruMap } from "./bounded-lru";
|
|
3
4
|
|
|
4
5
|
export class RoundRobinState {
|
|
5
6
|
private idx = 0;
|
|
@@ -22,29 +23,16 @@ export class RoundRobinState {
|
|
|
22
23
|
* combinations would retain one entry forever. 1000 is far above the working
|
|
23
24
|
* set of any real catalog, so eviction only ever targets genuinely cold keys.
|
|
24
25
|
*/
|
|
25
|
-
const MAX_ROUND_ROBIN_ENTRIES = 1000;
|
|
26
|
+
export const MAX_ROUND_ROBIN_ENTRIES = 1000;
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* the
|
|
29
|
+
* Creates the bounded LRU map the router keeps round-robin state in.
|
|
30
|
+
*
|
|
31
|
+
* The bound is the same 1000-entry cap the hand-rolled version enforced;
|
|
32
|
+
* `BoundedLruMap` is now the single implementation of that behavior, so the
|
|
33
|
+
* eviction rule (get refreshes recency, overflow evicts the LRU head) cannot
|
|
34
|
+
* drift between this cache and the capability-profile cache.
|
|
32
35
|
*/
|
|
33
|
-
export function
|
|
34
|
-
|
|
35
|
-
key: string,
|
|
36
|
-
): RoundRobinState {
|
|
37
|
-
const existing = map.get(key);
|
|
38
|
-
if (existing) {
|
|
39
|
-
map.delete(key);
|
|
40
|
-
map.set(key, existing);
|
|
41
|
-
return existing;
|
|
42
|
-
}
|
|
43
|
-
if (map.size >= MAX_ROUND_ROBIN_ENTRIES) {
|
|
44
|
-
const oldest = map.keys().next().value;
|
|
45
|
-
if (oldest !== undefined) map.delete(oldest);
|
|
46
|
-
}
|
|
47
|
-
const created = new RoundRobinState();
|
|
48
|
-
map.set(key, created);
|
|
49
|
-
return created;
|
|
36
|
+
export function createRoundRobinMap(): BoundedLruMap<string, RoundRobinState> {
|
|
37
|
+
return new BoundedLruMap<string, RoundRobinState>(MAX_ROUND_ROBIN_ENTRIES);
|
|
50
38
|
}
|
|
@@ -30,6 +30,7 @@ import { DEFAULT_PROXY_SKIP_PROVIDER_IDS } from "../../providers/provider-metada
|
|
|
30
30
|
import { buildClaudeCliUserAgent } from "../../providers/operations/custom-cli-headers";
|
|
31
31
|
import { isBundledProviderId } from "../../providers/provider-contracts";
|
|
32
32
|
import type { ServiceKind, WireFamily } from "../../protocol/canonical-model";
|
|
33
|
+
import { BoundedLruMap } from "./bounded-lru";
|
|
33
34
|
import { providerHasAdapterUserAgent, providerUsesBespokeWire } from "../../providers/provider-metadata";
|
|
34
35
|
|
|
35
36
|
const CLAUDE_MODEL_FAMILIES = new Set(["opus", "sonnet", "haiku", "fable", "mythos"]);
|
|
@@ -156,17 +157,28 @@ export function buildCapabilityProfile(row: {
|
|
|
156
157
|
}
|
|
157
158
|
|
|
158
159
|
/**
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
* the row's metadata, so two rebuilds that see the same row get the same object
|
|
163
|
-
* instead of two identical ones. The bound keeps a long-lived process from
|
|
164
|
-
* pinning one profile per row it has ever seen — a model deleted from the
|
|
165
|
-
* catalog must not leave its profile behind forever.
|
|
160
|
+
* Upper bound on the capability-profile memo. Same value as the hand-rolled
|
|
161
|
+
* bound it replaces: far above any real catalog, so eviction only ever targets
|
|
162
|
+
* rows deleted from the catalog.
|
|
166
163
|
*/
|
|
167
164
|
const CAPABILITY_PROFILE_CACHE_MAX_ENTRIES = 4096;
|
|
168
|
-
const capabilityProfileCache = new
|
|
165
|
+
const capabilityProfileCache = new BoundedLruMap<string, Record<string, boolean>>(
|
|
166
|
+
CAPABILITY_PROFILE_CACHE_MAX_ENTRIES,
|
|
167
|
+
);
|
|
169
168
|
|
|
169
|
+
/**
|
|
170
|
+
* The capability profile for one model row, memoized across rebuilds.
|
|
171
|
+
*
|
|
172
|
+
* The value is a pure function of the row's metadata, so two rebuilds that see
|
|
173
|
+
* the same row get the same object instead of two identical ones. The cache is
|
|
174
|
+
* a {@link BoundedLruMap} keyed by `providerId/modelId` **plus the row's
|
|
175
|
+
* modalities**: the key must change when the operator edits a row's modalities
|
|
176
|
+
* (text-only → vision), otherwise the rebuild would hand routing the stale
|
|
177
|
+
* pre-edit profile until the entry aged out of the LRU. A row that has never
|
|
178
|
+
* carried modality metadata keys with `null`, which only collides with another
|
|
179
|
+
* metadata-less row for the same provider/model — the same profile by
|
|
180
|
+
* construction.
|
|
181
|
+
*/
|
|
170
182
|
function capabilityProfileFor(row: {
|
|
171
183
|
modalities: unknown;
|
|
172
184
|
reasoning: boolean;
|
|
@@ -174,14 +186,10 @@ function capabilityProfileFor(row: {
|
|
|
174
186
|
providerId: string;
|
|
175
187
|
modelId: string;
|
|
176
188
|
}): Record<string, boolean> {
|
|
177
|
-
const key = `${row.providerId}/${row.modelId}`;
|
|
189
|
+
const key = `${row.providerId}/${row.modelId}/${JSON.stringify(row.modalities ?? null)}`;
|
|
178
190
|
const cached = capabilityProfileCache.get(key);
|
|
179
191
|
if (cached !== undefined) return cached;
|
|
180
192
|
const profile = buildCapabilityProfile(row);
|
|
181
|
-
if (capabilityProfileCache.size >= CAPABILITY_PROFILE_CACHE_MAX_ENTRIES) {
|
|
182
|
-
const oldest = capabilityProfileCache.keys().next().value;
|
|
183
|
-
if (oldest !== undefined) capabilityProfileCache.delete(oldest);
|
|
184
|
-
}
|
|
185
193
|
capabilityProfileCache.set(key, profile);
|
|
186
194
|
return profile;
|
|
187
195
|
}
|
|
@@ -421,10 +429,14 @@ class RouteCatalogRepository {
|
|
|
421
429
|
* preference's own doc.
|
|
422
430
|
*/
|
|
423
431
|
const cooldownOverrideTenants = new Set<string>();
|
|
432
|
+
const invokeDisabledTenants = new Set<string>();
|
|
424
433
|
for (const row of settingsRows ?? []) {
|
|
425
434
|
if (row.preferences?.forceDisableCooldown === true && row.tenantId) {
|
|
426
435
|
cooldownOverrideTenants.add(row.tenantId);
|
|
427
436
|
}
|
|
437
|
+
if (row.preferences?.invokeDisabled === true && row.tenantId) {
|
|
438
|
+
invokeDisabledTenants.add(row.tenantId);
|
|
439
|
+
}
|
|
428
440
|
}
|
|
429
441
|
/**
|
|
430
442
|
* A shared account has no tenant of its own (`tenant_id IS NULL`), so a
|
|
@@ -843,6 +855,7 @@ class RouteCatalogRepository {
|
|
|
843
855
|
combos,
|
|
844
856
|
...(routingMap ? { providerRouting: routingMap } : {}),
|
|
845
857
|
...(Object.keys(poolRouting).length > 0 ? { poolRouting } : {}),
|
|
858
|
+
...(invokeDisabledTenants.size > 0 ? { invokeDisabledTenants } : {}),
|
|
846
859
|
};
|
|
847
860
|
}
|
|
848
861
|
}
|
|
@@ -199,6 +199,15 @@ export interface RouteSnapshot {
|
|
|
199
199
|
readonly providerRouting?: ProviderRoutingMap;
|
|
200
200
|
/** Optional per-tenant pool selection strategy (absent = `least_loaded`). */
|
|
201
201
|
readonly poolRouting?: Readonly<Record<string, PoolRoutingSetting>>;
|
|
202
|
+
/**
|
|
203
|
+
* Tenants whose invoke switch is off, so `/v1/*` must be refused outright.
|
|
204
|
+
*
|
|
205
|
+
* Read from the cached snapshot rather than per request: it is a console
|
|
206
|
+
* preference write, and the snapshot is already invalidated when one lands, so
|
|
207
|
+
* the switch takes effect on the next request without adding a query to the
|
|
208
|
+
* proxy path.
|
|
209
|
+
*/
|
|
210
|
+
readonly invokeDisabledTenants?: ReadonlySet<string>;
|
|
202
211
|
}
|
|
203
212
|
|
|
204
213
|
export type EligibilityReason =
|
|
@@ -247,8 +256,19 @@ export interface AdmissionDecision {
|
|
|
247
256
|
export interface AdmissionController {
|
|
248
257
|
admit(candidate: RouteCandidate): Promise<AdmissionDecision>;
|
|
249
258
|
release(reservation: Reservation): Promise<void>;
|
|
250
|
-
/**
|
|
251
|
-
|
|
259
|
+
/**
|
|
260
|
+
* Current inflight counts for exactly the buckets the given candidates draw
|
|
261
|
+
* from, keyed by the unprefixed admission bucket (`provider:model[:account]`).
|
|
262
|
+
*
|
|
263
|
+
* Required, not optional: the previous fleet-wide
|
|
264
|
+
* `snapshotAccountInflight()` was the only reason `plan()` could issue a
|
|
265
|
+
* Redis `KEYS` on the request path. Scoped reads make the ordering cost
|
|
266
|
+
* proportional to the candidates being ordered, and an empty input must be
|
|
267
|
+
* answerable with zero backend commands.
|
|
268
|
+
*/
|
|
269
|
+
readAccountInflight(
|
|
270
|
+
candidates: readonly RouteCandidate[],
|
|
271
|
+
): Promise<ReadonlyMap<string, number>> | ReadonlyMap<string, number>;
|
|
252
272
|
}
|
|
253
273
|
|
|
254
274
|
/**
|
|
@@ -25,24 +25,17 @@ import {
|
|
|
25
25
|
import { planAllowsCandidate } from "./plan-gate";
|
|
26
26
|
import { InMemoryAdmissionController, admissionKey } from "./admission";
|
|
27
27
|
import { ReservationManager } from "./reservations";
|
|
28
|
-
import {
|
|
28
|
+
import { createRoundRobinMap, RoundRobinState } from "./round-robin";
|
|
29
|
+
import {
|
|
30
|
+
lookupRouteCandidates,
|
|
31
|
+
parseModelReference,
|
|
32
|
+
routeCandidateIndex,
|
|
33
|
+
} from "./candidate-index";
|
|
29
34
|
import { totalRemainingCredit, totalRemainingPercent, type ProviderQuotaWindow } from "../../providers/quota/quota-contracts";
|
|
30
35
|
import { getCachedQuotaEntry, quotaLens } from "../../console/quota/cache";
|
|
31
36
|
import type { RedisClient } from "../../persistence/redis";
|
|
32
37
|
import { sharedClaudeSlowLanes } from "../../providers/integrations/claude/claude-slow-lane";
|
|
33
38
|
|
|
34
|
-
function parseModel(model: string): { providerId: string | null; bare: string } {
|
|
35
|
-
const slash = model.indexOf("/");
|
|
36
|
-
if (slash === -1) return { providerId: null, bare: model };
|
|
37
|
-
return { providerId: model.slice(0, slash), bare: model.slice(slash + 1) };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function candidateMatches(candidate: RouteCandidate, model: string): boolean {
|
|
41
|
-
const parsed = parseModel(model);
|
|
42
|
-
if (parsed.providerId === null) return candidate.model_id === parsed.bare;
|
|
43
|
-
return candidate.model_id === parsed.bare && candidate.provider_id === parsed.providerId;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
39
|
/**
|
|
47
40
|
* True when the account's remaining allowance is at or below its provider's
|
|
48
41
|
* floor. Credit mode sums the absolute remaining credit; percent mode uses the
|
|
@@ -352,8 +345,8 @@ export class EligibilityEvaluator {
|
|
|
352
345
|
}
|
|
353
346
|
|
|
354
347
|
export class RoutingEngine {
|
|
355
|
-
private readonly _roundRobinByCombo =
|
|
356
|
-
private readonly _roundRobinByProvider =
|
|
348
|
+
private readonly _roundRobinByCombo = createRoundRobinMap();
|
|
349
|
+
private readonly _roundRobinByProvider = createRoundRobinMap();
|
|
357
350
|
private readonly eligibility = new EligibilityEvaluator();
|
|
358
351
|
private readonly reservations = new ReservationManager();
|
|
359
352
|
|
|
@@ -363,11 +356,19 @@ export class RoutingEngine {
|
|
|
363
356
|
) {}
|
|
364
357
|
|
|
365
358
|
private getRoundRobin(key: string): RoundRobinState {
|
|
366
|
-
|
|
359
|
+
const existing = this._roundRobinByCombo.get(key);
|
|
360
|
+
if (existing) return existing;
|
|
361
|
+
const created = new RoundRobinState();
|
|
362
|
+
this._roundRobinByCombo.set(key, created);
|
|
363
|
+
return created;
|
|
367
364
|
}
|
|
368
365
|
|
|
369
366
|
private getProviderRoundRobin(key: string): RoundRobinState {
|
|
370
|
-
|
|
367
|
+
const existing = this._roundRobinByProvider.get(key);
|
|
368
|
+
if (existing) return existing;
|
|
369
|
+
const created = new RoundRobinState();
|
|
370
|
+
this._roundRobinByProvider.set(key, created);
|
|
371
|
+
return created;
|
|
371
372
|
}
|
|
372
373
|
|
|
373
374
|
/** Observable bounded-collection sizes for the runtime metrics sampler. */
|
|
@@ -375,11 +376,20 @@ export class RoutingEngine {
|
|
|
375
376
|
return { combo: this._roundRobinByCombo.size, provider: this._roundRobinByProvider.size };
|
|
376
377
|
}
|
|
377
378
|
|
|
378
|
-
/**
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
379
|
+
/**
|
|
380
|
+
* Live admission counters for the buckets the given candidates draw from.
|
|
381
|
+
*
|
|
382
|
+
* Takes candidates rather than returning a fleet-wide snapshot: the scoped
|
|
383
|
+
* read is what keeps `plan()` off Redis `KEYS` (the fleet-wide read had no
|
|
384
|
+
* other implementation) and bounds the console's per-provider read to the
|
|
385
|
+
* accounts it will actually report on. Both controllers hand back a fresh
|
|
386
|
+
* map, so no defensive copy here — this feeds an admin read, not a mutation
|
|
387
|
+
* site.
|
|
388
|
+
*/
|
|
389
|
+
async accountInflightSnapshot(
|
|
390
|
+
candidates: readonly RouteCandidate[],
|
|
391
|
+
): Promise<ReadonlyMap<string, number>> {
|
|
392
|
+
return this.admission.readAccountInflight(candidates);
|
|
383
393
|
}
|
|
384
394
|
|
|
385
395
|
/**
|
|
@@ -402,7 +412,12 @@ export class RoutingEngine {
|
|
|
402
412
|
(candidate) => typeof candidate.max_inflight === "number" && candidate.max_inflight > 0,
|
|
403
413
|
);
|
|
404
414
|
if (bounded.length === 0) return new Set();
|
|
405
|
-
|
|
415
|
+
// Only the bounded candidates can be saturated, so only their buckets are
|
|
416
|
+
// read. A failed ordering read must never fail routing — the admission
|
|
417
|
+
// attempt itself is the real gate; this read only optimizes attempt order.
|
|
418
|
+
const snapshot = await this.accountInflightSnapshot(bounded).catch(
|
|
419
|
+
() => new Map<string, number>(),
|
|
420
|
+
);
|
|
406
421
|
if (snapshot.size === 0) return new Set();
|
|
407
422
|
const saturated = new Set<RouteCandidate>();
|
|
408
423
|
for (const candidate of bounded) {
|
|
@@ -792,16 +807,23 @@ export class RoutingEngine {
|
|
|
792
807
|
? combo.members.flatMap((name) => expandMember(name, new Set([resolved.model])))
|
|
793
808
|
: [resolved.model];
|
|
794
809
|
const modelIds = [...new Set(rawModelIds)];
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
810
|
+
// One index build per snapshot (cached on the immutable snapshot), one
|
|
811
|
+
// lookup per unique resolved model. The previous shape rescanned every
|
|
812
|
+
// candidate per model — O(C×K) string parses per plan — which was the
|
|
813
|
+
// quadratic cost the catalog rebuild had already paid once and the request
|
|
814
|
+
// path was paying again on every plan.
|
|
815
|
+
const index = routeCandidateIndex(snapshot);
|
|
816
|
+
const parsedModels = modelIds.map((id) => ({ id, parsed: parseModelReference(id) }));
|
|
817
|
+
let matching = parsedModels.flatMap((entry) => lookupRouteCandidates(index, entry.parsed, tid));
|
|
818
|
+
// Preserve the resolver's flatten-then-dedupe contract: one candidate can
|
|
819
|
+
// serve two member groups only via a combo listing the same model twice,
|
|
820
|
+
// and the plan must not dial it twice.
|
|
821
|
+
matching = [...new Set(matching)];
|
|
800
822
|
let unmatchedMembers: readonly string[] = [];
|
|
801
823
|
if (combo) {
|
|
802
|
-
const perId =
|
|
803
|
-
id,
|
|
804
|
-
group:
|
|
824
|
+
const perId = parsedModels.map((entry) => ({
|
|
825
|
+
id: entry.id,
|
|
826
|
+
group: lookupRouteCandidates(index, entry.parsed, tid),
|
|
805
827
|
}));
|
|
806
828
|
unmatchedMembers = perId.filter((entry) => entry.group.length === 0).map((entry) => entry.id);
|
|
807
829
|
const groups = perId.map((entry) => entry.group).filter((g) => g.length > 0);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CanonicalEvent, CanonicalStopReason } from "../../../protocol/canonical-model";
|
|
2
2
|
import { jsonrepair } from "jsonrepair";
|
|
3
3
|
import { reasoningSummaryIdentity, reasoningSummarySeparator, type ReasoningSummaryIdentity } from "../reasoning-summary";
|
|
4
|
+
import { reasoningTextOf } from "../../../protocol/reasoning-text";
|
|
4
5
|
import { eventText } from "../adapters";
|
|
5
6
|
import { isRecord } from "../../../protocol/codecs";
|
|
6
7
|
import {
|
|
@@ -113,9 +114,7 @@ export function eventAudio(event: CanonicalEvent): Record<string, unknown> | und
|
|
|
113
114
|
|
|
114
115
|
export function eventReasoningText(event: CanonicalEvent): string | undefined {
|
|
115
116
|
if (event.type !== "content_delta" || event.content.kind !== "reasoning") return undefined;
|
|
116
|
-
|
|
117
|
-
if (typeof event.content.payload === "string") return event.content.payload;
|
|
118
|
-
return typeof event.content.summary === "string" ? event.content.summary : undefined;
|
|
117
|
+
return reasoningTextOf(event.content);
|
|
119
118
|
}
|
|
120
119
|
|
|
121
120
|
function mergedReasoningText(events: readonly CanonicalEvent[]): string {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CanonicalMessage, ContentPart, GenerationControls } from "../../../protocol/canonical-model";
|
|
2
2
|
import type { SurfaceInput } from "../adapters";
|
|
3
3
|
import { isRecord } from "../../../protocol/codecs";
|
|
4
|
+
import { asNumber } from "../../../shared/coerce";
|
|
4
5
|
import { textPart, normalizeInlineFile } from "../content-parts";
|
|
5
6
|
import { SERVICE_TIERS } from "../dialects";
|
|
6
7
|
import { readNumber, readString } from "../../../protocol/codecs";
|
|
@@ -75,7 +76,7 @@ export function requestBody(input: unknown): unknown {
|
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
export function nonNegativeNumber(value: unknown): number | undefined {
|
|
78
|
-
const number =
|
|
79
|
+
const number = asNumber(value);
|
|
79
80
|
return number !== undefined && number >= 0 ? number : undefined;
|
|
80
81
|
}
|
|
81
82
|
|
|
@@ -71,11 +71,14 @@ function parseToolObject(item: Record<string, unknown>, dialect: ToolDialect): T
|
|
|
71
71
|
} else {
|
|
72
72
|
const functionValue = item["function"];
|
|
73
73
|
schema = isRecord(functionValue)
|
|
74
|
-
? functionValue["parameters"] ??
|
|
74
|
+
? functionValue["parameters"] ??
|
|
75
|
+
functionValue["jsonSchema"] ??
|
|
76
|
+
functionValue["parametersJsonSchema"] ??
|
|
77
|
+
{}
|
|
75
78
|
: // The Responses flat spelling (`{type:"function", parameters:…}`) is
|
|
76
79
|
// common here too; without this a tool arrived with an empty schema
|
|
77
80
|
// and the upstream rejected it.
|
|
78
|
-
item["parameters"] ?? item["input_schema"] ?? {};
|
|
81
|
+
item["parameters"] ?? item["input_schema"] ?? item["parametersJsonSchema"] ?? {};
|
|
79
82
|
}
|
|
80
83
|
} else if (dialect === "responses") {
|
|
81
84
|
// A Responses caller may still send the Chat-shaped envelope — the two
|
|
@@ -88,8 +91,13 @@ function parseToolObject(item: Record<string, unknown>, dialect: ToolDialect): T
|
|
|
88
91
|
schema =
|
|
89
92
|
item["input_schema"] ??
|
|
90
93
|
item["parameters"] ??
|
|
94
|
+
item["parametersJsonSchema"] ??
|
|
91
95
|
(wrapped !== undefined
|
|
92
|
-
? wrapped["parameters"] ??
|
|
96
|
+
? wrapped["parameters"] ??
|
|
97
|
+
wrapped["input_schema"] ??
|
|
98
|
+
wrapped["jsonSchema"] ??
|
|
99
|
+
wrapped["parametersJsonSchema"] ??
|
|
100
|
+
{}
|
|
93
101
|
: item);
|
|
94
102
|
} else {
|
|
95
103
|
schema = item["input_schema"] ?? item["inputSchema"] ?? {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CanonicalEvent, CanonicalStopReason, ContentPart, UsageRecord } from "../../../protocol/canonical-model";
|
|
2
2
|
import { assertBoundedJsonDepth, isRecord } from "../../../protocol/codecs";
|
|
3
3
|
import { messagesStopReason } from "../../../protocol/stop-reason";
|
|
4
|
+
import { reasoningTextOf } from "../../../protocol/reasoning-text";
|
|
4
5
|
import {
|
|
5
6
|
reasoningSummaryHasBoundary,
|
|
6
7
|
reasoningSummaryIdentity,
|
|
@@ -111,7 +112,7 @@ export function blockFromPart(part: ContentPart): MessagesWireObject | undefined
|
|
|
111
112
|
: { type: "redacted_thinking", data: part.payload };
|
|
112
113
|
return {
|
|
113
114
|
type: "thinking",
|
|
114
|
-
thinking: part
|
|
115
|
+
thinking: reasoningTextOf(part) ?? "",
|
|
115
116
|
...(part.signature === undefined ? {} : { signature: part.signature }),
|
|
116
117
|
};
|
|
117
118
|
}
|
|
@@ -145,8 +145,10 @@ export function parseBlock(value: unknown): ContentPart {
|
|
|
145
145
|
const thinking = stringValue(value.thinking) ? value.thinking : "";
|
|
146
146
|
return {
|
|
147
147
|
kind: "reasoning",
|
|
148
|
+
// Readable thinking lives in `payload` only. Duplicating it into
|
|
149
|
+
// `summary` too emitted the same text twice on surfaces that carry both
|
|
150
|
+
// channels (Responses `reasoning_text` + `reasoning_summary_text`).
|
|
148
151
|
payload: thinking,
|
|
149
|
-
summary: thinking,
|
|
150
152
|
...(stringValue(value.signature) ? { signature: value.signature } : {}),
|
|
151
153
|
};
|
|
152
154
|
}
|