cartethyia 2.7.7 → 2.7.9
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 +0 -1
- package/README.md +29 -59
- package/dist/dashboard/assets/BannedPage-BxoG1MrL.js +1 -0
- package/dist/dashboard/assets/BannedPage-BxoG1MrL.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-CLUhmjUy.js → CliToolDetailPage-ixwgKk53.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-ixwgKk53.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-CQmrqqrk.js → CliToolsPage-BmttNV3U.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-BmttNV3U.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-BE3qwwfC.js +1 -0
- package/dist/dashboard/assets/CombosPage-BE3qwwfC.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-CRZdmMIX.js +1 -0
- package/dist/dashboard/assets/ConsoleLogPage-CRZdmMIX.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-Dv_FJ6Er.js +1 -0
- package/dist/dashboard/assets/CustomizationPage-Dv_FJ6Er.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js.gz +0 -0
- package/dist/dashboard/assets/{InjectSystemPromptPanel-C-e2AMz6.js → InjectSystemPromptPanel-BOoO8rpz.js} +2 -2
- package/dist/dashboard/assets/InjectSystemPromptPanel-BOoO8rpz.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-J8wPbmfT.js → LoginPage-ChJuNNVN.js} +1 -1
- package/dist/dashboard/assets/LoginPage-ChJuNNVN.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-C6WlpU5E.js → ModelPicker-BZAJtPg1.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-BZAJtPg1.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-BE40s3u5.js +1 -0
- package/dist/dashboard/assets/OverviewPage-BE40s3u5.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js +8 -0
- package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-CQ6bPUf_.js +1 -0
- package/dist/dashboard/assets/ProviderIcon-CQ6bPUf_.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js.gz +0 -0
- package/dist/dashboard/assets/{ProxyPage-BBTRBAQ3.js → ProxyPage-BJ87Pp0U.js} +1 -1
- package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js.gz +0 -0
- package/dist/dashboard/assets/{QuotaPage-B2WqMSG1.js → QuotaPage-gkSyRAKa.js} +1 -1
- package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-C5HPWqEv.js +1 -0
- package/dist/dashboard/assets/SettingsPage-C5HPWqEv.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-LND6_Ss7.js → SetupPage-CFNcATJJ.js} +1 -1
- package/dist/dashboard/assets/SetupPage-CFNcATJJ.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-BzNnm6Bi.js → StudioPage-YoD4tY5l.js} +1 -1
- package/dist/dashboard/assets/StudioPage-YoD4tY5l.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-DxjaAhb_.js +56 -0
- package/dist/dashboard/assets/UsagePage-DxjaAhb_.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-BTlkq_Uk.js +1 -0
- package/dist/dashboard/assets/api-keys-BTlkq_Uk.js.gz +0 -0
- package/dist/dashboard/assets/app-BX2nnKqs.css +2 -0
- package/dist/dashboard/assets/app-BX2nnKqs.css.gz +0 -0
- package/dist/dashboard/assets/assert-CndthXFQ.js +1 -0
- package/dist/dashboard/assets/assert-CndthXFQ.js.gz +0 -0
- package/dist/dashboard/assets/{badge-BaX2GTrn.js → badge-DdbT_7Am.js} +1 -1
- package/dist/dashboard/assets/badge-DdbT_7Am.js.gz +0 -0
- package/dist/dashboard/assets/ban-durations-B58FyRxC.js +1 -0
- package/dist/dashboard/assets/ban-durations-B58FyRxC.js.gz +0 -0
- package/dist/dashboard/assets/{banner-DFH4Bkte.js → banner-CsGN-nbm.js} +1 -1
- package/dist/dashboard/assets/banner-CsGN-nbm.js.gz +0 -0
- package/dist/dashboard/assets/brand/bansos_page.webp +0 -0
- package/dist/dashboard/assets/brand/denia_bansos.webp +0 -0
- package/dist/dashboard/assets/brand/favicon.webp +0 -0
- package/dist/dashboard/assets/brand/login_register.webp +0 -0
- package/dist/dashboard/assets/brand/login_register_initial.webp +0 -0
- package/dist/dashboard/assets/brand/og_bansos.webp +0 -0
- package/dist/dashboard/assets/brand/og_image.webp +0 -0
- package/dist/dashboard/assets/brand/sidebar_ico.webp +0 -0
- package/dist/dashboard/assets/{card-CxDw0lK6.js → card-mgZ_TLLZ.js} +1 -1
- package/dist/dashboard/assets/card-mgZ_TLLZ.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-A1-v8-03.js +1 -0
- package/dist/dashboard/assets/cli-tools-A1-v8-03.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-DfS1ntiq.js → clipboard-button-CJ4Zty89.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-CJ4Zty89.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-BK-NRr5u.js → dialog-B5L2jPQ_.js} +1 -1
- package/dist/dashboard/assets/dialog-B5L2jPQ_.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-QqPul6sC.js → drawer-DkjUouAj.js} +1 -1
- package/dist/dashboard/assets/drawer-DkjUouAj.js.gz +0 -0
- package/dist/dashboard/assets/{entry-BMlYJVFj.js → entry-B-LVLV6o.js} +1 -1
- package/dist/dashboard/assets/entry-B-LVLV6o.js.gz +0 -0
- package/dist/dashboard/assets/entry-CAhdYl3B.js +1 -0
- package/dist/dashboard/assets/entry-CAhdYl3B.js.gz +0 -0
- package/dist/dashboard/assets/entry-D2xD0Tmd.js +4 -0
- package/dist/dashboard/assets/entry-D2xD0Tmd.js.gz +0 -0
- package/dist/dashboard/assets/{formatters-DP-5E01-.js → formatters-CrsdLVsn.js} +1 -1
- package/dist/dashboard/assets/formatters-CrsdLVsn.js.gz +0 -0
- package/dist/dashboard/assets/{index-D36vCZht.js → index-BU64Lqvp.js} +2 -2
- package/dist/dashboard/assets/index-BU64Lqvp.js.gz +0 -0
- package/dist/dashboard/assets/{inline-CHJQwxLa.js → inline-B2aDLyyg.js} +1 -1
- package/dist/dashboard/assets/inline-B2aDLyyg.js.gz +0 -0
- package/dist/dashboard/assets/{input-BlmlizVV.js → input-BCVkdR9d.js} +1 -1
- package/dist/dashboard/assets/input-BCVkdR9d.js.gz +0 -0
- package/dist/dashboard/assets/{layout-Bz93I-yE.js → layout-DNcAdMK-.js} +1 -1
- package/dist/dashboard/assets/layout-DNcAdMK-.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-Ml16KCfu.js → list-row-DgrIH0hw.js} +1 -1
- package/dist/dashboard/assets/list-row-DgrIH0hw.js.gz +0 -0
- package/dist/dashboard/assets/{live-7_QpnYGu.js → live-Cae01iu8.js} +1 -1
- package/dist/dashboard/assets/live-Cae01iu8.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-GU7XdEjz.js → mutation-BQ0ccH_A.js} +1 -1
- package/dist/dashboard/assets/mutation-BQ0ccH_A.js.gz +0 -0
- package/dist/dashboard/assets/{network-CuMDaTzT.js → network-CRib7H_2.js} +1 -1
- package/dist/dashboard/assets/network-CRib7H_2.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-CoqpaDMx.js → numbers-BKQE8Xpz.js} +1 -1
- package/dist/dashboard/assets/numbers-BKQE8Xpz.js.gz +0 -0
- package/dist/dashboard/assets/{pagination-B5aOoweZ.js → pagination-ecPvG_8g.js} +1 -1
- package/dist/dashboard/assets/pagination-ecPvG_8g.js.gz +0 -0
- package/dist/dashboard/assets/providers/devin.svg +9 -0
- package/dist/dashboard/assets/providers/devin.svg.gz +0 -0
- package/dist/dashboard/assets/providers/windsurf.svg +10 -0
- package/dist/dashboard/assets/providers/windsurf.svg.gz +0 -0
- package/dist/dashboard/assets/{query-keys-YSoQOUh6.js → query-keys-IKLh0YFd.js} +1 -1
- package/dist/dashboard/assets/query-keys-IKLh0YFd.js.gz +0 -0
- package/dist/dashboard/assets/routing-Ckm1UXuC.js +1 -0
- package/dist/dashboard/assets/routing-Ckm1UXuC.js.gz +0 -0
- package/dist/dashboard/assets/{select-C9Ssle9b.js → select-ChoFWyKz.js} +1 -1
- package/dist/dashboard/assets/select-ChoFWyKz.js.gz +0 -0
- package/dist/dashboard/assets/settings-CKjFPlKW.js +1 -0
- package/dist/dashboard/assets/settings-CKjFPlKW.js.gz +0 -0
- package/dist/dashboard/assets/{stack-j7cXjgYX.js → stack-DWCwU3fh.js} +1 -1
- package/dist/dashboard/assets/stack-DWCwU3fh.js.gz +0 -0
- package/dist/dashboard/assets/{state-DHmEnLa2.js → state-Cg3VL5J_.js} +1 -1
- package/dist/dashboard/assets/state-Cg3VL5J_.js.gz +0 -0
- package/dist/dashboard/assets/story/25817331.webp +0 -0
- package/dist/dashboard/assets/story/Shorekeeper.webp +0 -0
- package/dist/dashboard/assets/story/cartethyia-god.webp +0 -0
- package/dist/dashboard/assets/story/fleurdelys_plus.webp +0 -0
- package/dist/dashboard/assets/story/jinhsi-blossom.webp +0 -0
- package/dist/dashboard/assets/story/phrolova-a.webp +0 -0
- package/dist/dashboard/assets/story/requestdeniawokkjpg.webp +0 -0
- package/dist/dashboard/assets/story/wuthering-waves-hiyuki-aemeath-hiyuki-rover.webp +0 -0
- package/dist/dashboard/assets/{switch-DmZcyTIC.js → switch-Db70BMix.js} +1 -1
- package/dist/dashboard/assets/switch-Db70BMix.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-CbCP4w1o.js → useMutation-BqdE0LuM.js} +1 -1
- package/dist/dashboard/assets/useMutation-BqdE0LuM.js.gz +0 -0
- package/dist/dashboard/console-backgrounds.webp +0 -0
- package/dist/dashboard/console-backgrounds_initial.webp +0 -0
- package/dist/dashboard/donation/qris-risun-marke.webp +0 -0
- package/dist/dashboard/index.html +3 -3
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +57 -55
- package/migrations/full-postgres/0044_add_models_warning_metadata.sql +10 -0
- package/migrations/full-postgres/0045_migrate_workbuddy_to_codebuddy.sql +156 -0
- package/migrations/lite-sqlite3/0044_add_models_warning_metadata.sql +11 -0
- package/migrations/lite-sqlite3/0045_migrate_workbuddy_to_codebuddy.sql +156 -0
- package/package.json +1 -1
- package/src/boot/dependencies.ts +11 -3
- package/src/config/cooldowns.ts +1 -1
- package/src/config/spec.ts +0 -6
- package/src/console/api-keys/routes.ts +11 -4
- package/src/console/console-router.ts +7 -0
- package/src/console/dashboard-assets.ts +5 -5
- package/src/console/domain-registration.ts +27 -0
- package/src/console/ip-bans/routes.ts +99 -0
- package/src/console/providers/catalog/catalog-projections.ts +5 -2
- package/src/console/providers/catalog/contracts.ts +123 -0
- package/src/console/providers/catalog/model-operations.ts +122 -10
- package/src/console/providers/catalog/provider-operations.ts +116 -8
- package/src/console/providers/catalog/public-model-store.ts +0 -0
- package/src/console/providers/catalog/routes.ts +64 -4
- package/src/console/providers/catalog/store-helpers.ts +30 -1
- package/src/console/providers/catalog/store.ts +243 -3
- package/src/console/quota/account-quota-global.ts +89 -1
- package/src/console/quota/native-identity.ts +2 -0
- package/src/console/settings/contracts.ts +18 -0
- package/src/console/settings/store.ts +32 -0
- package/src/console/system-prompt/contracts.ts +0 -1
- package/src/main.ts +1 -0
- package/src/observability/event-query.ts +18 -4
- package/src/persistence/dialect.ts +12 -0
- package/src/persistence/schema.ts +20 -2
- package/src/protocol/dsml.ts +1 -1
- package/src/protocol/gateway-error.ts +1 -0
- package/src/protocol/reasoning.ts +1 -1
- package/src/protocol/request/chat.ts +4 -4
- package/src/protocol/request/messages.ts +1 -1
- package/src/protocol/response/gemini.ts +6 -1
- package/src/protocol/sse-decoder.ts +25 -8
- package/src/protocol/stream-error-frames.ts +13 -3
- package/src/protocol/translation/quirks.ts +1 -1
- package/src/protocol/translation/tool-repair.ts +2 -2
- package/src/protocol/upstream/send-claude-messages.ts +15 -4
- package/src/protocol/upstream-parsing.ts +1 -1
- package/src/providers/adapters/compatible-adapter.ts +7 -5
- package/src/providers/authentication/oauth-refresh-service.ts +149 -36
- package/src/providers/default-registry.ts +68 -3
- package/src/providers/discovery/discovery-types.ts +12 -0
- package/src/providers/discovery/probe-phases.ts +61 -30
- package/src/providers/discovery/probing-service.ts +279 -164
- package/src/providers/integrations/antigravity/antigravity-protocol.ts +117 -2
- package/src/providers/integrations/antigravity/antigravity.ts +13 -2
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +72 -34
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +18 -18
- package/src/providers/integrations/buddy/buddy-checkin.ts +17 -39
- package/src/providers/integrations/buddy/buddy-conversation-headers.ts +2 -2
- package/src/providers/integrations/buddy/buddy-discovery-shared.ts +5 -5
- package/src/providers/integrations/buddy/buddy-identity.ts +70 -0
- package/src/providers/integrations/buddy/buddy-media.ts +4 -5
- package/src/providers/integrations/buddy/buddy-oauth-shared.ts +2 -2
- package/src/providers/integrations/buddy/buddy-quota-shared.ts +1 -1
- package/src/providers/integrations/buddy/buddy-usage-shared.ts +1 -3
- package/src/providers/integrations/buddy/capabilities.ts +4 -41
- package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +2 -1
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +20 -1
- package/src/providers/integrations/buddy/codebuddy/provider.ts +2 -2
- package/src/providers/integrations/buddy/codebuddy/shared.ts +6 -10
- package/src/providers/integrations/cursor/cursor-catalog.ts +2 -0
- package/src/providers/integrations/cursor/cursor-exec.ts +738 -0
- package/src/providers/integrations/cursor/cursor-http2.ts +2 -0
- package/src/providers/integrations/cursor/cursor-request.ts +9 -8
- package/src/providers/integrations/cursor/cursor-shared.ts +19 -1
- package/src/providers/integrations/cursor/cursor.ts +50 -30
- package/src/providers/integrations/cursor/protocol/tool-canonical.ts +26 -100
- package/src/providers/integrations/devin/devin-auth.ts +353 -0
- package/src/providers/integrations/devin/devin-catalog.ts +207 -0
- package/src/providers/integrations/devin/devin-http.ts +91 -0
- package/src/providers/integrations/devin/devin-quota.ts +214 -0
- package/src/providers/integrations/devin/devin-shared.ts +111 -0
- package/src/providers/integrations/devin/devin.ts +539 -0
- package/src/providers/integrations/devin/protocol/devin-proto.ts +584 -0
- package/src/providers/integrations/devin/protocol/framing.ts +124 -0
- package/src/providers/integrations/factory/factory.ts +2 -2
- package/src/providers/integrations/freepi/freepi-catalog.ts +60 -0
- package/src/providers/integrations/freepi/freepi-discovery.ts +82 -0
- package/src/providers/integrations/freepi/freepi-headers.ts +21 -0
- package/src/providers/integrations/freepi/freepi-oauth.ts +142 -0
- package/src/providers/integrations/freepi/freepi-protocol.ts +242 -0
- package/src/providers/integrations/freepi/freepi-quota.ts +126 -0
- package/src/providers/integrations/freepi/freepi-realm.ts +35 -0
- package/src/providers/integrations/freepi/freepi.ts +292 -0
- package/src/providers/integrations/gemini.ts +2 -2
- package/src/providers/integrations/hermes/hermes-oauth.ts +94 -0
- package/src/providers/integrations/hermes.ts +4 -12
- package/src/providers/integrations/opencode/fingerprint.ts +1 -1
- package/src/providers/integrations/perch/perch-catalog.ts +105 -0
- package/src/providers/integrations/perch/perch-oauth.ts +170 -0
- package/src/providers/integrations/perch/perch-protocol.ts +211 -0
- package/src/providers/integrations/perch/perch-quota.ts +137 -0
- package/src/providers/integrations/perch/perch-realm.ts +82 -0
- package/src/providers/integrations/perch/perch-system-prompt.ts +13 -0
- package/src/providers/integrations/perch/perch.ts +315 -0
- package/src/providers/integrations/perch/saffron-system-prompt.txt +8 -0
- package/src/providers/integrations/qoder/chat-proto.ts +328 -328
- package/src/providers/integrations/qoder/claim-qoder-credits.ts +1 -1
- package/src/providers/integrations/qoder/qoder-adapter.ts +89 -28
- package/src/providers/integrations/qoder/qoder-cache.ts +32 -0
- package/src/providers/integrations/qoder/qoder-catalog.ts +6 -1
- package/src/providers/integrations/qoder/qoder-checkin.ts +2 -2
- package/src/providers/integrations/qoder/qoder-grpc.ts +209 -5
- package/src/providers/integrations/qoder/qoder-ide.ts +5 -1
- package/src/providers/integrations/qoder/qoder-identity-cache.ts +184 -0
- package/src/providers/integrations/qoder/qoder-native-identity-store.ts +83 -18
- package/src/providers/integrations/qoder/qoder-realm.ts +9 -6
- package/src/providers/integrations/qoder/qoder-umid.ts +109 -342
- package/src/providers/integrations/qoder/qoder-virtual.ts +33 -1
- package/src/providers/integrations/qoder/qoder.ts +3 -6
- package/src/providers/integrations/trae/trae-catalog.ts +9 -14
- package/src/providers/integrations/trae/trae-headers.ts +36 -11
- package/src/providers/integrations/trae/trae-oauth.ts +14 -17
- package/src/providers/integrations/trae/trae-protocol.ts +3 -6
- package/src/providers/integrations/trae/trae-quota.ts +103 -21
- package/src/providers/integrations/trae/trae-realm.ts +36 -15
- package/src/providers/integrations/trae/trae.ts +97 -7
- package/src/providers/operations/account-error-classification.ts +66 -1
- package/src/providers/operations/account-health-service.ts +192 -26
- package/src/providers/operations/account-refresh-status.ts +284 -0
- package/src/providers/operations/account-tier.ts +67 -0
- package/src/providers/operations/byok-wire-profile.ts +9 -12
- package/src/providers/operations/client-versions.ts +37 -99
- package/src/providers/operations/cooldown-rules.ts +61 -0
- package/src/providers/operations/provider-catalog-seeder.ts +4 -0
- package/src/providers/provider-contracts.ts +29 -0
- package/src/providers/provider-metadata.ts +59 -4
- package/src/providers/provider-model-definition.ts +36 -0
- package/src/providers/quota/refresh.ts +21 -0
- package/src/security/abuse.ts +71 -0
- package/src/transport/dispatch/proxy-request.ts +1 -0
- package/src/transport/dispatch/streaming/stream.ts +1 -0
- package/src/transport/dispatch/upstream.ts +4 -1
- package/src/transport/failure-policy.ts +113 -7
- package/src/transport/routing/router.ts +25 -1
- package/src/transport/surface/messages/adapter.ts +2 -1
- package/src/transport/surface/responses/encode.ts +12 -1
- package/src/workers/daily-checkin.ts +1 -1
- package/src/workers/quota-refresh-sweep.ts +1 -1
- package/src/workers/scheduler.ts +22 -4
- package/dist/dashboard/assets/BannedPage-B05aF8Dg.js +0 -1
- package/dist/dashboard/assets/BannedPage-B05aF8Dg.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-CLUhmjUy.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-CQmrqqrk.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-CHG_1Wr3.js +0 -1
- package/dist/dashboard/assets/CombosPage-CHG_1Wr3.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-DPgbklad.js +0 -1
- package/dist/dashboard/assets/ConsoleLogPage-DPgbklad.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-pnfAqr_-.js +0 -1
- package/dist/dashboard/assets/CustomizationPage-pnfAqr_-.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-DGJuU2A6.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-DGJuU2A6.js.gz +0 -0
- package/dist/dashboard/assets/InjectSystemPromptPanel-C-e2AMz6.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-J8wPbmfT.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-C6WlpU5E.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-BSQnF2Lp.js +0 -1
- package/dist/dashboard/assets/OverviewPage-BSQnF2Lp.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-DK_e0FJz.js +0 -7
- package/dist/dashboard/assets/ProviderDetailPage-DK_e0FJz.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js +0 -1
- package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-D_a3r6Gc.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-D_a3r6Gc.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-BBTRBAQ3.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-B2WqMSG1.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-D7xp5Ij_.js +0 -1
- package/dist/dashboard/assets/SettingsPage-D7xp5Ij_.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-LND6_Ss7.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-BzNnm6Bi.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-C1TbL24N.js +0 -56
- package/dist/dashboard/assets/UsagePage-C1TbL24N.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-KvX83mRe.js +0 -1
- package/dist/dashboard/assets/api-keys-KvX83mRe.js.gz +0 -0
- package/dist/dashboard/assets/app-mweRcsKp.css +0 -2
- package/dist/dashboard/assets/app-mweRcsKp.css.gz +0 -0
- package/dist/dashboard/assets/assert-CidQVoNo.js +0 -1
- package/dist/dashboard/assets/assert-CidQVoNo.js.gz +0 -0
- package/dist/dashboard/assets/badge-BaX2GTrn.js.gz +0 -0
- package/dist/dashboard/assets/banner-DFH4Bkte.js.gz +0 -0
- package/dist/dashboard/assets/brand/bansos_page.png +0 -0
- package/dist/dashboard/assets/brand/denia_bansos.jpg +0 -0
- package/dist/dashboard/assets/brand/favicon.jpg +0 -0
- package/dist/dashboard/assets/brand/login_register.jpg +0 -0
- package/dist/dashboard/assets/brand/login_register_initial.jpg +0 -0
- package/dist/dashboard/assets/brand/og_bansos.jpg +0 -0
- package/dist/dashboard/assets/brand/og_image.jpg +0 -0
- package/dist/dashboard/assets/brand/sidebar_ico.jpg +0 -0
- package/dist/dashboard/assets/card-CxDw0lK6.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js +0 -1
- package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-DfS1ntiq.js.gz +0 -0
- package/dist/dashboard/assets/dialog-BK-NRr5u.js.gz +0 -0
- package/dist/dashboard/assets/drawer-QqPul6sC.js.gz +0 -0
- package/dist/dashboard/assets/entry-BMlYJVFj.js.gz +0 -0
- package/dist/dashboard/assets/entry-Dmx7gBiN.js +0 -1
- package/dist/dashboard/assets/entry-Dmx7gBiN.js.gz +0 -0
- package/dist/dashboard/assets/entry-vqGb8yVz.js +0 -4
- package/dist/dashboard/assets/entry-vqGb8yVz.js.gz +0 -0
- package/dist/dashboard/assets/formatters-DP-5E01-.js.gz +0 -0
- package/dist/dashboard/assets/index-D36vCZht.js.gz +0 -0
- package/dist/dashboard/assets/inline-CHJQwxLa.js.gz +0 -0
- package/dist/dashboard/assets/input-BlmlizVV.js.gz +0 -0
- package/dist/dashboard/assets/layout-Bz93I-yE.js.gz +0 -0
- package/dist/dashboard/assets/list-row-Ml16KCfu.js.gz +0 -0
- package/dist/dashboard/assets/live-7_QpnYGu.js.gz +0 -0
- package/dist/dashboard/assets/minimize-2-BTmMTK4o.js +0 -1
- package/dist/dashboard/assets/minimize-2-BTmMTK4o.js.gz +0 -0
- package/dist/dashboard/assets/mutation-GU7XdEjz.js.gz +0 -0
- package/dist/dashboard/assets/network-CuMDaTzT.js.gz +0 -0
- package/dist/dashboard/assets/numbers-CoqpaDMx.js.gz +0 -0
- package/dist/dashboard/assets/pagination-B5aOoweZ.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-YSoQOUh6.js.gz +0 -0
- package/dist/dashboard/assets/routing-fnCsxgNO.js +0 -1
- package/dist/dashboard/assets/routing-fnCsxgNO.js.gz +0 -0
- package/dist/dashboard/assets/select-C9Ssle9b.js.gz +0 -0
- package/dist/dashboard/assets/settings-BxslsErn.js +0 -1
- package/dist/dashboard/assets/settings-BxslsErn.js.gz +0 -0
- package/dist/dashboard/assets/stack-j7cXjgYX.js.gz +0 -0
- package/dist/dashboard/assets/state-DHmEnLa2.js.gz +0 -0
- package/dist/dashboard/assets/switch-DmZcyTIC.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-CbCP4w1o.js.gz +0 -0
- package/dist/dashboard/console-backgrounds.jpg +0 -0
- package/dist/dashboard/console-backgrounds_initial.jpg +0 -0
- package/dist/dashboard/donation/qris-risun-marke.png +0 -0
- package/src/providers/integrations/buddy/turing-device-token.ts +0 -262
- package/src/providers/integrations/buddy/workbuddy/cn-oauth.ts +0 -30
- package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +0 -58
- package/src/providers/integrations/buddy/workbuddy/oauth.ts +0 -31
- package/src/providers/integrations/buddy/workbuddy/provider.ts +0 -209
- package/src/providers/integrations/buddy/workbuddy/quota.ts +0 -161
- package/src/providers/integrations/buddy/workbuddy/shared.ts +0 -285
- /package/dist/dashboard/assets/{app-DRnQLhsc.js → app-B7kIWFm5.js} +0 -0
- /package/dist/dashboard/assets/{app-DRnQLhsc.js.gz → app-B7kIWFm5.js.gz} +0 -0
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
ConsoleDomainError,
|
|
16
16
|
requireGlobalAdmin,
|
|
17
17
|
requireTenantAnyScope,
|
|
18
|
+
requireTenantScope,
|
|
18
19
|
} from "../../shared/errors";
|
|
19
20
|
import { cachedReadWithEtag, conditionalResponse } from "../../shared/read-through-cache";
|
|
20
21
|
import type { RedisClient } from "../../../persistence/redis";
|
|
@@ -24,10 +25,11 @@ import { isBundledProviderId } from "../../../providers/provider-contracts";
|
|
|
24
25
|
import type { ProviderRegistry } from "../../../providers/provider-registry";
|
|
25
26
|
import { getProviderClientVersion, getRouteIdentityVersions } from "../../../providers/operations/client-versions";
|
|
26
27
|
import type { OAuthLoginField } from "../../../providers/authentication/oauth-flow-store";
|
|
27
|
-
import { providerCredentialHint, providerCredentialUrl, providerDisplayName, providerHasAdapterUserAgent, providerOperatorNotice, providerServiceKinds, type CompatibilityProfile } from "../../../providers/provider-metadata";
|
|
28
|
+
import { providerCredentialHint, providerCredentialUrl, providerDisplayName, providerHasAdapterUserAgent, providerOperatorNotice, providerServiceKinds, customProviderServiceKinds, type CompatibilityProfile } from "../../../providers/provider-metadata";
|
|
28
29
|
import type { AccountHealthEventRecord } from "../../../providers/operations/account-health-service";
|
|
29
30
|
import { healthEventBuffer } from "../../../persistence/health-event-flush";
|
|
30
|
-
import { CREDENTIAL_MODES, PROVIDER_READ_SCOPES, PROVIDER_WRITE_SCOPES, type BuddyGrowthTaskLedgerEntry } from "./contracts";
|
|
31
|
+
import { CREDENTIAL_MODES, PROVIDER_READ_SCOPES, PROVIDER_WRITE_SCOPES, type BuddyGrowthTaskLedgerEntry, type ProviderAccountBulkActionResult, type RefreshAccountTokenResponse } from "./contracts";
|
|
32
|
+
import type { RefreshAccountStatusOutcome } from "../../../providers/operations/account-refresh-status";
|
|
31
33
|
import { enqueueCheckin } from "../../../workers/daily-checkin";
|
|
32
34
|
import {
|
|
33
35
|
isWireFamily,
|
|
@@ -87,12 +89,16 @@ export function sanitizeProviderResponse(
|
|
|
87
89
|
const credentialHint = providerCredentialHint(p.providerId);
|
|
88
90
|
const operatorNotice = providerOperatorNotice(p.providerId);
|
|
89
91
|
const clientVersion = getProviderClientVersion(p.providerId);
|
|
92
|
+
const compatibilityProfile = p.compatibilityProfile as CompatibilityProfile | undefined;
|
|
93
|
+
const isBuiltIn = (p.isBuiltIn as boolean | undefined) ?? false;
|
|
90
94
|
const response: ProviderResponse = {
|
|
91
95
|
providerId: p.providerId,
|
|
92
|
-
serviceKinds:
|
|
96
|
+
serviceKinds: isBuiltIn
|
|
97
|
+
? providerServiceKinds(p.providerId)
|
|
98
|
+
: customProviderServiceKinds(compatibilityProfile),
|
|
93
99
|
displayName: providerDisplayName(p.providerId),
|
|
94
100
|
enabled: (p.enabled as boolean | undefined) ?? true,
|
|
95
|
-
isBuiltIn
|
|
101
|
+
isBuiltIn,
|
|
96
102
|
requiresAccount: (p.requiresAccount as boolean | undefined) ?? true,
|
|
97
103
|
hasAdapterUserAgent: providerHasAdapterUserAgent(p.providerId),
|
|
98
104
|
// No model advertises search; see the note in provider-metadata.ts.
|
|
@@ -102,7 +108,7 @@ export function sanitizeProviderResponse(
|
|
|
102
108
|
...(operatorNotice === undefined ? {} : { operatorNotice }),
|
|
103
109
|
...(clientVersion === undefined ? {} : { clientVersion }),
|
|
104
110
|
routeIdentityVersions: getRouteIdentityVersions(),
|
|
105
|
-
supportsModelDiscovery: (p.supportsModelDiscovery as boolean | undefined) ?? !
|
|
111
|
+
supportsModelDiscovery: (p.supportsModelDiscovery as boolean | undefined) ?? !isBuiltIn,
|
|
106
112
|
...(typeof p.createdAt === "string" ? { createdAt: p.createdAt } : {}),
|
|
107
113
|
...(typeof p.updatedAt === "string" ? { updatedAt: p.updatedAt } : {}),
|
|
108
114
|
};
|
|
@@ -182,6 +188,15 @@ export interface ProviderCatalogConfig {
|
|
|
182
188
|
providerId: string,
|
|
183
189
|
accountId: string,
|
|
184
190
|
) => Promise<{ readonly accessToken: string; readonly refreshToken?: string }>;
|
|
191
|
+
/**
|
|
192
|
+
* One-shot manual OAuth token refresh (area 04). Provided by the composition
|
|
193
|
+
* root; when absent the refresh-token route answers 501 rather than silently
|
|
194
|
+
* reporting success.
|
|
195
|
+
*/
|
|
196
|
+
readonly refreshAccountStatus?: (
|
|
197
|
+
accountId: string,
|
|
198
|
+
opts?: { readonly origin?: "ui" | "sweep" | "dispatch" },
|
|
199
|
+
) => Promise<RefreshAccountStatusOutcome>;
|
|
185
200
|
}
|
|
186
201
|
async function attachProviderCapabilities(
|
|
187
202
|
response: ProviderResponse,
|
|
@@ -715,6 +730,101 @@ export function createProviderCatalogOperations(config: ProviderCatalogConfig) {
|
|
|
715
730
|
await config.snapshotInvalidator?.invalidate();
|
|
716
731
|
return { success: true };
|
|
717
732
|
},
|
|
733
|
+
/**
|
|
734
|
+
* One UPDATE over the tenant's cooling accounts, with `cooldownUntil` and
|
|
735
|
+
* `modelCooldowns` preserved as evidence (D07/D08). Shared rows are not
|
|
736
|
+
* writable here: the platform-admin pair of routes owns them (D10).
|
|
737
|
+
*/
|
|
738
|
+
async disableCooldowns(
|
|
739
|
+
access: AccessDecision | undefined,
|
|
740
|
+
providerId: string,
|
|
741
|
+
): Promise<ProviderAccountBulkActionResult> {
|
|
742
|
+
const a = requireTenantAnyScope(access, PROVIDER_WRITE_SCOPES);
|
|
743
|
+
const result = await config.store.disableCooldowns(a.tenantId, providerId);
|
|
744
|
+
await config.auditSink?.record({
|
|
745
|
+
access: a,
|
|
746
|
+
action: "provider_account.bulk_disabled_cooldowns",
|
|
747
|
+
target: providerId,
|
|
748
|
+
detail: { providerId, updated: result.updated, globalSkipped: result.globalSkipped },
|
|
749
|
+
});
|
|
750
|
+
await config.snapshotInvalidator?.invalidate();
|
|
751
|
+
return { ok: true, updated: result.updated, globalSkipped: result.globalSkipped };
|
|
752
|
+
},
|
|
753
|
+
/**
|
|
754
|
+
* One DELETE over the tenant's strictly banned accounts (D12). Empty
|
|
755
|
+
* matches are a successful no-op, so a second click is idempotent.
|
|
756
|
+
*/
|
|
757
|
+
async deleteBanned(
|
|
758
|
+
access: AccessDecision | undefined,
|
|
759
|
+
providerId: string,
|
|
760
|
+
): Promise<ProviderAccountBulkActionResult> {
|
|
761
|
+
const a = requireTenantAnyScope(access, PROVIDER_WRITE_SCOPES);
|
|
762
|
+
const result = await config.store.deleteBanned(a.tenantId, providerId);
|
|
763
|
+
await config.auditSink?.record({
|
|
764
|
+
access: a,
|
|
765
|
+
action: "provider_account.bulk_deleted_banned",
|
|
766
|
+
target: providerId,
|
|
767
|
+
detail: {
|
|
768
|
+
providerId,
|
|
769
|
+
deleted: result.deleted ?? [],
|
|
770
|
+
missing: result.missing ?? [],
|
|
771
|
+
globalSkipped: result.globalSkipped,
|
|
772
|
+
},
|
|
773
|
+
});
|
|
774
|
+
await config.snapshotInvalidator?.invalidate();
|
|
775
|
+
return {
|
|
776
|
+
ok: true,
|
|
777
|
+
updated: 0,
|
|
778
|
+
deleted: result.deleted ?? [],
|
|
779
|
+
missing: result.missing ?? [],
|
|
780
|
+
globalSkipped: result.globalSkipped,
|
|
781
|
+
};
|
|
782
|
+
},
|
|
783
|
+
async refreshAccountToken(
|
|
784
|
+
access: AccessDecision | undefined,
|
|
785
|
+
providerId: string,
|
|
786
|
+
accountId: string,
|
|
787
|
+
): Promise<RefreshAccountTokenResponse> {
|
|
788
|
+
// The refresh changes no routing state and is a per-account operator
|
|
789
|
+
// action, so the browser session's own write scope is sufficient.
|
|
790
|
+
const a = requireTenantScope(access, "dashboard:write");
|
|
791
|
+
// 404 unless the account exists and belongs to this provider/tenant.
|
|
792
|
+
const accounts = await config.store.listAccounts(a.tenantId, providerId);
|
|
793
|
+
const owned = accounts.find((account) => account.id === accountId && account.tenantId === a.tenantId);
|
|
794
|
+
if (!owned)
|
|
795
|
+
throw new ConsoleDomainError("account_not_found", 404, `Account ${accountId} not found`);
|
|
796
|
+
// 409 for a static token (nothing to re-mint), 400 for non-OAuth —
|
|
797
|
+
// before calling the refresher, so the row is never touched on a
|
|
798
|
+
// rejectable request.
|
|
799
|
+
if (owned.staticToken === true)
|
|
800
|
+
throw new ConsoleDomainError("static_token", 409, "Account carries a static token that cannot be refreshed");
|
|
801
|
+
if (owned.credentialKind !== "oauth")
|
|
802
|
+
throw new ConsoleDomainError("not_oauth", 400, "Account is not OAuth; token refresh is not applicable");
|
|
803
|
+
if (!config.refreshAccountStatus)
|
|
804
|
+
throw new ConsoleDomainError("refresh_unavailable", 501, "Token refresh is not configured on this host");
|
|
805
|
+
const outcome = await config.refreshAccountStatus(accountId, { origin: "ui" });
|
|
806
|
+
await config.auditSink?.record({
|
|
807
|
+
access: a,
|
|
808
|
+
action: "provider_account.token_refreshed",
|
|
809
|
+
target: accountId,
|
|
810
|
+
detail: {
|
|
811
|
+
providerId,
|
|
812
|
+
ok: outcome.ok,
|
|
813
|
+
statusAfter: outcome.statusAfter,
|
|
814
|
+
...(outcome.error === undefined ? {} : { error: outcome.error }),
|
|
815
|
+
},
|
|
816
|
+
});
|
|
817
|
+
await config.snapshotInvalidator?.invalidate();
|
|
818
|
+
return {
|
|
819
|
+
ok: outcome.ok,
|
|
820
|
+
statusAfter: outcome.statusAfter,
|
|
821
|
+
...(outcome.tokenRefreshed ? { tokenRefreshed: true } : {}),
|
|
822
|
+
...(outcome.expiresAt === undefined ? {} : { expiresAt: outcome.expiresAt }),
|
|
823
|
+
...(outcome.refreshedAt === undefined ? {} : { refreshedAt: outcome.refreshedAt }),
|
|
824
|
+
...(outcome.error === undefined ? {} : { error: outcome.error }),
|
|
825
|
+
...(outcome.category === undefined ? {} : { category: outcome.category }),
|
|
826
|
+
};
|
|
827
|
+
},
|
|
718
828
|
/**
|
|
719
829
|
* Exports the requested accounts with their decrypted credentials.
|
|
720
830
|
*
|
|
@@ -778,9 +888,7 @@ export function createProviderCatalogOperations(config: ProviderCatalogConfig) {
|
|
|
778
888
|
// channel check (11128) rejects the imported account.
|
|
779
889
|
const materialized =
|
|
780
890
|
account.providerId === "cb" ||
|
|
781
|
-
account.providerId === "cbcn"
|
|
782
|
-
account.providerId === "workbuddy" ||
|
|
783
|
-
account.providerId === "workbuddycn"
|
|
891
|
+
account.providerId === "cbcn"
|
|
784
892
|
// dynamic-import: B-02 lazy graph — buddy device-binding materializer, buddy-family only.
|
|
785
893
|
// The buddy-identity graph is lazy-loaded: the device-binding
|
|
786
894
|
// materializer only runs for buddy-family accounts (B-02).
|
|
Binary file
|
|
@@ -2,9 +2,10 @@ import { consoleErrorHandler } from "../../shared/errors";
|
|
|
2
2
|
import { literalUnion } from "../../shared/elysia-schema";
|
|
3
3
|
import { Elysia, t, type AnyElysia } from "elysia";
|
|
4
4
|
import { SERVICE_KINDS, WIRE_FAMILIES } from "../../../protocol/canonical-model";
|
|
5
|
-
import { PROBE_REASONING_EFFORTS } from "../../../providers/discovery/discovery-types";
|
|
5
|
+
import { PROBE_MODES, PROBE_REASONING_EFFORTS } from "../../../providers/discovery/discovery-types";
|
|
6
6
|
import {
|
|
7
7
|
ACCOUNT_STATUSES,
|
|
8
|
+
DISCOVER_MODEL_COMMIT_MAX,
|
|
8
9
|
CREDENTIAL_KINDS,
|
|
9
10
|
CREDENTIAL_MODES,
|
|
10
11
|
} from "./contracts";
|
|
@@ -18,6 +19,7 @@ import type {
|
|
|
18
19
|
ByokConnectionTestRequest,
|
|
19
20
|
CreateProviderAccountRequest,
|
|
20
21
|
CreateProviderRequest,
|
|
22
|
+
DiscoverModelCommitRequest,
|
|
21
23
|
ProbeModelRequest,
|
|
22
24
|
RegisterModelsRequest,
|
|
23
25
|
SetModelEnabledRequest,
|
|
@@ -110,12 +112,28 @@ const modelMetadataOverrideBody = t.Object({
|
|
|
110
112
|
image: t.Optional(t.Boolean()),
|
|
111
113
|
reasoning: t.Optional(t.Boolean()),
|
|
112
114
|
toolCall: t.Optional(t.Boolean()),
|
|
115
|
+
warnings: t.Optional(
|
|
116
|
+
t.Union([
|
|
117
|
+
t.Null(),
|
|
118
|
+
t.Array(
|
|
119
|
+
t.Object({
|
|
120
|
+
kind: t.String(),
|
|
121
|
+
message: t.String(),
|
|
122
|
+
severity: t.Optional(literalUnion(["info", "warn", "critical"])),
|
|
123
|
+
updatedAt: t.Optional(t.String()),
|
|
124
|
+
}),
|
|
125
|
+
),
|
|
126
|
+
]),
|
|
127
|
+
),
|
|
113
128
|
});
|
|
114
129
|
const registerModelsBody = t.Object({
|
|
115
130
|
modelIds: t.Array(t.String()),
|
|
116
131
|
wireFamily: t.Optional(t.String()),
|
|
117
132
|
metadata: t.Optional(modelMetadataOverrideBody),
|
|
118
133
|
});
|
|
134
|
+
const discoverCommitBody = t.Object({
|
|
135
|
+
modelIds: t.Array(t.String(), { minItems: 1, maxItems: DISCOVER_MODEL_COMMIT_MAX }),
|
|
136
|
+
});
|
|
119
137
|
const probeModelBody = t.Object({
|
|
120
138
|
modelId: t.String(),
|
|
121
139
|
serviceKind: t.Optional(literalUnion(SERVICE_KINDS)),
|
|
@@ -127,6 +145,7 @@ const probeModelBody = t.Object({
|
|
|
127
145
|
maxOutputTokens: t.Optional(t.Integer({ minimum: 1, maximum: 131072 })),
|
|
128
146
|
stream: t.Optional(t.Boolean()),
|
|
129
147
|
surface: t.Optional(literalUnion(["default", "cosy", "grpc", "ide"])),
|
|
148
|
+
mode: t.Optional(literalUnion(PROBE_MODES)),
|
|
130
149
|
});
|
|
131
150
|
const byokConnectionTestBody = t.Object({
|
|
132
151
|
baseUrl: t.String(),
|
|
@@ -275,9 +294,42 @@ export function createProviderCatalogRoutes(config: ProviderCatalogConfig): AnyE
|
|
|
275
294
|
);
|
|
276
295
|
},
|
|
277
296
|
)
|
|
278
|
-
.post(
|
|
279
|
-
|
|
280
|
-
})
|
|
297
|
+
.post(
|
|
298
|
+
"/:providerId/models/test-models",
|
|
299
|
+
{ body: t.Optional(t.Object({ mode: t.Optional(literalUnion(PROBE_MODES)) })) },
|
|
300
|
+
async ({ request, params, body }) => {
|
|
301
|
+
return await modelFactory.probeAllModels(
|
|
302
|
+
config.accessResolver(request),
|
|
303
|
+
params.providerId,
|
|
304
|
+
body?.mode,
|
|
305
|
+
);
|
|
306
|
+
},
|
|
307
|
+
)
|
|
308
|
+
// Discovery must be declared before `/:providerId/models/discover/commit`
|
|
309
|
+
// would be shadowed by a dynamic segment; Elysia resolves both here.
|
|
310
|
+
.get("/:providerId/models/discover", async ({ request, params }) => {
|
|
311
|
+
return await modelFactory.listDiscoveryCandidates(
|
|
312
|
+
config.accessResolver(request),
|
|
313
|
+
params.providerId,
|
|
314
|
+
);
|
|
315
|
+
})
|
|
316
|
+
.post(
|
|
317
|
+
"/:providerId/models/discover/commit",
|
|
318
|
+
{ body: discoverCommitBody },
|
|
319
|
+
async ({ request, params, body }) => {
|
|
320
|
+
return await modelFactory.commitDiscoveredModels(
|
|
321
|
+
config.accessResolver(request),
|
|
322
|
+
params.providerId,
|
|
323
|
+
(body as DiscoverModelCommitRequest).modelIds,
|
|
324
|
+
);
|
|
325
|
+
},
|
|
326
|
+
)
|
|
327
|
+
.post("/:providerId/accounts/actions/disable-cooldowns", async ({ request, params }) => {
|
|
328
|
+
return await factory.disableCooldowns(config.accessResolver(request), params.providerId);
|
|
329
|
+
})
|
|
330
|
+
.post("/:providerId/accounts/actions/delete-banned", async ({ request, params }) => {
|
|
331
|
+
return await factory.deleteBanned(config.accessResolver(request), params.providerId);
|
|
332
|
+
})
|
|
281
333
|
.post(
|
|
282
334
|
"/:providerId/accounts/probe",
|
|
283
335
|
{ body: probeModelBody },
|
|
@@ -408,6 +460,14 @@ export function createProviderCatalogRoutes(config: ProviderCatalogConfig): AnyE
|
|
|
408
460
|
params.accountId,
|
|
409
461
|
);
|
|
410
462
|
return recovered;
|
|
463
|
+
})
|
|
464
|
+
.post("/:providerId/accounts/:accountId/refresh-token", async ({ request, params }) => {
|
|
465
|
+
const refreshed = await factory.refreshAccountToken(
|
|
466
|
+
config.accessResolver(request),
|
|
467
|
+
params.providerId,
|
|
468
|
+
params.accountId,
|
|
469
|
+
);
|
|
470
|
+
return refreshed;
|
|
411
471
|
})
|
|
412
472
|
.post(
|
|
413
473
|
"/:providerId/accounts/export",
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
firstNonEmptyString as firstString,
|
|
11
11
|
tryParseJson as safeParseJson,
|
|
12
12
|
} from "../../../shared/coerce";
|
|
13
|
-
import type { CredentialMode, ModelCatalogEntry } from "./contracts";
|
|
13
|
+
import type { CredentialMode, ModelCatalogEntry, ModelWarning } from "./contracts";
|
|
14
14
|
import {
|
|
15
15
|
FALLBACK_CONTEXT_LIMIT,
|
|
16
16
|
FALLBACK_OUTPUT_LIMIT,
|
|
@@ -285,6 +285,32 @@ export function liveModelCooldowns(
|
|
|
285
285
|
}
|
|
286
286
|
|
|
287
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Reads the display-only warning blob. Postgres returns `jsonb` already
|
|
290
|
+
* decoded; SQLite returns the JSON string its `text` column carries, so both
|
|
291
|
+
* shapes are accepted. A value that is not `{ warnings: [...] }` is ignored —
|
|
292
|
+
* a malformed row must not break the entire model list.
|
|
293
|
+
*/
|
|
294
|
+
function parseWarningMetadata(raw: unknown): { warnings: ModelWarning[] } | undefined {
|
|
295
|
+
let value: unknown = raw;
|
|
296
|
+
if (typeof value === "string") {
|
|
297
|
+
try {
|
|
298
|
+
value = JSON.parse(value) as unknown;
|
|
299
|
+
} catch {
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
304
|
+
const warnings = (value as Record<string, unknown>)["warnings"];
|
|
305
|
+
if (!Array.isArray(warnings)) return undefined;
|
|
306
|
+
const projected = warnings.filter((warning): warning is ModelWarning => {
|
|
307
|
+
if (warning === null || typeof warning !== "object" || Array.isArray(warning)) return false;
|
|
308
|
+
const item = warning as Record<string, unknown>;
|
|
309
|
+
return typeof item["kind"] === "string" && typeof item["message"] === "string";
|
|
310
|
+
});
|
|
311
|
+
return projected.length > 0 ? { warnings: projected } : undefined;
|
|
312
|
+
}
|
|
313
|
+
|
|
288
314
|
/**
|
|
289
315
|
* Projects one persisted `models` row into the console's catalog entry.
|
|
290
316
|
*
|
|
@@ -344,6 +370,9 @@ export function mapModelRow(
|
|
|
344
370
|
// Same three-valued rule, from the provider's own directory. Qoder states
|
|
345
371
|
// `is_free` per row, so a plan change moves the pill without a code change.
|
|
346
372
|
...(row.freeTier === true ? { freeTier: true } : {}),
|
|
373
|
+
// Display-only: a `NULL` column (or a malformed legacy value) projects no
|
|
374
|
+
// warnings at all rather than leaking the raw JSON shape to the dashboard.
|
|
375
|
+
...(parseWarningMetadata(row.warningMetadata) ?? {}),
|
|
347
376
|
sourceUpdatedAt: row.sourceUpdatedAt?.toISOString() ?? null,
|
|
348
377
|
} satisfies ModelCatalogEntry;
|
|
349
378
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Drizzle-backed provider, account, model, routing, and API-key repositories.
|
|
2
2
|
import { and, asc, eq, gte, inArray, isNull, or, sql, type SQL } from "drizzle-orm";
|
|
3
|
+
import { isSqliteDatabase } from "../../../persistence/dialect";
|
|
3
4
|
import { ConsoleDomainError } from "../../shared/errors";
|
|
4
5
|
import { parseCustomProviderId, isBundledProviderId } from "../../../providers/provider-contracts";
|
|
5
6
|
import { providerRateLimitIsIpScoped } from "../../../providers/provider-metadata";
|
|
@@ -13,11 +14,13 @@ import { buddyTaskLedger, models, networkPools, providerAccounts, providerOauthS
|
|
|
13
14
|
import { IS_GATEWAY_ERROR_SQL } from "../../../observability/event-query";
|
|
14
15
|
import type { WireFamily } from "../../../protocol/canonical-model";
|
|
15
16
|
import { listAccountHealthEvents, recoverAccount, deleteAccountHealthEvents, type AccountHealthEventRecord } from "../../../providers/operations/account-health-service";
|
|
16
|
-
import { invalidateCredentialCache } from "../../../providers/operations/provider-credential-service";
|
|
17
|
+
import { invalidateCredentialCache, invalidateCredentialCaches } from "../../../providers/operations/provider-credential-service";
|
|
18
|
+
import { invalidateQuotaCaches } from "../../../providers/quota/cache";
|
|
17
19
|
import { encryptCredential, hashSecret } from "../../../security/crypto";
|
|
18
20
|
import type { TelemetryBatchBuffer } from "../../../observability/telemetry-buffer";
|
|
21
|
+
import type { RedisClient } from "../../../persistence/redis";
|
|
19
22
|
import { bumpCatalogRevision, type BundledProviderCatalog } from "../../../providers/operations/provider-catalog-service";
|
|
20
|
-
import type { AccountInflightReading, BuddyGrowthTaskLedgerEntry, ByokConnectionTestRequest, ByokConnectionTestResult, CreateProviderAccountBatchItemResult, CreateProviderAccountBatchResult, CreateProviderAccountRequest, CredentialMode, ModelCatalogEntry, ProbeAllAccountsResult, ProbeAllModelsResult, ProbeModelRequest, ProbeModelResult, ProviderAccountResponse, ProviderAccountTokenUsage, ProviderCatalogStore, ProviderRecord, SetModelEnabledRequest, UpdateModelMetadataRequest, UpdateProviderAccountRequest, ModelMetadataOverride } from "./contracts";
|
|
23
|
+
import type { AccountInflightReading, BuddyGrowthTaskLedgerEntry, ByokConnectionTestRequest, ByokConnectionTestResult, CreateProviderAccountBatchItemResult, CreateProviderAccountBatchResult, CreateProviderAccountRequest, CredentialMode, DiscoverModelCandidate, ModelCatalogEntry, ProbeAllAccountsResult, ProbeAllModelsResult, ProbeModelRequest, ProbeModelResult, ProviderAccountBulkActionResult, ProviderAccountResponse, ProviderAccountTokenUsage, ProviderCatalogStore, ProviderRecord, SetModelEnabledRequest, UpdateModelMetadataRequest, UpdateProviderAccountRequest, ModelMetadataOverride } from "./contracts";
|
|
21
24
|
import { isServiceKind, validateCompatibilityProfile } from "./contracts";
|
|
22
25
|
import { ProviderProbingService, type ProbeOutboundResolver } from "../../../providers/discovery/probing-service";
|
|
23
26
|
import {
|
|
@@ -75,6 +78,7 @@ export class DrizzleProviderCatalogStore implements ProviderCatalogStore {
|
|
|
75
78
|
private readonly probing: ProviderProbingService;
|
|
76
79
|
private readonly bundledModelCatalog: ReadonlyMap<string, readonly ModelDefinition[]>;
|
|
77
80
|
private readonly providerRegistry: ProviderRegistry;
|
|
81
|
+
private readonly redis: RedisClient | undefined;
|
|
78
82
|
|
|
79
83
|
private readonly readAccountInflight:
|
|
80
84
|
| ((
|
|
@@ -97,11 +101,14 @@ export class DrizzleProviderCatalogStore implements ProviderCatalogStore {
|
|
|
97
101
|
tenantId: string,
|
|
98
102
|
) => Promise<readonly AccountInflightReading[]>)
|
|
99
103
|
| undefined;
|
|
104
|
+
/** Shared Redis handle for quota-cache invalidation on bulk delete. */
|
|
105
|
+
readonly redis?: RedisClient | undefined;
|
|
100
106
|
},
|
|
101
107
|
) {
|
|
102
108
|
this.bundledModelCatalog = options.bundledModelCatalog.modelsByProvider;
|
|
103
109
|
this.providerRegistry = options.providerRegistry;
|
|
104
110
|
this.readAccountInflight = options.readAccountInflight;
|
|
111
|
+
this.redis = options.redis;
|
|
105
112
|
this.probing = new ProviderProbingService({
|
|
106
113
|
db,
|
|
107
114
|
telemetryBuffer: options.telemetryBuffer,
|
|
@@ -430,12 +437,17 @@ export class DrizzleProviderCatalogStore implements ProviderCatalogStore {
|
|
|
430
437
|
* (warms OAuth token / connection pools), the rest run concurrently bounded
|
|
431
438
|
* to 5. Registered model ids come from this store's own catalog read.
|
|
432
439
|
*/
|
|
433
|
-
async probeAllModels(
|
|
440
|
+
async probeAllModels(
|
|
441
|
+
tenantId: string,
|
|
442
|
+
providerId: string,
|
|
443
|
+
mode?: ProbeModelRequest["mode"],
|
|
444
|
+
): Promise<ProbeAllModelsResult> {
|
|
434
445
|
const entries = await this.listModels(tenantId, providerId);
|
|
435
446
|
const requests = entries.map((entry) => ({
|
|
436
447
|
modelId: entry.modelId,
|
|
437
448
|
route: entry.route,
|
|
438
449
|
serviceKind: isServiceKind(entry.serviceKind) ? entry.serviceKind : ("llm" as const),
|
|
450
|
+
...(mode === undefined ? {} : { mode }),
|
|
439
451
|
}));
|
|
440
452
|
const uniqueRequests = requests.filter(
|
|
441
453
|
(request, index, all) =>
|
|
@@ -559,6 +571,8 @@ export class DrizzleProviderCatalogStore implements ProviderCatalogStore {
|
|
|
559
571
|
const findRow = (ownership: SQL) =>
|
|
560
572
|
this.db
|
|
561
573
|
.select({ id: models.id, modalities: models.modalities, source: models.source })
|
|
574
|
+
// source is read for both the global builtin guard above and the new
|
|
575
|
+
// non-builtin-only warning edit.
|
|
562
576
|
.from(models)
|
|
563
577
|
.innerJoin(providers, eq(models.providerId, providers.id))
|
|
564
578
|
.where(
|
|
@@ -594,6 +608,29 @@ export class DrizzleProviderCatalogStore implements ProviderCatalogStore {
|
|
|
594
608
|
// per-flag field: the point of the flag is that the operator does not know
|
|
595
609
|
// which capabilities the model has, so an explicit `false` alongside it is
|
|
596
610
|
// either stale or a mistake.
|
|
611
|
+
// Builtin rows are re-seeded from the static catalog on every boot, so a
|
|
612
|
+
// warning edit there would be reverted. Non-builtin rows (including ones a
|
|
613
|
+
// discovery commit created) are operator-owned and may set or clear them.
|
|
614
|
+
if (override?.warnings !== undefined) {
|
|
615
|
+
if (row.source === "builtin") {
|
|
616
|
+
throw new ConsoleDomainError(
|
|
617
|
+
"builtin_model_immutable",
|
|
618
|
+
403,
|
|
619
|
+
`Built-in model ${providerId}/${request.modelId} warnings cannot be edited — the bundle re-seeds them on every boot`,
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
set.warningMetadata =
|
|
623
|
+
override.warnings === null
|
|
624
|
+
? null
|
|
625
|
+
: {
|
|
626
|
+
warnings: override.warnings.map((warning) => ({
|
|
627
|
+
kind: warning.kind,
|
|
628
|
+
message: warning.message,
|
|
629
|
+
...(warning.severity === undefined ? {} : { severity: warning.severity }),
|
|
630
|
+
...(warning.updatedAt === undefined ? {} : { updatedAt: warning.updatedAt }),
|
|
631
|
+
})),
|
|
632
|
+
};
|
|
633
|
+
}
|
|
597
634
|
if (override?.passthrough === true) {
|
|
598
635
|
set.reasoning = true;
|
|
599
636
|
set.toolCall = true;
|
|
@@ -1655,8 +1692,211 @@ export class DrizzleProviderCatalogStore implements ProviderCatalogStore {
|
|
|
1655
1692
|
return recoverAccount(this.db, accountId, "manual_operator_recovery");
|
|
1656
1693
|
}
|
|
1657
1694
|
|
|
1695
|
+
/**
|
|
1696
|
+
* One `UPDATE ... WHERE <cooldown predicate>` (D07/D08/D09). The predicate
|
|
1697
|
+
* is evaluated in SQL, so the whole provider is handled in one statement
|
|
1698
|
+
* without reading rows into the process. `cooldown_until` and
|
|
1699
|
+
* `model_cooldowns` are left untouched: they are the evidence the operator
|
|
1700
|
+
* sees on a disabled row until an explicit Recover clears them.
|
|
1701
|
+
*
|
|
1702
|
+
* Pool-as-account rows live in `provider_pool_accounts`, not this table, so
|
|
1703
|
+
* they are excluded by construction. Shared (`tenant_id IS NULL`) rows are
|
|
1704
|
+
* counted in `globalSkipped` but never written by a tenant; the global-admin
|
|
1705
|
+
* pair of routes owns those.
|
|
1706
|
+
*/
|
|
1707
|
+
async disableCooldowns(
|
|
1708
|
+
tenantId: string,
|
|
1709
|
+
providerId: string,
|
|
1710
|
+
): Promise<ProviderAccountBulkActionResult> {
|
|
1711
|
+
const nowIso = new Date().toISOString();
|
|
1712
|
+
const [globalRows, updatedRows] = await Promise.all([
|
|
1713
|
+
this.db
|
|
1714
|
+
.select({ id: providerAccounts.id })
|
|
1715
|
+
.from(providerAccounts)
|
|
1716
|
+
.where(
|
|
1717
|
+
and(
|
|
1718
|
+
eq(providerAccounts.providerId, providerId),
|
|
1719
|
+
isNull(providerAccounts.tenantId),
|
|
1720
|
+
this.liveCooldownPredicate(nowIso),
|
|
1721
|
+
),
|
|
1722
|
+
),
|
|
1723
|
+
this.db
|
|
1724
|
+
.update(providerAccounts)
|
|
1725
|
+
.set({ status: "disabled" })
|
|
1726
|
+
.where(
|
|
1727
|
+
and(
|
|
1728
|
+
eq(providerAccounts.providerId, providerId),
|
|
1729
|
+
eq(providerAccounts.tenantId, tenantId),
|
|
1730
|
+
this.liveCooldownPredicate(nowIso),
|
|
1731
|
+
),
|
|
1732
|
+
)
|
|
1733
|
+
.returning({ id: providerAccounts.id }),
|
|
1734
|
+
]);
|
|
1735
|
+
// Status is part of the cached credential's admission decision, so every
|
|
1736
|
+
// newly disabled id must be dropped here — not only on delete.
|
|
1737
|
+
invalidateCredentialCaches(updatedRows.map((row) => row.id));
|
|
1738
|
+
return { ok: true, updated: updatedRows.length, globalSkipped: globalRows.length };
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
/**
|
|
1742
|
+
* One `DELETE ... WHERE <strict banned predicate> RETURNING id` (D12).
|
|
1743
|
+
* Credential and quota caches are dropped for the deleted ids in one pass
|
|
1744
|
+
* each, exactly like the existing batch-delete route. A delete is strictly
|
|
1745
|
+
* `status='disabled' AND last_error_category='policy_blocked_banned'`;
|
|
1746
|
+
* `auth_invalidated` is remediated by replacing the credential, not here.
|
|
1747
|
+
*/
|
|
1748
|
+
async deleteBanned(
|
|
1749
|
+
tenantId: string,
|
|
1750
|
+
providerId: string,
|
|
1751
|
+
): Promise<ProviderAccountBulkActionResult> {
|
|
1752
|
+
const globalRows = await this.db
|
|
1753
|
+
.select({ id: providerAccounts.id })
|
|
1754
|
+
.from(providerAccounts)
|
|
1755
|
+
.where(
|
|
1756
|
+
and(
|
|
1757
|
+
eq(providerAccounts.providerId, providerId),
|
|
1758
|
+
isNull(providerAccounts.tenantId),
|
|
1759
|
+
this.bannedPredicate(),
|
|
1760
|
+
),
|
|
1761
|
+
);
|
|
1762
|
+
const deletedRows = await this.db
|
|
1763
|
+
.delete(providerAccounts)
|
|
1764
|
+
.where(
|
|
1765
|
+
and(
|
|
1766
|
+
eq(providerAccounts.providerId, providerId),
|
|
1767
|
+
eq(providerAccounts.tenantId, tenantId),
|
|
1768
|
+
this.bannedPredicate(),
|
|
1769
|
+
),
|
|
1770
|
+
)
|
|
1771
|
+
.returning({ id: providerAccounts.id });
|
|
1772
|
+
const deletedIds = deletedRows.map((row) => row.id);
|
|
1773
|
+
invalidateCredentialCaches(deletedIds);
|
|
1774
|
+
await invalidateQuotaCaches(tenantId, deletedIds, this.redis);
|
|
1775
|
+
return {
|
|
1776
|
+
ok: true,
|
|
1777
|
+
updated: 0,
|
|
1778
|
+
deleted: deletedIds,
|
|
1779
|
+
missing: [],
|
|
1780
|
+
globalSkipped: globalRows.length,
|
|
1781
|
+
};
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
/** Strict banned definition shared by the tenant and global DELETE paths. */
|
|
1785
|
+
private bannedPredicate(): SQL {
|
|
1786
|
+
return and(
|
|
1787
|
+
eq(providerAccounts.status, "disabled"),
|
|
1788
|
+
eq(providerAccounts.lastErrorCategory, "policy_blocked_banned"),
|
|
1789
|
+
) as SQL;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
/**
|
|
1793
|
+
* `status='cooldown' OR cooldown_until > now OR model_cooldowns` carries a
|
|
1794
|
+
* live entry. The JSON check is dialect-specific: PostgreSQL uses
|
|
1795
|
+
* `jsonb_each_text` (with a shape guard), SQLite `json_each` on its `text`
|
|
1796
|
+
* column. Only entries whose ISO deadline is still in the future count, mirroring
|
|
1797
|
+
* `liveModelCooldowns`. Timestamps are stored as ISO strings on both backends,
|
|
1798
|
+
* so the lexical comparison against `nowIso` is stable.
|
|
1799
|
+
*/
|
|
1800
|
+
private liveCooldownPredicate(nowIso: string): SQL {
|
|
1801
|
+
const liveModelCooldown = isSqliteDatabase(this.db)
|
|
1802
|
+
? sql`EXISTS (SELECT 1 FROM json_each(${providerAccounts.modelCooldowns}) AS entry WHERE json_valid(${providerAccounts.modelCooldowns}) AND json_type(${providerAccounts.modelCooldowns}) = 'object' AND entry.value > ${nowIso})`
|
|
1803
|
+
: sql`EXISTS (SELECT 1 FROM jsonb_each_text(${providerAccounts.modelCooldowns}) AS entry WHERE jsonb_typeof(${providerAccounts.modelCooldowns}) = 'object' AND entry.value > ${nowIso})`;
|
|
1804
|
+
return or(
|
|
1805
|
+
eq(providerAccounts.status, "cooldown"),
|
|
1806
|
+
sql`${providerAccounts.cooldownUntil} > ${nowIso}`,
|
|
1807
|
+
liveModelCooldown,
|
|
1808
|
+
) as SQL;
|
|
1809
|
+
}
|
|
1658
1810
|
|
|
1659
1811
|
async syncModels(tenantId: string, providerId: string): Promise<{ synced: number }> {
|
|
1660
1812
|
return this.probing.syncModels(tenantId, providerId);
|
|
1661
1813
|
}
|
|
1814
|
+
|
|
1815
|
+
/** Read-only discovery; delegates to the same fetch path as sync (area 05). */
|
|
1816
|
+
async listDiscoveryCandidates(
|
|
1817
|
+
tenantId: string,
|
|
1818
|
+
providerId: string,
|
|
1819
|
+
): Promise<{
|
|
1820
|
+
readonly supported: boolean;
|
|
1821
|
+
readonly candidates: readonly DiscoverModelCandidate[];
|
|
1822
|
+
readonly accountLabel?: string;
|
|
1823
|
+
}> {
|
|
1824
|
+
const discovery = await this.probing.discoverModels(tenantId, providerId);
|
|
1825
|
+
if (!discovery.supported) return { supported: false, candidates: [] };
|
|
1826
|
+
const registered = await this.listModelsForTenant(tenantId, providerId);
|
|
1827
|
+
const ids = new Set(
|
|
1828
|
+
(registered.get(providerId) ?? []).map((entry) => entry.modelId),
|
|
1829
|
+
);
|
|
1830
|
+
// The picker exists to add models, so rows already on the roster are
|
|
1831
|
+
// excluded rather than listed disabled — a fully-registered catalog would
|
|
1832
|
+
// otherwise render an all-"Registered" list that reads as empty.
|
|
1833
|
+
const candidates: DiscoverModelCandidate[] = discovery.models
|
|
1834
|
+
.filter((definition) => !ids.has(definition.modelId))
|
|
1835
|
+
.map((definition) => ({
|
|
1836
|
+
modelId: definition.modelId,
|
|
1837
|
+
wireFamily: definition.wireFamily,
|
|
1838
|
+
serviceKind: definition.serviceKind ?? "llm",
|
|
1839
|
+
endpointPath: definition.endpointPath,
|
|
1840
|
+
contextLimit: definition.contextLimit,
|
|
1841
|
+
outputLimit: definition.outputLimit,
|
|
1842
|
+
modalities: definition.modalities,
|
|
1843
|
+
reasoning: definition.reasoning,
|
|
1844
|
+
toolCall: definition.toolCall,
|
|
1845
|
+
...(definition.freeTier === undefined ? {} : { freeTier: definition.freeTier }),
|
|
1846
|
+
...(definition.proOnly === undefined ? {} : { proOnly: definition.proOnly }),
|
|
1847
|
+
alreadyRegistered: false,
|
|
1848
|
+
}));
|
|
1849
|
+
return {
|
|
1850
|
+
supported: true,
|
|
1851
|
+
candidates,
|
|
1852
|
+
...(discovery.accountLabel === undefined ? {} : { accountLabel: discovery.accountLabel }),
|
|
1853
|
+
};
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
/**
|
|
1857
|
+
* Re-runs discovery on the server, then persists only ids the fresh roster
|
|
1858
|
+
* actually advertised (D28/D29/D30/D31). This is the anti-forgery gate: a
|
|
1859
|
+
* caller cannot attach a hand-built metadata object to an arbitrary id.
|
|
1860
|
+
*/
|
|
1861
|
+
async commitDiscoveredModels(
|
|
1862
|
+
tenantId: string,
|
|
1863
|
+
providerId: string,
|
|
1864
|
+
modelIds: readonly string[],
|
|
1865
|
+
): Promise<{
|
|
1866
|
+
readonly committed: readonly string[];
|
|
1867
|
+
readonly skipped: readonly string[];
|
|
1868
|
+
readonly synced: number;
|
|
1869
|
+
}> {
|
|
1870
|
+
const discovery = await this.probing.discoverModels(tenantId, providerId);
|
|
1871
|
+
if (!discovery.supported) {
|
|
1872
|
+
throw new ConsoleDomainError(
|
|
1873
|
+
"model_discovery_unsupported",
|
|
1874
|
+
400,
|
|
1875
|
+
`${providerId} publishes no model-listing endpoint; add the model manually`,
|
|
1876
|
+
);
|
|
1877
|
+
}
|
|
1878
|
+
const byId = new Map(discovery.models.map((definition) => [definition.modelId, definition]));
|
|
1879
|
+
const wanted = [...new Set(modelIds)];
|
|
1880
|
+
const unknown = wanted.filter((id) => !byId.has(id));
|
|
1881
|
+
if (unknown.length > 0) {
|
|
1882
|
+
throw new ConsoleDomainError(
|
|
1883
|
+
"invalid_request",
|
|
1884
|
+
400,
|
|
1885
|
+
`Models are no longer in the provider's listing: ${unknown.join(", ")} (request {requestId}). Fetch the model list again 🪞`,
|
|
1886
|
+
);
|
|
1887
|
+
}
|
|
1888
|
+
const registered = await this.listModelsForTenant(tenantId, providerId);
|
|
1889
|
+
const existing = new Set(
|
|
1890
|
+
(registered.get(providerId) ?? []).map((entry) => entry.modelId),
|
|
1891
|
+
);
|
|
1892
|
+
const selected = wanted.filter((id) => !existing.has(id));
|
|
1893
|
+
const skipped = wanted.filter((id) => existing.has(id));
|
|
1894
|
+
if (selected.length === 0) return { committed: [], skipped, synced: 0 };
|
|
1895
|
+
const { synced } = await this.probing.commitDiscoveredModels(
|
|
1896
|
+
tenantId,
|
|
1897
|
+
providerId,
|
|
1898
|
+
selected,
|
|
1899
|
+
);
|
|
1900
|
+
return { committed: selected, skipped, synced };
|
|
1901
|
+
}
|
|
1662
1902
|
}
|