cartethyia 2.4.5 → 2.4.6
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 +4 -1
- package/README.md +18 -0
- package/dist/dashboard/assets/AccountCooldown-ChpdzxRH.js.gz +0 -0
- package/dist/dashboard/assets/{BannedPage-DUrXpp-X.js → BannedPage-QhvbJB9o.js} +1 -1
- package/dist/dashboard/assets/BannedPage-QhvbJB9o.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-BzGevuql.js → CliToolDetailPage-Bcy150Jo.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-Bcy150Jo.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-BaRM_7We.js → CliToolsPage-DyNXefph.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-DyNXefph.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-Cak0ljnO.js +1 -0
- package/dist/dashboard/assets/CombosPage-Cak0ljnO.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-CATpAEOz.js +1 -0
- package/dist/dashboard/assets/ConsoleLogPage-CATpAEOz.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-DqUGcoH0.js → CustomizationPage-D6jEU7nL.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-D6jEU7nL.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-CZkpn0a7.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-CZkpn0a7.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-DbhTGfhb.js → LoginPage-BJlMVN4U.js} +1 -1
- package/dist/dashboard/assets/LoginPage-BJlMVN4U.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-Br3CXEau.js +1 -0
- package/dist/dashboard/assets/ModelPicker-Br3CXEau.js.gz +0 -0
- package/dist/dashboard/assets/{OverviewPage-C3NIzsDx.js → OverviewPage-CQxXbo2g.js} +1 -1
- package/dist/dashboard/assets/OverviewPage-CQxXbo2g.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-CXiumA53.js +7 -0
- package/dist/dashboard/assets/ProxyPage-CXiumA53.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js +1 -0
- package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-YxRml8AT.js → SettingsPage-BHW4Zt06.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-BHW4Zt06.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-VOYC_lIa.js +1 -0
- package/dist/dashboard/assets/SetupPage-VOYC_lIa.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-DpOTnGuH.js +1 -0
- package/dist/dashboard/assets/SortableList-DpOTnGuH.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-BNIDLiU6.js +31 -0
- package/dist/dashboard/assets/StudioPage-BNIDLiU6.js.gz +0 -0
- package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js.gz +0 -0
- package/dist/dashboard/assets/{UsagePage-Djes-Epy.js → UsagePage-BAFaSDQV.js} +14 -14
- package/dist/dashboard/assets/UsagePage-BAFaSDQV.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-BETfaMYZ.js +1 -0
- package/dist/dashboard/assets/api-keys-BETfaMYZ.js.gz +0 -0
- package/dist/dashboard/assets/app-BLCvGdzb.css +2 -0
- package/dist/dashboard/assets/app-BLCvGdzb.css.gz +0 -0
- package/dist/dashboard/assets/{app-BDJWMThu.js → app-Cjq7nrwu.js} +3 -3
- package/dist/dashboard/assets/app-Cjq7nrwu.js.gz +0 -0
- package/dist/dashboard/assets/arrow-left-CpBxkoxI.js.gz +0 -0
- package/dist/dashboard/assets/arrow-right-Dl5yKnp8.js.gz +0 -0
- package/dist/dashboard/assets/{assert-Bwh6DJTK.js → assert-7J6_LVD3.js} +1 -1
- package/dist/dashboard/assets/assert-7J6_LVD3.js.gz +0 -0
- package/dist/dashboard/assets/backup-file-gdKIlg8U.js.gz +0 -0
- package/dist/dashboard/assets/{badge-BwmwEOgA.js → badge-BTyGk_DL.js} +1 -1
- package/dist/dashboard/assets/badge-BTyGk_DL.js.gz +0 -0
- package/dist/dashboard/assets/{banner-BKBfKTqn.js → banner-bujTWYM4.js} +1 -1
- package/dist/dashboard/assets/banner-bujTWYM4.js.gz +0 -0
- package/dist/dashboard/assets/brain-Cwed_72z.js.gz +0 -0
- package/dist/dashboard/assets/{card-BTdbRFFg.js → card-B0Bnx5CA.js} +1 -1
- package/dist/dashboard/assets/card-B0Bnx5CA.js.gz +0 -0
- package/dist/dashboard/assets/check-D69FzdvC.js.gz +0 -0
- package/dist/dashboard/assets/chevron-right-BXIy5l3h.js.gz +0 -0
- package/dist/dashboard/assets/circle-alert-CntYHV5v.js.gz +0 -0
- package/dist/dashboard/assets/circle-check-BU3v_mFW.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-DE2ug812.js +1 -0
- package/dist/dashboard/assets/cli-tools-DE2ug812.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-R_rArH8S.js +1 -0
- package/dist/dashboard/assets/clipboard-button-R_rArH8S.js.gz +0 -0
- package/dist/dashboard/assets/cog-CdWhsajs.js.gz +0 -0
- package/dist/dashboard/assets/copy-ngmQ9zcZ.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-iocKA3jr.js → dialog-BWIP_EvW.js} +1 -1
- package/dist/dashboard/assets/dialog-BWIP_EvW.js.gz +0 -0
- package/dist/dashboard/assets/dialog-actions-B1axpEg7.js.gz +0 -0
- package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
- package/dist/dashboard/assets/download-dRFo5_Wu.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-CDKOTZdY.js → drawer-BKcgVugF.js} +1 -1
- package/dist/dashboard/assets/drawer-BKcgVugF.js.gz +0 -0
- package/dist/dashboard/assets/entry-7dpgvpJ3.js +4 -0
- package/dist/dashboard/assets/entry-7dpgvpJ3.js.gz +0 -0
- package/dist/dashboard/assets/entry-CcWkGR4a.js +1 -0
- package/dist/dashboard/assets/entry-CcWkGR4a.js.gz +0 -0
- package/dist/dashboard/assets/entry-kTiTFrvh.js +1 -0
- package/dist/dashboard/assets/entry-kTiTFrvh.js.gz +0 -0
- package/dist/dashboard/assets/eye-C_hawfH4.js.gz +0 -0
- package/dist/dashboard/assets/eye-off-Bk5g2luF.js.gz +0 -0
- package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
- package/dist/dashboard/assets/gauge-BbIMy7MB.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-Booc5mNe.js +1 -0
- package/dist/dashboard/assets/github-badge-Booc5mNe.js.gz +0 -0
- package/dist/dashboard/assets/image-BPaouViG.js.gz +0 -0
- package/dist/dashboard/assets/{index-C4hXfAMs.js → index-D70pChfn.js} +2 -2
- package/dist/dashboard/assets/index-D70pChfn.js.gz +0 -0
- package/dist/dashboard/assets/{inline-DCejSLlA.js → inline-D1TCqM2D.js} +1 -1
- package/dist/dashboard/assets/inline-D1TCqM2D.js.gz +0 -0
- package/dist/dashboard/assets/{input-Bn4_kNav.js → input-B9oyU93y.js} +1 -1
- package/dist/dashboard/assets/input-B9oyU93y.js.gz +0 -0
- package/dist/dashboard/assets/jsx-runtime-CXUMngI_.js.gz +0 -0
- package/dist/dashboard/assets/key-round-UkavsGxp.js.gz +0 -0
- package/dist/dashboard/assets/layers-2-glBbjl.js.gz +0 -0
- package/dist/dashboard/assets/{layout-Bv-NxqZ5.js → layout-BArzXirQ.js} +1 -1
- package/dist/dashboard/assets/layout-BArzXirQ.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-BLA0sldD.js → list-row-BVZzaoHF.js} +1 -1
- package/dist/dashboard/assets/list-row-BVZzaoHF.js.gz +0 -0
- package/dist/dashboard/assets/{live-YlifPYox.js → live-EmuIayqp.js} +1 -1
- package/dist/dashboard/assets/live-EmuIayqp.js.gz +0 -0
- package/dist/dashboard/assets/log-in-BmRjy60c.js.gz +0 -0
- package/dist/dashboard/assets/message-circle-CCxGwRvv.js.gz +0 -0
- package/dist/dashboard/assets/{model-bans-CVvHZ-qz.js → model-bans-CSPQ9xB0.js} +1 -1
- package/dist/dashboard/assets/model-bans-CSPQ9xB0.js.gz +0 -0
- package/dist/dashboard/assets/mutation-BkoLM5Ba.js +1 -0
- package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
- package/dist/dashboard/assets/numbers-CFL4_Dqc.js.gz +0 -0
- package/dist/dashboard/assets/pencil-768ysRAr.js.gz +0 -0
- package/dist/dashboard/assets/plus-DIdX_g-x.js.gz +0 -0
- package/dist/dashboard/assets/power-off-Yjw9GMJh.js +1 -0
- package/dist/dashboard/assets/power-off-Yjw9GMJh.js.gz +0 -0
- package/dist/dashboard/assets/privacy-mode-GM8bej85.js +1 -0
- package/dist/dashboard/assets/privacy-mode-GM8bej85.js.gz +0 -0
- package/dist/dashboard/assets/providers/agentrouter.svg.gz +0 -0
- package/dist/dashboard/assets/providers/aihubmix.svg.gz +0 -0
- package/dist/dashboard/assets/providers/alibaba-coding-plan.svg.gz +0 -0
- package/dist/dashboard/assets/providers/alibaba.svg.gz +0 -0
- package/dist/dashboard/assets/providers/antigravity.svg.gz +0 -0
- package/dist/dashboard/assets/providers/bai.svg.gz +0 -0
- package/dist/dashboard/assets/providers/blackbox.svg.gz +0 -0
- package/dist/dashboard/assets/providers/claude-code.svg.gz +0 -0
- package/dist/dashboard/assets/providers/exa.svg.gz +0 -0
- package/dist/dashboard/assets/providers/gmi-cloud.svg.gz +0 -0
- package/dist/dashboard/assets/providers/inferhub.svg.gz +0 -0
- package/dist/dashboard/assets/providers/muse.svg.gz +0 -0
- package/dist/dashboard/assets/providers/nous-research.svg.gz +0 -0
- package/dist/dashboard/assets/providers/openai-light.svg.gz +0 -0
- package/dist/dashboard/assets/providers/parallel.svg.gz +0 -0
- package/dist/dashboard/assets/providers/tokenharbor.svg.gz +0 -0
- package/dist/dashboard/assets/providers/zai-coding-plan.svg.gz +0 -0
- package/dist/dashboard/assets/{query-keys-ROf-g1Wi.js → query-keys-D7wX2vM5.js} +1 -1
- package/dist/dashboard/assets/query-keys-D7wX2vM5.js.gz +0 -0
- package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
- package/dist/dashboard/assets/refresh-cw-BqJFk4sU.js.gz +0 -0
- package/dist/dashboard/assets/rotate-ccw-CufcqITi.js.gz +0 -0
- package/dist/dashboard/assets/route-CNSNj9Zm.js.gz +0 -0
- package/dist/dashboard/assets/{routing-jntw9-Ui.js → routing-BExgxxBU.js} +1 -1
- package/dist/dashboard/assets/routing-BExgxxBU.js.gz +0 -0
- package/dist/dashboard/assets/scaling-nWE4Cx8f.js.gz +0 -0
- package/dist/dashboard/assets/search-JOKus9rb.js.gz +0 -0
- package/dist/dashboard/assets/{select-CGUuTQmj.js → select-C9mMoEpJ.js} +1 -1
- package/dist/dashboard/assets/select-C9mMoEpJ.js.gz +0 -0
- package/dist/dashboard/assets/settings-C4_Gn_wq.js +1 -0
- package/dist/dashboard/assets/settings-C4_Gn_wq.js.gz +0 -0
- package/dist/dashboard/assets/shield-check-DVS0Foqh.js.gz +0 -0
- package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js.gz +0 -0
- package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
- package/dist/dashboard/assets/{stack-C1kTcOmP.js → stack-Ca5HxI5i.js} +1 -1
- package/dist/dashboard/assets/stack-Ca5HxI5i.js.gz +0 -0
- package/dist/dashboard/assets/{state-DHKdg-pm.js → state-CzUPfLXo.js} +1 -1
- package/dist/dashboard/assets/state-CzUPfLXo.js.gz +0 -0
- package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
- package/dist/dashboard/assets/{switch-DIDeAiz9.js → switch-DOUIky_i.js} +1 -1
- package/dist/dashboard/assets/switch-DOUIky_i.js.gz +0 -0
- package/dist/dashboard/assets/terminal-Bd4ee8TA.js +1 -0
- package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
- package/dist/dashboard/assets/toast-C76WrMhc.js +2 -0
- package/dist/dashboard/assets/toast-C76WrMhc.js.gz +0 -0
- package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
- package/dist/dashboard/assets/trash-Cppn5c3R.js.gz +0 -0
- package/dist/dashboard/assets/use-clipboard-B0Sy03fE.js.gz +0 -0
- package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
- package/dist/dashboard/assets/use-reduced-motion-C4B0R6Sz.js.gz +0 -0
- package/dist/dashboard/assets/use-timeout-BwcBBRrT.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-DVmwOKU-.js → useMutation-D87FZwH0.js} +1 -1
- package/dist/dashboard/assets/useMutation-D87FZwH0.js.gz +0 -0
- package/dist/dashboard/assets/wrench-B6E1oEuY.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 +123 -0
- package/migrations/full-postgres/0013_add_providers_tenant_id_index.sql +22 -0
- package/migrations/full-postgres/0014_add_provider_accounts_tenant_id_index.sql +21 -0
- package/migrations/lite-sqlite3/0000_baseline.sql +4 -2
- package/migrations/lite-sqlite3/0013_add_providers_tenant_id_index.sql +4 -0
- package/migrations/lite-sqlite3/0014_add_provider_accounts_tenant_id_index.sql +4 -0
- package/package.json +25 -30
- package/scripts/cli/{carte-launcher.cjs → bin/carte.cjs} +8 -8
- package/scripts/cli/{cli-launcher.cjs → bin/cartethyia.cjs} +167 -134
- package/scripts/cli/command/admin.ts +65 -0
- package/scripts/cli/command/console-menu.ts +227 -0
- package/scripts/cli/{doctor.ts → command/doctor.ts} +257 -258
- package/scripts/cli/{setup.ts → command/setup.ts} +8 -12
- package/scripts/cli/command/start.ts +13 -0
- package/scripts/cli/command/tray.ts +165 -0
- package/scripts/cli/command/update.ts +23 -0
- package/scripts/cli/{runtime-state.ts → gateway/drain-token.ts} +78 -78
- package/scripts/cli/gateway/spawn.ts +101 -0
- package/scripts/cli/gateway/stop.ts +51 -0
- package/scripts/cli/main.ts +94 -0
- package/scripts/cli/support/bun.ts +27 -0
- package/scripts/cli/support/env.ts +99 -0
- package/scripts/cli/support/platform.ts +64 -0
- package/scripts/cli/support/services.ts +88 -0
- package/scripts/cli/support/startup-signal.ts +23 -0
- package/scripts/cli/{version.ts → support/version.ts} +1 -1
- package/scripts/cli/tray/hidden-window.vbs +28 -0
- package/scripts/cli/{tray.ts → tray/index.ts} +3 -3
- package/scripts/cli/{tray.ps1 → tray/windows.ps1} +107 -98
- package/scripts/cli/ui/background.ts +49 -0
- package/src/boot/app.ts +3 -1
- package/src/boot/dependencies.ts +36 -13
- package/src/config/providers.ts +27 -1
- package/src/config/spec.ts +11 -0
- package/src/console/backup/routes.ts +8 -0
- package/src/console/cli-tools/contracts.ts +6 -6
- package/src/console/console-router.ts +5 -3
- package/src/console/dashboard-assets.ts +81 -11
- package/src/console/observability/contracts.ts +54 -22
- package/src/console/observability/routes.ts +31 -6
- package/src/console/observability/store.ts +68 -28
- package/src/console/providers/catalog/public-model-store.ts +91 -60
- package/src/console/providers/catalog/store.ts +5 -1
- package/src/console/quota/account-quota-tenant.ts +9 -6
- package/src/console/response-compression.ts +69 -2
- package/src/console/routing/pools/probe-verdict.ts +72 -0
- package/src/console/share/share-router.ts +7 -4
- package/src/console/share/share-stats.ts +12 -5
- package/src/console/shared/read-through-cache.ts +133 -7
- package/src/console/updates/repository-updates.ts +39 -34
- package/src/network/outbound-fetch.ts +113 -25
- package/src/network/tls/bun-client-hello.ts +112 -0
- package/src/observability/event-log.ts +29 -8
- package/src/observability/metrics.ts +40 -13
- package/src/observability/runtime-metrics.ts +7 -0
- package/src/persistence/schema.ts +40 -2
- package/src/protocol/response/codex.ts +9 -75
- package/src/protocol/tool-identity.ts +43 -9
- package/src/providers/authentication/jwt-validator.ts +130 -187
- package/src/providers/authentication/oauth-flow-store.ts +16 -9
- package/src/providers/default-registry.ts +4 -0
- package/src/providers/discovery/catalog/catalog-format.ts +20 -0
- package/src/providers/discovery/models-dev-catalog.ts +39 -22
- package/src/providers/discovery/probe-wire.ts +29 -5
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +3 -2
- package/src/providers/integrations/buddy/buddy-cli-headers.ts +88 -0
- package/src/providers/integrations/buddy/buddy-conversation-headers.ts +4 -0
- package/src/providers/integrations/buddy/buddy-discovery-shared.ts +33 -1
- package/src/providers/integrations/buddy/buddy-identity.ts +46 -0
- package/src/providers/integrations/buddy/buddy-oauth-shared.ts +37 -1
- package/src/providers/integrations/buddy/buddy-usage-store.ts +40 -5
- package/src/providers/integrations/buddy/codebuddy/provider.ts +2 -2
- package/src/providers/integrations/buddy/codebuddy/shared.ts +106 -73
- package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +5 -2
- package/src/providers/integrations/buddy/workbuddy/quota.ts +172 -159
- package/src/providers/integrations/buddy/workbuddy/shared.ts +200 -171
- package/src/providers/integrations/codex/codex.ts +5 -8
- package/src/providers/integrations/factory/factory-oauth.ts +7 -11
- package/src/providers/integrations/opencode/opencode-go-quota.ts +130 -0
- package/src/providers/integrations/opencode/provider.ts +42 -3
- package/src/providers/integrations/qoder/qoder-oauth.ts +3 -9
- package/src/providers/integrations/shared/install-id.ts +29 -2
- package/src/providers/operations/client-versions.ts +11 -1
- package/src/providers/operations/model-catalog-cache.ts +18 -3
- package/src/providers/operations/provider-catalog-service.ts +76 -0
- package/src/transport/dispatch/attempt-finalize.ts +7 -1
- package/src/transport/dispatch/attempt-loop.ts +9 -7
- package/src/transport/dispatch/empty-stream.ts +17 -26
- package/src/transport/failure-policy.ts +20 -0
- package/src/transport/routing/route-catalog.ts +42 -3
- package/src/transport/surface/chat/encode.ts +18 -31
- package/src/transport/surface/messages/stream.ts +3 -2
- package/src/transport/translation/tool-repair.ts +37 -16
- package/src/workers/account-identity-sweep.ts +14 -12
- package/src/workers/daily-checkin.ts +62 -0
- package/src/workers/quota-refresh-sweep.ts +27 -2
- package/dist/dashboard/assets/CombosPage-B7ETkfVH.js +0 -1
- package/dist/dashboard/assets/ConsoleLogPage-AisQzCI1.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-BNflDm_f.js +0 -1
- package/dist/dashboard/assets/ModelPicker-CAiPR5Vd.js +0 -1
- package/dist/dashboard/assets/ProviderDetailPage-BK8i5B5X.js +0 -3
- package/dist/dashboard/assets/ProvidersPage-CvCVgm1e.js +0 -1
- package/dist/dashboard/assets/ProxyPage-DjabkuzP.js +0 -7
- package/dist/dashboard/assets/QuotaPage-Dxe0B0iu.js +0 -1
- package/dist/dashboard/assets/SetupPage-BaD4P_jW.js +0 -1
- package/dist/dashboard/assets/SortableList-Bol1ASmI.js +0 -1
- package/dist/dashboard/assets/StudioPage-CLP4le3U.js +0 -31
- package/dist/dashboard/assets/api-keys-CjIGQhe7.js +0 -1
- package/dist/dashboard/assets/app-Cjgo4-Lf.css +0 -2
- package/dist/dashboard/assets/cli-tools-DccaOfhX.js +0 -1
- package/dist/dashboard/assets/clipboard-button-DQmMikNQ.js +0 -1
- package/dist/dashboard/assets/entry-Cpi0rph1.js +0 -4
- package/dist/dashboard/assets/entry-Dm3PpDCO.js +0 -1
- package/dist/dashboard/assets/entry-keNDZ_Wz.js +0 -1
- package/dist/dashboard/assets/github-badge-BuAJr8oU.js +0 -1
- package/dist/dashboard/assets/mutation-BIizFZav.js +0 -1
- package/dist/dashboard/assets/privacy-mode-CQjbZ3xk.js +0 -1
- package/dist/dashboard/assets/settings-BAS6QbmJ.js +0 -1
- package/dist/dashboard/assets/terminal-C9mZ4Ugm.js +0 -1
- package/dist/dashboard/assets/toast-DKAdP2mo.js +0 -2
- package/scripts/cli/cli.ts +0 -771
- package/scripts/operations/probe-egress.ts +0 -335
- package/scripts/operations/restart.ts +0 -120
- package/scripts/shared/env.ts +0 -269
- /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.ico +0 -0
- /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.png +0 -0
- /package/scripts/cli/{menu.ts → ui/prompt.ts} +0 -0
- /package/scripts/cli/{wordmark.ts → ui/wordmark.ts} +0 -0
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
type TelemetryEventRecord,
|
|
9
9
|
} from "../../observability/event-log";
|
|
10
10
|
import { isGatewayError } from "../../observability/telemetry-status";
|
|
11
|
-
import {
|
|
11
|
+
import { cachedReadWithEtag, type CachedResult } from "../shared/read-through-cache";
|
|
12
12
|
import { decodeDatedCursor, encodeCursor } from "../shared/cursor";
|
|
13
13
|
import { CARTETHYIA_VERSION } from "../../version";
|
|
14
14
|
import { CachedPreferencesReader, DrizzlePreferencesReader, type PreferencesReader } from "../../persistence/tenant-preferences";
|
|
@@ -298,8 +298,14 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
|
|
|
298
298
|
),
|
|
299
299
|
) {}
|
|
300
300
|
|
|
301
|
-
async health(tenantId: string): Promise<SystemHealthResponse
|
|
302
|
-
|
|
301
|
+
async health(tenantId: string): Promise<CachedResult<SystemHealthResponse>> {
|
|
302
|
+
// The only usage-family read that used to bypass both the 5 s in-process
|
|
303
|
+
// memo and the Redis read-through, so it re-parsed 24 hourly files every
|
|
304
|
+
// page load. Same 10 s TTL as `/system/usage`.
|
|
305
|
+
return this.readThroughWithEtag(
|
|
306
|
+
this.usageCacheKey(tenantId, "24h", "health"),
|
|
307
|
+
() => this.computeHealth(tenantId),
|
|
308
|
+
);
|
|
303
309
|
}
|
|
304
310
|
|
|
305
311
|
private async computeHealth(tenantId: string): Promise<SystemHealthResponse> {
|
|
@@ -401,8 +407,8 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
|
|
|
401
407
|
avg_tokens_per_sec: avgTokensPerSec,
|
|
402
408
|
};
|
|
403
409
|
}
|
|
404
|
-
async usage(tenantId: string, period: string): Promise<UsageResponse
|
|
405
|
-
return this.
|
|
410
|
+
async usage(tenantId: string, period: string): Promise<CachedResult<UsageResponse>> {
|
|
411
|
+
return this.readThroughWithEtag(this.usageCacheKey(tenantId, period, "usage"), () =>
|
|
406
412
|
this.computeUsage(tenantId, period),
|
|
407
413
|
);
|
|
408
414
|
}
|
|
@@ -415,9 +421,13 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
|
|
|
415
421
|
return `cartethyia:usage:v1:${shape}:${tenantId}:${period}`;
|
|
416
422
|
}
|
|
417
423
|
|
|
418
|
-
/**
|
|
419
|
-
|
|
420
|
-
|
|
424
|
+
/**
|
|
425
|
+
* Reads a usage response through Redis — computing when no cache is
|
|
426
|
+
* configured — and returns the strong validator alongside it, so a repeated
|
|
427
|
+
* poll can be answered with a `304` instead of the whole payload.
|
|
428
|
+
*/
|
|
429
|
+
private readThroughWithEtag<T>(key: string, compute: () => Promise<T>): Promise<CachedResult<T>> {
|
|
430
|
+
return cachedReadWithEtag(this.redis, key, compute);
|
|
421
431
|
}
|
|
422
432
|
|
|
423
433
|
private async computeUsage(tenantId: string, period: string): Promise<UsageResponse> {
|
|
@@ -467,8 +477,8 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
|
|
|
467
477
|
return promise;
|
|
468
478
|
}
|
|
469
479
|
|
|
470
|
-
async usageSummary(tenantId: string, period: string): Promise<UsageSummaryResponse
|
|
471
|
-
return this.
|
|
480
|
+
async usageSummary(tenantId: string, period: string): Promise<CachedResult<UsageSummaryResponse>> {
|
|
481
|
+
return this.readThroughWithEtag(this.usageCacheKey(tenantId, period, "summary"), () =>
|
|
472
482
|
this.computeSummary(tenantId, period),
|
|
473
483
|
);
|
|
474
484
|
}
|
|
@@ -527,8 +537,8 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
|
|
|
527
537
|
};
|
|
528
538
|
}
|
|
529
539
|
|
|
530
|
-
async usageChart(tenantId: string, period: string): Promise<UsageChartResponse
|
|
531
|
-
return this.
|
|
540
|
+
async usageChart(tenantId: string, period: string): Promise<CachedResult<UsageChartResponse>> {
|
|
541
|
+
return this.readThroughWithEtag(this.usageCacheKey(tenantId, period, "chart"), () =>
|
|
532
542
|
this.computeChart(tenantId, period),
|
|
533
543
|
);
|
|
534
544
|
}
|
|
@@ -568,8 +578,8 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
|
|
|
568
578
|
tenantId: string,
|
|
569
579
|
dimension: UsageDimension,
|
|
570
580
|
period: string,
|
|
571
|
-
): Promise<UsageByResponse
|
|
572
|
-
return this.
|
|
581
|
+
): Promise<CachedResult<UsageByResponse>> {
|
|
582
|
+
return this.readThroughWithEtag(this.usageCacheKey(tenantId, `${dimension}:${period}`, "by"), () =>
|
|
573
583
|
this.computeBy(tenantId, dimension, period),
|
|
574
584
|
);
|
|
575
585
|
}
|
|
@@ -676,8 +686,8 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
|
|
|
676
686
|
return new Map(keyRows.map((keyRow) => [keyRow.id, keyRow.label] as const));
|
|
677
687
|
}
|
|
678
688
|
|
|
679
|
-
async usageCache(tenantId: string, period: string): Promise<UsageCacheResponse
|
|
680
|
-
return this.
|
|
689
|
+
async usageCache(tenantId: string, period: string): Promise<CachedResult<UsageCacheResponse>> {
|
|
690
|
+
return this.readThroughWithEtag(this.usageCacheKey(tenantId, period, "cache"), () =>
|
|
681
691
|
this.computeCache(tenantId, period),
|
|
682
692
|
);
|
|
683
693
|
}
|
|
@@ -709,7 +719,7 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
|
|
|
709
719
|
period: string,
|
|
710
720
|
limit: number,
|
|
711
721
|
httpStatus?: number,
|
|
712
|
-
): Promise<UsageRequestsResponse
|
|
722
|
+
): Promise<CachedResult<UsageRequestsResponse>> {
|
|
713
723
|
// Ask the reader for `limit` rows instead of folding the whole window and
|
|
714
724
|
// slicing it. It stops at the newest file that fills the page, so the
|
|
715
725
|
// console's most-polled endpoint no longer parses the retention window just
|
|
@@ -723,15 +733,29 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
|
|
|
723
733
|
});
|
|
724
734
|
const rows = events.slice(0, limit);
|
|
725
735
|
const hideClientIp = await shouldMaskClientIp(this.preferences, tenantId);
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
736
|
+
// Cached like its siblings, so a repeated poll costs a `304` instead of
|
|
737
|
+
// another read. The mask flag is part of the key: it is a privacy setting,
|
|
738
|
+
// so a page must never be served from an entry computed under the other
|
|
739
|
+
// value. The rows were already read above — this bounds the label lookup
|
|
740
|
+
// and the mapping, not the telemetry read.
|
|
741
|
+
return this.readThroughWithEtag(
|
|
742
|
+
this.usageCacheKey(
|
|
743
|
+
tenantId,
|
|
744
|
+
`${period}:${limit}:${httpStatus ?? "*"}:${hideClientIp}`,
|
|
745
|
+
"requests",
|
|
746
|
+
),
|
|
747
|
+
async () => {
|
|
748
|
+
const keyIds = [...new Set(rows.map((row) => row.apiKeyId).filter((id): id is string => id !== null))];
|
|
749
|
+
const labels = await this.apiKeyLabels(tenantId, keyIds);
|
|
750
|
+
return {
|
|
751
|
+
items: rows.map((row) => {
|
|
752
|
+
const item = mapUsageRequestItem(row, hideClientIp);
|
|
753
|
+
const label = row.apiKeyId ? labels.get(row.apiKeyId) : undefined;
|
|
754
|
+
return label === undefined ? item : { ...item, apiKeyLabel: label };
|
|
755
|
+
}),
|
|
756
|
+
};
|
|
757
|
+
},
|
|
758
|
+
);
|
|
735
759
|
}
|
|
736
760
|
async usageRequestDetail(
|
|
737
761
|
tenantId: string,
|
|
@@ -795,7 +819,19 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
|
|
|
795
819
|
cursor?: string,
|
|
796
820
|
): Promise<TelemetryEventListPage> {
|
|
797
821
|
const before = decodeDatedCursor(cursor);
|
|
798
|
-
|
|
822
|
+
// The cursor becomes an `until` bound inside the query, so `limit` applies
|
|
823
|
+
// to every page and not only the first. Filtering after the read forced the
|
|
824
|
+
// previous shape: a cursor page had to fetch the whole window to know where
|
|
825
|
+
// it ended, so `limit` was dropped the moment a cursor appeared.
|
|
826
|
+
//
|
|
827
|
+
// `until` is inclusive while the cursor is exclusive, so rows at the exact
|
|
828
|
+
// cursor timestamp are still compared by id here — the cheap part of the
|
|
829
|
+
// predicate, now over one timestamp's worth of rows instead of the window.
|
|
830
|
+
const events = await queryTelemetryEvents({
|
|
831
|
+
tenantId,
|
|
832
|
+
limit: limit + 1,
|
|
833
|
+
...(before === undefined ? {} : { until: new Date(before.createdAt) }),
|
|
834
|
+
});
|
|
799
835
|
const rows = (before
|
|
800
836
|
? events.filter((event) => event.createdAt < before.createdAt || (event.createdAt === before.createdAt && event.id < before.id))
|
|
801
837
|
: events
|
|
@@ -817,7 +853,11 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
|
|
|
817
853
|
includePayload = false,
|
|
818
854
|
createdAt?: string,
|
|
819
855
|
): Promise<TelemetryEventDetail | undefined> {
|
|
820
|
-
|
|
856
|
+
// Matched by `id`, which is the field the list cursor encodes — not by
|
|
857
|
+
// `requestId`, which is a different column that merely defaults to the
|
|
858
|
+
// same value. Predicating on it lets the reader stop at the first hit
|
|
859
|
+
// instead of parsing the retention window to find one row.
|
|
860
|
+
const events = await queryTelemetryEvents({ tenantId, id: eventId });
|
|
821
861
|
const event = events.find(
|
|
822
862
|
(candidate) =>
|
|
823
863
|
candidate.id === eventId && (createdAt === undefined || candidate.createdAt === createdAt),
|
|
@@ -104,16 +104,6 @@ function commonCost(entries: readonly unknown[]): unknown {
|
|
|
104
104
|
return present.every((entry) => costSignature(entry) === signature) ? present[0] : undefined;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
function matchesModelPrefix(
|
|
108
|
-
modelPrefix: string | undefined,
|
|
109
|
-
bareModelId: string,
|
|
110
|
-
qualifiedModelId?: string,
|
|
111
|
-
): boolean {
|
|
112
|
-
const prefix = modelPrefix?.trim();
|
|
113
|
-
if (!prefix) return true;
|
|
114
|
-
return bareModelId.startsWith(prefix) || qualifiedModelId?.startsWith(prefix) === true;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
107
|
/** Catalog metadata mirrored onto an alias or combo entry. */
|
|
118
108
|
export interface ModelMetadata {
|
|
119
109
|
readonly contextLimit: number | null;
|
|
@@ -153,15 +143,42 @@ export function resolveTargetIds(
|
|
|
153
143
|
name: string,
|
|
154
144
|
aliasTargets: ReadonlyMap<string, string>,
|
|
155
145
|
comboMembers: ReadonlyMap<string, readonly string[]>,
|
|
156
|
-
seen:
|
|
146
|
+
seen: Set<string> = new Set(),
|
|
147
|
+
): string[] {
|
|
148
|
+
return collectTargetIds(name, aliasTargets, comboMembers, seen);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Walks alias → combo → member with one mutable visited set.
|
|
153
|
+
*
|
|
154
|
+
* The previous form copied the whole set per edge (`new Set(seen).add(name)`),
|
|
155
|
+
* which is called per alias and per combo row over the whole catalog. Adding
|
|
156
|
+
* and removing on one set keeps the same cycle protection — each branch still
|
|
157
|
+
* sees its own ancestors — without an allocation per step.
|
|
158
|
+
*/
|
|
159
|
+
function collectTargetIds(
|
|
160
|
+
name: string,
|
|
161
|
+
aliasTargets: ReadonlyMap<string, string>,
|
|
162
|
+
comboMembers: ReadonlyMap<string, readonly string[]>,
|
|
163
|
+
seen: Set<string>,
|
|
157
164
|
): string[] {
|
|
158
165
|
if (seen.has(name) || seen.size >= MAX_TARGET_DEPTH) return [];
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
166
|
+
seen.add(name);
|
|
167
|
+
try {
|
|
168
|
+
const members = comboMembers.get(name);
|
|
169
|
+
if (members) {
|
|
170
|
+
return members.flatMap((m) => collectTargetIds(m, aliasTargets, comboMembers, seen));
|
|
171
|
+
}
|
|
172
|
+
const target = aliasTargets.get(name);
|
|
173
|
+
if (target !== undefined) {
|
|
174
|
+
return collectTargetIds(target, aliasTargets, comboMembers, seen);
|
|
175
|
+
}
|
|
176
|
+
return [name];
|
|
177
|
+
} finally {
|
|
178
|
+
// Removing on the way out keeps `seen` meaning "ancestors on this path",
|
|
179
|
+
// so a name reachable by two sibling branches still resolves in both.
|
|
180
|
+
seen.delete(name);
|
|
181
|
+
}
|
|
165
182
|
}
|
|
166
183
|
|
|
167
184
|
/**
|
|
@@ -339,10 +356,21 @@ export class PublicModelCatalogStore {
|
|
|
339
356
|
return result;
|
|
340
357
|
}
|
|
341
358
|
|
|
359
|
+
/**
|
|
360
|
+
* Lists the models this key may see.
|
|
361
|
+
*
|
|
362
|
+
* `modelPrefix` is accepted and currently has NO effect on the result. It
|
|
363
|
+
* used to feed a `matchesModelPrefix(...) || isModelAllowed(...)` disjunct
|
|
364
|
+
* that was immediately followed by `isModelAllowed(...)` on its own — so the
|
|
365
|
+
* prefix could never add a row, and dropping it changes nothing. The
|
|
366
|
+
* parameter is kept so the call sites and the auth shape stay stable; if a
|
|
367
|
+
* real prefix filter is wanted later it needs to be an AND, not the OR it
|
|
368
|
+
* was written as.
|
|
369
|
+
*/
|
|
342
370
|
async listPublicModels(
|
|
343
371
|
tenantId: string | null,
|
|
344
372
|
snapshot: ApiKeyAuthorizationSnapshot,
|
|
345
|
-
|
|
373
|
+
_modelPrefix?: string,
|
|
346
374
|
): Promise<AllowedModelEntry[]> {
|
|
347
375
|
const providerScope =
|
|
348
376
|
tenantId === null
|
|
@@ -407,40 +435,46 @@ export class PublicModelCatalogStore {
|
|
|
407
435
|
if (typeof combo.name === "string") comboNames.add(combo.name);
|
|
408
436
|
}
|
|
409
437
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
.
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
)
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
}
|
|
438
|
+
// One clock reading for the whole response: `created` is the same
|
|
439
|
+
// timestamp for every entry, and reading it per row made the field vary
|
|
440
|
+
// across a single listing.
|
|
441
|
+
const now = Math.floor(Date.now() / 1000);
|
|
442
|
+
const modelEntries: AllowedModelEntry[] = [];
|
|
443
|
+
for (const row of rows) {
|
|
444
|
+
// A provider is listed only when the tenant has an active account for
|
|
445
|
+
// it — the same rule the console model picker applies. A no-auth
|
|
446
|
+
// provider with nothing configured is not "set", so it stays hidden
|
|
447
|
+
// until the operator adds an account.
|
|
448
|
+
const listed =
|
|
449
|
+
tenantId === null ? row.providerRequiresAccount === false : activeProviderIds.has(row.providerId);
|
|
450
|
+
if (!listed) continue;
|
|
451
|
+
const qualified = `${row.providerId}/${row.modelId}`;
|
|
452
|
+
// The prefix test is deliberately gone from this path: the chain used to
|
|
453
|
+
// be `(prefixMatch || isAllowed)` followed by `isAllowed`, which reduces
|
|
454
|
+
// to `isAllowed` alone — the prefix disjunct could never change the
|
|
455
|
+
// outcome, only cost a second full pass over the catalog. Collapsing the
|
|
456
|
+
// four chained filters into one loop also drops the three intermediate
|
|
457
|
+
// arrays they materialized.
|
|
458
|
+
if (!isModelAllowed(snapshot, row.modelId, row.providerId)) continue;
|
|
459
|
+
if (shadowsAliasOrCombo(row, aliasTargets, comboNames, snapshot)) continue;
|
|
460
|
+
const capabilities = normalizeModalities(row.modalities);
|
|
461
|
+
modelEntries.push({
|
|
462
|
+
id: qualified,
|
|
463
|
+
object: "model" as const,
|
|
464
|
+
created: now,
|
|
465
|
+
owned_by: row.providerId,
|
|
466
|
+
...(row.contextLimit != null ? { context_length: row.contextLimit } : {}),
|
|
467
|
+
...(row.outputLimit != null ? { max_completion_tokens: row.outputLimit } : {}),
|
|
468
|
+
...(capabilities ? { capabilities } : {}),
|
|
469
|
+
...(row.reasoning ? { reasoning: true } : {}),
|
|
470
|
+
...(row.toolCall ? { tool_call: true } : {}),
|
|
471
|
+
...(row.cost != null ? { cost: row.cost } : {}),
|
|
472
|
+
// Only a non-`llm` row is labelled: absence already means "chat wire".
|
|
473
|
+
...(row.serviceKind && row.serviceKind !== "llm"
|
|
474
|
+
? { service_kind: row.serviceKind }
|
|
475
|
+
: {}),
|
|
443
476
|
});
|
|
477
|
+
}
|
|
444
478
|
|
|
445
479
|
// Expose tenant model aliases and combos as first-class public models.
|
|
446
480
|
// `aliasRows`/`comboRows` were loaded above for the shadow filter.
|
|
@@ -465,9 +499,10 @@ export class PublicModelCatalogStore {
|
|
|
465
499
|
].flat();
|
|
466
500
|
const targetMeta = await this.modelMetadataById(tenantId, targetIds);
|
|
467
501
|
|
|
468
|
-
|
|
502
|
+
|
|
469
503
|
for (const a of aliasRows) {
|
|
470
|
-
|
|
504
|
+
// `!A && !B` followed by `!B` — the second subsumes the first, so the
|
|
505
|
+
// prefix test never changed the outcome. Collapsed to the real gate.
|
|
471
506
|
if (!isModelAllowed(snapshot, a.alias)) continue;
|
|
472
507
|
const target = advertisedMetadata(resolveTargetIds(a.targetModel, aliasTargets, comboMembers), targetMeta);
|
|
473
508
|
const capabilities = normalizeModalities(target.modalities);
|
|
@@ -486,7 +521,6 @@ export class PublicModelCatalogStore {
|
|
|
486
521
|
}
|
|
487
522
|
|
|
488
523
|
for (const c of comboRows) {
|
|
489
|
-
if (!matchesModelPrefix(modelPrefix, c.name) && !isModelAllowed(snapshot, c.name)) continue;
|
|
490
524
|
if (!isModelAllowed(snapshot, c.name)) continue;
|
|
491
525
|
const target = advertisedMetadata(resolveTargetIds(c.name, aliasTargets, comboMembers), targetMeta);
|
|
492
526
|
const capabilities = normalizeModalities(target.modalities);
|
|
@@ -648,12 +682,9 @@ export class PublicModelCatalogStore {
|
|
|
648
682
|
)
|
|
649
683
|
.limit(1);
|
|
650
684
|
const row = rows[0];
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
isModelAllowed(snapshot, row.modelId, row.providerId)) &&
|
|
655
|
-
isModelAllowed(snapshot, row.modelId, row.providerId)
|
|
656
|
-
) {
|
|
685
|
+
// Same reduction as the listing path: `(prefix || allowed) && allowed`
|
|
686
|
+
// is just `allowed`, and the prefix disjunct cost a redundant call.
|
|
687
|
+
if (row && isModelAllowed(snapshot, row.modelId, row.providerId)) {
|
|
657
688
|
const capabilities = normalizeModalities(row.modalities);
|
|
658
689
|
return {
|
|
659
690
|
id: `${row.providerId}/${row.modelId}`,
|
|
@@ -17,7 +17,7 @@ import { listAccountHealthEvents, recoverAccount, type AccountHealthEventRecord
|
|
|
17
17
|
import { invalidateCredentialCache } from "../../../providers/operations/provider-credential-service";
|
|
18
18
|
import { encryptCredential, hashSecret } from "../../../security/crypto";
|
|
19
19
|
import type { TelemetryBatchBuffer } from "../../../observability/telemetry-buffer";
|
|
20
|
-
import type
|
|
20
|
+
import { bumpCatalogRevision, type BundledProviderCatalog } from "../../../providers/operations/provider-catalog-service";
|
|
21
21
|
import type { AccountInflightReading, BuddyGrowthTaskLedgerEntry, ByokConnectionTestRequest, ByokConnectionTestResult, CreateProviderAccountBatchItemResult, CreateProviderAccountBatchResult, CreateProviderAccountRequest, CredentialMode, ModelCatalogEntry, ProbeAllAccountsResult, ProbeAllModelsResult, ProbeModelRequest, ProbeModelResult, ProviderAccountResponse, ProviderAccountTokenUsage, ProviderCatalogStore, ProviderRecord, SetModelEnabledRequest, UpdateModelMetadataRequest, UpdateProviderAccountRequest, ModelMetadataOverride } from "./contracts";
|
|
22
22
|
import { isServiceKind, validateCompatibilityProfile } from "./contracts";
|
|
23
23
|
import { ProviderProbingService, type ProbeOutboundResolver } from "../../../providers/discovery/probing-service";
|
|
@@ -487,6 +487,10 @@ export class DrizzleProviderCatalogStore implements ProviderCatalogStore {
|
|
|
487
487
|
.returning({ id: providers.id });
|
|
488
488
|
if (deleted.length === 0) return false;
|
|
489
489
|
this.providerRegistry.unregister(providerId);
|
|
490
|
+
// The provider left the registry, so the compiled catalog no longer
|
|
491
|
+
// contains it — the snapshot's re-seed is gated on that revision, and a
|
|
492
|
+
// delete that did not bump it would leave the provider's model rows behind.
|
|
493
|
+
bumpCatalogRevision();
|
|
490
494
|
return true;
|
|
491
495
|
}
|
|
492
496
|
async updateGlobal(
|
|
@@ -29,6 +29,7 @@ import type { ProviderId } from "../../providers/provider-contracts";
|
|
|
29
29
|
import {
|
|
30
30
|
DAILY_CHECKIN_PROVIDER_IDS,
|
|
31
31
|
checkinAttemptedToday,
|
|
32
|
+
checkinAttemptedTodayBatch,
|
|
32
33
|
dailyCheckinEgressForPass,
|
|
33
34
|
markCheckinAttempted,
|
|
34
35
|
} from "../../workers/daily-checkin";
|
|
@@ -334,12 +335,14 @@ export function registerAccountQuotaTenantRoutes(
|
|
|
334
335
|
// request; only check-in-capable accounts can carry one, so only they
|
|
335
336
|
// are read.
|
|
336
337
|
const checkinCandidates = rows.filter((row) => supportsAccountCheckin(row.providerId));
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
338
|
+
// One batched read for the whole page. Per-account reads made opening
|
|
339
|
+
// this route scale with the account count — a round trip each, on the
|
|
340
|
+
// route the dashboard polls — for a marker that is one key per account.
|
|
341
|
+
const checkinAttempted = await checkinAttemptedTodayBatch({
|
|
342
|
+
db,
|
|
343
|
+
redis,
|
|
344
|
+
accountIds: checkinCandidates.map((row) => row.id),
|
|
345
|
+
});
|
|
343
346
|
// Real credit deductions, read from the stored snapshots rather than from
|
|
344
347
|
// the upstream: the page renders every account at once, and re-hitting the
|
|
345
348
|
// billing endpoint per account would make opening it scale with the
|
|
@@ -9,6 +9,18 @@
|
|
|
9
9
|
* what the browser spends its time receiving, and shaving microseconds off a
|
|
10
10
|
* handler cannot compete with removing 94% of the bytes.
|
|
11
11
|
*
|
|
12
|
+
* ## Why the gate is a path allowlist inside the function
|
|
13
|
+
*
|
|
14
|
+
* The wrap is mounted on the console Elysia instance (`console-router.ts`),
|
|
15
|
+
* but Elysia's `use()` merges the child's `hoc` into the **root** app's ext
|
|
16
|
+
* (`node_modules/elysia/dist/base.js:886-887`). Measured: the wrap therefore
|
|
17
|
+
* runs for `/v1/*` too, so instance-level scoping cannot express "console
|
|
18
|
+
* only". A path test can, and it lives inside `gzipResponse` — the one place
|
|
19
|
+
* that runs on every request.
|
|
20
|
+
*
|
|
21
|
+
* Consequence: `/v1/models` was previously compressed as a side effect of
|
|
22
|
+
* that `hoc` propagation. It deliberately is not now.
|
|
23
|
+
*
|
|
12
24
|
* ## Why this is scoped to the console, not the proxy
|
|
13
25
|
*
|
|
14
26
|
* `/v1/*` is deliberately excluded, for two independent reasons:
|
|
@@ -26,9 +38,26 @@
|
|
|
26
38
|
* Below `MIN_COMPRESSIBLE_BYTES` the envelope, the header, and the CPU are all
|
|
27
39
|
* pure loss: a 200-byte reply is not worth a gzip stream plus a browser
|
|
28
40
|
* decompression step. The threshold sits where the saving stops being noise.
|
|
41
|
+
*
|
|
42
|
+
* ## Opting a route out
|
|
43
|
+
*
|
|
44
|
+
* A route that knows its body is already compressed — or is an encoded
|
|
45
|
+
* envelope that would gain nothing — sets {@link NO_COMPRESS_HEADER}. The
|
|
46
|
+
* honest-sounding alternative, declaring `content-encoding: gzip`, is a lie
|
|
47
|
+
* when the bytes on the wire are not gzip, and the client that believes it
|
|
48
|
+
* fails trying to inflate them.
|
|
29
49
|
*/
|
|
30
50
|
import { gzipSync } from "node:zlib";
|
|
31
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Header a route sets to say "do not compress this", popped before the
|
|
54
|
+
* response leaves.
|
|
55
|
+
*
|
|
56
|
+
* Deleted rather than left on the response, so it stays an internal signal
|
|
57
|
+
* between two layers in this process and never reaches a client or a cache.
|
|
58
|
+
*/
|
|
59
|
+
export const NO_COMPRESS_HEADER = "x-cartethyia-no-compress";
|
|
60
|
+
|
|
32
61
|
/**
|
|
33
62
|
* Smallest body worth compressing.
|
|
34
63
|
*
|
|
@@ -37,6 +66,19 @@ import { gzipSync } from "node:zlib";
|
|
|
37
66
|
*/
|
|
38
67
|
export const MIN_COMPRESSIBLE_BYTES = 1024;
|
|
39
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Largest body this module will compress, as a ceiling on the event-loop
|
|
71
|
+
* stall.
|
|
72
|
+
*
|
|
73
|
+
* `gzipSync` is synchronous, so its cost is paid out of the request's own
|
|
74
|
+
* turn and is not interruptible. `/console/api/backup/export` answers with a
|
|
75
|
+
* multi-MB JSON document; at that size level 6 costs tens of milliseconds,
|
|
76
|
+
* which is longer than an entire dashboard poll. Those bytes go to disk
|
|
77
|
+
* rather than being polled, so the compression ratio is the wrong thing to
|
|
78
|
+
* optimise — above this size the response is returned as-is.
|
|
79
|
+
*/
|
|
80
|
+
export const MAX_COMPRESSIBLE_BYTES = 1024 * 1024;
|
|
81
|
+
|
|
40
82
|
/** Content types worth compressing, matched by prefix. */
|
|
41
83
|
const COMPRESSIBLE_TYPES: readonly string[] = [
|
|
42
84
|
"application/json",
|
|
@@ -99,20 +141,42 @@ export function isCompressible(contentType: string | null, bodyLength: number):
|
|
|
99
141
|
* type: `text/event-stream` is not in {@link COMPRESSIBLE_TYPES}, so a stream
|
|
100
142
|
* can never reach the buffering path.
|
|
101
143
|
*
|
|
102
|
-
* The size gate runs on the **actual** byte length, not a declared
|
|
144
|
+
* The size gate runs on the **actual** byte length, not only a declared
|
|
103
145
|
* `content-length`: Elysia does not set that header for a serialized object
|
|
104
|
-
* (measured), so gating on it rejected every route worth compressing.
|
|
146
|
+
* (measured), so gating on it alone rejected every route worth compressing. A
|
|
147
|
+
* declared length is still used as a fast path — it can skip the read for a
|
|
148
|
+
* small reply — but silence from the header must never mean "too small". The
|
|
105
149
|
* content-type gate is what keeps a stream from being read here, and it is
|
|
106
150
|
* checked before the body is touched.
|
|
107
151
|
*/
|
|
108
152
|
export async function gzipResponse(request: Request, response: Response): Promise<Response> {
|
|
153
|
+
// Scoped to the paths this module documents. Instance-level scoping is not
|
|
154
|
+
// available here: the wrap's `hoc` propagates to the root app, so every
|
|
155
|
+
// route in the process — `/v1/*` included — reaches this function.
|
|
156
|
+
const pathname = new URL(request.url).pathname;
|
|
157
|
+
if (!pathname.startsWith("/console/api/") && pathname !== "/metrics") return response;
|
|
109
158
|
if (request.method === "HEAD") return response;
|
|
110
159
|
if (response.status === 204 || response.status === 304) return response;
|
|
111
160
|
// Already encoded — by a route or by an upstream. Never encode twice.
|
|
112
161
|
if (response.headers.has("content-encoding")) return response;
|
|
162
|
+
// A route that knows better: the body is an encoded envelope, or already
|
|
163
|
+
// compressed in a form this wire layer cannot describe. Deleted here so the
|
|
164
|
+
// signal stays internal and never reaches a client or a cache.
|
|
165
|
+
if (response.headers.has(NO_COMPRESS_HEADER)) {
|
|
166
|
+
response.headers.delete(NO_COMPRESS_HEADER);
|
|
167
|
+
return response;
|
|
168
|
+
}
|
|
113
169
|
if (!acceptsGzip(request.headers.get("accept-encoding"))) return response;
|
|
114
170
|
// Type first, before any read: this is what keeps an event stream out.
|
|
115
171
|
if (!isCompressibleType(response.headers.get("content-type"))) return response;
|
|
172
|
+
// A bodiless reply has nothing to compress, and `arrayBuffer()` on a null
|
|
173
|
+
// body yields a 0-byte buffer that would rebuild into a non-null body.
|
|
174
|
+
if (response.body === null) return response;
|
|
175
|
+
// A declared length below the threshold skips the read entirely. It cannot
|
|
176
|
+
// be the only gate — Elysia sets no `content-length` for a serialized
|
|
177
|
+
// object (measured) — so the byte-length read below still has to exist.
|
|
178
|
+
const declared = response.headers.get("content-length");
|
|
179
|
+
if (declared !== null && Number(declared) < MIN_COMPRESSIBLE_BYTES) return response;
|
|
116
180
|
|
|
117
181
|
const body = await response.arrayBuffer();
|
|
118
182
|
// Below the threshold the envelope, the header, and the CPU are all pure
|
|
@@ -120,6 +184,9 @@ export async function gzipResponse(request: Request, response: Response): Promis
|
|
|
120
184
|
// than compressed. A response that was read must always be rebuilt, or the
|
|
121
185
|
// client would receive an empty body.
|
|
122
186
|
if (body.byteLength < MIN_COMPRESSIBLE_BYTES) return rebuild(response, body);
|
|
187
|
+
// Above the ceiling the synchronous `gzipSync` stall costs more than the
|
|
188
|
+
// bytes it would save. Same rebuild obligation as the threshold path.
|
|
189
|
+
if (body.byteLength > MAX_COMPRESSIBLE_BYTES) return rebuild(response, body);
|
|
123
190
|
|
|
124
191
|
const compressed = gzipSync(new Uint8Array(body), { level: GZIP_LEVEL });
|
|
125
192
|
const headers = new Headers(response.headers);
|
|
@@ -58,3 +58,75 @@ export function classifyProviderProbeAnswer(httpStatus: number, body: string): P
|
|
|
58
58
|
reason: `Reachable; upstream answered ${httpStatus} for the probe body`,
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Error codes that mean "this attempt never reached the upstream", not "the
|
|
64
|
+
* upstream refused this address".
|
|
65
|
+
*
|
|
66
|
+
* Every one of them is worth a second attempt, and the reason is the same in
|
|
67
|
+
* each case: the failure is a property of one socket, not of the egress. A
|
|
68
|
+
* proxy that has just been dialled for the first time has to resolve, open a
|
|
69
|
+
* tunnel, and complete a TLS handshake, and any of those steps can be refused
|
|
70
|
+
* or reset by a middlebox that accepts the very next attempt — which is exactly
|
|
71
|
+
* the "fails once, works on retry" report. Retiring the pool on the first one
|
|
72
|
+
* blames the address for a cold-start race.
|
|
73
|
+
*/
|
|
74
|
+
const RETRYABLE_TRANSPORT_CODES = new Set([
|
|
75
|
+
"ECONNREFUSED",
|
|
76
|
+
"ECONNRESET",
|
|
77
|
+
"ECONNABORTED",
|
|
78
|
+
"EHOSTUNREACH",
|
|
79
|
+
"ENETUNREACH",
|
|
80
|
+
"ENETRESET",
|
|
81
|
+
"EPIPE",
|
|
82
|
+
"ETIMEDOUT",
|
|
83
|
+
"EAI_AGAIN",
|
|
84
|
+
"UND_ERR_SOCKET",
|
|
85
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
86
|
+
"ECONNECT_TIMEOUT",
|
|
87
|
+
]);
|
|
88
|
+
/** How many `cause` links to follow; wraps are shallow in practice. */
|
|
89
|
+
const PROBE_ERROR_CAUSE_DEPTH = 5;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Whether one probe error should be retried before it becomes a verdict.
|
|
93
|
+
*
|
|
94
|
+
* Deliberately narrow: an abort is *our* deadline firing, so retrying it would
|
|
95
|
+
* spend the whole probe budget on a slow egress, and an unknown error is left
|
|
96
|
+
* alone rather than guessed at. Only transport codes that describe a single
|
|
97
|
+
* dead connection qualify.
|
|
98
|
+
*/
|
|
99
|
+
export function isRetryableProbeError(error: unknown): boolean {
|
|
100
|
+
|
|
101
|
+
// Walk the `cause` chain: a transport failure is routinely wrapped — the
|
|
102
|
+
// proxy layer raises `ProxyConnectHandshakeError` whose *message* carries the
|
|
103
|
+
// real code, with nothing on `code`. Checking only the top error would miss
|
|
104
|
+
// exactly the failure this exists for.
|
|
105
|
+
let current: unknown = error;
|
|
106
|
+
for (let depth = 0; depth < PROBE_ERROR_CAUSE_DEPTH && current !== undefined && current !== null; depth += 1) {
|
|
107
|
+
if (!(current instanceof Error)) break;
|
|
108
|
+
if (current.name === "AbortError" || current.name === "TimeoutError") return false;
|
|
109
|
+
// `code` is not on the base `Error`; narrow before reading it rather than
|
|
110
|
+
// asserting a shape the runtime may not have.
|
|
111
|
+
const code: string | undefined =
|
|
112
|
+
"code" in current && typeof current.code === "string" ? current.code : undefined;
|
|
113
|
+
if (code !== undefined && RETRYABLE_TRANSPORT_CODES.has(code.toUpperCase())) return true;
|
|
114
|
+
const message = String(current.message ?? "");
|
|
115
|
+
// The wrapped code is often only in the message, so match it there too.
|
|
116
|
+
for (const candidate of RETRYABLE_TRANSPORT_CODES) {
|
|
117
|
+
if (message.includes(candidate)) return true;
|
|
118
|
+
}
|
|
119
|
+
if (
|
|
120
|
+
message.includes("fetch failed") ||
|
|
121
|
+
message.includes("socket connection was closed unexpectedly") ||
|
|
122
|
+
message.includes("other side closed") ||
|
|
123
|
+
message.startsWith("terminated") ||
|
|
124
|
+
/connection (?:was|is) closed/i.test(message)
|
|
125
|
+
) {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
current = current.cause;
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
@@ -33,11 +33,14 @@ import {
|
|
|
33
33
|
const MIN_TOKEN_LENGTH = 20;
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
* Cadence of the share stats SSE snapshot.
|
|
37
|
-
*
|
|
38
|
-
*
|
|
36
|
+
* Cadence of the share stats SSE snapshot.
|
|
37
|
+
*
|
|
38
|
+
* Raised with `SHARE_STATS_CACHE_TTL_SECONDS` (10 s). At 2 s the tick was
|
|
39
|
+
* shorter than the rollup's own cache TTL, so every push re-parsed the whole
|
|
40
|
+
* retained telemetry window — the interval was the thing making this read
|
|
41
|
+
* expensive, not the number of viewers. Short enough to still read as live.
|
|
39
42
|
*/
|
|
40
|
-
const SHARE_STATS_STREAM_INTERVAL_MS =
|
|
43
|
+
const SHARE_STATS_STREAM_INTERVAL_MS = 10_000;
|
|
41
44
|
|
|
42
45
|
/**
|
|
43
46
|
* Context and capabilities for one allowed model, sourced from the public
|