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
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InferHub market routes — `/console/api/inferhub/*`.
|
|
3
|
+
*
|
|
4
|
+
* Global-admin only: the `config_read` key behind the catalog is one shared
|
|
5
|
+
* operator credential, so a tenant-wide read would leak one tenant's view of
|
|
6
|
+
* shared configuration and invite per-tenant load on a fixed request budget.
|
|
7
|
+
*
|
|
8
|
+
* Every handler is on-demand. `getFresh*` reports a miss and the handler then
|
|
9
|
+
* fetches exactly once; nothing here schedules, polls, or streams. Every
|
|
10
|
+
* response carries `fetchedAt` and `ageSeconds`, and a stale snapshot is served
|
|
11
|
+
* with `stale: true` rather than being hidden — an operator must be able to see
|
|
12
|
+
* how old the numbers they are reading are.
|
|
13
|
+
*/
|
|
14
|
+
import { Elysia } from "elysia";
|
|
15
|
+
import type { AccessDecision } from "../../security/access-control";
|
|
16
|
+
import { consoleErrorHandler, requireGlobalAdmin } from "../shared/errors";
|
|
17
|
+
import {
|
|
18
|
+
fetchCatalog,
|
|
19
|
+
fetchMarket,
|
|
20
|
+
fetchPricingConfig,
|
|
21
|
+
fetchStatus,
|
|
22
|
+
objectArray,
|
|
23
|
+
type InferhubClientOptions,
|
|
24
|
+
} from "./client";
|
|
25
|
+
import {
|
|
26
|
+
applyFamilyHealth,
|
|
27
|
+
capacityWeightedPrice,
|
|
28
|
+
mergeMarketPrices,
|
|
29
|
+
mergeModelHealth,
|
|
30
|
+
familyStateOf,
|
|
31
|
+
normalizeCatalogModel,
|
|
32
|
+
numOrNull,
|
|
33
|
+
} from "./normalize";
|
|
34
|
+
import {
|
|
35
|
+
getFreshModels,
|
|
36
|
+
getFreshPricingConfig,
|
|
37
|
+
getFreshStatus,
|
|
38
|
+
getStaleModels,
|
|
39
|
+
getStaleStatus,
|
|
40
|
+
metaFor,
|
|
41
|
+
setModels,
|
|
42
|
+
setPricingConfig,
|
|
43
|
+
setStatus,
|
|
44
|
+
__resetStore,
|
|
45
|
+
} from "./store";
|
|
46
|
+
import type {
|
|
47
|
+
ModelMarket,
|
|
48
|
+
ProviderMarket,
|
|
49
|
+
RawCatalogModel,
|
|
50
|
+
RawMarketModel,
|
|
51
|
+
RawStatusFamily,
|
|
52
|
+
RawStatusModel,
|
|
53
|
+
RawUpstreamCard,
|
|
54
|
+
SnapshotMeta,
|
|
55
|
+
} from "./types";
|
|
56
|
+
|
|
57
|
+
/** Success rate below which a model is not routed to, when one is reported. */
|
|
58
|
+
const MIN_SUCCESS_RATE_PCT = 80;
|
|
59
|
+
|
|
60
|
+
export interface InferhubRoutesConfig {
|
|
61
|
+
readonly accessResolver: (request: Request) => AccessDecision | undefined;
|
|
62
|
+
/** Resolves the `config_read` key. Absent means the catalog cannot be read. */
|
|
63
|
+
readonly resolveConfigKey?: () => Promise<string | undefined> | string | undefined;
|
|
64
|
+
/** Transport seam for tests. Production uses the validated outbound fetch. */
|
|
65
|
+
readonly fetchFn?: typeof fetch;
|
|
66
|
+
/**
|
|
67
|
+
* Dedicated encrypted storage for the `config_read` key. Absent means the key
|
|
68
|
+
* cannot be persisted (the read-only routes still work).
|
|
69
|
+
*/
|
|
70
|
+
readonly keyStore?: {
|
|
71
|
+
readonly status: () => Promise<{
|
|
72
|
+
readonly configured: boolean;
|
|
73
|
+
readonly last4: string | null;
|
|
74
|
+
readonly updatedAt: string | null;
|
|
75
|
+
}>;
|
|
76
|
+
readonly write: (value: string) => Promise<void>;
|
|
77
|
+
readonly clear: () => Promise<void>;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Money is rendered to at most 4 decimals: asks are fractions of a dollar per Mtok. */
|
|
82
|
+
function money(value: number | null): number | null {
|
|
83
|
+
return value === null ? null : Math.round(value * 10_000) / 10_000;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function project(model: ModelMarket) {
|
|
87
|
+
return {
|
|
88
|
+
...model,
|
|
89
|
+
minAskIn: money(model.minAskIn),
|
|
90
|
+
minAskOut: money(model.minAskOut),
|
|
91
|
+
capacityPriceIn: money(capacityWeightedPrice(model.tiersIn)),
|
|
92
|
+
capacityPriceOut: money(capacityWeightedPrice(model.tiersOut)),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Refreshes the model list from `/catalog` + `/market`, or keeps the last good one. */
|
|
97
|
+
async function ensureModels(
|
|
98
|
+
config: InferhubRoutesConfig,
|
|
99
|
+
now: number,
|
|
100
|
+
): Promise<{ models: readonly ModelMarket[]; meta: SnapshotMeta; error?: string }> {
|
|
101
|
+
const fresh = getFreshModels(now);
|
|
102
|
+
if (fresh !== undefined) return { models: fresh, meta: metaFor(now, "market", now) };
|
|
103
|
+
|
|
104
|
+
const configKey = await config.resolveConfigKey?.();
|
|
105
|
+
if (configKey === undefined || configKey.length === 0) {
|
|
106
|
+
const stale = getStaleModels();
|
|
107
|
+
return {
|
|
108
|
+
models: stale?.value ?? [],
|
|
109
|
+
meta: metaFor(stale?.fetchedAt, "market", now),
|
|
110
|
+
error: "no_config_key",
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const options: InferhubClientOptions = {
|
|
114
|
+
configKey,
|
|
115
|
+
...(config.fetchFn === undefined ? {} : { fetchFn: config.fetchFn }),
|
|
116
|
+
};
|
|
117
|
+
const [catalog, market] = await Promise.all([fetchCatalog(options), fetchMarket(options)]);
|
|
118
|
+
if (catalog === undefined) {
|
|
119
|
+
const stale = getStaleModels();
|
|
120
|
+
return {
|
|
121
|
+
models: stale?.value ?? [],
|
|
122
|
+
meta: metaFor(stale?.fetchedAt, "market", now),
|
|
123
|
+
error: "upstream_unavailable",
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const models: ModelMarket[] = [];
|
|
128
|
+
// `/api/catalog` returns a bare array of upstreams, not an envelope. Each
|
|
129
|
+
// upstream carries its own `models` array.
|
|
130
|
+
const upstreams = Array.isArray(catalog)
|
|
131
|
+
? (catalog as readonly RawUpstreamCard[])
|
|
132
|
+
: (objectArray(catalog, "upstreams") as readonly RawUpstreamCard[]);
|
|
133
|
+
for (const card of upstreams) {
|
|
134
|
+
const upstream = {
|
|
135
|
+
prefix: card.prefix,
|
|
136
|
+
label: card.label,
|
|
137
|
+
systemPromptNote: card.systemPromptNote,
|
|
138
|
+
};
|
|
139
|
+
const rawModels = Array.isArray(card.models)
|
|
140
|
+
? (card.models as readonly RawCatalogModel[])
|
|
141
|
+
: [];
|
|
142
|
+
for (const raw of rawModels) {
|
|
143
|
+
const model = normalizeCatalogModel(raw, upstream);
|
|
144
|
+
if (model !== null) models.push(model);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const withPrices = mergeMarketPrices(
|
|
148
|
+
models,
|
|
149
|
+
objectArray(market, "models") as readonly RawMarketModel[],
|
|
150
|
+
);
|
|
151
|
+
setModels(withPrices, now);
|
|
152
|
+
return { models: withPrices, meta: metaFor(now, "market", now) };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Refreshes `/status` and merges health, or keeps the last good merged list. */
|
|
156
|
+
async function ensureStatus(
|
|
157
|
+
config: InferhubRoutesConfig,
|
|
158
|
+
base: readonly ModelMarket[],
|
|
159
|
+
now: number,
|
|
160
|
+
): Promise<{
|
|
161
|
+
models: readonly ModelMarket[];
|
|
162
|
+
providers: readonly ProviderMarket[];
|
|
163
|
+
meta: SnapshotMeta;
|
|
164
|
+
}> {
|
|
165
|
+
const fresh = getFreshStatus(now);
|
|
166
|
+
if (fresh !== undefined) return { ...fresh, meta: metaFor(now, "status", now) };
|
|
167
|
+
|
|
168
|
+
const payload = await fetchStatus(
|
|
169
|
+
config === undefined ? {} : { ...(config.fetchFn === undefined ? {} : { fetchFn: config.fetchFn }) },
|
|
170
|
+
);
|
|
171
|
+
if (payload === undefined) {
|
|
172
|
+
const stale = getStaleStatus();
|
|
173
|
+
return {
|
|
174
|
+
models: stale?.value.models ?? base,
|
|
175
|
+
providers: stale?.value.providers ?? [],
|
|
176
|
+
meta: metaFor(stale?.fetchedAt, "status", now),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const statusModels = objectArray(payload, "models") as readonly RawStatusModel[];
|
|
180
|
+
const families = objectArray(payload, "families") as readonly RawStatusFamily[];
|
|
181
|
+
const merged = mergeModelHealth(base, statusModels);
|
|
182
|
+
const models = applyFamilyHealth(merged, families, MIN_SUCCESS_RATE_PCT);
|
|
183
|
+
const providers: ProviderMarket[] = families.map((family) => ({
|
|
184
|
+
prefix: typeof family.prefix === "string" ? family.prefix : String(family.slug ?? ""),
|
|
185
|
+
// `family` is the display name ("Antigravity"); `slug` is the lowercase
|
|
186
|
+
// key ("antigravity") and is only the fallback.
|
|
187
|
+
family:
|
|
188
|
+
(typeof family.family === "string" ? family.family : null) ??
|
|
189
|
+
(typeof family.slug === "string" ? family.slug : ""),
|
|
190
|
+
// The catalog carries per-upstream provider counts; `/status` does not, so
|
|
191
|
+
// this stays 0 rather than inventing one.
|
|
192
|
+
activeProviders: 0,
|
|
193
|
+
familyState: familyStateOf(family.state),
|
|
194
|
+
availability24hPct: numOrNull(family.availability24hPct),
|
|
195
|
+
availability7dPct: numOrNull(family.availability7dPct),
|
|
196
|
+
p50DurationMs: numOrNull(family.p50DurationMs),
|
|
197
|
+
p95DurationMs: numOrNull(family.p95DurationMs),
|
|
198
|
+
avgTtftMs1h: numOrNull(family.avgTtftMs1h),
|
|
199
|
+
avgTps1h: numOrNull(family.avgTps1h),
|
|
200
|
+
}));
|
|
201
|
+
const value = { models, providers };
|
|
202
|
+
setStatus(value, now);
|
|
203
|
+
return { ...value, meta: metaFor(now, "status", now) };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Quotes one CSV field and neutralizes a leading formula character. */
|
|
207
|
+
function csvField(value: unknown): string {
|
|
208
|
+
const text = value === null || value === undefined ? "" : String(value);
|
|
209
|
+
const safe = /^[=+@\t\r]/.test(text) ? `'${text}` : text;
|
|
210
|
+
return `"${safe.replace(/"/g, '""')}"`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function createInferhubRoutes(config: InferhubRoutesConfig) {
|
|
214
|
+
// No `prefix`: the returned instance is merged into the `/console/api`
|
|
215
|
+
// router, and Elysia's prefix is part of the instance's type — a prefix here
|
|
216
|
+
// would not type-check against the plain `Elysia` this file declares. The
|
|
217
|
+
// paths therefore carry their own segment.
|
|
218
|
+
return new Elysia()
|
|
219
|
+
.error(consoleErrorHandler("InferHub market read failed"))
|
|
220
|
+
.get("/inferhub/summary", async ({ request }) => {
|
|
221
|
+
requireGlobalAdmin(config.accessResolver(request));
|
|
222
|
+
const now = Date.now();
|
|
223
|
+
const loaded = await ensureModels(config, now);
|
|
224
|
+
const withHealth = await ensureStatus(config, loaded.models, now);
|
|
225
|
+
const pricing = getFreshPricingConfig(now);
|
|
226
|
+
return {
|
|
227
|
+
models: withHealth.models.map(project),
|
|
228
|
+
providers: withHealth.providers,
|
|
229
|
+
pricingConfig: pricing ?? null,
|
|
230
|
+
meta: withHealth.meta,
|
|
231
|
+
// Explicit rather than a conditional spread: an optional property
|
|
232
|
+
// produced by spreading `{}` widens the route's response type past
|
|
233
|
+
// what the Elysia signature accepts under exactOptionalPropertyTypes.
|
|
234
|
+
error: loaded.error ?? null,
|
|
235
|
+
};
|
|
236
|
+
})
|
|
237
|
+
.get("/inferhub/models", async ({ request, query }) => {
|
|
238
|
+
requireGlobalAdmin(config.accessResolver(request));
|
|
239
|
+
const now = Date.now();
|
|
240
|
+
const loaded = await ensureModels(config, now);
|
|
241
|
+
const withHealth = await ensureStatus(config, loaded.models, now);
|
|
242
|
+
let rows = withHealth.models;
|
|
243
|
+
const prefix = typeof query.prefix === "string" ? query.prefix : undefined;
|
|
244
|
+
const alias = typeof query.alias === "string" ? query.alias : undefined;
|
|
245
|
+
const minAvail = Number(query.minAvail);
|
|
246
|
+
if (prefix !== undefined && prefix.length > 0)
|
|
247
|
+
rows = rows.filter((model) => model.prefix === prefix);
|
|
248
|
+
if (alias !== undefined && alias.length > 0)
|
|
249
|
+
rows = rows.filter((model) => model.aliasKey === alias);
|
|
250
|
+
if (Number.isFinite(minAvail) && minAvail > 0)
|
|
251
|
+
rows = rows.filter((model) => model.providersLive >= minAvail);
|
|
252
|
+
if (query.onlyAvailable === "1") rows = rows.filter((model) => model.routable);
|
|
253
|
+
const sorted = [...rows];
|
|
254
|
+
if (query.sort === "minAskOut")
|
|
255
|
+
sorted.sort((a, b) => (a.minAskOut ?? Infinity) - (b.minAskOut ?? Infinity));
|
|
256
|
+
else if (query.sort === "discount")
|
|
257
|
+
sorted.sort(
|
|
258
|
+
(a, b) => (b.tiersIn[0]?.discountPct ?? -Infinity) - (a.tiersIn[0]?.discountPct ?? -Infinity),
|
|
259
|
+
);
|
|
260
|
+
else sorted.sort((a, b) => (a.minAskIn ?? Infinity) - (b.minAskIn ?? Infinity));
|
|
261
|
+
return { models: sorted.map(project), meta: withHealth.meta };
|
|
262
|
+
})
|
|
263
|
+
.get("/inferhub/models/:id", async ({ request, params, set }) => {
|
|
264
|
+
requireGlobalAdmin(config.accessResolver(request));
|
|
265
|
+
const now = Date.now();
|
|
266
|
+
const loaded = await ensureModels(config, now);
|
|
267
|
+
const withHealth = await ensureStatus(config, loaded.models, now);
|
|
268
|
+
const id = decodeURIComponent(String(params.id));
|
|
269
|
+
const model = withHealth.models.find((entry) => entry.id === id);
|
|
270
|
+
if (model === undefined) {
|
|
271
|
+
set.status = 404;
|
|
272
|
+
return { error: "not_found", code: "not_found" };
|
|
273
|
+
}
|
|
274
|
+
return { model: project(model), meta: withHealth.meta };
|
|
275
|
+
})
|
|
276
|
+
.get("/inferhub/compare", async ({ request, query }) => {
|
|
277
|
+
requireGlobalAdmin(config.accessResolver(request));
|
|
278
|
+
const now = Date.now();
|
|
279
|
+
const loaded = await ensureModels(config, now);
|
|
280
|
+
const alias = typeof query.alias === "string" ? query.alias : "";
|
|
281
|
+
const rows = loaded.models
|
|
282
|
+
.filter((model) => model.aliasKey === alias)
|
|
283
|
+
.sort((a, b) => (a.minAskIn ?? Infinity) - (b.minAskIn ?? Infinity));
|
|
284
|
+
return { alias, models: rows.map(project), meta: loaded.meta };
|
|
285
|
+
})
|
|
286
|
+
.get("/inferhub/providers", async ({ request }) => {
|
|
287
|
+
requireGlobalAdmin(config.accessResolver(request));
|
|
288
|
+
const now = Date.now();
|
|
289
|
+
const loaded = await ensureModels(config, now);
|
|
290
|
+
const withHealth = await ensureStatus(config, loaded.models, now);
|
|
291
|
+
return { providers: withHealth.providers, meta: withHealth.meta };
|
|
292
|
+
})
|
|
293
|
+
.get("/inferhub/status", async ({ request }) => {
|
|
294
|
+
requireGlobalAdmin(config.accessResolver(request));
|
|
295
|
+
const now = Date.now();
|
|
296
|
+
const loaded = await ensureModels(config, now);
|
|
297
|
+
const withHealth = await ensureStatus(config, loaded.models, now);
|
|
298
|
+
const pricing = getFreshPricingConfig(now) ?? (await readPricingConfig(now, config.fetchFn));
|
|
299
|
+
return {
|
|
300
|
+
providers: withHealth.providers,
|
|
301
|
+
pricingConfig: pricing ?? null,
|
|
302
|
+
meta: withHealth.meta,
|
|
303
|
+
};
|
|
304
|
+
})
|
|
305
|
+
.get("/inferhub/export.csv", async ({ request, set }) => {
|
|
306
|
+
requireGlobalAdmin(config.accessResolver(request));
|
|
307
|
+
const now = Date.now();
|
|
308
|
+
const loaded = await ensureModels(config, now);
|
|
309
|
+
const withHealth = await ensureStatus(config, loaded.models, now);
|
|
310
|
+
const header = [
|
|
311
|
+
"id",
|
|
312
|
+
"prefix",
|
|
313
|
+
"family",
|
|
314
|
+
"aliasKey",
|
|
315
|
+
"providersLive",
|
|
316
|
+
"minAskIn",
|
|
317
|
+
"minAskOut",
|
|
318
|
+
"capacityPriceIn",
|
|
319
|
+
"officialIn",
|
|
320
|
+
"officialOut",
|
|
321
|
+
"discountPctIn",
|
|
322
|
+
"successRatePct",
|
|
323
|
+
"familyState",
|
|
324
|
+
"routable",
|
|
325
|
+
];
|
|
326
|
+
const lines = [header.join(",")];
|
|
327
|
+
for (const model of withHealth.models) {
|
|
328
|
+
lines.push(
|
|
329
|
+
[
|
|
330
|
+
model.id,
|
|
331
|
+
model.prefix,
|
|
332
|
+
model.family,
|
|
333
|
+
model.aliasKey,
|
|
334
|
+
model.providersLive,
|
|
335
|
+
model.minAskIn,
|
|
336
|
+
model.minAskOut,
|
|
337
|
+
capacityWeightedPrice(model.tiersIn),
|
|
338
|
+
model.officialIn,
|
|
339
|
+
model.officialOut,
|
|
340
|
+
model.tiersIn[0]?.discountPct,
|
|
341
|
+
model.health.successRatePct,
|
|
342
|
+
model.health.familyState,
|
|
343
|
+
model.routable,
|
|
344
|
+
]
|
|
345
|
+
.map(csvField)
|
|
346
|
+
.join(","),
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
set.headers["content-type"] = "text/csv; charset=utf-8";
|
|
350
|
+
return lines.join("\r\n");
|
|
351
|
+
})
|
|
352
|
+
// ── Key management ──────────────────────────────────────────────────────
|
|
353
|
+
// Stored on its own encrypted row, never in `provider_accounts`.
|
|
354
|
+
.get("/inferhub/key", async ({ request }) => {
|
|
355
|
+
requireGlobalAdmin(config.accessResolver(request));
|
|
356
|
+
if (config.keyStore === undefined) return { configured: false, last4: null, updatedAt: null };
|
|
357
|
+
return await config.keyStore.status();
|
|
358
|
+
})
|
|
359
|
+
.put("/inferhub/key", async ({ request, body, set }) => {
|
|
360
|
+
requireGlobalAdmin(config.accessResolver(request));
|
|
361
|
+
if (config.keyStore === undefined) {
|
|
362
|
+
set.status = 501;
|
|
363
|
+
return { error: "not_configured", code: "not_configured" };
|
|
364
|
+
}
|
|
365
|
+
const value = readKeyValue(body);
|
|
366
|
+
if (value === undefined) {
|
|
367
|
+
set.status = 400;
|
|
368
|
+
return { error: "invalid_key", code: "invalid_key" };
|
|
369
|
+
}
|
|
370
|
+
await config.keyStore.write(value);
|
|
371
|
+
// The write invalidates any cached snapshot: the freshness window would
|
|
372
|
+
// otherwise keep serving a "no key" result after one is added.
|
|
373
|
+
__resetStore();
|
|
374
|
+
return await config.keyStore.status();
|
|
375
|
+
})
|
|
376
|
+
.delete("/inferhub/key", async ({ request }) => {
|
|
377
|
+
requireGlobalAdmin(config.accessResolver(request));
|
|
378
|
+
if (config.keyStore === undefined) return { configured: false, last4: null, updatedAt: null };
|
|
379
|
+
await config.keyStore.clear();
|
|
380
|
+
__resetStore();
|
|
381
|
+
return { configured: false, last4: null, updatedAt: null };
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** Reads `key` from an opaque body, rejecting a blank or oversized value. */
|
|
386
|
+
function readKeyValue(body: unknown): string | undefined {
|
|
387
|
+
if (typeof body !== "object" || body === null) return undefined;
|
|
388
|
+
const value = (body as Record<string, unknown>).key;
|
|
389
|
+
if (typeof value !== "string") return undefined;
|
|
390
|
+
const trimmed = value.trim();
|
|
391
|
+
// A real InferHub key is ~30 chars; 512 catches a paste of the wrong thing
|
|
392
|
+
// (a JWT, a whole JSON blob) before it is committed.
|
|
393
|
+
if (trimmed.length === 0 || trimmed.length > 512) return undefined;
|
|
394
|
+
return trimmed;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** Reads `/pricing/config` once and caches it for an hour. */
|
|
398
|
+
async function readPricingConfig(now: number, fetchFn?: typeof fetch) {
|
|
399
|
+
const payload = await fetchPricingConfig(fetchFn === undefined ? {} : { fetchFn });
|
|
400
|
+
if (payload === null || payload === undefined || typeof payload !== "object") return undefined;
|
|
401
|
+
const record = payload as Record<string, unknown>;
|
|
402
|
+
const value = {
|
|
403
|
+
maxAskPctOfOfficial:
|
|
404
|
+
typeof record.maxAskPctOfOfficial === "number" ? record.maxAskPctOfOfficial : null,
|
|
405
|
+
platformFeePct: typeof record.platformFeePct === "number" ? record.platformFeePct : null,
|
|
406
|
+
publisherSharePct:
|
|
407
|
+
typeof record.publisherSharePct === "number" ? record.publisherSharePct : null,
|
|
408
|
+
};
|
|
409
|
+
setPricingConfig(value, now);
|
|
410
|
+
return value;
|
|
411
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InferHub market snapshot store.
|
|
3
|
+
*
|
|
4
|
+
* A read-through cache with a freshness window — deliberately **not** a poller.
|
|
5
|
+
* There is no timer, no interval, and no scheduler registration: a request
|
|
6
|
+
* either finds a fresh snapshot and returns it, or reports a miss and the route
|
|
7
|
+
* triggers one fetch. A page nobody opens causes no upstream traffic.
|
|
8
|
+
*
|
|
9
|
+
* A failed refresh never overwrites good data. The previous snapshot stays
|
|
10
|
+
* readable and the route reports it as stale, so an operator sees the age of
|
|
11
|
+
* what they are looking at rather than an empty table.
|
|
12
|
+
*/
|
|
13
|
+
import type {
|
|
14
|
+
InferhubSource,
|
|
15
|
+
ModelMarket,
|
|
16
|
+
PricingConfig,
|
|
17
|
+
ProviderMarket,
|
|
18
|
+
SnapshotMeta,
|
|
19
|
+
} from "./types";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* How long a snapshot is served before a route re-fetches.
|
|
23
|
+
*
|
|
24
|
+
* `/status` and `/catalog` are server-cached for about a minute upstream, so
|
|
25
|
+
* polling them faster would return the same bytes; the catalog is heavy, so it
|
|
26
|
+
* gets a longer window.
|
|
27
|
+
*/
|
|
28
|
+
const FRESHNESS_MS: Readonly<Record<InferhubSource, number>> = {
|
|
29
|
+
market: 60_000,
|
|
30
|
+
status: 60_000,
|
|
31
|
+
catalog: 300_000,
|
|
32
|
+
pricingConfig: 3_600_000,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
interface Entry<T> {
|
|
36
|
+
readonly value: T;
|
|
37
|
+
readonly fetchedAt: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const market = { current: undefined as Entry<readonly ModelMarket[]> | undefined };
|
|
41
|
+
const status = { current: undefined as Entry<{ models: readonly ModelMarket[]; providers: readonly ProviderMarket[] }> | undefined };
|
|
42
|
+
const pricingConfig = { current: undefined as Entry<PricingConfig> | undefined };
|
|
43
|
+
|
|
44
|
+
/** Freshness and age of one snapshot, as the API reports it. */
|
|
45
|
+
export function metaFor(fetchedAt: number | undefined, source: InferhubSource, now: number): SnapshotMeta {
|
|
46
|
+
if (fetchedAt === undefined) return { fetchedAt: 0, ageSeconds: 0, stale: true };
|
|
47
|
+
const ageSeconds = Math.max(0, Math.round((now - fetchedAt) / 1000));
|
|
48
|
+
return { fetchedAt, ageSeconds, stale: now - fetchedAt > FRESHNESS_MS[source] };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Returns the cached value when it is still inside its freshness window.
|
|
53
|
+
*
|
|
54
|
+
* A miss returns `undefined` **without** fetching — the fetch decision belongs
|
|
55
|
+
* to the caller, which is what keeps this module free of background work.
|
|
56
|
+
*/
|
|
57
|
+
export function getFreshModels(now: number = Date.now()): readonly ModelMarket[] | undefined {
|
|
58
|
+
const entry = market.current;
|
|
59
|
+
if (entry === undefined) return undefined;
|
|
60
|
+
return now - entry.fetchedAt <= FRESHNESS_MS.market ? entry.value : undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getFreshStatus(
|
|
64
|
+
now: number = Date.now(),
|
|
65
|
+
): { models: readonly ModelMarket[]; providers: readonly ProviderMarket[] } | undefined {
|
|
66
|
+
const entry = status.current;
|
|
67
|
+
if (entry === undefined) return undefined;
|
|
68
|
+
return now - entry.fetchedAt <= FRESHNESS_MS.status ? entry.value : undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function getFreshPricingConfig(now: number = Date.now()): PricingConfig | undefined {
|
|
72
|
+
const entry = pricingConfig.current;
|
|
73
|
+
if (entry === undefined) return undefined;
|
|
74
|
+
return now - entry.fetchedAt <= FRESHNESS_MS.pricingConfig ? entry.value : undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Last-known value regardless of age, for serving a stale-but-real view. */
|
|
78
|
+
export function getStaleModels(): Entry<readonly ModelMarket[]> | undefined {
|
|
79
|
+
return market.current;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function getStaleStatus(): Entry<{
|
|
83
|
+
models: readonly ModelMarket[];
|
|
84
|
+
providers: readonly ProviderMarket[];
|
|
85
|
+
}> | undefined {
|
|
86
|
+
return status.current;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function setModels(value: readonly ModelMarket[], fetchedAt: number): void {
|
|
90
|
+
market.current = { value, fetchedAt };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function setStatus(
|
|
94
|
+
value: { models: readonly ModelMarket[]; providers: readonly ProviderMarket[] },
|
|
95
|
+
fetchedAt: number,
|
|
96
|
+
): void {
|
|
97
|
+
status.current = { value, fetchedAt };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function setPricingConfig(value: PricingConfig, fetchedAt: number): void {
|
|
101
|
+
pricingConfig.current = { value, fetchedAt };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Test seam: clears every snapshot. */
|
|
105
|
+
export function __resetStore(): void {
|
|
106
|
+
market.current = undefined;
|
|
107
|
+
status.current = undefined;
|
|
108
|
+
pricingConfig.current = undefined;
|
|
109
|
+
}
|