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,450 +1,450 @@
|
|
|
1
|
-
// Qoder model catalogs, one per realm, plus the live listing that supersedes them.
|
|
2
|
-
//
|
|
3
|
-
// The two realms publish different rosters: the donor's snapshot files
|
|
4
|
-
// (`qoder_catalog_cn.json`, `qoder_catalog_intl.json`) disagree on 5 keys and on
|
|
5
|
-
// the context window of the keys they share. A single merged list therefore
|
|
6
|
-
// advertises models a given account cannot reach, and hides the ones only the
|
|
7
|
-
// other realm serves — so each realm keeps its own static floor.
|
|
8
|
-
//
|
|
9
|
-
// The static rows are the *offline floor*, not the truth: the authoritative list
|
|
10
|
-
// is `/algo/api/v2/model/list` for the account's own realm (`discoverQoderModels`).
|
|
11
|
-
import { GatewayError } from "../../../protocol/gateway-error";
|
|
12
|
-
import { defineModel } from "../../provider-model-definition";
|
|
13
|
-
import type { ModelDefinition } from "../../provider-model-definition";
|
|
14
|
-
import { QODER_CHAT_PATH, QODER_MODELS_PATH, qoderGatewayCandidates, type QoderRealm } from "./qoder-realm";
|
|
15
|
-
import { qoderVirtualIdFor, QODER_VIRTUAL_MODELS } from "./qoder-virtual";
|
|
16
|
-
|
|
17
|
-
/** One upstream catalog row, as `/algo/api/v2/model/list` reports it. */
|
|
18
|
-
export interface QoderCatalogEntry {
|
|
19
|
-
readonly key: string;
|
|
20
|
-
readonly displayName: string;
|
|
21
|
-
readonly isVl: boolean;
|
|
22
|
-
readonly isReasoning: boolean;
|
|
23
|
-
/**
|
|
24
|
-
* Largest window this model can be switched to.
|
|
25
|
-
*
|
|
26
|
-
* Upstream states two different numbers: `max_input_tokens` is the window
|
|
27
|
-
* *currently selected* (often 180k), while `context_config` lists the
|
|
28
|
-
* selectable windows — `{"200K":…, "400K":…, "1M":…}` — which for the same
|
|
29
|
-
* row go up to 1M. Taking `max_input_tokens` alone published and dispatched
|
|
30
|
-
* the smaller figure, so Qoder was told the conversation was 180k when the
|
|
31
|
-
* account could use 1M.
|
|
32
|
-
*/
|
|
33
|
-
readonly maxInputTokens: number | undefined;
|
|
34
|
-
readonly enable: boolean;
|
|
35
|
-
/**
|
|
36
|
-
* Whether upstream publishes this row as free.
|
|
37
|
-
*
|
|
38
|
-
* Qoder states it per row (`is_free`) and it is not inferable: `price_factor`
|
|
39
|
-
* is 0 on both free and some billed rows, and the lite-plan flag describes a
|
|
40
|
-
* plan tier rather than a model. So the free tier is read from this field
|
|
41
|
-
* instead of being hardcoded as a list, which is what lets a plan change
|
|
42
|
-
* move a model between tiers without a code change.
|
|
43
|
-
*/
|
|
44
|
-
readonly isFree: boolean;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* The widest selectable window on one catalog row.
|
|
49
|
-
*
|
|
50
|
-
* `context_config` is the authority when present: it names every window the
|
|
51
|
-
* account can switch to, whereas `max_input_tokens` only names the one active
|
|
52
|
-
* now. Absent both, `undefined` — an unknown window is not zero.
|
|
53
|
-
*/
|
|
54
|
-
export function qoderMaxContextWindow(raw: unknown): number | undefined {
|
|
55
|
-
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
|
56
|
-
const record = raw as Record<string, unknown>;
|
|
57
|
-
const candidates: number[] = [];
|
|
58
|
-
const config = record["context_config"];
|
|
59
|
-
if (config !== null && typeof config === "object" && !Array.isArray(config)) {
|
|
60
|
-
for (const option of Object.values(config as Record<string, unknown>)) {
|
|
61
|
-
if (option === null || typeof option !== "object" || Array.isArray(option)) continue;
|
|
62
|
-
const tokenCount = (option as Record<string, unknown>)["token_count"];
|
|
63
|
-
if (typeof tokenCount === "number" && Number.isFinite(tokenCount) && tokenCount > 0) {
|
|
64
|
-
candidates.push(Math.floor(tokenCount));
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
const stated = record["max_input_tokens"];
|
|
69
|
-
if (typeof stated === "number" && Number.isFinite(stated) && stated > 0) {
|
|
70
|
-
candidates.push(Math.floor(stated));
|
|
71
|
-
}
|
|
72
|
-
return candidates.length === 0 ? undefined : Math.max(...candidates);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** One static row's shape, mirrored on `QODER_MODEL_CONFIGS` in `qoder.ts`. */
|
|
76
|
-
interface QoderModelConfig {
|
|
77
|
-
readonly id: string;
|
|
78
|
-
readonly display_name: string;
|
|
79
|
-
readonly max_input_tokens: number;
|
|
80
|
-
readonly max_output_tokens?: number;
|
|
81
|
-
readonly is_vl: boolean;
|
|
82
|
-
readonly is_reasoning: boolean;
|
|
83
|
-
/**
|
|
84
|
-
* Whether upstream publishes this row as free. Absent means upstream did not
|
|
85
|
-
* state it for the row, which is not the same as false — only an explicit
|
|
86
|
-
* `false` marks the model as billed.
|
|
87
|
-
*/
|
|
88
|
-
readonly is_free?: boolean;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* China roster (14 keys).
|
|
93
|
-
*
|
|
94
|
-
* Captured from the donor's `qoder_catalog_cn.json`, which is the decoded
|
|
95
|
-
* `chat` array of a real CN account's model list.
|
|
96
|
-
*/
|
|
97
|
-
export const QODER_CN_MODEL_CONFIGS: Readonly<Record<string, QoderModelConfig>> = {
|
|
98
|
-
auto: { id: "auto", display_name: "Auto", max_output_tokens: 64000, max_input_tokens: 200000, is_vl: true, is_reasoning: true },
|
|
99
|
-
qmodel_38max: { id: "qmodel_38max", display_name: "Qwen3.8-Max", max_output_tokens: 131072, max_input_tokens: 1000000, is_vl: true, is_reasoning: true },
|
|
100
|
-
qfmodel: { id: "qfmodel", display_name: "Qwen3.8-Flash", max_output_tokens: 131072, max_input_tokens: 1000000, is_vl: true, is_reasoning: true },
|
|
101
|
-
qmodel_latest: { id: "qmodel_latest", display_name: "Qwen3.7-Max", max_output_tokens: 131072, max_input_tokens: 1000000, is_vl: true, is_reasoning: true },
|
|
102
|
-
qmodel: { id: "qmodel", display_name: "Qwen3.7-Plus", max_output_tokens: 131072, max_input_tokens: 1000000, is_vl: true, is_reasoning: true },
|
|
103
|
-
q37fmodel: { id: "q37fmodel", display_name: "Qwen3.7-Flash", max_output_tokens: 131072, max_input_tokens: 1000000, is_vl: true, is_reasoning: true },
|
|
104
|
-
dmodel: { id: "dmodel", display_name: "DeepSeek-V4-Pro", max_output_tokens: 384000, max_input_tokens: 1000000, is_vl: true, is_reasoning: true },
|
|
105
|
-
dfmodel: { id: "dfmodel", display_name: "DeepSeek-Flash", max_output_tokens: 384000, max_input_tokens: 1000000, is_vl: true, is_reasoning: false },
|
|
106
|
-
gmodel: { id: "gmodel", display_name: "GLM-5.3", max_output_tokens: 131072, max_input_tokens: 1000000, is_vl: true, is_reasoning: true },
|
|
107
|
-
gfmodel: { id: "gfmodel", display_name: "GLM-5.3-Flash", max_output_tokens: 131072, max_input_tokens: 1000000, is_vl: true, is_reasoning: true },
|
|
108
|
-
gm51model: { id: "gm51model", display_name: "GLM-5.2", max_output_tokens: 131072, max_input_tokens: 1000000, is_vl: true, is_reasoning: true },
|
|
109
|
-
kmodel_latest: { id: "kmodel_latest", display_name: "Kimi-K3", max_output_tokens: 131072, max_input_tokens: 1000000, is_vl: true, is_reasoning: false },
|
|
110
|
-
kmodel: { id: "kmodel", display_name: "Kimi-K2.8-Preview", max_output_tokens: 65536, max_input_tokens: 1000000, is_vl: true, is_reasoning: true },
|
|
111
|
-
mmodel: { id: "mmodel", display_name: "MiniMax-M2.7", max_output_tokens: 131072, max_input_tokens: 200000, is_vl: false, is_reasoning: false },
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Global/intl roster (17 keys), from the donor's `qoder_catalog_intl.json`.
|
|
116
|
-
*
|
|
117
|
-
* Note how the shared keys differ from CN — `auto` is non-reasoning here and
|
|
118
|
-
* `qmodel` carries a 1M window. That is why the rosters are not merged.
|
|
119
|
-
*/
|
|
120
|
-
export const QODER_INTL_MODEL_CONFIGS: Readonly<Record<string, QoderModelConfig>> = {
|
|
121
|
-
auto: { id: "auto", display_name: "Auto", max_input_tokens: 200000, max_output_tokens: 64_000, is_vl: true, is_reasoning: false },
|
|
122
|
-
ultimate: { id: "ultimate", display_name: "Ultimate", max_input_tokens: 1000000, max_output_tokens: 128_000, is_vl: true, is_free: false, is_reasoning: true },
|
|
123
|
-
// `fast` is what the live roster reports today; `performance` is the older
|
|
124
|
-
// key for the same slot. Both are kept so a hand-added model id written
|
|
125
|
-
// against either name still resolves — the roster is the offline floor, and
|
|
126
|
-
// discovery supersedes it on the first successful fetch.
|
|
127
|
-
fast: { id: "fast", display_name: "Fast", max_input_tokens: 1000000, max_output_tokens: 64_000, is_vl: true, is_reasoning: false },
|
|
128
|
-
performance: { id: "performance", display_name: "Performance", max_input_tokens: 1000000, max_output_tokens: 64_000, is_vl: true, is_reasoning: false },
|
|
129
|
-
efficient: { id: "efficient", display_name: "Efficient", max_input_tokens: 200000, max_output_tokens: 64_000, is_vl: true, is_reasoning: false },
|
|
130
|
-
smodel: { id: "smodel", display_name: "Sonus", max_input_tokens: 1000000, max_output_tokens: 128_000, is_vl: true, is_free: false, is_reasoning: true },
|
|
131
|
-
cmodel: { id: "cmodel", display_name: "Cantus", max_input_tokens: 1000000, max_output_tokens: 128_000, is_vl: true, is_free: false, is_reasoning: true },
|
|
132
|
-
qmodel_38max: { id: "qmodel_38max", display_name: "Qwen3.8-Max", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_reasoning: true },
|
|
133
|
-
qfmodel: { id: "qfmodel", display_name: "Qwen3.8-Flash", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_free: true, is_reasoning: true },
|
|
134
|
-
qmodel_latest: { id: "qmodel_latest", display_name: "Qwen3.7-Max", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_reasoning: false },
|
|
135
|
-
qmodel: { id: "qmodel", display_name: "Qwen3.7-Plus", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_reasoning: false },
|
|
136
|
-
kmodel_latest: { id: "kmodel_latest", display_name: "Kimi-K3", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_free: false, is_reasoning: false },
|
|
137
|
-
kmodel: { id: "kmodel", display_name: "Kimi-K2.8-Preview", max_input_tokens: 1000000, max_output_tokens: 65_536, is_vl: true, is_free: false, is_reasoning: false },
|
|
138
|
-
gmodel: { id: "gmodel", display_name: "GLM-5.3", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_free: false, is_reasoning: true },
|
|
139
|
-
gfmodel: { id: "gfmodel", display_name: "GLM-5.3-Flash", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_reasoning: true },
|
|
140
|
-
dmodel: { id: "dmodel", display_name: "DeepSeek-V4-Pro", max_input_tokens: 1000000, max_output_tokens: 384_000, is_vl: true, is_free: false, is_reasoning: true },
|
|
141
|
-
dfmodel: { id: "dfmodel", display_name: "DeepSeek-Flash", max_input_tokens: 1000000, max_output_tokens: 384_000, is_vl: true, is_free: true, is_reasoning: true },
|
|
142
|
-
mmodel: { id: "mmodel", display_name: "MiniMax-M3", max_input_tokens: 1000000, max_output_tokens: 512_000, is_vl: true, is_free: false, is_reasoning: false },
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Output ceilings are stated per row, not derived.
|
|
147
|
-
*
|
|
148
|
-
* Qoder's `/model/list` reports no output limit at all — none of the 21 fields
|
|
149
|
-
* it returns for any row mentions output — so these come from the static
|
|
150
|
-
* roster, set to what the same model is filed at elsewhere in the catalog
|
|
151
|
-
* (DeepSeek-V4 at 384k, GLM/Qwen/Kimi/MiniMax at 131k, the GPT-class slots at
|
|
152
|
-
* 128k).
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
* A row with no stated ceiling leaves `out` unset so `defineModel` can still
|
|
156
|
-
* consult the base catalog rather than publishing a guess.
|
|
157
|
-
*/
|
|
158
|
-
|
|
159
|
-
function qoderModels(
|
|
160
|
-
configs: Readonly<Record<string, QoderModelConfig>>,
|
|
161
|
-
providerId: string,
|
|
162
|
-
): readonly ModelDefinition[] {
|
|
163
|
-
return Object.entries(configs).map(([id, cfg]) =>
|
|
164
|
-
defineModel({
|
|
165
|
-
id,
|
|
166
|
-
providerId,
|
|
167
|
-
wireFamily: "chat",
|
|
168
|
-
endpoint: QODER_CHAT_PATH,
|
|
169
|
-
ctx: cfg.max_input_tokens,
|
|
170
|
-
...(cfg.max_output_tokens === undefined ? {} : { out: cfg.max_output_tokens }),
|
|
171
|
-
vision: cfg.is_vl,
|
|
172
|
-
reasoning: cfg.is_reasoning,
|
|
173
|
-
// Only an explicit `true` marks the row free; an unstated flag is left
|
|
174
|
-
// unset so nothing claims a tier the roster did not state.
|
|
175
|
-
...(cfg.is_free === undefined ? {} : { free: cfg.is_free, freeTier: cfg.is_free }),
|
|
176
|
-
}),
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/** Static offline catalog for the China realm. */
|
|
181
|
-
export const QODER_CN_MODELS: readonly ModelDefinition[] = qoderModels(QODER_CN_MODEL_CONFIGS, "qodercn");
|
|
182
|
-
|
|
183
|
-
/** Static offline catalog for the global/intl realm. */
|
|
184
|
-
const QODER_INTL_ROSTER: readonly ModelDefinition[] = qoderModels(QODER_INTL_MODEL_CONFIGS, "qoder");
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Virtual rows, one per roster key that has a caller-facing name.
|
|
188
|
-
*
|
|
189
|
-
* A virtual row inherits its target's limits and capabilities rather than
|
|
190
|
-
* restating them: the two ids are the same upstream model, so a window or a
|
|
191
|
-
* vision flag that disagreed between them would be a lie in one of the two
|
|
192
|
-
* places. A target this realm does not serve contributes no row, which is what
|
|
193
|
-
* keeps the CN and intl catalogs honest about their own rosters.
|
|
194
|
-
*/
|
|
195
|
-
export function qoderVirtualModels(
|
|
196
|
-
roster: readonly ModelDefinition[],
|
|
197
|
-
providerId: string,
|
|
198
|
-
): readonly ModelDefinition[] {
|
|
199
|
-
const byId = new Map(roster.map((row) => [row.modelId, row]));
|
|
200
|
-
const rows: ModelDefinition[] = [];
|
|
201
|
-
for (const virtual of QODER_VIRTUAL_MODELS) {
|
|
202
|
-
const target = byId.get(virtual.target);
|
|
203
|
-
if (target === undefined) continue;
|
|
204
|
-
rows.push(
|
|
205
|
-
defineModel({
|
|
206
|
-
id: virtual.id,
|
|
207
|
-
providerId,
|
|
208
|
-
wireFamily: "chat",
|
|
209
|
-
endpoint: QODER_CHAT_PATH,
|
|
210
|
-
// `null`, not a number: an unknown window is unknown, and `defineModel`
|
|
211
|
-
// substitutes the shared unfiled fallback for null — the same marker a
|
|
212
|
-
// hand-added row gets.
|
|
213
|
-
ctx: target.contextLimit ?? null,
|
|
214
|
-
...(target.outputLimit === null ? {} : { out: target.outputLimit }),
|
|
215
|
-
vision: target.modalities.input.includes("image"),
|
|
216
|
-
reasoning: target.reasoning ?? false,
|
|
217
|
-
toolCall: target.toolCall ?? false,
|
|
218
|
-
// The same model under its caller-facing name belongs to the same tier,
|
|
219
|
-
// so the pill follows the roster row rather than being restated.
|
|
220
|
-
...(target.freeTier === undefined ? {} : { free: target.freeTier, freeTier: target.freeTier }),
|
|
221
|
-
}),
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
return rows;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Static offline catalog for the global/intl realm.
|
|
229
|
-
*
|
|
230
|
-
* A roster key that has a caller-facing name is published under that name
|
|
231
|
-
* only: upstream's keys are opaque slots (`dfmodel`, `cmodel`), and offering
|
|
232
|
-
* both the slot and its name shows one model twice under two ids nobody can
|
|
233
|
-
* tell apart. A key with no known name is kept as-is — an unnamed slot is
|
|
234
|
-
* still reachable, and inventing a name for it would assert what upstream
|
|
235
|
-
* never stated.
|
|
236
|
-
*
|
|
237
|
-
* The key remains what dispatch sends; see `qoder-virtual.ts`.
|
|
238
|
-
*/
|
|
239
|
-
export const QODER_INTL_MODELS: readonly ModelDefinition[] = [
|
|
240
|
-
...QODER_INTL_ROSTER.filter((row) => qoderVirtualIdFor(row.modelId) === undefined),
|
|
241
|
-
...qoderVirtualModels(QODER_INTL_ROSTER, "qoder"),
|
|
242
|
-
];
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Reads upstream's `{"chat": [...]}` envelope.
|
|
246
|
-
*
|
|
247
|
-
* Rows without a usable `key` are dropped rather than coerced: a model id the
|
|
248
|
-
* upstream did not state would become an unroutable catalog row.
|
|
249
|
-
*/
|
|
250
|
-
export function mapQoderCatalogResponse(raw: unknown): QoderCatalogEntry[] {
|
|
251
|
-
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return [];
|
|
252
|
-
const chat = (raw as Record<string, unknown>)["chat"];
|
|
253
|
-
if (!Array.isArray(chat)) return [];
|
|
254
|
-
const entries: QoderCatalogEntry[] = [];
|
|
255
|
-
for (const row of chat) {
|
|
256
|
-
if (row === null || typeof row !== "object" || Array.isArray(row)) continue;
|
|
257
|
-
const record = row as Record<string, unknown>;
|
|
258
|
-
const key = record["key"];
|
|
259
|
-
if (typeof key !== "string" || key.trim().length === 0) continue;
|
|
260
|
-
const displayName = record["display_name"];
|
|
261
|
-
entries.push({
|
|
262
|
-
key: key.trim(),
|
|
263
|
-
displayName: typeof displayName === "string" && displayName.length > 0 ? displayName : key.trim(),
|
|
264
|
-
isVl: record["is_vl"] === true,
|
|
265
|
-
isReasoning: record["is_reasoning"] === true,
|
|
266
|
-
maxInputTokens: qoderMaxContextWindow(record),
|
|
267
|
-
isFree: record["is_free"] === true,
|
|
268
|
-
// Absent means "not stated", which is not the same as disabled.
|
|
269
|
-
enable: record["enable"] !== false,
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
return entries;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/** Projects one live catalog row onto a Cartethyia catalog row. */
|
|
276
|
-
export function qoderCatalogModel(
|
|
277
|
-
entry: QoderCatalogEntry,
|
|
278
|
-
providerId: string,
|
|
279
|
-
): ModelDefinition {
|
|
280
|
-
return defineModel({
|
|
281
|
-
id: entry.key,
|
|
282
|
-
providerId,
|
|
283
|
-
wireFamily: "chat",
|
|
284
|
-
endpoint: QODER_CHAT_PATH,
|
|
285
|
-
// `null`, not omitted: an upstream row that stated no window is unknown,
|
|
286
|
-
// not unlimited, and `defineModel` substitutes its own "unfiled" estimate
|
|
287
|
-
// for null — the same marker a hand-added row gets.
|
|
288
|
-
ctx: entry.maxInputTokens ?? null,
|
|
289
|
-
vision: entry.isVl,
|
|
290
|
-
reasoning: entry.isReasoning,
|
|
291
|
-
// Free tier straight from upstream's own flag, so the pill on a model card
|
|
292
|
-
// tracks the plan rather than a hardcoded id list. Both statements hold:
|
|
293
|
-
// a free row bills zero per token and belongs to the provider's free tier.
|
|
294
|
-
free: entry.isFree,
|
|
295
|
-
freeTier: entry.isFree,
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* Header factory for one signed request.
|
|
301
|
-
*
|
|
302
|
-
* Injected rather than imported: the COSY signer lives in `qoder.ts`, which
|
|
303
|
-
* imports this module's rosters — importing it back would be a cycle. The
|
|
304
|
-
* callback receives the URL and the exact body that will be sent, because the
|
|
305
|
-
* signature covers both.
|
|
306
|
-
*/
|
|
307
|
-
export type QoderSigner = (url: string, body: string) => Promise<Record<string, string>>;
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* The account's own live model list.
|
|
311
|
-
*
|
|
312
|
-
* Two details are load-bearing and non-obvious:
|
|
313
|
-
*
|
|
314
|
-
* - the request is a **GET that carries the signed body**. A bare GET answers
|
|
315
|
-
* 403, because the server validates signature against body; the donor
|
|
316
|
-
* (`read_dynamic_models`) signs `{}` and sends it as the GET's data.
|
|
317
|
-
* - the signature covers only the path, so switching gateway hosts between
|
|
318
|
-
* attempts does not invalidate it.
|
|
319
|
-
*
|
|
320
|
-
* Hosts are tried primary → fallbacks (intl has three, CN one) and the first
|
|
321
|
-
* host that answers with a usable list wins.
|
|
322
|
-
*
|
|
323
|
-
* Signing is injected, never reimplemented: `headers(url, body)` is the COSY
|
|
324
|
-
* signer from `qoder.ts`, passed in because that module imports this one and
|
|
325
|
-
* importing it back would be a cycle. `encode` is the same arrangement for
|
|
326
|
-
* Qoder's scrambled body alphabet — omitted, the literal `{}` is both signed
|
|
327
|
-
* and sent, which is what a caller whose signer already binds the credential
|
|
328
|
-
* wants.
|
|
329
|
-
*
|
|
330
|
-
* `fetcher` is optional in the type but required at runtime, and there is
|
|
331
|
-
* deliberately no global-`fetch` default: a test cannot reach the network by
|
|
332
|
-
* accident, and production is forced through the SSRF-validated egress
|
|
333
|
-
* (`network/outbound-fetch.ts`). That egress is a `node:http` path, which is
|
|
334
|
-
* also the only way a body reaches the wire on a GET — `fetch` itself rejects
|
|
335
|
-
* `GET` + `body` with "request with GET/HEAD method cannot have body".
|
|
336
|
-
*/
|
|
337
|
-
/**
|
|
338
|
-
* How long one realm's live list is reused, in milliseconds.
|
|
339
|
-
*
|
|
340
|
-
* The roster is account-wide and changes rarely — upstream adds or retires a
|
|
341
|
-
* key, it does not reshuffle per request — so every catalog refresh inside the
|
|
342
|
-
* window would re-sign and re-fetch an identical list. The donor caches the
|
|
343
|
-
* same way (`fetch_models`, 300 s per realm).
|
|
344
|
-
*/
|
|
345
|
-
const DISCOVERY_CACHE_TTL_MS = 300_000;
|
|
346
|
-
|
|
347
|
-
/** One realm's cached roster, with the instant it was read. */
|
|
348
|
-
interface CachedRoster {
|
|
349
|
-
readonly at: number;
|
|
350
|
-
readonly models: readonly ModelDefinition[];
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/** Per-realm live-list cache; keyed by realm, not by account. */
|
|
354
|
-
const discoveryCache = new Map<QoderRealm, CachedRoster>();
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* Drops one realm's cached list.
|
|
358
|
-
*
|
|
359
|
-
* Exported for tests: the cache is module state, so a test that builds a
|
|
360
|
-
* roster and then asserts a fresh fetch would otherwise read the entry the
|
|
361
|
-
* previous assertion left behind.
|
|
362
|
-
*/
|
|
363
|
-
export function resetQoderDiscoveryCache(realm?: QoderRealm): void {
|
|
364
|
-
if (realm === undefined) discoveryCache.clear();
|
|
365
|
-
else discoveryCache.delete(realm);
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
export async function discoverQoderModels(input: {
|
|
369
|
-
readonly realm: QoderRealm;
|
|
370
|
-
/** Account secret the injected signer signs with; blank fails fast. */
|
|
371
|
-
readonly credential: string;
|
|
372
|
-
readonly headers: QoderSigner;
|
|
373
|
-
readonly fetcher?: typeof fetch;
|
|
374
|
-
readonly signal?: AbortSignal;
|
|
375
|
-
/** Defaults to the realm's bundled provider id (`qodercn` | `qoder`). */
|
|
376
|
-
readonly providerId?: string;
|
|
377
|
-
/** Body encoder; Qoder uses its own scrambled base64 alphabet. */
|
|
378
|
-
readonly encode?: (body: Uint8Array) => string;
|
|
379
|
-
}): Promise<readonly ModelDefinition[]> {
|
|
380
|
-
const providerId = input.providerId ?? (input.realm === "cn" ? "qodercn" : "qoder");
|
|
381
|
-
const cached = discoveryCache.get(input.realm);
|
|
382
|
-
if (cached !== undefined && Date.now() - cached.at < DISCOVERY_CACHE_TTL_MS) {
|
|
383
|
-
return cached.models;
|
|
384
|
-
}
|
|
385
|
-
// The credential belongs to the caller and is never echoed into `details`;
|
|
386
|
-
// this only refuses the case where discovery was asked for without one,
|
|
387
|
-
// which would otherwise surface as an opaque 403 from upstream.
|
|
388
|
-
if (typeof input.credential !== "string" || input.credential.trim().length === 0) {
|
|
389
|
-
throw new GatewayError(
|
|
390
|
-
"authentication_failed",
|
|
391
|
-
401,
|
|
392
|
-
"Qoder model discovery requires an account credential.",
|
|
393
|
-
{ providerId, realm: input.realm },
|
|
394
|
-
);
|
|
395
|
-
}
|
|
396
|
-
const fetcher = input.fetcher;
|
|
397
|
-
if (fetcher === undefined) {
|
|
398
|
-
throw new GatewayError(
|
|
399
|
-
"invalid_request",
|
|
400
|
-
400,
|
|
401
|
-
"Qoder model discovery requires a validated outbound fetch",
|
|
402
|
-
{ providerId, realm: input.realm },
|
|
403
|
-
);
|
|
404
|
-
}
|
|
405
|
-
const signedBody =
|
|
406
|
-
input.encode === undefined ? "{}" : input.encode(new TextEncoder().encode("{}"));
|
|
407
|
-
let lastError: unknown;
|
|
408
|
-
for (const host of qoderGatewayCandidates(input.realm)) {
|
|
409
|
-
const url = `${host}${QODER_MODELS_PATH}`;
|
|
410
|
-
try {
|
|
411
|
-
const headers = await input.headers(url, signedBody);
|
|
412
|
-
const response = await fetcher(url, {
|
|
413
|
-
method: "GET",
|
|
414
|
-
headers: headers as unknown as HeadersInit,
|
|
415
|
-
// The signed body must accompany the GET or upstream refuses it.
|
|
416
|
-
body: signedBody,
|
|
417
|
-
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
418
|
-
});
|
|
419
|
-
if (!response.ok) {
|
|
420
|
-
lastError = new Error(`Qoder model list answered ${response.status}`);
|
|
421
|
-
continue;
|
|
422
|
-
}
|
|
423
|
-
const entries = mapQoderCatalogResponse((await response.json()) as unknown);
|
|
424
|
-
// Emptiness is judged after `enable` filtering, not before: a roster
|
|
425
|
-
// whose every row is disabled has no usable model and must fall through
|
|
426
|
-
// to the next host rather than return an empty catalog.
|
|
427
|
-
const enabled = entries.filter((entry) => entry.enable);
|
|
428
|
-
if (enabled.length === 0) {
|
|
429
|
-
lastError = new Error("Qoder model list returned no usable models");
|
|
430
|
-
continue;
|
|
431
|
-
}
|
|
432
|
-
const models = enabled.map((entry) => qoderCatalogModel(entry, providerId));
|
|
433
|
-
// Cached only after a host actually answered: a failure must stay
|
|
434
|
-
// retryable on the next call rather than being frozen for five minutes.
|
|
435
|
-
discoveryCache.set(input.realm, { at: Date.now(), models });
|
|
436
|
-
return models;
|
|
437
|
-
} catch (error) {
|
|
438
|
-
lastError = error;
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
throw new GatewayError(
|
|
442
|
-
"transport_unavailable",
|
|
443
|
-
502,
|
|
444
|
-
`Qoder model discovery failed for ${input.realm}: ${
|
|
445
|
-
lastError instanceof Error ? lastError.message : "unknown error"
|
|
446
|
-
}`,
|
|
447
|
-
{ providerId, realm: input.realm },
|
|
448
|
-
"upstream",
|
|
449
|
-
);
|
|
450
|
-
}
|
|
1
|
+
// Qoder model catalogs, one per realm, plus the live listing that supersedes them.
|
|
2
|
+
//
|
|
3
|
+
// The two realms publish different rosters: the donor's snapshot files
|
|
4
|
+
// (`qoder_catalog_cn.json`, `qoder_catalog_intl.json`) disagree on 5 keys and on
|
|
5
|
+
// the context window of the keys they share. A single merged list therefore
|
|
6
|
+
// advertises models a given account cannot reach, and hides the ones only the
|
|
7
|
+
// other realm serves — so each realm keeps its own static floor.
|
|
8
|
+
//
|
|
9
|
+
// The static rows are the *offline floor*, not the truth: the authoritative list
|
|
10
|
+
// is `/algo/api/v2/model/list` for the account's own realm (`discoverQoderModels`).
|
|
11
|
+
import { GatewayError } from "../../../protocol/gateway-error";
|
|
12
|
+
import { defineModel } from "../../provider-model-definition";
|
|
13
|
+
import type { ModelDefinition } from "../../provider-model-definition";
|
|
14
|
+
import { QODER_CHAT_PATH, QODER_MODELS_PATH, qoderGatewayCandidates, type QoderRealm } from "./qoder-realm";
|
|
15
|
+
import { qoderVirtualIdFor, QODER_VIRTUAL_MODELS } from "./qoder-virtual";
|
|
16
|
+
|
|
17
|
+
/** One upstream catalog row, as `/algo/api/v2/model/list` reports it. */
|
|
18
|
+
export interface QoderCatalogEntry {
|
|
19
|
+
readonly key: string;
|
|
20
|
+
readonly displayName: string;
|
|
21
|
+
readonly isVl: boolean;
|
|
22
|
+
readonly isReasoning: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Largest window this model can be switched to.
|
|
25
|
+
*
|
|
26
|
+
* Upstream states two different numbers: `max_input_tokens` is the window
|
|
27
|
+
* *currently selected* (often 180k), while `context_config` lists the
|
|
28
|
+
* selectable windows — `{"200K":…, "400K":…, "1M":…}` — which for the same
|
|
29
|
+
* row go up to 1M. Taking `max_input_tokens` alone published and dispatched
|
|
30
|
+
* the smaller figure, so Qoder was told the conversation was 180k when the
|
|
31
|
+
* account could use 1M.
|
|
32
|
+
*/
|
|
33
|
+
readonly maxInputTokens: number | undefined;
|
|
34
|
+
readonly enable: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Whether upstream publishes this row as free.
|
|
37
|
+
*
|
|
38
|
+
* Qoder states it per row (`is_free`) and it is not inferable: `price_factor`
|
|
39
|
+
* is 0 on both free and some billed rows, and the lite-plan flag describes a
|
|
40
|
+
* plan tier rather than a model. So the free tier is read from this field
|
|
41
|
+
* instead of being hardcoded as a list, which is what lets a plan change
|
|
42
|
+
* move a model between tiers without a code change.
|
|
43
|
+
*/
|
|
44
|
+
readonly isFree: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The widest selectable window on one catalog row.
|
|
49
|
+
*
|
|
50
|
+
* `context_config` is the authority when present: it names every window the
|
|
51
|
+
* account can switch to, whereas `max_input_tokens` only names the one active
|
|
52
|
+
* now. Absent both, `undefined` — an unknown window is not zero.
|
|
53
|
+
*/
|
|
54
|
+
export function qoderMaxContextWindow(raw: unknown): number | undefined {
|
|
55
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
|
56
|
+
const record = raw as Record<string, unknown>;
|
|
57
|
+
const candidates: number[] = [];
|
|
58
|
+
const config = record["context_config"];
|
|
59
|
+
if (config !== null && typeof config === "object" && !Array.isArray(config)) {
|
|
60
|
+
for (const option of Object.values(config as Record<string, unknown>)) {
|
|
61
|
+
if (option === null || typeof option !== "object" || Array.isArray(option)) continue;
|
|
62
|
+
const tokenCount = (option as Record<string, unknown>)["token_count"];
|
|
63
|
+
if (typeof tokenCount === "number" && Number.isFinite(tokenCount) && tokenCount > 0) {
|
|
64
|
+
candidates.push(Math.floor(tokenCount));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const stated = record["max_input_tokens"];
|
|
69
|
+
if (typeof stated === "number" && Number.isFinite(stated) && stated > 0) {
|
|
70
|
+
candidates.push(Math.floor(stated));
|
|
71
|
+
}
|
|
72
|
+
return candidates.length === 0 ? undefined : Math.max(...candidates);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** One static row's shape, mirrored on `QODER_MODEL_CONFIGS` in `qoder.ts`. */
|
|
76
|
+
interface QoderModelConfig {
|
|
77
|
+
readonly id: string;
|
|
78
|
+
readonly display_name: string;
|
|
79
|
+
readonly max_input_tokens: number;
|
|
80
|
+
readonly max_output_tokens?: number;
|
|
81
|
+
readonly is_vl: boolean;
|
|
82
|
+
readonly is_reasoning: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Whether upstream publishes this row as free. Absent means upstream did not
|
|
85
|
+
* state it for the row, which is not the same as false — only an explicit
|
|
86
|
+
* `false` marks the model as billed.
|
|
87
|
+
*/
|
|
88
|
+
readonly is_free?: boolean;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* China roster (14 keys).
|
|
93
|
+
*
|
|
94
|
+
* Captured from the donor's `qoder_catalog_cn.json`, which is the decoded
|
|
95
|
+
* `chat` array of a real CN account's model list.
|
|
96
|
+
*/
|
|
97
|
+
export const QODER_CN_MODEL_CONFIGS: Readonly<Record<string, QoderModelConfig>> = {
|
|
98
|
+
auto: { id: "auto", display_name: "Auto", max_output_tokens: 64000, max_input_tokens: 200000, is_vl: true, is_reasoning: true },
|
|
99
|
+
qmodel_38max: { id: "qmodel_38max", display_name: "Qwen3.8-Max", max_output_tokens: 131072, max_input_tokens: 1000000, is_vl: true, is_reasoning: true },
|
|
100
|
+
qfmodel: { id: "qfmodel", display_name: "Qwen3.8-Flash", max_output_tokens: 131072, max_input_tokens: 1000000, is_vl: true, is_reasoning: true },
|
|
101
|
+
qmodel_latest: { id: "qmodel_latest", display_name: "Qwen3.7-Max", max_output_tokens: 131072, max_input_tokens: 1000000, is_vl: true, is_reasoning: true },
|
|
102
|
+
qmodel: { id: "qmodel", display_name: "Qwen3.7-Plus", max_output_tokens: 131072, max_input_tokens: 1000000, is_vl: true, is_reasoning: true },
|
|
103
|
+
q37fmodel: { id: "q37fmodel", display_name: "Qwen3.7-Flash", max_output_tokens: 131072, max_input_tokens: 1000000, is_vl: true, is_reasoning: true },
|
|
104
|
+
dmodel: { id: "dmodel", display_name: "DeepSeek-V4-Pro", max_output_tokens: 384000, max_input_tokens: 1000000, is_vl: true, is_reasoning: true },
|
|
105
|
+
dfmodel: { id: "dfmodel", display_name: "DeepSeek-Flash", max_output_tokens: 384000, max_input_tokens: 1000000, is_vl: true, is_reasoning: false },
|
|
106
|
+
gmodel: { id: "gmodel", display_name: "GLM-5.3", max_output_tokens: 131072, max_input_tokens: 1000000, is_vl: true, is_reasoning: true },
|
|
107
|
+
gfmodel: { id: "gfmodel", display_name: "GLM-5.3-Flash", max_output_tokens: 131072, max_input_tokens: 1000000, is_vl: true, is_reasoning: true },
|
|
108
|
+
gm51model: { id: "gm51model", display_name: "GLM-5.2", max_output_tokens: 131072, max_input_tokens: 1000000, is_vl: true, is_reasoning: true },
|
|
109
|
+
kmodel_latest: { id: "kmodel_latest", display_name: "Kimi-K3", max_output_tokens: 131072, max_input_tokens: 1000000, is_vl: true, is_reasoning: false },
|
|
110
|
+
kmodel: { id: "kmodel", display_name: "Kimi-K2.8-Preview", max_output_tokens: 65536, max_input_tokens: 1000000, is_vl: true, is_reasoning: true },
|
|
111
|
+
mmodel: { id: "mmodel", display_name: "MiniMax-M2.7", max_output_tokens: 131072, max_input_tokens: 200000, is_vl: false, is_reasoning: false },
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Global/intl roster (17 keys), from the donor's `qoder_catalog_intl.json`.
|
|
116
|
+
*
|
|
117
|
+
* Note how the shared keys differ from CN — `auto` is non-reasoning here and
|
|
118
|
+
* `qmodel` carries a 1M window. That is why the rosters are not merged.
|
|
119
|
+
*/
|
|
120
|
+
export const QODER_INTL_MODEL_CONFIGS: Readonly<Record<string, QoderModelConfig>> = {
|
|
121
|
+
auto: { id: "auto", display_name: "Auto", max_input_tokens: 200000, max_output_tokens: 64_000, is_vl: true, is_reasoning: false },
|
|
122
|
+
ultimate: { id: "ultimate", display_name: "Ultimate", max_input_tokens: 1000000, max_output_tokens: 128_000, is_vl: true, is_free: false, is_reasoning: true },
|
|
123
|
+
// `fast` is what the live roster reports today; `performance` is the older
|
|
124
|
+
// key for the same slot. Both are kept so a hand-added model id written
|
|
125
|
+
// against either name still resolves — the roster is the offline floor, and
|
|
126
|
+
// discovery supersedes it on the first successful fetch.
|
|
127
|
+
fast: { id: "fast", display_name: "Fast", max_input_tokens: 1000000, max_output_tokens: 64_000, is_vl: true, is_reasoning: false },
|
|
128
|
+
performance: { id: "performance", display_name: "Performance", max_input_tokens: 1000000, max_output_tokens: 64_000, is_vl: true, is_reasoning: false },
|
|
129
|
+
efficient: { id: "efficient", display_name: "Efficient", max_input_tokens: 200000, max_output_tokens: 64_000, is_vl: true, is_reasoning: false },
|
|
130
|
+
smodel: { id: "smodel", display_name: "Sonus", max_input_tokens: 1000000, max_output_tokens: 128_000, is_vl: true, is_free: false, is_reasoning: true },
|
|
131
|
+
cmodel: { id: "cmodel", display_name: "Cantus", max_input_tokens: 1000000, max_output_tokens: 128_000, is_vl: true, is_free: false, is_reasoning: true },
|
|
132
|
+
qmodel_38max: { id: "qmodel_38max", display_name: "Qwen3.8-Max", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_reasoning: true },
|
|
133
|
+
qfmodel: { id: "qfmodel", display_name: "Qwen3.8-Flash", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_free: true, is_reasoning: true },
|
|
134
|
+
qmodel_latest: { id: "qmodel_latest", display_name: "Qwen3.7-Max", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_reasoning: false },
|
|
135
|
+
qmodel: { id: "qmodel", display_name: "Qwen3.7-Plus", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_reasoning: false },
|
|
136
|
+
kmodel_latest: { id: "kmodel_latest", display_name: "Kimi-K3", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_free: false, is_reasoning: false },
|
|
137
|
+
kmodel: { id: "kmodel", display_name: "Kimi-K2.8-Preview", max_input_tokens: 1000000, max_output_tokens: 65_536, is_vl: true, is_free: false, is_reasoning: false },
|
|
138
|
+
gmodel: { id: "gmodel", display_name: "GLM-5.3", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_free: false, is_reasoning: true },
|
|
139
|
+
gfmodel: { id: "gfmodel", display_name: "GLM-5.3-Flash", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_reasoning: true },
|
|
140
|
+
dmodel: { id: "dmodel", display_name: "DeepSeek-V4-Pro", max_input_tokens: 1000000, max_output_tokens: 384_000, is_vl: true, is_free: false, is_reasoning: true },
|
|
141
|
+
dfmodel: { id: "dfmodel", display_name: "DeepSeek-Flash", max_input_tokens: 1000000, max_output_tokens: 384_000, is_vl: true, is_free: true, is_reasoning: true },
|
|
142
|
+
mmodel: { id: "mmodel", display_name: "MiniMax-M3", max_input_tokens: 1000000, max_output_tokens: 512_000, is_vl: true, is_free: false, is_reasoning: false },
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Output ceilings are stated per row, not derived.
|
|
147
|
+
*
|
|
148
|
+
* Qoder's `/model/list` reports no output limit at all — none of the 21 fields
|
|
149
|
+
* it returns for any row mentions output — so these come from the static
|
|
150
|
+
* roster, set to what the same model is filed at elsewhere in the catalog
|
|
151
|
+
* (DeepSeek-V4 at 384k, GLM/Qwen/Kimi/MiniMax at 131k, the GPT-class slots at
|
|
152
|
+
* 128k). Hardcoding one ceiling here would apply it to every row alike and make
|
|
153
|
+
* the whole catalog look capped at that number.
|
|
154
|
+
*
|
|
155
|
+
* A row with no stated ceiling leaves `out` unset so `defineModel` can still
|
|
156
|
+
* consult the base catalog rather than publishing a guess.
|
|
157
|
+
*/
|
|
158
|
+
|
|
159
|
+
function qoderModels(
|
|
160
|
+
configs: Readonly<Record<string, QoderModelConfig>>,
|
|
161
|
+
providerId: string,
|
|
162
|
+
): readonly ModelDefinition[] {
|
|
163
|
+
return Object.entries(configs).map(([id, cfg]) =>
|
|
164
|
+
defineModel({
|
|
165
|
+
id,
|
|
166
|
+
providerId,
|
|
167
|
+
wireFamily: "chat",
|
|
168
|
+
endpoint: QODER_CHAT_PATH,
|
|
169
|
+
ctx: cfg.max_input_tokens,
|
|
170
|
+
...(cfg.max_output_tokens === undefined ? {} : { out: cfg.max_output_tokens }),
|
|
171
|
+
vision: cfg.is_vl,
|
|
172
|
+
reasoning: cfg.is_reasoning,
|
|
173
|
+
// Only an explicit `true` marks the row free; an unstated flag is left
|
|
174
|
+
// unset so nothing claims a tier the roster did not state.
|
|
175
|
+
...(cfg.is_free === undefined ? {} : { free: cfg.is_free, freeTier: cfg.is_free }),
|
|
176
|
+
}),
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Static offline catalog for the China realm. */
|
|
181
|
+
export const QODER_CN_MODELS: readonly ModelDefinition[] = qoderModels(QODER_CN_MODEL_CONFIGS, "qodercn");
|
|
182
|
+
|
|
183
|
+
/** Static offline catalog for the global/intl realm. */
|
|
184
|
+
const QODER_INTL_ROSTER: readonly ModelDefinition[] = qoderModels(QODER_INTL_MODEL_CONFIGS, "qoder");
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Virtual rows, one per roster key that has a caller-facing name.
|
|
188
|
+
*
|
|
189
|
+
* A virtual row inherits its target's limits and capabilities rather than
|
|
190
|
+
* restating them: the two ids are the same upstream model, so a window or a
|
|
191
|
+
* vision flag that disagreed between them would be a lie in one of the two
|
|
192
|
+
* places. A target this realm does not serve contributes no row, which is what
|
|
193
|
+
* keeps the CN and intl catalogs honest about their own rosters.
|
|
194
|
+
*/
|
|
195
|
+
export function qoderVirtualModels(
|
|
196
|
+
roster: readonly ModelDefinition[],
|
|
197
|
+
providerId: string,
|
|
198
|
+
): readonly ModelDefinition[] {
|
|
199
|
+
const byId = new Map(roster.map((row) => [row.modelId, row]));
|
|
200
|
+
const rows: ModelDefinition[] = [];
|
|
201
|
+
for (const virtual of QODER_VIRTUAL_MODELS) {
|
|
202
|
+
const target = byId.get(virtual.target);
|
|
203
|
+
if (target === undefined) continue;
|
|
204
|
+
rows.push(
|
|
205
|
+
defineModel({
|
|
206
|
+
id: virtual.id,
|
|
207
|
+
providerId,
|
|
208
|
+
wireFamily: "chat",
|
|
209
|
+
endpoint: QODER_CHAT_PATH,
|
|
210
|
+
// `null`, not a number: an unknown window is unknown, and `defineModel`
|
|
211
|
+
// substitutes the shared unfiled fallback for null — the same marker a
|
|
212
|
+
// hand-added row gets.
|
|
213
|
+
ctx: target.contextLimit ?? null,
|
|
214
|
+
...(target.outputLimit === null ? {} : { out: target.outputLimit }),
|
|
215
|
+
vision: target.modalities.input.includes("image"),
|
|
216
|
+
reasoning: target.reasoning ?? false,
|
|
217
|
+
toolCall: target.toolCall ?? false,
|
|
218
|
+
// The same model under its caller-facing name belongs to the same tier,
|
|
219
|
+
// so the pill follows the roster row rather than being restated.
|
|
220
|
+
...(target.freeTier === undefined ? {} : { free: target.freeTier, freeTier: target.freeTier }),
|
|
221
|
+
}),
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
return rows;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Static offline catalog for the global/intl realm.
|
|
229
|
+
*
|
|
230
|
+
* A roster key that has a caller-facing name is published under that name
|
|
231
|
+
* only: upstream's keys are opaque slots (`dfmodel`, `cmodel`), and offering
|
|
232
|
+
* both the slot and its name shows one model twice under two ids nobody can
|
|
233
|
+
* tell apart. A key with no known name is kept as-is — an unnamed slot is
|
|
234
|
+
* still reachable, and inventing a name for it would assert what upstream
|
|
235
|
+
* never stated.
|
|
236
|
+
*
|
|
237
|
+
* The key remains what dispatch sends; see `qoder-virtual.ts`.
|
|
238
|
+
*/
|
|
239
|
+
export const QODER_INTL_MODELS: readonly ModelDefinition[] = [
|
|
240
|
+
...QODER_INTL_ROSTER.filter((row) => qoderVirtualIdFor(row.modelId) === undefined),
|
|
241
|
+
...qoderVirtualModels(QODER_INTL_ROSTER, "qoder"),
|
|
242
|
+
];
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Reads upstream's `{"chat": [...]}` envelope.
|
|
246
|
+
*
|
|
247
|
+
* Rows without a usable `key` are dropped rather than coerced: a model id the
|
|
248
|
+
* upstream did not state would become an unroutable catalog row.
|
|
249
|
+
*/
|
|
250
|
+
export function mapQoderCatalogResponse(raw: unknown): QoderCatalogEntry[] {
|
|
251
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return [];
|
|
252
|
+
const chat = (raw as Record<string, unknown>)["chat"];
|
|
253
|
+
if (!Array.isArray(chat)) return [];
|
|
254
|
+
const entries: QoderCatalogEntry[] = [];
|
|
255
|
+
for (const row of chat) {
|
|
256
|
+
if (row === null || typeof row !== "object" || Array.isArray(row)) continue;
|
|
257
|
+
const record = row as Record<string, unknown>;
|
|
258
|
+
const key = record["key"];
|
|
259
|
+
if (typeof key !== "string" || key.trim().length === 0) continue;
|
|
260
|
+
const displayName = record["display_name"];
|
|
261
|
+
entries.push({
|
|
262
|
+
key: key.trim(),
|
|
263
|
+
displayName: typeof displayName === "string" && displayName.length > 0 ? displayName : key.trim(),
|
|
264
|
+
isVl: record["is_vl"] === true,
|
|
265
|
+
isReasoning: record["is_reasoning"] === true,
|
|
266
|
+
maxInputTokens: qoderMaxContextWindow(record),
|
|
267
|
+
isFree: record["is_free"] === true,
|
|
268
|
+
// Absent means "not stated", which is not the same as disabled.
|
|
269
|
+
enable: record["enable"] !== false,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
return entries;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Projects one live catalog row onto a Cartethyia catalog row. */
|
|
276
|
+
export function qoderCatalogModel(
|
|
277
|
+
entry: QoderCatalogEntry,
|
|
278
|
+
providerId: string,
|
|
279
|
+
): ModelDefinition {
|
|
280
|
+
return defineModel({
|
|
281
|
+
id: entry.key,
|
|
282
|
+
providerId,
|
|
283
|
+
wireFamily: "chat",
|
|
284
|
+
endpoint: QODER_CHAT_PATH,
|
|
285
|
+
// `null`, not omitted: an upstream row that stated no window is unknown,
|
|
286
|
+
// not unlimited, and `defineModel` substitutes its own "unfiled" estimate
|
|
287
|
+
// for null — the same marker a hand-added row gets.
|
|
288
|
+
ctx: entry.maxInputTokens ?? null,
|
|
289
|
+
vision: entry.isVl,
|
|
290
|
+
reasoning: entry.isReasoning,
|
|
291
|
+
// Free tier straight from upstream's own flag, so the pill on a model card
|
|
292
|
+
// tracks the plan rather than a hardcoded id list. Both statements hold:
|
|
293
|
+
// a free row bills zero per token and belongs to the provider's free tier.
|
|
294
|
+
free: entry.isFree,
|
|
295
|
+
freeTier: entry.isFree,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Header factory for one signed request.
|
|
301
|
+
*
|
|
302
|
+
* Injected rather than imported: the COSY signer lives in `qoder.ts`, which
|
|
303
|
+
* imports this module's rosters — importing it back would be a cycle. The
|
|
304
|
+
* callback receives the URL and the exact body that will be sent, because the
|
|
305
|
+
* signature covers both.
|
|
306
|
+
*/
|
|
307
|
+
export type QoderSigner = (url: string, body: string) => Promise<Record<string, string>>;
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* The account's own live model list.
|
|
311
|
+
*
|
|
312
|
+
* Two details are load-bearing and non-obvious:
|
|
313
|
+
*
|
|
314
|
+
* - the request is a **GET that carries the signed body**. A bare GET answers
|
|
315
|
+
* 403, because the server validates signature against body; the donor
|
|
316
|
+
* (`read_dynamic_models`) signs `{}` and sends it as the GET's data.
|
|
317
|
+
* - the signature covers only the path, so switching gateway hosts between
|
|
318
|
+
* attempts does not invalidate it.
|
|
319
|
+
*
|
|
320
|
+
* Hosts are tried primary → fallbacks (intl has three, CN one) and the first
|
|
321
|
+
* host that answers with a usable list wins.
|
|
322
|
+
*
|
|
323
|
+
* Signing is injected, never reimplemented: `headers(url, body)` is the COSY
|
|
324
|
+
* signer from `qoder.ts`, passed in because that module imports this one and
|
|
325
|
+
* importing it back would be a cycle. `encode` is the same arrangement for
|
|
326
|
+
* Qoder's scrambled body alphabet — omitted, the literal `{}` is both signed
|
|
327
|
+
* and sent, which is what a caller whose signer already binds the credential
|
|
328
|
+
* wants.
|
|
329
|
+
*
|
|
330
|
+
* `fetcher` is optional in the type but required at runtime, and there is
|
|
331
|
+
* deliberately no global-`fetch` default: a test cannot reach the network by
|
|
332
|
+
* accident, and production is forced through the SSRF-validated egress
|
|
333
|
+
* (`network/outbound-fetch.ts`). That egress is a `node:http` path, which is
|
|
334
|
+
* also the only way a body reaches the wire on a GET — `fetch` itself rejects
|
|
335
|
+
* `GET` + `body` with "request with GET/HEAD method cannot have body".
|
|
336
|
+
*/
|
|
337
|
+
/**
|
|
338
|
+
* How long one realm's live list is reused, in milliseconds.
|
|
339
|
+
*
|
|
340
|
+
* The roster is account-wide and changes rarely — upstream adds or retires a
|
|
341
|
+
* key, it does not reshuffle per request — so every catalog refresh inside the
|
|
342
|
+
* window would re-sign and re-fetch an identical list. The donor caches the
|
|
343
|
+
* same way (`fetch_models`, 300 s per realm).
|
|
344
|
+
*/
|
|
345
|
+
const DISCOVERY_CACHE_TTL_MS = 300_000;
|
|
346
|
+
|
|
347
|
+
/** One realm's cached roster, with the instant it was read. */
|
|
348
|
+
interface CachedRoster {
|
|
349
|
+
readonly at: number;
|
|
350
|
+
readonly models: readonly ModelDefinition[];
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Per-realm live-list cache; keyed by realm, not by account. */
|
|
354
|
+
const discoveryCache = new Map<QoderRealm, CachedRoster>();
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Drops one realm's cached list.
|
|
358
|
+
*
|
|
359
|
+
* Exported for tests: the cache is module state, so a test that builds a
|
|
360
|
+
* roster and then asserts a fresh fetch would otherwise read the entry the
|
|
361
|
+
* previous assertion left behind.
|
|
362
|
+
*/
|
|
363
|
+
export function resetQoderDiscoveryCache(realm?: QoderRealm): void {
|
|
364
|
+
if (realm === undefined) discoveryCache.clear();
|
|
365
|
+
else discoveryCache.delete(realm);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export async function discoverQoderModels(input: {
|
|
369
|
+
readonly realm: QoderRealm;
|
|
370
|
+
/** Account secret the injected signer signs with; blank fails fast. */
|
|
371
|
+
readonly credential: string;
|
|
372
|
+
readonly headers: QoderSigner;
|
|
373
|
+
readonly fetcher?: typeof fetch;
|
|
374
|
+
readonly signal?: AbortSignal;
|
|
375
|
+
/** Defaults to the realm's bundled provider id (`qodercn` | `qoder`). */
|
|
376
|
+
readonly providerId?: string;
|
|
377
|
+
/** Body encoder; Qoder uses its own scrambled base64 alphabet. */
|
|
378
|
+
readonly encode?: (body: Uint8Array) => string;
|
|
379
|
+
}): Promise<readonly ModelDefinition[]> {
|
|
380
|
+
const providerId = input.providerId ?? (input.realm === "cn" ? "qodercn" : "qoder");
|
|
381
|
+
const cached = discoveryCache.get(input.realm);
|
|
382
|
+
if (cached !== undefined && Date.now() - cached.at < DISCOVERY_CACHE_TTL_MS) {
|
|
383
|
+
return cached.models;
|
|
384
|
+
}
|
|
385
|
+
// The credential belongs to the caller and is never echoed into `details`;
|
|
386
|
+
// this only refuses the case where discovery was asked for without one,
|
|
387
|
+
// which would otherwise surface as an opaque 403 from upstream.
|
|
388
|
+
if (typeof input.credential !== "string" || input.credential.trim().length === 0) {
|
|
389
|
+
throw new GatewayError(
|
|
390
|
+
"authentication_failed",
|
|
391
|
+
401,
|
|
392
|
+
"Qoder model discovery requires an account credential.",
|
|
393
|
+
{ providerId, realm: input.realm },
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
const fetcher = input.fetcher;
|
|
397
|
+
if (fetcher === undefined) {
|
|
398
|
+
throw new GatewayError(
|
|
399
|
+
"invalid_request",
|
|
400
|
+
400,
|
|
401
|
+
"Qoder model discovery requires a validated outbound fetch",
|
|
402
|
+
{ providerId, realm: input.realm },
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
const signedBody =
|
|
406
|
+
input.encode === undefined ? "{}" : input.encode(new TextEncoder().encode("{}"));
|
|
407
|
+
let lastError: unknown;
|
|
408
|
+
for (const host of qoderGatewayCandidates(input.realm)) {
|
|
409
|
+
const url = `${host}${QODER_MODELS_PATH}`;
|
|
410
|
+
try {
|
|
411
|
+
const headers = await input.headers(url, signedBody);
|
|
412
|
+
const response = await fetcher(url, {
|
|
413
|
+
method: "GET",
|
|
414
|
+
headers: headers as unknown as HeadersInit,
|
|
415
|
+
// The signed body must accompany the GET or upstream refuses it.
|
|
416
|
+
body: signedBody,
|
|
417
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
418
|
+
});
|
|
419
|
+
if (!response.ok) {
|
|
420
|
+
lastError = new Error(`Qoder model list answered ${response.status}`);
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
const entries = mapQoderCatalogResponse((await response.json()) as unknown);
|
|
424
|
+
// Emptiness is judged after `enable` filtering, not before: a roster
|
|
425
|
+
// whose every row is disabled has no usable model and must fall through
|
|
426
|
+
// to the next host rather than return an empty catalog.
|
|
427
|
+
const enabled = entries.filter((entry) => entry.enable);
|
|
428
|
+
if (enabled.length === 0) {
|
|
429
|
+
lastError = new Error("Qoder model list returned no usable models");
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
const models = enabled.map((entry) => qoderCatalogModel(entry, providerId));
|
|
433
|
+
// Cached only after a host actually answered: a failure must stay
|
|
434
|
+
// retryable on the next call rather than being frozen for five minutes.
|
|
435
|
+
discoveryCache.set(input.realm, { at: Date.now(), models });
|
|
436
|
+
return models;
|
|
437
|
+
} catch (error) {
|
|
438
|
+
lastError = error;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
throw new GatewayError(
|
|
442
|
+
"transport_unavailable",
|
|
443
|
+
502,
|
|
444
|
+
`Qoder model discovery failed for ${input.realm}: ${
|
|
445
|
+
lastError instanceof Error ? lastError.message : "unknown error"
|
|
446
|
+
}`,
|
|
447
|
+
{ providerId, realm: input.realm },
|
|
448
|
+
"upstream",
|
|
449
|
+
);
|
|
450
|
+
}
|