cartethyia 2.5.4 → 2.5.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 +13 -4
- package/README.md +2 -2
- package/dist/dashboard/assets/{BannedPage-C2orBsI2.js → BannedPage-CADDq4IU.js} +1 -1
- package/dist/dashboard/assets/BannedPage-CADDq4IU.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-_z1hID7E.js +1 -0
- package/dist/dashboard/assets/CliToolDetailPage-_z1hID7E.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-BV6pxVQo.js → CliToolsPage-DuO1Z7dq.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-DuO1Z7dq.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-DNEPi3xU.js +1 -0
- package/dist/dashboard/assets/CombosPage-DNEPi3xU.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-CW-wPmJ6.js → ConsoleLogPage-BVdpfiz1.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-BVdpfiz1.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-0p8uX7vB.js → CustomizationPage-BR5HHvbi.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-BR5HHvbi.js.gz +0 -0
- package/dist/dashboard/assets/{HealthEventsModal-BjqSMfko.js → HealthEventsModal-mUfcknqm.js} +1 -1
- package/dist/dashboard/assets/HealthEventsModal-mUfcknqm.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-BvzCx8Kr.js → LoginPage-V5faV9V2.js} +1 -1
- package/dist/dashboard/assets/LoginPage-V5faV9V2.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-C7pDb41R.js +1 -0
- package/dist/dashboard/assets/ModelPicker-C7pDb41R.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-BDu20Jdx.js +1 -0
- package/dist/dashboard/assets/OverviewPage-BDu20Jdx.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-Dllr_aFO.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage-Dllr_aFO.js.gz +0 -0
- package/dist/dashboard/assets/{ProvidersPage-Js9jUPSb.js → ProvidersPage-uPiGVTTK.js} +1 -1
- package/dist/dashboard/assets/ProvidersPage-uPiGVTTK.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-BaUkO-4O.js +7 -0
- package/dist/dashboard/assets/ProxyPage-BaUkO-4O.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-o0IFAsFb.js +1 -0
- package/dist/dashboard/assets/QuotaPage-o0IFAsFb.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CYWqbXjp.js +1 -0
- package/dist/dashboard/assets/SettingsPage-CYWqbXjp.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-DDbMQsoo.js → SetupPage-UB690SE2.js} +1 -1
- package/dist/dashboard/assets/SetupPage-UB690SE2.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-CB3Z7VTf.js → SortableList-BLujdX7N.js} +1 -1
- package/dist/dashboard/assets/SortableList-BLujdX7N.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-D95UCPHI.js → StudioPage-D5C2raAc.js} +3 -3
- package/dist/dashboard/assets/StudioPage-D5C2raAc.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-8uIcxa1W.js +56 -0
- package/dist/dashboard/assets/UsagePage-8uIcxa1W.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-DQYLQxpW.js +1 -0
- package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
- package/dist/dashboard/assets/app-CX_Z8cMs.css +2 -0
- package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
- package/dist/dashboard/assets/{assert-Chi7pCnt.js → assert-C9FD_JwJ.js} +1 -1
- package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
- package/dist/dashboard/assets/{badge-D4pQapKj.js → badge-CR72dasT.js} +1 -1
- package/dist/dashboard/assets/badge-CR72dasT.js.gz +0 -0
- package/dist/dashboard/assets/{banner-DH6cg-_B.js → banner-CI9_eSfO.js} +1 -1
- package/dist/dashboard/assets/banner-CI9_eSfO.js.gz +0 -0
- package/dist/dashboard/assets/{card-BKqlUOe4.js → card-DS7CQEsO.js} +1 -1
- package/dist/dashboard/assets/card-DS7CQEsO.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-fTgljTGA.js → cli-tools-CNqzrfdb.js} +1 -1
- package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-D5Ys_sM0.js → clipboard-button-611pSsaX.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-611pSsaX.js.gz +0 -0
- package/dist/dashboard/assets/{fingerprint-pattern-BWuHtbW4.js → concurrency-BDF0ok7y.js} +1 -1
- package/dist/dashboard/assets/concurrency-BDF0ok7y.js.gz +0 -0
- package/dist/dashboard/assets/dialog-k37exjNt.js +1 -0
- package/dist/dashboard/assets/dialog-k37exjNt.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CX7N8ucR.js +1 -0
- package/dist/dashboard/assets/drawer-CX7N8ucR.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CQBr6U9c.js → entry-B9Xvtnfj.js} +1 -1
- package/dist/dashboard/assets/entry-B9Xvtnfj.js.gz +0 -0
- package/dist/dashboard/assets/entry-CTP8AyiS.js +1 -0
- package/dist/dashboard/assets/entry-CTP8AyiS.js.gz +0 -0
- package/dist/dashboard/assets/entry-DxBHnauy.js +4 -0
- package/dist/dashboard/assets/entry-DxBHnauy.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-BlptL8Uf.js → github-badge-sltk1qUi.js} +1 -1
- package/dist/dashboard/assets/github-badge-sltk1qUi.js.gz +0 -0
- package/dist/dashboard/assets/{index-js6TJvdr.js → index-CmxZr14F.js} +2 -2
- package/dist/dashboard/assets/index-CmxZr14F.js.gz +0 -0
- package/dist/dashboard/assets/{inline-DUWDSmDV.js → inline-BhY2sSnO.js} +1 -1
- package/dist/dashboard/assets/inline-BhY2sSnO.js.gz +0 -0
- package/dist/dashboard/assets/{input-LVjgZq4f.js → input-V1umFiBk.js} +1 -1
- package/dist/dashboard/assets/input-V1umFiBk.js.gz +0 -0
- package/dist/dashboard/assets/layout-BBOYycyS.js +1 -0
- package/dist/dashboard/assets/layout-BBOYycyS.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-DupvLfDx.js → list-row-1zzZYM0B.js} +1 -1
- package/dist/dashboard/assets/list-row-1zzZYM0B.js.gz +0 -0
- package/dist/dashboard/assets/{live-C4pEJUqI.js → live-BivGfuUs.js} +1 -1
- package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
- package/dist/dashboard/assets/{model-bans-BRiuufkB.js → model-bans-CmffjLuG.js} +1 -1
- package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-B4ROwk8L.js → mutation-46P0oWMe.js} +1 -1
- package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
- package/dist/dashboard/assets/numbers-DkfMR5Qs.js +2 -0
- package/dist/dashboard/assets/numbers-DkfMR5Qs.js.gz +0 -0
- package/dist/dashboard/assets/{query-keys-gmjzC3RS.js → query-keys-BFhGpwR1.js} +1 -1
- package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
- package/dist/dashboard/assets/{routing-C7tBT5o-.js → routing-CAb4F37R.js} +1 -1
- package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
- package/dist/dashboard/assets/{select-WUCXorXw.js → select-BYR_5K2c.js} +1 -1
- package/dist/dashboard/assets/select-BYR_5K2c.js.gz +0 -0
- package/dist/dashboard/assets/{settings-C_zKl1GS.js → settings-DNX9sydi.js} +1 -1
- package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
- package/dist/dashboard/assets/{stack-_3O0F8Xn.js → stack-BHTfXdz_.js} +1 -1
- package/dist/dashboard/assets/stack-BHTfXdz_.js.gz +0 -0
- package/dist/dashboard/assets/{state-BgTxHcJS.js → state-mPDRlQkl.js} +1 -1
- package/dist/dashboard/assets/state-mPDRlQkl.js.gz +0 -0
- package/dist/dashboard/assets/{switch-DEjTtOWf.js → switch-DOmJxg7H.js} +1 -1
- package/dist/dashboard/assets/switch-DOmJxg7H.js.gz +0 -0
- package/dist/dashboard/assets/tokens-COzario9.js +1 -0
- package/dist/dashboard/assets/tokens-COzario9.js.gz +0 -0
- package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js +1 -0
- package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-BWSC0UAS.js → useMutation-Dem0lXQj.js} +1 -1
- package/dist/dashboard/assets/useMutation-Dem0lXQj.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 +54 -54
- package/migrations/README.md +52 -52
- package/migrations/lite-sqlite3/0000_baseline.sql +2 -2
- package/package.json +96 -95
- package/scripts/cli/admin.ts +125 -0
- package/scripts/cli/{ui/background.ts → background.ts} +1 -1
- package/scripts/cli/{command/console-menu.ts → console-menu.ts} +275 -244
- package/scripts/cli/doctor.ts +37 -0
- package/scripts/cli/{support/env.ts → env.ts} +99 -99
- package/scripts/cli/{gateway/spawn.ts → gateway.ts} +112 -24
- package/scripts/cli/instance.ts +224 -0
- package/scripts/cli/main.ts +61 -18
- package/scripts/cli/{support/platform.ts → platform.ts} +64 -64
- package/scripts/cli/port.ts +100 -0
- package/scripts/cli/{support/services.ts → services.ts} +88 -88
- package/scripts/cli/{command/setup.ts → setup.ts} +35 -34
- package/scripts/cli/{command/start.ts → start.ts} +10 -5
- package/scripts/cli/{support/startup-signal.ts → startup-signal.ts} +23 -23
- package/scripts/cli/tray/index.ts +62 -12
- package/scripts/cli/{command/tray.ts → tray.ts} +208 -208
- package/scripts/cli/{command/update.ts → update.ts} +39 -39
- package/scripts/cli/version.ts +67 -0
- package/scripts/cli/{ui/wordmark.ts → wordmark.ts} +58 -58
- package/src/boot/app.ts +697 -693
- package/src/boot/dependencies.ts +647 -646
- package/src/boot/timeout.ts +2 -13
- package/src/config/cooldowns.ts +186 -138
- package/src/config/network.ts +8 -14
- package/src/config/providers.ts +7 -10
- package/src/config/runtime.ts +76 -76
- package/src/config/security.ts +15 -0
- package/src/config/spec.ts +20 -30
- package/src/config/telemetry.ts +0 -20
- package/src/config/timeouts.ts +0 -6
- package/src/console/admin/model-abuse/contracts.ts +28 -6
- package/src/console/admin/model-abuse/routes.ts +56 -3
- package/src/console/api-keys/contracts.ts +410 -414
- package/src/console/api-keys/routes.ts +881 -880
- package/src/console/auth/credentials.ts +3 -3
- package/src/console/auth/setup-service.ts +2 -2
- package/src/console/backup/export-endpoint.ts +6 -2
- package/src/console/backup/nine-router.ts +2 -5
- package/src/console/backup/service.ts +4 -0
- package/src/console/backup/store.ts +192 -29
- package/src/console/backup/validate.ts +3 -45
- package/src/console/cli-tools/fs-ops.ts +487 -487
- package/src/console/console-router-guards.ts +1 -1
- package/src/console/data-management/contracts.ts +13 -6
- package/src/console/data-management/operations.ts +23 -18
- package/src/console/data-management/routes.ts +0 -1
- package/src/console/domain-registration.ts +10 -5
- package/src/console/observability/live.ts +22 -0
- package/src/console/observability/store.ts +39 -16
- package/src/console/observability/usage-dimensions.ts +4 -4
- package/src/console/providers/catalog/catalog-projections.ts +6 -7
- package/src/console/providers/catalog/model-operations.ts +5 -2
- package/src/console/providers/catalog/provider-operations.ts +4 -4
- package/src/console/providers/catalog/public-model-store.ts +8 -11
- package/src/console/providers/catalog/store.ts +1553 -1553
- package/src/console/providers/oauth/routes.ts +8 -12
- package/src/console/quota/account-quota-tenant.ts +6 -6
- package/src/console/quota/quota-view.ts +1 -1
- package/src/console/remote-support/contracts.ts +61 -0
- package/src/console/remote-support/routes.ts +65 -0
- package/src/console/remote-support/service.ts +207 -0
- package/src/console/response-compression.ts +0 -3
- package/src/console/routing/pools/probe-result.ts +3 -2
- package/src/console/routing/pools/probe-verdict.ts +132 -132
- package/src/console/routing/pools/store.ts +8 -8
- package/src/console/settings/contracts.ts +58 -2
- package/src/console/settings/store.ts +30 -3
- package/src/console/shared/cursor.ts +8 -8
- package/src/console/shared/errors.ts +1 -3
- package/src/network/http2-bidi.ts +2 -4
- package/src/network/pool/agent.ts +478 -478
- package/src/network/pool/selector.ts +5 -4
- package/src/network/retry.ts +1 -2
- package/src/network/ssrf.ts +5 -4
- package/src/observability/event-compaction.ts +80 -78
- package/src/observability/event-day-index.ts +13 -15
- package/src/observability/event-log.ts +4 -4
- package/src/observability/logger.ts +6 -6
- package/src/observability/payload-capture.ts +151 -33
- package/src/observability/payload-store.ts +397 -440
- package/src/observability/redaction.ts +5 -6
- package/src/observability/request-notify.ts +43 -0
- package/src/observability/telemetry-status.ts +8 -9
- package/src/observability/token-speed.ts +5 -4
- package/src/persistence/db-mode.ts +7 -7
- package/src/persistence/readiness.ts +1 -1
- package/src/persistence/schema.ts +1406 -1402
- package/src/persistence/shared-cache.ts +1 -2
- package/src/persistence/stores/api-key-store.ts +18 -6
- package/src/persistence/stores/share-store.ts +4 -3
- package/src/persistence/stores/telemetry-store.ts +236 -237
- package/src/protocol/canonical-model.ts +11 -3
- package/src/protocol/codecs.ts +138 -4
- package/src/protocol/gateway-error.ts +6 -4
- package/src/protocol/request/chat.ts +21 -1
- package/src/protocol/request/codex.ts +10 -1
- package/src/protocol/request/messages.ts +4 -5
- package/src/protocol/request/responses.ts +9 -1
- package/src/protocol/response/chat.ts +34 -4
- package/src/protocol/response/codex.ts +7 -4
- package/src/protocol/response/dsml.ts +140 -28
- package/src/protocol/response/responses.ts +11 -5
- package/src/protocol/stream-error-frames.ts +4 -5
- package/src/providers/authentication/jwt-validator.ts +9 -10
- package/src/providers/authentication/oauth-refresh-service.ts +9 -11
- package/src/providers/configured-openai-providers.ts +134 -136
- package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
- package/src/providers/discovery/catalog/catalog-format.ts +3 -6
- package/src/providers/discovery/models-dev-catalog.ts +354 -354
- package/src/providers/discovery/probe-phases.ts +4 -4
- package/src/providers/discovery/probe-wire.ts +6 -13
- package/src/providers/discovery/probing-service.ts +1117 -1045
- package/src/providers/integrations/alibabacp/alibabacp.ts +1 -1
- package/src/providers/integrations/antigravity/antigravity-quota.ts +5 -7
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +23 -2
- package/src/providers/integrations/buddy/buddy-checkin.ts +1 -4
- package/src/providers/integrations/buddy/buddy-cli-headers.ts +23 -6
- package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -1
- package/src/providers/integrations/buddy/buddy-identity.ts +52 -0
- package/src/providers/integrations/buddy/buddy-media.ts +2 -2
- package/src/providers/integrations/buddy/buddy-usage-store.ts +3 -4
- package/src/providers/integrations/buddy/codebuddy/quota.ts +2 -13
- package/src/providers/integrations/buddy/codebuddy/shared.ts +118 -115
- package/src/providers/integrations/buddy/workbuddy/quota.ts +0 -11
- package/src/providers/integrations/buddy/workbuddy/shared.ts +5 -2
- package/src/providers/integrations/claude/claude.ts +608 -608
- package/src/providers/integrations/cline/cline-quota.ts +132 -132
- package/src/providers/integrations/cline/cline.ts +537 -539
- package/src/providers/integrations/codex/codex-oauth.ts +1 -1
- package/src/providers/integrations/commandcode/commandcode.ts +674 -675
- package/src/providers/integrations/commandcode/discovery.ts +117 -116
- package/src/providers/integrations/cursor/cursor-auth.ts +418 -418
- package/src/providers/integrations/cursor/cursor-quota.ts +309 -310
- package/src/providers/integrations/cursor/cursor.ts +11 -163
- package/src/providers/integrations/factory/factory-models.ts +3 -5
- package/src/providers/integrations/grok/grok-install-id.ts +2 -3
- package/src/providers/integrations/grok/grok.ts +6 -7
- package/src/providers/integrations/hermes.ts +14 -18
- package/src/providers/integrations/kiro/kiro-discovery.ts +1 -1
- package/src/providers/integrations/kiro/kiro.ts +1 -1
- package/src/providers/integrations/mistral.ts +6 -6
- package/src/providers/integrations/opencode/fingerprint.ts +3 -3
- package/src/providers/integrations/opencode/provider.ts +8 -8
- package/src/providers/integrations/qoder/qoder-catalog.ts +450 -450
- package/src/providers/integrations/qoder/qoder-oauth.ts +388 -310
- package/src/providers/integrations/qoder/qoder.ts +884 -865
- package/src/providers/integrations/shared/connect.ts +6 -6
- package/src/providers/integrations/trae/trae-catalog.ts +1 -2
- package/src/providers/integrations/trae/trae.ts +2 -2
- package/src/providers/integrations/zcode/oauth.ts +6 -1
- package/src/providers/operations/account-health-service.ts +1528 -1523
- package/src/providers/operations/account-reset-service.ts +1 -1
- package/src/providers/operations/client-versions.ts +6 -6
- package/src/providers/operations/custom-cli-headers.ts +3 -4
- package/src/providers/operations/provider-catalog-seeder.ts +95 -95
- package/src/providers/operations/provider-catalog-service.ts +3 -4
- package/src/providers/provider-metadata.ts +4 -8
- package/src/providers/provider-registry.ts +6 -7
- package/src/providers/provider-usage.ts +1 -1
- package/src/security/abuse.ts +15 -10
- package/src/security/access-control.ts +144 -146
- package/src/security/admission/contracts.ts +11 -0
- package/src/security/admission/in-memory-store.ts +4 -3
- package/src/security/admission/lease-sweep.ts +6 -6
- package/src/security/admission/reasons.ts +5 -1
- package/src/security/admission/redis-store.ts +211 -210
- package/src/security/admission/service.ts +4 -0
- package/src/security/api-key-auth.ts +504 -504
- package/src/security/ip-boundary.ts +155 -15
- package/src/security/model-abuse.ts +366 -94
- package/src/security/outbound-headers.ts +3 -4
- package/src/shared/sleep.ts +4 -0
- package/src/transport/dispatch/attempt-finalize.ts +47 -5
- package/src/transport/dispatch/attempt-loop.ts +531 -530
- package/src/transport/dispatch/stream-bridge.ts +1 -3
- package/src/transport/dispatch/streaming/finalize.ts +1 -1
- package/src/transport/dispatch/streaming/stream.ts +5 -6
- package/src/transport/failure-policy.ts +162 -8
- package/src/transport/guards/auth-guard.ts +7 -4
- package/src/transport/guards/concurrency-guard.ts +0 -5
- package/src/transport/guards/error-lifecycle.ts +13 -6
- package/src/transport/guards/pipeline.ts +15 -3
- package/src/transport/guards/request-context.ts +8 -2
- package/src/transport/request/inflight.ts +5 -6
- package/src/transport/request/preparer.ts +2 -2
- package/src/transport/request/state.ts +12 -8
- package/src/transport/routing/admission.ts +4 -0
- package/src/transport/routing/route-catalog.ts +8 -1
- package/src/transport/routing/route-model.ts +46 -14
- package/src/transport/routing/router.ts +21 -14
- package/src/transport/surface/completion.ts +2 -3
- package/src/transport/surface/dialects.ts +5 -6
- package/src/transport/surface/responses/encode.ts +38 -0
- package/src/transport/surface/responses/parse.ts +4 -0
- package/src/transport/translation/capabilities.ts +2 -3
- package/src/transport/translation/quirks.ts +3 -3
- package/src/workers/buddy-growth-tasks.ts +5 -10
- package/src/workers/daily-checkin.ts +17 -23
- package/src/workers/quota-refresh-sweep.ts +26 -26
- package/dist/dashboard/assets/BannedPage-C2orBsI2.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js +0 -1
- package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-BV6pxVQo.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-Bsan4679.js +0 -1
- package/dist/dashboard/assets/CombosPage-Bsan4679.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-CW-wPmJ6.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-0p8uX7vB.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-BjqSMfko.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-BvzCx8Kr.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-BY1aKEkP.js +0 -1
- package/dist/dashboard/assets/ModelPicker-BY1aKEkP.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js +0 -1
- package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-Js9jUPSb.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js +0 -7
- package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js +0 -1
- package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CS86ho5X.js +0 -1
- package/dist/dashboard/assets/SettingsPage-CS86ho5X.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-DDbMQsoo.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-CB3Z7VTf.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-D95UCPHI.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-Cnae9SWA.js +0 -56
- package/dist/dashboard/assets/UsagePage-Cnae9SWA.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-VY68Mhd1.js +0 -1
- package/dist/dashboard/assets/api-keys-VY68Mhd1.js.gz +0 -0
- package/dist/dashboard/assets/app-COlK95oi.css +0 -2
- package/dist/dashboard/assets/app-COlK95oi.css.gz +0 -0
- package/dist/dashboard/assets/assert-Chi7pCnt.js.gz +0 -0
- package/dist/dashboard/assets/badge-D4pQapKj.js.gz +0 -0
- package/dist/dashboard/assets/banner-DH6cg-_B.js.gz +0 -0
- package/dist/dashboard/assets/card-BKqlUOe4.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-fTgljTGA.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-D5Ys_sM0.js.gz +0 -0
- package/dist/dashboard/assets/dialog-BYj-8WuK.js +0 -1
- package/dist/dashboard/assets/dialog-BYj-8WuK.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CmXIZ6H1.js +0 -1
- package/dist/dashboard/assets/drawer-CmXIZ6H1.js.gz +0 -0
- package/dist/dashboard/assets/entry-CBg5zeu8.js +0 -1
- package/dist/dashboard/assets/entry-CBg5zeu8.js.gz +0 -0
- package/dist/dashboard/assets/entry-CQBr6U9c.js.gz +0 -0
- package/dist/dashboard/assets/entry-CgdSAzEV.js +0 -4
- package/dist/dashboard/assets/entry-CgdSAzEV.js.gz +0 -0
- package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-BlptL8Uf.js.gz +0 -0
- package/dist/dashboard/assets/index-js6TJvdr.js.gz +0 -0
- package/dist/dashboard/assets/inline-DUWDSmDV.js.gz +0 -0
- package/dist/dashboard/assets/input-LVjgZq4f.js.gz +0 -0
- package/dist/dashboard/assets/layout-CPfxpsfy.js +0 -1
- package/dist/dashboard/assets/layout-CPfxpsfy.js.gz +0 -0
- package/dist/dashboard/assets/list-row-DupvLfDx.js.gz +0 -0
- package/dist/dashboard/assets/live-C4pEJUqI.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-BRiuufkB.js.gz +0 -0
- package/dist/dashboard/assets/mutation-B4ROwk8L.js.gz +0 -0
- package/dist/dashboard/assets/numbers-YNcHvRY7.js +0 -2
- package/dist/dashboard/assets/numbers-YNcHvRY7.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-gmjzC3RS.js.gz +0 -0
- package/dist/dashboard/assets/routing-C7tBT5o-.js.gz +0 -0
- package/dist/dashboard/assets/select-WUCXorXw.js.gz +0 -0
- package/dist/dashboard/assets/settings-C_zKl1GS.js.gz +0 -0
- package/dist/dashboard/assets/stack-_3O0F8Xn.js.gz +0 -0
- package/dist/dashboard/assets/state-BgTxHcJS.js.gz +0 -0
- package/dist/dashboard/assets/switch-DEjTtOWf.js.gz +0 -0
- package/dist/dashboard/assets/tokens-DDxMbSRL.js +0 -1
- package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
- package/dist/dashboard/assets/use-presence-BSZEBx7U.js +0 -1
- package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-BWSC0UAS.js.gz +0 -0
- package/scripts/cli/command/admin.ts +0 -65
- package/scripts/cli/command/doctor.ts +0 -257
- package/scripts/cli/gateway/drain-token.ts +0 -78
- package/scripts/cli/gateway/instance-lock.ts +0 -105
- package/scripts/cli/gateway/stop.ts +0 -51
- package/scripts/cli/support/bun.ts +0 -27
- package/scripts/cli/support/version.ts +0 -37
- /package/dist/dashboard/assets/{app-DCQZRmHY.js → app-BIt446yE.js} +0 -0
- /package/dist/dashboard/assets/{app-DCQZRmHY.js.gz → app-BIt446yE.js.gz} +0 -0
- /package/scripts/cli/{ui/prompt.ts → prompt.ts} +0 -0
|
@@ -1,354 +1,354 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Offline model catalog: limits, capabilities, and pricing for every model the
|
|
3
|
-
* bundled providers can route to.
|
|
4
|
-
*
|
|
5
|
-
* The data is a packed, brotli-compressed binary blob (`catalog-blob.ts`, built
|
|
6
|
-
* by `scripts/generate
|
|
7
|
-
* There is no JSON parse and no per-row object: a lookup is a binary search
|
|
8
|
-
* over a typed-array column, and the metadata object for a row is built only
|
|
9
|
-
* when something asks for that row.
|
|
10
|
-
*
|
|
11
|
-
* Sources, both fetched by the generator:
|
|
12
|
-
* - **models.dev** — the broad catalog and the only per-model pricing.
|
|
13
|
-
* - **models.opencode.ai** — the authoritative limits for the OpenCode family,
|
|
14
|
-
* whose dispatch listing (`/zen/v1/models`) publishes ids with no limits at
|
|
15
|
-
* all. Without it every fetched OpenCode model published an invented
|
|
16
|
-
* 200k/64k.
|
|
17
|
-
*
|
|
18
|
-
* Runtime network access is intentionally absent: a deployed instance uses the
|
|
19
|
-
* exact catalog selected at build time.
|
|
20
|
-
*/
|
|
21
|
-
import { brotliDecompressSync } from "node:zlib";
|
|
22
|
-
import {
|
|
23
|
-
lowerAscii,
|
|
24
|
-
PackedCatalog,
|
|
25
|
-
type ModelsDevModelCost,
|
|
26
|
-
type ModelsDevModelMetadata,
|
|
27
|
-
} from "./catalog/catalog-format";
|
|
28
|
-
import {
|
|
29
|
-
CATALOG_BLOB_BROTLI_BASE64,
|
|
30
|
-
CATALOG_BLOB_BYTES,
|
|
31
|
-
} from "./catalog/catalog-blob";
|
|
32
|
-
|
|
33
|
-
export type { ModelsDevModelCost, ModelsDevModelMetadata };
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Cartethyia provider id → the provider key the catalog files it under.
|
|
37
|
-
*
|
|
38
|
-
* Only ids that genuinely differ belong here, and each entry is backed by the
|
|
39
|
-
* deployment's own upstream: `opencode` serves `opencode.ai`, and its model
|
|
40
|
-
* ids also appear under the catalog's `opencode`. Without this the exact
|
|
41
|
-
* lookup missed for every one of those models, so each fell through to the bare
|
|
42
|
-
* lookup — which fails closed when providers disagree — and the entry was
|
|
43
|
-
* published with invented defaults.
|
|
44
|
-
*
|
|
45
|
-
* Deliberately absent: providers with no catalog counterpart (a private
|
|
46
|
-
* gateway, a BYOK endpoint) and any id whose match rests on model ids alone.
|
|
47
|
-
* A shared model id is not proof of identity — the same generic id appears
|
|
48
|
-
* under hundreds of resellers — so a guess here would attribute a stranger's
|
|
49
|
-
* limits to this gateway's serving, which is the exact failure this catalog's
|
|
50
|
-
* disagreement rule exists to prevent.
|
|
51
|
-
*
|
|
52
|
-
* This table is for **metadata** (limits, modalities), where a wrong row is
|
|
53
|
-
* harmful and failing closed is right. Pricing does not need it: see
|
|
54
|
-
* {@link ModelsDevCatalog.costFor}, which falls back to the model's own rate.
|
|
55
|
-
*/
|
|
56
|
-
const CATALOG_PROVIDER_IDS: Readonly<Record<string, string>> = {
|
|
57
|
-
// `opencode` is both a Cartethyia provider id and a catalog key, so it needs
|
|
58
|
-
// no entry: `exactMetadata` tries the id directly first. Only the tiers whose
|
|
59
|
-
// id differs from the catalog key belong here.
|
|
60
|
-
"opencode-zen": "opencode",
|
|
61
|
-
"opencode-go": "opencode-go",
|
|
62
|
-
ollamacloud: "ollama-cloud",
|
|
63
|
-
xiaomipg: "xiaomi-token-plan-sgp",
|
|
64
|
-
xiaomitp: "xiaomi-token-plan-cn",
|
|
65
|
-
// Cartethyia ids that are shorter than the name the catalog files the
|
|
66
|
-
// provider under. Without these the exact lookup misses for every model, and
|
|
67
|
-
// each row falls through to the bare lookup — which fails closed when
|
|
68
|
-
// providers disagree about the id — so the catalog would publish invented
|
|
69
|
-
// defaults.
|
|
70
|
-
github: "github-copilot",
|
|
71
|
-
// Discovery-backed aggregators whose catalog key is spelled differently.
|
|
72
|
-
// `qianfan` and `helmcode` are deliberately absent: the catalog files no rows
|
|
73
|
-
// for either, and an entry pointing at a provider it does not carry would
|
|
74
|
-
// make every lookup miss and publish invented limits.
|
|
75
|
-
siliconflowcn: "siliconflow-cn",
|
|
76
|
-
nanogpt: "nano-gpt",
|
|
77
|
-
novita: "novita-ai",
|
|
78
|
-
together: "togetherai",
|
|
79
|
-
wafer: "wafer.ai",
|
|
80
|
-
// The Token Plan and the mainland DashScope host. Each points at the key the
|
|
81
|
-
// catalog files the *same* service under, so without these every one of their
|
|
82
|
-
// rows missed the exact lookup and fell through to the bare one, which fails
|
|
83
|
-
// closed when providers disagree about an id that resellers share.
|
|
84
|
-
//
|
|
85
|
-
// `cloudflareai` is deliberately absent: the gateway files its rows as
|
|
86
|
-
// `anthropic/claude-opus-5` while the provider dispatches the bare
|
|
87
|
-
// `claude-opus-5`, so the exact lookup misses under either key. An entry here
|
|
88
|
-
// would only restate the miss.
|
|
89
|
-
alibabacp: "alibaba-token-plan",
|
|
90
|
-
dashscope: "alibaba-coding-plan-cn",
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Removes a trailing release date from a model id, in either spelling the
|
|
95
|
-
* catalog uses: `-YYYY-MM-DD` (`gpt-4o-2024-11-20`) or `-YYYYMMDD`
|
|
96
|
-
* (`claude-haiku-4-5-20251001`). A dated snapshot bills the same rate as the
|
|
97
|
-
* model it snapshots, so both forms must reach the same row.
|
|
98
|
-
*/
|
|
99
|
-
function stripDateSuffix(modelId: string): string {
|
|
100
|
-
return modelId.replace(/-\d{4}-\d{2}-\d{2}$/, "").replace(/-\d{8}$/, "");
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Suffixes a reseller appends to a model id it republishes unchanged.
|
|
105
|
-
*
|
|
106
|
-
* OpenRouter lists `deepseek/deepseek-v4.1-flash:free` — the `:free` marks its
|
|
107
|
-
* own billing tier, not a different model, so the underlying row is the one
|
|
108
|
-
* carrying the real context and output limits. `@variant` is the same idea (a
|
|
109
|
-
* pinned quantization or provider route).
|
|
110
|
-
*
|
|
111
|
-
* Stripped for *lookup only* — the id sent upstream is always the caller's.
|
|
112
|
-
*/
|
|
113
|
-
function stripVariantSuffix(modelId: string): string {
|
|
114
|
-
return modelId.replace(/[:@][^/@:]*$/, "");
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* The bare id: what follows a reseller's `provider/` prefix, if any.
|
|
119
|
-
*
|
|
120
|
-
* Returned as bytes because every lookup compares against the packed blob
|
|
121
|
-
* without allocating a string.
|
|
122
|
-
*/
|
|
123
|
-
function bareBytes(idLower: Uint8Array): Uint8Array {
|
|
124
|
-
for (let i = 0; i < idLower.length; i += 1) {
|
|
125
|
-
if (idLower[i] === 0x2f /* "/" */) return idLower.subarray(i + 1);
|
|
126
|
-
}
|
|
127
|
-
return idLower;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const UNKNOWN_COST: ModelsDevModelCost = {
|
|
131
|
-
input: null,
|
|
132
|
-
output: null,
|
|
133
|
-
pricing_model: "unknown",
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* The packed catalog, decompressed on first use and memoized.
|
|
138
|
-
*
|
|
139
|
-
* Brotli + base64, not `JSON.parse`: the blob is columnar, so this allocates
|
|
140
|
-
* one buffer and takes typed-array views over it rather than materialising
|
|
141
|
-
* 8900 objects. `CATALOG_BLOB_BYTES` is the decompressed length, checked here
|
|
142
|
-
* so a consumer paired with a half-regenerated blob fails on first access
|
|
143
|
-
* rather than resolving a truncated catalog.
|
|
144
|
-
*
|
|
145
|
-
* The decode is lazy on purpose. It is ~105 KB of base64 that expands to a
|
|
146
|
-
* much larger buffer, and it
|
|
147
|
-
* including the ones that never resolve a model (a CLI subcommand, a migration
|
|
148
|
-
* run, a test that only touches the console),
|
|
149
|
-
*
|
|
150
|
-
*/
|
|
151
|
-
let catalogCache: PackedCatalog | undefined;
|
|
152
|
-
|
|
153
|
-
function catalog(): PackedCatalog {
|
|
154
|
-
const cached = catalogCache;
|
|
155
|
-
if (cached !== undefined) return cached;
|
|
156
|
-
const bytes = brotliDecompressSync(Buffer.from(CATALOG_BLOB_BROTLI_BASE64, "base64"));
|
|
157
|
-
if (bytes.byteLength !== CATALOG_BLOB_BYTES) {
|
|
158
|
-
throw new Error(
|
|
159
|
-
`The packed model catalog is ${bytes.byteLength} B, expected ` +
|
|
160
|
-
`${CATALOG_BLOB_BYTES} B — regenerate it.`,
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
// `PackedCatalog` copies into a 4-byte-aligned buffer of its own, so the
|
|
164
|
-
// decompressed buffer is dropped here rather than held beside the copy.
|
|
165
|
-
const packed = new PackedCatalog(bytes);
|
|
166
|
-
catalogCache = packed;
|
|
167
|
-
return packed;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Resolves model metadata and pricing exclusively from the packed base catalog
|
|
172
|
-
* shipped with Cartethyia.
|
|
173
|
-
*/
|
|
174
|
-
export class ModelsDevCatalog {
|
|
175
|
-
/** When the generator fetched the upstream catalogs. */
|
|
176
|
-
get fetchedAt(): string {
|
|
177
|
-
return catalog().fetchedAt;
|
|
178
|
-
}
|
|
179
|
-
/** Every provider key the catalog files. */
|
|
180
|
-
get providers(): readonly string[] {
|
|
181
|
-
return catalog().providers;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Metadata for an exact `(providerIndex, idLower)` pair, or `undefined`.
|
|
186
|
-
*
|
|
187
|
-
* The provider is tried as given first and then under its mapped key: the
|
|
188
|
-
* mapped name is a second chance, not a replacement, so an id that already
|
|
189
|
-
* matches exactly keeps winning.
|
|
190
|
-
*/
|
|
191
|
-
private exactMetadata(providerLower: string, idLower: Uint8Array): ModelsDevModelMetadata | undefined {
|
|
192
|
-
const direct = catalog().exactIndex(catalog().providerIndex(providerLower), idLower);
|
|
193
|
-
if (direct >= 0) return catalog().metadataAt(direct);
|
|
194
|
-
const mapped = CATALOG_PROVIDER_IDS[providerLower];
|
|
195
|
-
if (mapped === undefined) return undefined;
|
|
196
|
-
return catalog().metadataAt(catalog().exactIndex(catalog().providerIndex(mapped), idLower));
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/** Metadata rows whose *bare* id equals `bare`, in catalog order. */
|
|
200
|
-
private metadataForBare(bare: Uint8Array): ModelsDevModelMetadata[] {
|
|
201
|
-
const [start, end] = catalog().bareRange(bare);
|
|
202
|
-
const out: ModelsDevModelMetadata[] = [];
|
|
203
|
-
for (let position = start; position < end; position += 1) {
|
|
204
|
-
const metadata = catalog().metadataAt(catalog().bareOrderAt(position));
|
|
205
|
-
if (metadata !== undefined) out.push(metadata);
|
|
206
|
-
}
|
|
207
|
-
return out;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Resolves metadata for one model id.
|
|
212
|
-
*
|
|
213
|
-
* An exact `provider:model` hit always wins — it is the only lookup that
|
|
214
|
-
* names the provider actually serving the request. The bare-id fallback is
|
|
215
|
-
* used only when it is **unambiguous**: if the catalog recorded the id under
|
|
216
|
-
* several providers with different facts, no row can be called the model's
|
|
217
|
-
* own, so the answer is `undefined` and the caller keeps its own declared
|
|
218
|
-
* limits instead of inheriting a stranger's.
|
|
219
|
-
*
|
|
220
|
-
* Callers that know the provider should pass it; passing `""` opts into the
|
|
221
|
-
* bare lookup, which is why it fails closed on disagreement.
|
|
222
|
-
*/
|
|
223
|
-
resolve(providerId: string, modelId: string): ModelsDevModelMetadata | undefined {
|
|
224
|
-
const provider = providerId.trim().toLowerCase();
|
|
225
|
-
const idLower = lowerAscii(modelId);
|
|
226
|
-
if (idLower.length === 0) return undefined;
|
|
227
|
-
const exact = this.exactMetadata(provider, idLower);
|
|
228
|
-
if (exact !== undefined) return exact;
|
|
229
|
-
// A reseller suffix is the reseller's, not the model's: retry the exact
|
|
230
|
-
// lookup without it before falling through to the bare id. Doing this here
|
|
231
|
-
// rather than stripping up front keeps the provider-scoped match.
|
|
232
|
-
const unsuffixedText = stripVariantSuffix(modelId.trim().toLowerCase());
|
|
233
|
-
if (unsuffixedText !== modelId.trim().toLowerCase()) {
|
|
234
|
-
const unsuffixed = this.exactMetadata(provider, lowerAscii(unsuffixedText));
|
|
235
|
-
if (unsuffixed !== undefined) return unsuffixed;
|
|
236
|
-
}
|
|
237
|
-
const bare = bareBytes(idLower);
|
|
238
|
-
const unambiguous = this.unambiguousBare(bare);
|
|
239
|
-
if (unambiguous !== undefined) return unambiguous;
|
|
240
|
-
const strippedText = stripDateSuffix(modelId.trim().toLowerCase());
|
|
241
|
-
const bareText = new TextDecoder().decode(bare);
|
|
242
|
-
if (strippedText !== bareText) {
|
|
243
|
-
const dated = this.unambiguousBare(lowerAscii(strippedText));
|
|
244
|
-
if (dated !== undefined) return dated;
|
|
245
|
-
}
|
|
246
|
-
// Last chance: the bare id may carry a reseller suffix of its own
|
|
247
|
-
// (`provider/model:free` → bare `model:free`).
|
|
248
|
-
const unsuffixedBareText = stripVariantSuffix(bareText);
|
|
249
|
-
if (unsuffixedBareText !== bareText && unsuffixedBareText !== strippedText) {
|
|
250
|
-
return this.unambiguousBare(lowerAscii(unsuffixedBareText));
|
|
251
|
-
}
|
|
252
|
-
return undefined;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* The single row for a bare id, or `undefined` when the catalog disagrees.
|
|
257
|
-
*
|
|
258
|
-
* Two rows that agree on every reported fact are interchangeable, so the
|
|
259
|
-
* first is returned rather than treating agreement as ambiguity.
|
|
260
|
-
*/
|
|
261
|
-
private unambiguousBare(bare: Uint8Array): ModelsDevModelMetadata | undefined {
|
|
262
|
-
const rows = this.metadataForBare(bare);
|
|
263
|
-
const first = rows[0];
|
|
264
|
-
if (first === undefined) return undefined;
|
|
265
|
-
if (rows.length === 1) return first;
|
|
266
|
-
const signature = (metadata: ModelsDevModelMetadata): string =>
|
|
267
|
-
JSON.stringify([metadata.contextLimit, metadata.outputLimit, metadata.cost, metadata.modalities]);
|
|
268
|
-
const expected = signature(first);
|
|
269
|
-
for (const row of rows) {
|
|
270
|
-
if (signature(row) !== expected) return undefined;
|
|
271
|
-
}
|
|
272
|
-
return first;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* The most widely agreed row for a bare id, or `undefined` when nothing
|
|
277
|
-
* records it.
|
|
278
|
-
*
|
|
279
|
-
* `resolve`/`unambiguousBare` fail closed on any disagreement, which is right
|
|
280
|
-
* when the caller can leave a limit unstated. A caller that must publish a
|
|
281
|
-
* number for a model the catalog knows about but disagrees on — a roster
|
|
282
|
-
* whose own upstream reports nothing, where the alternative is a fixed
|
|
283
|
-
* default — is better served by the majority answer: 18 of 19 rows file
|
|
284
|
-
* `claude-opus-5` as 1000000/128000, and the single dissenter (a reseller's
|
|
285
|
-
* 64k output cap) is the outlier, not the model.
|
|
286
|
-
*
|
|
287
|
-
* The vote is per-field and only among rows that state the field, so a row
|
|
288
|
-
* omitting `output` cannot win that field by abstaining. Ties resolve to the
|
|
289
|
-
* larger value: a limit stated too low truncates a caller's work, while one
|
|
290
|
-
* stated too high is caught by the provider.
|
|
291
|
-
*/
|
|
292
|
-
majorityFor(modelId: string): { contextLimit: number; outputLimit: number } | undefined {
|
|
293
|
-
const idLower = lowerAscii(modelId);
|
|
294
|
-
const bare = bareBytes(idLower);
|
|
295
|
-
// The bare index is keyed by the id as filed, so a dated spelling needs its
|
|
296
|
-
// undated form tried too. Merge rather than pick: both name one model.
|
|
297
|
-
const bareText = new TextDecoder().decode(bare);
|
|
298
|
-
const rows = [
|
|
299
|
-
...this.metadataForBare(bare),
|
|
300
|
-
...(stripDateSuffix(bareText) === bareText
|
|
301
|
-
? []
|
|
302
|
-
: this.metadataForBare(lowerAscii(stripDateSuffix(bareText)))),
|
|
303
|
-
];
|
|
304
|
-
if (rows.length === 0) return undefined;
|
|
305
|
-
const vote = (values: ReadonlyArray<number | null>): number | undefined => {
|
|
306
|
-
const counts = new Map<number, number>();
|
|
307
|
-
for (const value of values) {
|
|
308
|
-
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) continue;
|
|
309
|
-
counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
310
|
-
}
|
|
311
|
-
let best: number | undefined;
|
|
312
|
-
let bestCount = 0;
|
|
313
|
-
for (const [value, count] of counts) {
|
|
314
|
-
if (count > bestCount || (count === bestCount && best !== undefined && value > best)) {
|
|
315
|
-
best = value;
|
|
316
|
-
bestCount = count;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
return best;
|
|
320
|
-
};
|
|
321
|
-
const contextLimit = vote(rows.map((row) => row.contextLimit));
|
|
322
|
-
const outputLimit = vote(rows.map((row) => row.outputLimit));
|
|
323
|
-
if (contextLimit === undefined && outputLimit === undefined) return undefined;
|
|
324
|
-
return {
|
|
325
|
-
contextLimit: contextLimit ?? outputLimit ?? 0,
|
|
326
|
-
outputLimit: outputLimit ?? contextLimit ?? 0,
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* The price to bill for `modelId`, preferring the routed provider's own row.
|
|
332
|
-
*
|
|
333
|
-
* Unlike {@link resolve}, this does **not** fail closed when the provider has
|
|
334
|
-
* no row: a gateway that republishes a model without publishing its own rate
|
|
335
|
-
* still owes a cost, and the model's own global rate is the honest answer.
|
|
336
|
-
* `resolve` keeps its fail-closed metadata rule — a wrong context limit is
|
|
337
|
-
* harmful, while a missing price makes every request on that route read
|
|
338
|
-
* `$0.00` as if it were measured.
|
|
339
|
-
*/
|
|
340
|
-
costFor(providerId: string, modelId: string): ModelsDevModelCost {
|
|
341
|
-
if (modelId.trim().length === 0 && providerId.trim().length === 0) return UNKNOWN_COST;
|
|
342
|
-
const own = this.resolve(providerId, modelId)?.cost;
|
|
343
|
-
if (own !== undefined && (typeof own.input === "number" || typeof own.output === "number")) {
|
|
344
|
-
return own;
|
|
345
|
-
}
|
|
346
|
-
const idLower = lowerAscii(modelId);
|
|
347
|
-
const row = catalog().globalCostRow(bareBytes(idLower));
|
|
348
|
-
const global = row >= 0 ? catalog().rowCost(row) : undefined;
|
|
349
|
-
if (global !== undefined && global.pricing_model !== "unknown") return global;
|
|
350
|
-
return own ?? global ?? UNKNOWN_COST;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
export const modelsDevCatalog = new ModelsDevCatalog();
|
|
1
|
+
/**
|
|
2
|
+
* Offline model catalog: limits, capabilities, and pricing for every model the
|
|
3
|
+
* bundled providers can route to.
|
|
4
|
+
*
|
|
5
|
+
* The data is a packed, brotli-compressed binary blob (`catalog-blob.ts`, built
|
|
6
|
+
* by `scripts/tools/generate-models.ts`) read through {@link PackedCatalog}.
|
|
7
|
+
* There is no JSON parse and no per-row object: a lookup is a binary search
|
|
8
|
+
* over a typed-array column, and the metadata object for a row is built only
|
|
9
|
+
* when something asks for that row.
|
|
10
|
+
*
|
|
11
|
+
* Sources, both fetched by the generator:
|
|
12
|
+
* - **models.dev** — the broad catalog and the only per-model pricing.
|
|
13
|
+
* - **models.opencode.ai** — the authoritative limits for the OpenCode family,
|
|
14
|
+
* whose dispatch listing (`/zen/v1/models`) publishes ids with no limits at
|
|
15
|
+
* all. Without it every fetched OpenCode model published an invented
|
|
16
|
+
* 200k/64k.
|
|
17
|
+
*
|
|
18
|
+
* Runtime network access is intentionally absent: a deployed instance uses the
|
|
19
|
+
* exact catalog selected at build time.
|
|
20
|
+
*/
|
|
21
|
+
import { brotliDecompressSync } from "node:zlib";
|
|
22
|
+
import {
|
|
23
|
+
lowerAscii,
|
|
24
|
+
PackedCatalog,
|
|
25
|
+
type ModelsDevModelCost,
|
|
26
|
+
type ModelsDevModelMetadata,
|
|
27
|
+
} from "./catalog/catalog-format";
|
|
28
|
+
import {
|
|
29
|
+
CATALOG_BLOB_BROTLI_BASE64,
|
|
30
|
+
CATALOG_BLOB_BYTES,
|
|
31
|
+
} from "./catalog/catalog-blob";
|
|
32
|
+
|
|
33
|
+
export type { ModelsDevModelCost, ModelsDevModelMetadata };
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Cartethyia provider id → the provider key the catalog files it under.
|
|
37
|
+
*
|
|
38
|
+
* Only ids that genuinely differ belong here, and each entry is backed by the
|
|
39
|
+
* deployment's own upstream: `opencode` serves `opencode.ai`, and its model
|
|
40
|
+
* ids also appear under the catalog's `opencode`. Without this the exact
|
|
41
|
+
* lookup missed for every one of those models, so each fell through to the bare
|
|
42
|
+
* lookup — which fails closed when providers disagree — and the entry was
|
|
43
|
+
* published with invented defaults.
|
|
44
|
+
*
|
|
45
|
+
* Deliberately absent: providers with no catalog counterpart (a private
|
|
46
|
+
* gateway, a BYOK endpoint) and any id whose match rests on model ids alone.
|
|
47
|
+
* A shared model id is not proof of identity — the same generic id appears
|
|
48
|
+
* under hundreds of resellers — so a guess here would attribute a stranger's
|
|
49
|
+
* limits to this gateway's serving, which is the exact failure this catalog's
|
|
50
|
+
* disagreement rule exists to prevent.
|
|
51
|
+
*
|
|
52
|
+
* This table is for **metadata** (limits, modalities), where a wrong row is
|
|
53
|
+
* harmful and failing closed is right. Pricing does not need it: see
|
|
54
|
+
* {@link ModelsDevCatalog.costFor}, which falls back to the model's own rate.
|
|
55
|
+
*/
|
|
56
|
+
const CATALOG_PROVIDER_IDS: Readonly<Record<string, string>> = {
|
|
57
|
+
// `opencode` is both a Cartethyia provider id and a catalog key, so it needs
|
|
58
|
+
// no entry: `exactMetadata` tries the id directly first. Only the tiers whose
|
|
59
|
+
// id differs from the catalog key belong here.
|
|
60
|
+
"opencode-zen": "opencode",
|
|
61
|
+
"opencode-go": "opencode-go",
|
|
62
|
+
ollamacloud: "ollama-cloud",
|
|
63
|
+
xiaomipg: "xiaomi-token-plan-sgp",
|
|
64
|
+
xiaomitp: "xiaomi-token-plan-cn",
|
|
65
|
+
// Cartethyia ids that are shorter than the name the catalog files the
|
|
66
|
+
// provider under. Without these the exact lookup misses for every model, and
|
|
67
|
+
// each row falls through to the bare lookup — which fails closed when
|
|
68
|
+
// providers disagree about the id — so the catalog would publish invented
|
|
69
|
+
// defaults.
|
|
70
|
+
github: "github-copilot",
|
|
71
|
+
// Discovery-backed aggregators whose catalog key is spelled differently.
|
|
72
|
+
// `qianfan` and `helmcode` are deliberately absent: the catalog files no rows
|
|
73
|
+
// for either, and an entry pointing at a provider it does not carry would
|
|
74
|
+
// make every lookup miss and publish invented limits.
|
|
75
|
+
siliconflowcn: "siliconflow-cn",
|
|
76
|
+
nanogpt: "nano-gpt",
|
|
77
|
+
novita: "novita-ai",
|
|
78
|
+
together: "togetherai",
|
|
79
|
+
wafer: "wafer.ai",
|
|
80
|
+
// The Token Plan and the mainland DashScope host. Each points at the key the
|
|
81
|
+
// catalog files the *same* service under, so without these every one of their
|
|
82
|
+
// rows missed the exact lookup and fell through to the bare one, which fails
|
|
83
|
+
// closed when providers disagree about an id that resellers share.
|
|
84
|
+
//
|
|
85
|
+
// `cloudflareai` is deliberately absent: the gateway files its rows as
|
|
86
|
+
// `anthropic/claude-opus-5` while the provider dispatches the bare
|
|
87
|
+
// `claude-opus-5`, so the exact lookup misses under either key. An entry here
|
|
88
|
+
// would only restate the miss.
|
|
89
|
+
alibabacp: "alibaba-token-plan",
|
|
90
|
+
dashscope: "alibaba-coding-plan-cn",
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Removes a trailing release date from a model id, in either spelling the
|
|
95
|
+
* catalog uses: `-YYYY-MM-DD` (`gpt-4o-2024-11-20`) or `-YYYYMMDD`
|
|
96
|
+
* (`claude-haiku-4-5-20251001`). A dated snapshot bills the same rate as the
|
|
97
|
+
* model it snapshots, so both forms must reach the same row.
|
|
98
|
+
*/
|
|
99
|
+
function stripDateSuffix(modelId: string): string {
|
|
100
|
+
return modelId.replace(/-\d{4}-\d{2}-\d{2}$/, "").replace(/-\d{8}$/, "");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Suffixes a reseller appends to a model id it republishes unchanged.
|
|
105
|
+
*
|
|
106
|
+
* OpenRouter lists `deepseek/deepseek-v4.1-flash:free` — the `:free` marks its
|
|
107
|
+
* own billing tier, not a different model, so the underlying row is the one
|
|
108
|
+
* carrying the real context and output limits. `@variant` is the same idea (a
|
|
109
|
+
* pinned quantization or provider route).
|
|
110
|
+
*
|
|
111
|
+
* Stripped for *lookup only* — the id sent upstream is always the caller's.
|
|
112
|
+
*/
|
|
113
|
+
function stripVariantSuffix(modelId: string): string {
|
|
114
|
+
return modelId.replace(/[:@][^/@:]*$/, "");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The bare id: what follows a reseller's `provider/` prefix, if any.
|
|
119
|
+
*
|
|
120
|
+
* Returned as bytes because every lookup compares against the packed blob
|
|
121
|
+
* without allocating a string.
|
|
122
|
+
*/
|
|
123
|
+
function bareBytes(idLower: Uint8Array): Uint8Array {
|
|
124
|
+
for (let i = 0; i < idLower.length; i += 1) {
|
|
125
|
+
if (idLower[i] === 0x2f /* "/" */) return idLower.subarray(i + 1);
|
|
126
|
+
}
|
|
127
|
+
return idLower;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const UNKNOWN_COST: ModelsDevModelCost = {
|
|
131
|
+
input: null,
|
|
132
|
+
output: null,
|
|
133
|
+
pricing_model: "unknown",
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The packed catalog, decompressed on first use and memoized.
|
|
138
|
+
*
|
|
139
|
+
* Brotli + base64, not `JSON.parse`: the blob is columnar, so this allocates
|
|
140
|
+
* one buffer and takes typed-array views over it rather than materialising
|
|
141
|
+
* 8900 objects. `CATALOG_BLOB_BYTES` is the decompressed length, checked here
|
|
142
|
+
* so a consumer paired with a half-regenerated blob fails on first access
|
|
143
|
+
* rather than resolving a truncated catalog.
|
|
144
|
+
*
|
|
145
|
+
* The decode is lazy on purpose. It is ~105 KB of base64 that expands to a
|
|
146
|
+
* much larger buffer, and running it at module load would bill every process,
|
|
147
|
+
* including the ones that never resolve a model (a CLI subcommand, a migration
|
|
148
|
+
* run, a test that only touches the console), on the boot path. Deferring it
|
|
149
|
+
* moves that cost to the first catalog lookup and pays it once.
|
|
150
|
+
*/
|
|
151
|
+
let catalogCache: PackedCatalog | undefined;
|
|
152
|
+
|
|
153
|
+
function catalog(): PackedCatalog {
|
|
154
|
+
const cached = catalogCache;
|
|
155
|
+
if (cached !== undefined) return cached;
|
|
156
|
+
const bytes = brotliDecompressSync(Buffer.from(CATALOG_BLOB_BROTLI_BASE64, "base64"));
|
|
157
|
+
if (bytes.byteLength !== CATALOG_BLOB_BYTES) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`The packed model catalog is ${bytes.byteLength} B, expected ` +
|
|
160
|
+
`${CATALOG_BLOB_BYTES} B — regenerate it.`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
// `PackedCatalog` copies into a 4-byte-aligned buffer of its own, so the
|
|
164
|
+
// decompressed buffer is dropped here rather than held beside the copy.
|
|
165
|
+
const packed = new PackedCatalog(bytes);
|
|
166
|
+
catalogCache = packed;
|
|
167
|
+
return packed;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Resolves model metadata and pricing exclusively from the packed base catalog
|
|
172
|
+
* shipped with Cartethyia.
|
|
173
|
+
*/
|
|
174
|
+
export class ModelsDevCatalog {
|
|
175
|
+
/** When the generator fetched the upstream catalogs. */
|
|
176
|
+
get fetchedAt(): string {
|
|
177
|
+
return catalog().fetchedAt;
|
|
178
|
+
}
|
|
179
|
+
/** Every provider key the catalog files. */
|
|
180
|
+
get providers(): readonly string[] {
|
|
181
|
+
return catalog().providers;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Metadata for an exact `(providerIndex, idLower)` pair, or `undefined`.
|
|
186
|
+
*
|
|
187
|
+
* The provider is tried as given first and then under its mapped key: the
|
|
188
|
+
* mapped name is a second chance, not a replacement, so an id that already
|
|
189
|
+
* matches exactly keeps winning.
|
|
190
|
+
*/
|
|
191
|
+
private exactMetadata(providerLower: string, idLower: Uint8Array): ModelsDevModelMetadata | undefined {
|
|
192
|
+
const direct = catalog().exactIndex(catalog().providerIndex(providerLower), idLower);
|
|
193
|
+
if (direct >= 0) return catalog().metadataAt(direct);
|
|
194
|
+
const mapped = CATALOG_PROVIDER_IDS[providerLower];
|
|
195
|
+
if (mapped === undefined) return undefined;
|
|
196
|
+
return catalog().metadataAt(catalog().exactIndex(catalog().providerIndex(mapped), idLower));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Metadata rows whose *bare* id equals `bare`, in catalog order. */
|
|
200
|
+
private metadataForBare(bare: Uint8Array): ModelsDevModelMetadata[] {
|
|
201
|
+
const [start, end] = catalog().bareRange(bare);
|
|
202
|
+
const out: ModelsDevModelMetadata[] = [];
|
|
203
|
+
for (let position = start; position < end; position += 1) {
|
|
204
|
+
const metadata = catalog().metadataAt(catalog().bareOrderAt(position));
|
|
205
|
+
if (metadata !== undefined) out.push(metadata);
|
|
206
|
+
}
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Resolves metadata for one model id.
|
|
212
|
+
*
|
|
213
|
+
* An exact `provider:model` hit always wins — it is the only lookup that
|
|
214
|
+
* names the provider actually serving the request. The bare-id fallback is
|
|
215
|
+
* used only when it is **unambiguous**: if the catalog recorded the id under
|
|
216
|
+
* several providers with different facts, no row can be called the model's
|
|
217
|
+
* own, so the answer is `undefined` and the caller keeps its own declared
|
|
218
|
+
* limits instead of inheriting a stranger's.
|
|
219
|
+
*
|
|
220
|
+
* Callers that know the provider should pass it; passing `""` opts into the
|
|
221
|
+
* bare lookup, which is why it fails closed on disagreement.
|
|
222
|
+
*/
|
|
223
|
+
resolve(providerId: string, modelId: string): ModelsDevModelMetadata | undefined {
|
|
224
|
+
const provider = providerId.trim().toLowerCase();
|
|
225
|
+
const idLower = lowerAscii(modelId);
|
|
226
|
+
if (idLower.length === 0) return undefined;
|
|
227
|
+
const exact = this.exactMetadata(provider, idLower);
|
|
228
|
+
if (exact !== undefined) return exact;
|
|
229
|
+
// A reseller suffix is the reseller's, not the model's: retry the exact
|
|
230
|
+
// lookup without it before falling through to the bare id. Doing this here
|
|
231
|
+
// rather than stripping up front keeps the provider-scoped match.
|
|
232
|
+
const unsuffixedText = stripVariantSuffix(modelId.trim().toLowerCase());
|
|
233
|
+
if (unsuffixedText !== modelId.trim().toLowerCase()) {
|
|
234
|
+
const unsuffixed = this.exactMetadata(provider, lowerAscii(unsuffixedText));
|
|
235
|
+
if (unsuffixed !== undefined) return unsuffixed;
|
|
236
|
+
}
|
|
237
|
+
const bare = bareBytes(idLower);
|
|
238
|
+
const unambiguous = this.unambiguousBare(bare);
|
|
239
|
+
if (unambiguous !== undefined) return unambiguous;
|
|
240
|
+
const strippedText = stripDateSuffix(modelId.trim().toLowerCase());
|
|
241
|
+
const bareText = new TextDecoder().decode(bare);
|
|
242
|
+
if (strippedText !== bareText) {
|
|
243
|
+
const dated = this.unambiguousBare(lowerAscii(strippedText));
|
|
244
|
+
if (dated !== undefined) return dated;
|
|
245
|
+
}
|
|
246
|
+
// Last chance: the bare id may carry a reseller suffix of its own
|
|
247
|
+
// (`provider/model:free` → bare `model:free`).
|
|
248
|
+
const unsuffixedBareText = stripVariantSuffix(bareText);
|
|
249
|
+
if (unsuffixedBareText !== bareText && unsuffixedBareText !== strippedText) {
|
|
250
|
+
return this.unambiguousBare(lowerAscii(unsuffixedBareText));
|
|
251
|
+
}
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* The single row for a bare id, or `undefined` when the catalog disagrees.
|
|
257
|
+
*
|
|
258
|
+
* Two rows that agree on every reported fact are interchangeable, so the
|
|
259
|
+
* first is returned rather than treating agreement as ambiguity.
|
|
260
|
+
*/
|
|
261
|
+
private unambiguousBare(bare: Uint8Array): ModelsDevModelMetadata | undefined {
|
|
262
|
+
const rows = this.metadataForBare(bare);
|
|
263
|
+
const first = rows[0];
|
|
264
|
+
if (first === undefined) return undefined;
|
|
265
|
+
if (rows.length === 1) return first;
|
|
266
|
+
const signature = (metadata: ModelsDevModelMetadata): string =>
|
|
267
|
+
JSON.stringify([metadata.contextLimit, metadata.outputLimit, metadata.cost, metadata.modalities]);
|
|
268
|
+
const expected = signature(first);
|
|
269
|
+
for (const row of rows) {
|
|
270
|
+
if (signature(row) !== expected) return undefined;
|
|
271
|
+
}
|
|
272
|
+
return first;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* The most widely agreed row for a bare id, or `undefined` when nothing
|
|
277
|
+
* records it.
|
|
278
|
+
*
|
|
279
|
+
* `resolve`/`unambiguousBare` fail closed on any disagreement, which is right
|
|
280
|
+
* when the caller can leave a limit unstated. A caller that must publish a
|
|
281
|
+
* number for a model the catalog knows about but disagrees on — a roster
|
|
282
|
+
* whose own upstream reports nothing, where the alternative is a fixed
|
|
283
|
+
* default — is better served by the majority answer: 18 of 19 rows file
|
|
284
|
+
* `claude-opus-5` as 1000000/128000, and the single dissenter (a reseller's
|
|
285
|
+
* 64k output cap) is the outlier, not the model.
|
|
286
|
+
*
|
|
287
|
+
* The vote is per-field and only among rows that state the field, so a row
|
|
288
|
+
* omitting `output` cannot win that field by abstaining. Ties resolve to the
|
|
289
|
+
* larger value: a limit stated too low truncates a caller's work, while one
|
|
290
|
+
* stated too high is caught by the provider.
|
|
291
|
+
*/
|
|
292
|
+
majorityFor(modelId: string): { contextLimit: number; outputLimit: number } | undefined {
|
|
293
|
+
const idLower = lowerAscii(modelId);
|
|
294
|
+
const bare = bareBytes(idLower);
|
|
295
|
+
// The bare index is keyed by the id as filed, so a dated spelling needs its
|
|
296
|
+
// undated form tried too. Merge rather than pick: both name one model.
|
|
297
|
+
const bareText = new TextDecoder().decode(bare);
|
|
298
|
+
const rows = [
|
|
299
|
+
...this.metadataForBare(bare),
|
|
300
|
+
...(stripDateSuffix(bareText) === bareText
|
|
301
|
+
? []
|
|
302
|
+
: this.metadataForBare(lowerAscii(stripDateSuffix(bareText)))),
|
|
303
|
+
];
|
|
304
|
+
if (rows.length === 0) return undefined;
|
|
305
|
+
const vote = (values: ReadonlyArray<number | null>): number | undefined => {
|
|
306
|
+
const counts = new Map<number, number>();
|
|
307
|
+
for (const value of values) {
|
|
308
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) continue;
|
|
309
|
+
counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
310
|
+
}
|
|
311
|
+
let best: number | undefined;
|
|
312
|
+
let bestCount = 0;
|
|
313
|
+
for (const [value, count] of counts) {
|
|
314
|
+
if (count > bestCount || (count === bestCount && best !== undefined && value > best)) {
|
|
315
|
+
best = value;
|
|
316
|
+
bestCount = count;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return best;
|
|
320
|
+
};
|
|
321
|
+
const contextLimit = vote(rows.map((row) => row.contextLimit));
|
|
322
|
+
const outputLimit = vote(rows.map((row) => row.outputLimit));
|
|
323
|
+
if (contextLimit === undefined && outputLimit === undefined) return undefined;
|
|
324
|
+
return {
|
|
325
|
+
contextLimit: contextLimit ?? outputLimit ?? 0,
|
|
326
|
+
outputLimit: outputLimit ?? contextLimit ?? 0,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* The price to bill for `modelId`, preferring the routed provider's own row.
|
|
332
|
+
*
|
|
333
|
+
* Unlike {@link resolve}, this does **not** fail closed when the provider has
|
|
334
|
+
* no row: a gateway that republishes a model without publishing its own rate
|
|
335
|
+
* still owes a cost, and the model's own global rate is the honest answer.
|
|
336
|
+
* `resolve` keeps its fail-closed metadata rule — a wrong context limit is
|
|
337
|
+
* harmful, while a missing price makes every request on that route read
|
|
338
|
+
* `$0.00` as if it were measured.
|
|
339
|
+
*/
|
|
340
|
+
costFor(providerId: string, modelId: string): ModelsDevModelCost {
|
|
341
|
+
if (modelId.trim().length === 0 && providerId.trim().length === 0) return UNKNOWN_COST;
|
|
342
|
+
const own = this.resolve(providerId, modelId)?.cost;
|
|
343
|
+
if (own !== undefined && (typeof own.input === "number" || typeof own.output === "number")) {
|
|
344
|
+
return own;
|
|
345
|
+
}
|
|
346
|
+
const idLower = lowerAscii(modelId);
|
|
347
|
+
const row = catalog().globalCostRow(bareBytes(idLower));
|
|
348
|
+
const global = row >= 0 ? catalog().rowCost(row) : undefined;
|
|
349
|
+
if (global !== undefined && global.pricing_model !== "unknown") return global;
|
|
350
|
+
return own ?? global ?? UNKNOWN_COST;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export const modelsDevCatalog = new ModelsDevCatalog();
|