cartethyia 2.5.9 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +15 -42
- package/README.md +11 -10
- package/dist/dashboard/assets/{BannedPage-Brvuj_Eb.js → BannedPage-l6XMnfr9.js} +1 -1
- package/dist/dashboard/assets/BannedPage-l6XMnfr9.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-CqfXtI3y.js → CliToolDetailPage-DwOzAW45.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-DwOzAW45.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-GQeS91bg.js → CliToolsPage-BT3Up3QA.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-BT3Up3QA.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-BDRsTXBB.js → CombosPage-6kYIUpzR.js} +1 -1
- package/dist/dashboard/assets/CombosPage-6kYIUpzR.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-zP8AFHX0.js → ConsoleLogPage-zRnj1mlR.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-zRnj1mlR.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-BmfrNZ14.js → CustomizationPage-C7m-H1Rz.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-C7m-H1Rz.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-DoreIgm8.js → LoginPage-CbphLvjg.js} +1 -1
- package/dist/dashboard/assets/LoginPage-CbphLvjg.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-C7pDb41R.js → ModelPicker-DVny4S5_.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-DVny4S5_.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js +1 -0
- package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js.gz +0 -0
- package/dist/dashboard/assets/{ProvidersPage-BnoPgA7w.js → ProvidersPage-Dfb2896L.js} +1 -1
- package/dist/dashboard/assets/ProvidersPage-Dfb2896L.js.gz +0 -0
- package/dist/dashboard/assets/{ProxyPage-Bq_TGgdb.js → ProxyPage-BaCIdAUt.js} +1 -1
- package/dist/dashboard/assets/ProxyPage-BaCIdAUt.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js +1 -0
- package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js +1 -0
- package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-Day3mCOn.js → SetupPage-D4g6vOGc.js} +1 -1
- package/dist/dashboard/assets/SetupPage-D4g6vOGc.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-DQxllnB5.js → SortableList-C93i14BD.js} +1 -1
- package/dist/dashboard/assets/SortableList-C93i14BD.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-uMExoWu1.js → StudioPage-SgQpymjm.js} +1 -1
- package/dist/dashboard/assets/{StudioPage-uMExoWu1.js.gz → StudioPage-SgQpymjm.js.gz} +0 -0
- package/dist/dashboard/assets/{UsagePage-DXa1oR-4.js → UsagePage-BFME-eba.js} +2 -2
- package/dist/dashboard/assets/UsagePage-BFME-eba.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-DQYLQxpW.js → api-keys-C23S9sgp.js} +1 -1
- package/dist/dashboard/assets/api-keys-C23S9sgp.js.gz +0 -0
- package/dist/dashboard/assets/{app-BIt446yE.js → app-2KhtHCv9.js} +1 -1
- package/dist/dashboard/assets/{app-BIt446yE.js.gz → app-2KhtHCv9.js.gz} +0 -0
- package/dist/dashboard/assets/app-CEeaMHOy.css +2 -0
- package/dist/dashboard/assets/app-CEeaMHOy.css.gz +0 -0
- package/dist/dashboard/assets/{assert-C9FD_JwJ.js → assert-DU6qrMA_.js} +1 -1
- package/dist/dashboard/assets/assert-DU6qrMA_.js.gz +0 -0
- package/dist/dashboard/assets/{badge-CR72dasT.js → badge-CKsF123d.js} +1 -1
- package/dist/dashboard/assets/badge-CKsF123d.js.gz +0 -0
- package/dist/dashboard/assets/{banner-CI9_eSfO.js → banner-8j7uChA9.js} +1 -1
- package/dist/dashboard/assets/banner-8j7uChA9.js.gz +0 -0
- package/dist/dashboard/assets/{card-DS7CQEsO.js → card-CdsSlWA3.js} +1 -1
- package/dist/dashboard/assets/card-CdsSlWA3.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-CNqzrfdb.js → cli-tools-BaFUuFFF.js} +1 -1
- package/dist/dashboard/assets/cli-tools-BaFUuFFF.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-611pSsaX.js → clipboard-button-uL5aYLfW.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-uL5aYLfW.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-k37exjNt.js → dialog-C07hr5_G.js} +1 -1
- package/dist/dashboard/assets/dialog-C07hr5_G.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-CX7N8ucR.js → drawer-BPO1TcKH.js} +1 -1
- package/dist/dashboard/assets/drawer-BPO1TcKH.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CTP8AyiS.js → entry-Bz_CtDfC.js} +1 -1
- package/dist/dashboard/assets/entry-Bz_CtDfC.js.gz +0 -0
- package/dist/dashboard/assets/entry-CwPRKk0d.js +4 -0
- package/dist/dashboard/assets/entry-CwPRKk0d.js.gz +0 -0
- package/dist/dashboard/assets/entry-GJRbmrpA.js +1 -0
- package/dist/dashboard/assets/entry-GJRbmrpA.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-sltk1qUi.js → github-badge-DmQreSDd.js} +1 -1
- package/dist/dashboard/assets/github-badge-DmQreSDd.js.gz +0 -0
- package/dist/dashboard/assets/{index-z3zDOSMM.js → index-Bblu6erR.js} +2 -2
- package/dist/dashboard/assets/index-Bblu6erR.js.gz +0 -0
- package/dist/dashboard/assets/{inline-BhY2sSnO.js → inline-C9dCYaBZ.js} +1 -1
- package/dist/dashboard/assets/inline-C9dCYaBZ.js.gz +0 -0
- package/dist/dashboard/assets/{input-V1umFiBk.js → input-CjCnXweJ.js} +1 -1
- package/dist/dashboard/assets/input-CjCnXweJ.js.gz +0 -0
- package/dist/dashboard/assets/{layout-BBOYycyS.js → layout-V6y8DAkl.js} +1 -1
- package/dist/dashboard/assets/layout-V6y8DAkl.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-1zzZYM0B.js → list-row-CM1AaG_y.js} +1 -1
- package/dist/dashboard/assets/list-row-CM1AaG_y.js.gz +0 -0
- package/dist/dashboard/assets/{live-BivGfuUs.js → live-ta6GMb-8.js} +1 -1
- package/dist/dashboard/assets/live-ta6GMb-8.js.gz +0 -0
- package/dist/dashboard/assets/{model-bans-CmffjLuG.js → model-bans-BmYTmbAk.js} +1 -1
- package/dist/dashboard/assets/model-bans-BmYTmbAk.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-46P0oWMe.js → mutation-g_fduiNw.js} +1 -1
- package/dist/dashboard/assets/mutation-g_fduiNw.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-DkfMR5Qs.js → numbers-BOevJzdz.js} +1 -1
- package/dist/dashboard/assets/numbers-BOevJzdz.js.gz +0 -0
- package/dist/dashboard/assets/{query-keys-BFhGpwR1.js → query-keys-UETqfytp.js} +1 -1
- package/dist/dashboard/assets/query-keys-UETqfytp.js.gz +0 -0
- package/dist/dashboard/assets/{routing-CAb4F37R.js → routing-BPUo-W8d.js} +1 -1
- package/dist/dashboard/assets/routing-BPUo-W8d.js.gz +0 -0
- package/dist/dashboard/assets/{select-C6MMiaio.js → select-C980c4cz.js} +1 -1
- package/dist/dashboard/assets/select-C980c4cz.js.gz +0 -0
- package/dist/dashboard/assets/{settings-DNX9sydi.js → settings--3hxSyry.js} +1 -1
- package/dist/dashboard/assets/settings--3hxSyry.js.gz +0 -0
- package/dist/dashboard/assets/{stack-BHTfXdz_.js → stack-_gfTdLVB.js} +1 -1
- package/dist/dashboard/assets/stack-_gfTdLVB.js.gz +0 -0
- package/dist/dashboard/assets/{state-mPDRlQkl.js → state-2c5V4zo7.js} +1 -1
- package/dist/dashboard/assets/state-2c5V4zo7.js.gz +0 -0
- package/dist/dashboard/assets/{switch-DOmJxg7H.js → switch-Dkke7Naf.js} +1 -1
- package/dist/dashboard/assets/switch-Dkke7Naf.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-Dem0lXQj.js → useMutation-EDFRkn1i.js} +1 -1
- package/dist/dashboard/assets/useMutation-EDFRkn1i.js.gz +0 -0
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +51 -51
- package/migrations/README.md +52 -52
- package/migrations/full-postgres/0020_add_telemetry_event_analytics.sql +77 -0
- package/migrations/full-postgres/0021_add_api_key_concurrency_tolerance.sql +13 -0
- package/migrations/full-postgres/0022_add_inferhub_config_key.sql +17 -0
- package/migrations/full-postgres/0023_drop_api_key_concurrency_tolerance.sql +13 -0
- package/migrations/lite-sqlite3/0020_add_telemetry_event_analytics.sql +69 -0
- package/migrations/lite-sqlite3/0021_add_api_key_concurrency_tolerance.sql +13 -0
- package/migrations/lite-sqlite3/0022_add_inferhub_config_key.sql +17 -0
- package/migrations/lite-sqlite3/0023_drop_api_key_concurrency_tolerance.sql +13 -0
- package/package.json +96 -97
- package/scripts/cli/admin.ts +126 -125
- package/scripts/cli/autostart.ts +237 -237
- package/scripts/cli/background.ts +120 -110
- package/scripts/cli/bin/carte.cjs +8 -8
- package/scripts/cli/bin/cartethyia.cjs +167 -167
- package/scripts/cli/console-menu.ts +308 -275
- package/scripts/cli/controller.ts +145 -0
- package/scripts/cli/doctor.ts +1 -1
- package/scripts/cli/env.ts +99 -99
- package/scripts/cli/gateway.ts +264 -261
- package/scripts/{health.ts → cli/health.ts} +4 -4
- package/scripts/cli/instance.ts +234 -224
- package/scripts/cli/main.ts +188 -164
- package/scripts/cli/platform.ts +64 -64
- package/scripts/cli/prompt.ts +275 -107
- package/scripts/cli/services.ts +88 -88
- package/scripts/cli/setup.ts +445 -445
- package/scripts/cli/start.ts +85 -82
- package/scripts/cli/startup-signal.ts +23 -23
- package/scripts/cli/telemetry.ts +70 -0
- package/scripts/cli/tray/index.ts +263 -263
- package/scripts/cli/tray/windows.ps1 +107 -107
- package/scripts/cli/tray.ts +192 -208
- package/scripts/cli/update.ts +39 -39
- package/scripts/cli/version.ts +67 -67
- package/scripts/cli/wordmark.ts +58 -58
- package/src/boot/app.ts +16 -1
- package/src/boot/dependencies.ts +128 -25
- package/src/config/package-paths.ts +24 -0
- package/src/config/resources.ts +137 -0
- package/src/config/runtime.ts +133 -76
- package/src/config/spec.ts +30 -29
- package/src/config/telemetry.ts +0 -25
- package/src/console/api-keys/bucket-spend.ts +14 -17
- package/src/console/api-keys/contracts.ts +410 -410
- package/src/console/api-keys/routes.ts +12 -0
- package/src/console/audit/store.ts +2 -3
- package/src/console/auth/setup-service.ts +8 -1
- package/src/console/backup/routes.ts +7 -0
- package/src/console/backup/service.ts +463 -463
- package/src/console/backup/store.ts +16 -11
- package/src/console/backup/validate.ts +1 -4
- package/src/console/cli-tools/fs-ops.ts +18 -2
- package/src/console/data-management/contracts.ts +1 -8
- package/src/console/data-management/operations.ts +61 -48
- package/src/console/data-management/routes.ts +4 -2
- package/src/console/domain-registration.ts +25 -3
- package/src/console/inferhub/client.ts +139 -0
- package/src/console/inferhub/key-store.ts +129 -0
- package/src/console/inferhub/normalize.ts +347 -0
- package/src/console/inferhub/routes.ts +411 -0
- package/src/console/inferhub/store.ts +109 -0
- package/src/console/inferhub/types.ts +183 -0
- package/src/console/observability/contracts.ts +2 -41
- package/src/console/observability/store.ts +176 -371
- package/src/console/providers/catalog/public-model-store.ts +0 -0
- package/src/console/providers/catalog/store.ts +23 -36
- package/src/console/quota/account-quota-shared.ts +52 -52
- package/src/console/quota/account-quota-tenant.ts +16 -24
- package/src/console/quota/refresh.ts +441 -441
- package/src/console/remote-support/service.ts +270 -114
- package/src/console/routing/pools/import-parse.ts +206 -206
- package/src/console/routing/pools/operations.ts +543 -543
- package/src/console/routing/pools/probe-verdict.ts +132 -132
- package/src/console/routing/pools/routes.ts +181 -181
- package/src/console/routing/pools/store.ts +1 -1
- package/src/console/settings/contracts.ts +42 -40
- package/src/console/settings/store.ts +25 -25
- package/src/console/share/share-stats.ts +22 -4
- package/src/console/share/share-usage.ts +30 -4
- package/src/console/shared/cursor.ts +15 -0
- package/src/console/updates/repository-updates.ts +40 -39
- package/src/console/updates/routes.ts +12 -0
- package/src/main.ts +13 -5
- package/src/network/pool/resolver.ts +1104 -1104
- package/src/observability/client-name.ts +22 -0
- package/src/observability/event-log.ts +5 -29
- package/src/observability/event-query.ts +690 -0
- package/src/observability/log-ring.ts +26 -2
- package/src/observability/log-sanitize.ts +118 -0
- package/src/observability/logger.ts +17 -13
- package/src/observability/metrics.ts +35 -0
- package/src/observability/payload-capture.ts +114 -41
- package/src/observability/payload-store.ts +249 -24
- package/src/observability/raw-event-writer.ts +118 -0
- package/src/observability/runtime-metrics.ts +71 -1
- package/src/observability/telemetry-buffer.ts +100 -17
- package/src/observability/telemetry-reconciliation.ts +417 -0
- package/src/persistence/boot.ts +20 -9
- package/src/persistence/db-pg.ts +13 -0
- package/src/persistence/db-sqlite.ts +569 -497
- package/src/persistence/dialect.ts +3 -2
- package/src/persistence/migrate.ts +181 -181
- package/src/persistence/schema.ts +1496 -1406
- package/src/persistence/shared-cache.ts +5 -1
- package/src/persistence/stores/api-key-store.ts +581 -581
- package/src/persistence/stores/telemetry-store.ts +124 -63
- package/src/protocol/reasoning-text.ts +30 -0
- package/src/protocol/registry.ts +2 -0
- package/src/protocol/response/chat.ts +33 -7
- package/src/protocol/response/codex.ts +14 -2
- package/src/protocol/response/messages.ts +5 -2
- package/src/providers/authentication/oauth-flow-store.ts +3 -10
- package/src/providers/bundled-discovery-providers.ts +122 -122
- package/src/providers/default-registry.ts +14 -0
- package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
- package/src/providers/discovery/models-dev-catalog.ts +354 -354
- package/src/providers/discovery/probe-phases.ts +43 -7
- package/src/providers/discovery/probing-service.ts +70 -32
- package/src/providers/integrations/antigravity/antigravity.ts +9 -2
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +85 -17
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +934 -934
- package/src/providers/integrations/buddy/buddy-checkin.ts +5 -9
- package/src/providers/integrations/buddy/buddy-discovery-shared.ts +3 -7
- package/src/providers/integrations/buddy/buddy-quota-shared.ts +3 -1
- package/src/providers/integrations/buddy/capabilities.ts +4 -0
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +57 -49
- package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -180
- package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -206
- package/src/providers/integrations/buddy/workbuddy/quota.ts +161 -161
- package/src/providers/integrations/claude/claude-quota.ts +2 -2
- package/src/providers/integrations/claude/claude-user-profiles.ts +3 -2
- package/src/providers/integrations/claude/claude.ts +3 -4
- package/src/providers/integrations/cline/cline.ts +94 -8
- package/src/providers/integrations/codex/codex.ts +61 -37
- package/src/providers/integrations/cursor/cursor-catalog.ts +368 -368
- package/src/providers/integrations/cursor/cursor-http2.ts +300 -300
- package/src/providers/integrations/cursor/cursor-identity.ts +2 -4
- package/src/providers/integrations/cursor/cursor-request.ts +11 -0
- package/src/providers/integrations/cursor/cursor.ts +29 -17
- package/src/providers/integrations/cursor/protocol/framing.ts +129 -129
- package/src/providers/integrations/cursor/protocol/messages.ts +2380 -2380
- package/src/providers/integrations/cursor/protocol/struct.ts +305 -305
- package/src/providers/integrations/cursor/protocol/tool-canonical.ts +275 -0
- package/src/providers/integrations/github/github.ts +3 -1
- package/src/providers/integrations/grok/grok-quota.ts +279 -279
- package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -73
- package/src/providers/integrations/grok/grok-ratelimit.ts +218 -218
- package/src/providers/integrations/hermes.ts +7 -1
- package/src/providers/integrations/inferhub.ts +201 -73
- package/src/providers/integrations/kilo/kilo.ts +152 -152
- package/src/providers/integrations/kiro/kiro-machine-id.ts +2 -4
- package/src/providers/integrations/opencode/provider.ts +2 -0
- package/src/providers/integrations/openrouter/provider.ts +129 -129
- package/src/providers/integrations/qoder/qoder-quota.ts +193 -192
- package/src/providers/integrations/qoder/qoder-realm.ts +106 -106
- package/src/providers/integrations/trae/trae-headers.ts +17 -48
- package/src/providers/integrations/trae/trae-oauth.ts +4 -26
- package/src/providers/integrations/trae/trae-quota.ts +389 -384
- package/src/providers/integrations/trae/trae-realm.ts +15 -2
- package/src/providers/integrations/trae/trae.ts +22 -24
- package/src/providers/integrations/xiaomi-mimo/think-stream.ts +100 -24
- package/src/providers/integrations/zcode/oauth.ts +1 -1
- package/src/providers/operations/account-health-service.ts +605 -279
- package/src/providers/provider-metadata.ts +12 -1
- package/src/providers/provider-model-definition.ts +373 -373
- package/src/providers/provider-probe.ts +3 -0
- package/src/providers/quota/quota-contracts.ts +3 -7
- package/src/providers/search/search-provider.ts +2 -6
- package/src/security/admission/contracts.ts +10 -0
- package/src/security/admission/in-memory-store.ts +89 -14
- package/src/security/admission/lua.ts +191 -182
- package/src/security/admission/redis-store.ts +30 -0
- package/src/security/admission/service.ts +18 -0
- package/src/security/api-key-auth-cache.ts +96 -0
- package/src/security/api-key-auth.ts +11 -100
- package/src/security/client-router-fingerprint.ts +3 -7
- package/src/security/ip-boundary.ts +7 -3
- package/src/shared/coerce.ts +67 -0
- package/src/shared/hash.ts +6 -0
- package/src/shared/time.ts +7 -0
- package/src/transport/dispatch/attempt-finalize.ts +26 -138
- package/src/transport/dispatch/attempt-loop.ts +103 -81
- package/src/transport/dispatch/capture-policy.ts +78 -0
- package/src/transport/dispatch/proxy-request.ts +4 -2
- package/src/transport/dispatch/streaming/encoder.ts +111 -5
- package/src/transport/dispatch/streaming/stream.ts +19 -2
- package/src/transport/dispatch/streaming/thinking-loop.ts +300 -0
- package/src/transport/failure-policy.ts +27 -0
- package/src/transport/guards/abuse-guard.ts +2 -2
- package/src/transport/guards/auth-guard.ts +17 -0
- package/src/transport/guards/body-policy.ts +3 -2
- package/src/transport/guards/concurrency-guard.ts +89 -13
- package/src/transport/guards/early-rejection-capture.ts +43 -3
- package/src/transport/guards/error-lifecycle.ts +10 -2
- package/src/transport/guards/guard-errors.ts +23 -0
- package/src/transport/guards/headroom-guard.ts +41 -7
- package/src/transport/guards/pipeline.ts +24 -0
- package/src/transport/guards/request-context.ts +2 -1
- package/src/transport/routing/admission.ts +51 -9
- package/src/transport/routing/bounded-lru.ts +49 -0
- package/src/transport/routing/candidate-index.ts +88 -0
- package/src/transport/routing/reservations.ts +121 -7
- package/src/transport/routing/round-robin.ts +10 -22
- package/src/transport/routing/route-catalog.ts +26 -13
- package/src/transport/routing/route-model.ts +22 -2
- package/src/transport/routing/router.ts +53 -31
- package/src/transport/surface/chat/encode.ts +2 -3
- package/src/transport/surface/chat/parse.ts +2 -1
- package/src/transport/surface/dialects.ts +11 -3
- package/src/transport/surface/messages/encode.ts +2 -1
- package/src/transport/surface/messages/parse.ts +3 -1
- package/src/transport/surface/messages/stream.ts +2 -6
- package/src/workers/maintenance-lease.ts +200 -0
- package/src/workers/scheduler.ts +101 -1
- package/dist/dashboard/assets/BannedPage-Brvuj_Eb.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-CqfXtI3y.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-GQeS91bg.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-BDRsTXBB.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-zP8AFHX0.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-BmfrNZ14.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-Dn3M6WZc.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-Dn3M6WZc.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-DoreIgm8.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-C7pDb41R.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-CvvD1Ugk.js +0 -1
- package/dist/dashboard/assets/OverviewPage-CvvD1Ugk.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-Clbn1gsW.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage-Clbn1gsW.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-BnoPgA7w.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-Bq_TGgdb.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-D9DHXvom.js +0 -1
- package/dist/dashboard/assets/QuotaPage-D9DHXvom.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-7xV8WkWL.js +0 -1
- package/dist/dashboard/assets/SettingsPage-7xV8WkWL.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-Day3mCOn.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-DQxllnB5.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-DXa1oR-4.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
- package/dist/dashboard/assets/app-CX_Z8cMs.css +0 -2
- package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
- package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
- package/dist/dashboard/assets/badge-CR72dasT.js.gz +0 -0
- package/dist/dashboard/assets/banner-CI9_eSfO.js.gz +0 -0
- package/dist/dashboard/assets/card-DS7CQEsO.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-611pSsaX.js.gz +0 -0
- package/dist/dashboard/assets/dialog-k37exjNt.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CX7N8ucR.js.gz +0 -0
- package/dist/dashboard/assets/entry-B9Xvtnfj.js +0 -1
- package/dist/dashboard/assets/entry-B9Xvtnfj.js.gz +0 -0
- package/dist/dashboard/assets/entry-ByMGkR3C.js +0 -4
- package/dist/dashboard/assets/entry-ByMGkR3C.js.gz +0 -0
- package/dist/dashboard/assets/entry-CTP8AyiS.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-sltk1qUi.js.gz +0 -0
- package/dist/dashboard/assets/index-z3zDOSMM.js.gz +0 -0
- package/dist/dashboard/assets/inline-BhY2sSnO.js.gz +0 -0
- package/dist/dashboard/assets/input-V1umFiBk.js.gz +0 -0
- package/dist/dashboard/assets/layout-BBOYycyS.js.gz +0 -0
- package/dist/dashboard/assets/list-row-1zzZYM0B.js.gz +0 -0
- package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
- package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
- package/dist/dashboard/assets/numbers-DkfMR5Qs.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
- package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
- package/dist/dashboard/assets/select-C6MMiaio.js.gz +0 -0
- package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
- package/dist/dashboard/assets/stack-BHTfXdz_.js.gz +0 -0
- package/dist/dashboard/assets/state-mPDRlQkl.js.gz +0 -0
- package/dist/dashboard/assets/switch-DOmJxg7H.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
- package/scripts/build/aot.ts +0 -143
- package/scripts/build/build.ts +0 -24
- package/scripts/build/compile.ts +0 -88
- package/scripts/build/restart.ts +0 -120
- package/scripts/build/start.ts +0 -25
- package/src/observability/event-compaction.ts +0 -317
- package/src/observability/event-day-index.ts +0 -440
|
@@ -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
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
reasoningSummarySeparator,
|
|
6
6
|
type ReasoningSummaryIdentity,
|
|
7
7
|
} from "../reasoning-summary";
|
|
8
|
+
import { reasoningTextOf } from "../../../protocol/reasoning-text";
|
|
8
9
|
import {
|
|
9
10
|
argumentsFrom,
|
|
10
11
|
createToolCallTracker,
|
|
@@ -230,12 +231,7 @@ export class MessagesStreamEncoder extends SurfaceStreamEncoder<CanonicalEvent,
|
|
|
230
231
|
}
|
|
231
232
|
if (part.kind === "reasoning") {
|
|
232
233
|
const isRedacted = part.opaque === true;
|
|
233
|
-
const summary =
|
|
234
|
-
typeof part.summary === "string"
|
|
235
|
-
? part.summary
|
|
236
|
-
: typeof part.payload === "string"
|
|
237
|
-
? part.payload
|
|
238
|
-
: "";
|
|
234
|
+
const summary = reasoningTextOf(part) ?? "";
|
|
239
235
|
const signature = typeof part.signature === "string" ? part.signature : "";
|
|
240
236
|
// Nothing to render: an empty thinking delta must not open a block
|
|
241
237
|
// (same fragmentation as the empty-text case above).
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Redis lease so one maintenance task runs once across the fleet.
|
|
3
|
+
*
|
|
4
|
+
* Maintenance work is side-effecting and shared: telemetry retention deletes
|
|
5
|
+
* rows for every tenant, the payload sweeper deletes files, the session sweep
|
|
6
|
+
* deletes sessions. Two processes running "telemetry-retention" at once do not
|
|
7
|
+
* produce two correct halves — they produce two passes over the same batches,
|
|
8
|
+
* double the row churn, and a WAL spike on a box that is already short of
|
|
9
|
+
* memory. On a 1-core VPS the second pass is not parallel speedup; it is
|
|
10
|
+
* contention with the request path.
|
|
11
|
+
*
|
|
12
|
+
* The lease is a Redis key with a token. The token matters: release must be
|
|
13
|
+
* conditional (`compare-and-delete`), or a process that overran its lease and
|
|
14
|
+
* was replaced would delete its SUCCESSOR's lease and let a third process in.
|
|
15
|
+
* An unconditional `DEL` is the classic version of that bug.
|
|
16
|
+
*/
|
|
17
|
+
import { randomUUID } from "node:crypto";
|
|
18
|
+
|
|
19
|
+
import type { RedisClient } from "../persistence/redis";
|
|
20
|
+
|
|
21
|
+
/** Key prefix; the task name is the discriminator. */
|
|
22
|
+
const LEASE_KEY_PREFIX = "cartethyia:maintenance:lease:v1:";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Bounds on the lease TTL.
|
|
26
|
+
*
|
|
27
|
+
* The floor prevents a fast task from holding a lease so briefly that a peer
|
|
28
|
+
* with a slightly skewed clock steals it mid-run. The ceiling keeps a crashed
|
|
29
|
+
* holder from blocking the task for longer than a maintenance interval is
|
|
30
|
+
* likely to be.
|
|
31
|
+
*/
|
|
32
|
+
const MIN_LEASE_TTL_MS = 5_000;
|
|
33
|
+
const MAX_LEASE_TTL_MS = 60_000;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Acquires the lease: `SET key token PX ttl NX`.
|
|
37
|
+
*
|
|
38
|
+
* One atomic statement rather than GET-then-SET: two processes can both read
|
|
39
|
+
* "absent" before either writes, and both would then believe they held it.
|
|
40
|
+
*/
|
|
41
|
+
const ACQUIRE_SCRIPT = `
|
|
42
|
+
local ok = redis.call('set', KEYS[1], ARGV[1], 'PX', tonumber(ARGV[2]), 'NX')
|
|
43
|
+
if ok then return 1 end
|
|
44
|
+
return 0
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Renews only if the token still matches: `GET` + compare + `PEXPIRE`.
|
|
49
|
+
*
|
|
50
|
+
* Extending a lease this process no longer holds — because it overran its TTL
|
|
51
|
+
* and a peer took over — would make two holders of one lease.
|
|
52
|
+
*/
|
|
53
|
+
const RENEW_SCRIPT = `
|
|
54
|
+
if redis.call('get', KEYS[1]) == ARGV[1] then
|
|
55
|
+
redis.call('pexpire', KEYS[1], tonumber(ARGV[2]))
|
|
56
|
+
return 1
|
|
57
|
+
end
|
|
58
|
+
return 0
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Releases only if the token still matches.
|
|
63
|
+
*
|
|
64
|
+
* An unconditional DEL would delete a successor's lease, letting a third
|
|
65
|
+
* process acquire while the second is still running.
|
|
66
|
+
*/
|
|
67
|
+
const RELEASE_SCRIPT = `
|
|
68
|
+
if redis.call('get', KEYS[1]) == ARGV[1] then
|
|
69
|
+
redis.call('del', KEYS[1])
|
|
70
|
+
return 1
|
|
71
|
+
end
|
|
72
|
+
return 0
|
|
73
|
+
`;
|
|
74
|
+
|
|
75
|
+
export interface MaintenanceLease {
|
|
76
|
+
readonly taskName: string;
|
|
77
|
+
/** True when this process holds the lease. */
|
|
78
|
+
readonly held: boolean;
|
|
79
|
+
/** Extends the lease. Returns false when it has already been lost. */
|
|
80
|
+
renew(): Promise<boolean>;
|
|
81
|
+
/** Releases the lease. Safe (and a no-op) when it was never held or already lost. */
|
|
82
|
+
release(): Promise<void>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface MaintenanceLeaseStore {
|
|
86
|
+
/**
|
|
87
|
+
* One acquisition attempt.
|
|
88
|
+
*
|
|
89
|
+
* Deliberately not a retry loop: a tick that cannot acquire should SKIP, not
|
|
90
|
+
* wait. Waiting would hold the tick open, and the next tick would arrive and
|
|
91
|
+
* queue behind it — turning a missed interval into a catch-up storm.
|
|
92
|
+
*/
|
|
93
|
+
tryAcquire(taskName: string, ttlMs: number): Promise<MaintenanceLease>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The TTL for a task, clamped into the supported range. */
|
|
97
|
+
export function leaseTtlMs(intervalMs: number): number {
|
|
98
|
+
return Math.min(MAX_LEASE_TTL_MS, Math.max(MIN_LEASE_TTL_MS, Math.trunc(intervalMs)));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** A store that hands out no lease — used when Redis is absent. */
|
|
102
|
+
export function createNoopMaintenanceLeaseStore(): MaintenanceLeaseStore {
|
|
103
|
+
return {
|
|
104
|
+
async tryAcquire(taskName) {
|
|
105
|
+
return {
|
|
106
|
+
taskName,
|
|
107
|
+
// Held = true: with no shared store this process cannot coordinate, so
|
|
108
|
+
// the local registry's own re-entrancy rule is the only guard. The
|
|
109
|
+
// alternative — reporting not-held — would silently disable every
|
|
110
|
+
// maintenance task on single-process deployments, which is most of
|
|
111
|
+
// them.
|
|
112
|
+
held: true,
|
|
113
|
+
async renew() {
|
|
114
|
+
return true;
|
|
115
|
+
},
|
|
116
|
+
async release() {
|
|
117
|
+
/* nothing to give back */
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* A Redis-backed lease store.
|
|
126
|
+
*
|
|
127
|
+
* Every failure is non-throwing at the caller's level: a Redis that is down
|
|
128
|
+
* must not stop a task that only that process can do, and must not crash a
|
|
129
|
+
* maintenance tick. Acquisition failure is reported as "not held" so the
|
|
130
|
+
* registry skips and counts.
|
|
131
|
+
*/
|
|
132
|
+
export function createRedisMaintenanceLeaseStore(redis: RedisClient): MaintenanceLeaseStore {
|
|
133
|
+
const evalScript = async (
|
|
134
|
+
script: string,
|
|
135
|
+
key: string,
|
|
136
|
+
args: readonly (string | number)[],
|
|
137
|
+
): Promise<number> => {
|
|
138
|
+
const raw = await (
|
|
139
|
+
redis as unknown as {
|
|
140
|
+
eval: (
|
|
141
|
+
script: string,
|
|
142
|
+
numKeys: number,
|
|
143
|
+
...args: (string | number)[]
|
|
144
|
+
) => Promise<unknown>;
|
|
145
|
+
}
|
|
146
|
+
).eval(script, 1, key, ...args);
|
|
147
|
+
return Number(raw);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
async tryAcquire(taskName, ttlMs) {
|
|
152
|
+
const key = `${LEASE_KEY_PREFIX}${taskName}`;
|
|
153
|
+
const token = randomUUID();
|
|
154
|
+
const ttl = leaseTtlMs(ttlMs);
|
|
155
|
+
let held = false;
|
|
156
|
+
try {
|
|
157
|
+
held = (await evalScript(ACQUIRE_SCRIPT, key, [token, ttl])) === 1;
|
|
158
|
+
} catch {
|
|
159
|
+
// Redis unreachable: skip. Reported, not swallowed silently — the
|
|
160
|
+
// registry counts the skip so a persistent outage is visible.
|
|
161
|
+
held = false;
|
|
162
|
+
}
|
|
163
|
+
if (!held) {
|
|
164
|
+
return {
|
|
165
|
+
taskName,
|
|
166
|
+
held: false,
|
|
167
|
+
async renew() {
|
|
168
|
+
return false;
|
|
169
|
+
},
|
|
170
|
+
async release() {
|
|
171
|
+
/* nothing held */
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
let released = false;
|
|
176
|
+
return {
|
|
177
|
+
taskName,
|
|
178
|
+
held: true,
|
|
179
|
+
async renew() {
|
|
180
|
+
if (released) return false;
|
|
181
|
+
try {
|
|
182
|
+
return (await evalScript(RENEW_SCRIPT, key, [token, ttl])) === 1;
|
|
183
|
+
} catch {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
async release() {
|
|
188
|
+
if (released) return;
|
|
189
|
+
released = true;
|
|
190
|
+
try {
|
|
191
|
+
await evalScript(RELEASE_SCRIPT, key, [token]);
|
|
192
|
+
} catch {
|
|
193
|
+
// Best-effort: the TTL releases it anyway, and a failed release
|
|
194
|
+
// must not turn a successful task run into an error.
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|