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
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static in-code registry of provider × category × tier cooldown rules (D19).
|
|
3
|
+
*
|
|
4
|
+
* The registry ships empty by default: before a rule is filled in, behavior is
|
|
5
|
+
* byte-identical to the old hardcoded constants. Rules are keyed by
|
|
6
|
+
* `(providerId, category, tier)` and, when present, replace the provider's old
|
|
7
|
+
* cooldown constant for that combination — they sit between the provider-stated
|
|
8
|
+
* window (which always wins, D18) and the old constant/env fallback.
|
|
9
|
+
*
|
|
10
|
+
* The initial content is the example from the approved proposal (part 03):
|
|
11
|
+
* Trae's daily-quota verdict (`model_unreachable`, upstream 4008) cools a free
|
|
12
|
+
* account for a full day and a paid/pro account for 15 minutes (D17 — `paid`
|
|
13
|
+
* never gets `0`, which would hammer the upstream on a wrong verdict).
|
|
14
|
+
*/
|
|
15
|
+
import type { AccountErrorCategory } from "./account-error-classification";
|
|
16
|
+
import type { AccountTier } from "./account-tier";
|
|
17
|
+
|
|
18
|
+
export type CooldownRuleKey = {
|
|
19
|
+
readonly providerId: string;
|
|
20
|
+
readonly category: AccountErrorCategory;
|
|
21
|
+
readonly tier: AccountTier;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type TierRules = Partial<Record<NonNullable<AccountTier>, number>>;
|
|
25
|
+
type CategoryRules = Partial<Record<AccountErrorCategory, TierRules>>;
|
|
26
|
+
type ProviderRules = Record<string, CategoryRules>;
|
|
27
|
+
|
|
28
|
+
const DAY = 24 * 60 * 60 * 1000;
|
|
29
|
+
const MINUTE = 60 * 1000;
|
|
30
|
+
|
|
31
|
+
const COOLDOWN_RULES: ProviderRules = {
|
|
32
|
+
trae: {
|
|
33
|
+
model_unreachable: {
|
|
34
|
+
free: DAY,
|
|
35
|
+
paid: 15 * MINUTE,
|
|
36
|
+
pro: 15 * MINUTE,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
// Trae CN is the same upstream product under a second realm id — the
|
|
40
|
+
// classifier already treats the two identically (trae-region-unavailable,
|
|
41
|
+
// trae-quota-exhausted), so the rules mirror.
|
|
42
|
+
traecn: {
|
|
43
|
+
model_unreachable: {
|
|
44
|
+
free: DAY,
|
|
45
|
+
paid: 15 * MINUTE,
|
|
46
|
+
pro: 15 * MINUTE,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolves a cooldown rule for `(providerId, category, tier)`.
|
|
53
|
+
*
|
|
54
|
+
* Returns `undefined` when no rule exists — for an unknown provider, an
|
|
55
|
+
* unstored category, or an unknown tier — which is the signal to keep the old
|
|
56
|
+
* constant/fallback behavior.
|
|
57
|
+
*/
|
|
58
|
+
export function resolveCooldownRule(key: CooldownRuleKey): number | undefined {
|
|
59
|
+
if (key.tier === null || key.tier === undefined) return undefined;
|
|
60
|
+
return COOLDOWN_RULES[key.providerId.toLowerCase()]?.[key.category]?.[key.tier];
|
|
61
|
+
}
|
|
@@ -60,6 +60,9 @@ export async function seedBundledModels(
|
|
|
60
60
|
// states the tier, so the flag is carried rather than derived.
|
|
61
61
|
freeTier: definition.freeTier ?? null,
|
|
62
62
|
dialect: definition.dialect ?? null,
|
|
63
|
+
// Display-only: a static catalog that declares no warnings clears any
|
|
64
|
+
// stale value on the next boot rather than leaving an old banner.
|
|
65
|
+
warningMetadata: definition.warnings ?? null,
|
|
63
66
|
source: "builtin",
|
|
64
67
|
sourceUpdatedAt: now,
|
|
65
68
|
enabled: true,
|
|
@@ -90,6 +93,7 @@ export async function seedBundledModels(
|
|
|
90
93
|
proOnly: sql`excluded.pro_only`,
|
|
91
94
|
freeTier: sql`excluded.free_tier`,
|
|
92
95
|
dialect: sql`excluded.dialect`,
|
|
96
|
+
warningMetadata: sql`excluded.warning_metadata`,
|
|
93
97
|
source: sql`'builtin'`,
|
|
94
98
|
sourceUpdatedAt: sql`excluded.source_updated_at`,
|
|
95
99
|
},
|
|
@@ -187,6 +187,13 @@ export interface ProviderDispatchContext {
|
|
|
187
187
|
readonly outbound_attachment_fetch?: ValidatedOutboundFetch | undefined;
|
|
188
188
|
/** Validated bidirectional HTTP/2 egress bound to the same selected network pool. */
|
|
189
189
|
readonly outbound_http2?: ValidatedOutboundHttp2Bidi | undefined;
|
|
190
|
+
/**
|
|
191
|
+
* Capture sink for one attempt's provider exchange, filled by HTTP fetch
|
|
192
|
+
* wrapping. Adapters whose real upstream transport is not a `fetch`
|
|
193
|
+
* (Qoder's gRPC) write it directly so Request Detail shows the actual
|
|
194
|
+
* conversation call rather than an auth exchange or an empty panel.
|
|
195
|
+
*/
|
|
196
|
+
readonly provider_capture?: ProviderCaptureExchange | undefined;
|
|
190
197
|
/**
|
|
191
198
|
* Route-selected fallback User-Agent from Routing Strategy; set when the
|
|
192
199
|
* operator configured one (bundled API-key providers default to the Claude
|
|
@@ -261,6 +268,20 @@ export function applyRouteUserAgent(
|
|
|
261
268
|
}
|
|
262
269
|
|
|
263
270
|
/** ProviderDispatchTarget identity already resolved by routing before credential leasing. */
|
|
271
|
+
/**
|
|
272
|
+
* Capture sink for one attempt's provider exchange.
|
|
273
|
+
*
|
|
274
|
+
* HTTP transports fill this automatically by wrapping `outbound_fetch` (see
|
|
275
|
+
* `captureProviderExchange` in `transport/dispatch/attempt-finalize.ts`).
|
|
276
|
+
* Adapters whose real upstream call is not a fetch — Qoder's gRPC surface —
|
|
277
|
+
* write it directly so Request Detail shows the actual conversation call
|
|
278
|
+
* instead of an auth exchange or nothing.
|
|
279
|
+
*/
|
|
280
|
+
export interface ProviderCaptureExchange {
|
|
281
|
+
request: unknown;
|
|
282
|
+
response: Promise<unknown>;
|
|
283
|
+
}
|
|
284
|
+
|
|
264
285
|
export interface ProviderDispatchTarget {
|
|
265
286
|
readonly provider_id: ProviderId;
|
|
266
287
|
readonly model_id: string;
|
|
@@ -287,6 +308,14 @@ export interface ProviderAdapter {
|
|
|
287
308
|
candidate: ProviderDispatchTarget,
|
|
288
309
|
context: ProviderDispatchContext,
|
|
289
310
|
): AsyncIterable<CanonicalEvent>;
|
|
311
|
+
/**
|
|
312
|
+
* Optional provider-mandated system prompt, returned for probes and any
|
|
313
|
+
* caller that needs the upstream's required identity prompt. A provider
|
|
314
|
+
* whose wire refuses requests without a specific system prompt (Perch's
|
|
315
|
+
* Saffron fingerprint) declares it here so the probe path can carry it —
|
|
316
|
+
* the probe builder otherwise sends only the user message.
|
|
317
|
+
*/
|
|
318
|
+
systemPrompt?(): string | undefined;
|
|
290
319
|
/**
|
|
291
320
|
* Optional native-protocol capability for the public `POST /v1/systemone`
|
|
292
321
|
* route (the dispatch leg of the System One service, alongside the probing
|
|
@@ -32,6 +32,14 @@ export interface CompatibilityProfile {
|
|
|
32
32
|
* bundled specs declare it on `ApiKeyProviderSpec.gatewayUserAgent`.
|
|
33
33
|
*/
|
|
34
34
|
readonly gateway_user_agent?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Which service surfaces this custom provider may serve. Absent means every
|
|
37
|
+
* kind is enabled (the historical default). The operator narrows this by
|
|
38
|
+
* checking only the capabilities their upstream actually answers — agentic
|
|
39
|
+
* (LLM) is always on when the list is present, and a kind the upstream
|
|
40
|
+
* cannot serve stays out of the provider's tab set.
|
|
41
|
+
*/
|
|
42
|
+
readonly service_kinds?: readonly ServiceKind[];
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
/** Identity and routing defaults for every builtin provider. */
|
|
@@ -144,6 +152,20 @@ const RAW_BUNDLED_PROVIDER_METADATA = [
|
|
|
144
152
|
credentialUrl: "https://cursor.com",
|
|
145
153
|
credentialHint: "Sign in with Cursor or import a Cursor API key.",
|
|
146
154
|
},
|
|
155
|
+
{
|
|
156
|
+
id: "devin",
|
|
157
|
+
displayName: "Devin",
|
|
158
|
+
baseUrl: "https://server.codeium.com",
|
|
159
|
+
wireFamilyDefault: "chat",
|
|
160
|
+
requiresAccount: true,
|
|
161
|
+
bespokeWire: true,
|
|
162
|
+
hasAdapterUserAgent: true,
|
|
163
|
+
// Devin's upstream is a Connect-RPC protobuf service with its own framing;
|
|
164
|
+
// an egress proxy in front of it breaks the stream shape. Direct only.
|
|
165
|
+
defaultSkipProxy: true,
|
|
166
|
+
credentialUrl: "https://devin.ai",
|
|
167
|
+
credentialHint: "Sign in with Devin, Windsurf Enterprise, or import a Devin session token / Windsurf seat key.",
|
|
168
|
+
},
|
|
147
169
|
{ id: "kimi", displayName: "Kimi Code", baseUrl: "https://api.kimi.com/coding", credentialUrl: "https://platform.moonshot.ai/console/api-keys" },
|
|
148
170
|
{ id: "opencode", displayName: "OpenCode Free", baseUrl: "https://opencode.ai", requiresAccount: false, ipScopedRateLimit: true, hasAdapterUserAgent: true },
|
|
149
171
|
{ id: "opencode-zen", displayName: "OpenCode Zen", baseUrl: "https://opencode.ai", hasAdapterUserAgent: true, credentialUrl: "https://opencode.ai/auth" },
|
|
@@ -344,6 +366,7 @@ const RAW_BUNDLED_PROVIDER_METADATA = [
|
|
|
344
366
|
// User-Agent. Letting the operator overwrite it would let them break the
|
|
345
367
|
// provider, so the console locks the control.
|
|
346
368
|
hasAdapterUserAgent: true,
|
|
369
|
+
credentialHint: "Paste a Nous Portal API key (portal.nousresearch.com → API keys), or sign in with the Nous Portal device code (OAuth) for the subscription route.",
|
|
347
370
|
},
|
|
348
371
|
{ id: "bai", displayName: "B.AI", baseUrl: "https://api.b.ai/v1", credentialUrl: "https://b.ai" },
|
|
349
372
|
{
|
|
@@ -367,8 +390,6 @@ const RAW_BUNDLED_PROVIDER_METADATA = [
|
|
|
367
390
|
{ id: "cline", displayName: "Cline", baseUrl: "https://api.cline.bot/api/v1", hasAdapterUserAgent: true, defaultSkipProxy: true, credentialUrl: "https://app.cline.bot" },
|
|
368
391
|
{ id: "cb", displayName: "CodeBuddy", baseUrl: "https://www.codebuddy.ai/v2", hasAdapterUserAgent: true, serviceKinds: ["llm", "imageGeneration", "videoGeneration"], credentialUrl: "https://www.codebuddy.ai", credentialHint: "Sign in with the same CodeBuddy account you use in the desktop app." },
|
|
369
392
|
{ id: "cbcn", displayName: "CodeBuddy CN", baseUrl: "https://copilot.tencent.com/v2", hasAdapterUserAgent: true, credentialUrl: "https://copilot.tencent.com", credentialHint: "Sign in with the same CodeBuddy China account you use in the desktop app." },
|
|
370
|
-
{ id: "workbuddy", displayName: "WorkBuddy", baseUrl: "https://www.workbuddy.ai", hasAdapterUserAgent: true, serviceKinds: ["llm", "imageGeneration", "videoGeneration"], credentialUrl: "https://www.workbuddy.ai", credentialHint: "Sign in with the same WorkBuddy account you use in the desktop app." },
|
|
371
|
-
{ id: "workbuddycn", displayName: "WorkBuddy CN", baseUrl: "https://www.workbuddy.cn", hasAdapterUserAgent: true, serviceKinds: ["llm", "imageGeneration", "videoGeneration"], credentialUrl: "https://www.workbuddy.cn", credentialHint: "Sign in with the same WorkBuddy China account you use in the desktop app." },
|
|
372
393
|
{
|
|
373
394
|
// Kilo Code resells the OpenRouter catalog. The base URL carries the API's
|
|
374
395
|
// path prefix (`/api/openrouter`) and every model row carries only the wire
|
|
@@ -432,6 +453,22 @@ const RAW_BUNDLED_PROVIDER_METADATA = [
|
|
|
432
453
|
credentialUrl: "https://www.trae.com.cn/",
|
|
433
454
|
credentialHint: "Paste a Trae CN account export (accessToken + refreshToken) or the raw access token from a Trae China account.",
|
|
434
455
|
},
|
|
456
|
+
{
|
|
457
|
+
id: "freepi",
|
|
458
|
+
displayName: "Free-pi",
|
|
459
|
+
baseUrl: "https://api.freepi.ai/v1/chat/completions",
|
|
460
|
+
defaultSkipProxy: true,
|
|
461
|
+
credentialUrl: "https://freepi.ai",
|
|
462
|
+
credentialHint: "Sign in with a GitHub device code (free-pi login), or paste a free-pi JWT issued by the backend.",
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
id: "perch",
|
|
466
|
+
displayName: "Perch",
|
|
467
|
+
baseUrl: "https://app.perchai.app",
|
|
468
|
+
defaultSkipProxy: true,
|
|
469
|
+
credentialUrl: "https://chat.perchai.app",
|
|
470
|
+
credentialHint: "Sign in with Google or GitHub (Perch account). Starter includes Roost Standard and Standard 1M.",
|
|
471
|
+
},
|
|
435
472
|
{ id: "ollamacloud", displayName: "Ollama Cloud", baseUrl: "https://ollama.com/v1", credentialUrl: "https://ollama.com/settings/keys" },
|
|
436
473
|
{ id: "gemini", displayName: "Google Gemini", baseUrl: "https://generativelanguage.googleapis.com/v1beta", serviceKinds: ["llm", "websearch", "imageGeneration"], credentialUrl: "https://aistudio.google.com/app/apikey" },
|
|
437
474
|
{ id: "xiaomipg", displayName: "Xiaomi MiMo (PAYG)", baseUrl: "https://api.xiaomimimo.com/v1", credentialUrl: "https://platform.xiaomimimo.com" },
|
|
@@ -472,7 +509,7 @@ const RAW_BUNDLED_PROVIDER_METADATA = [
|
|
|
472
509
|
export type BundledProviderId = (typeof RAW_BUNDLED_PROVIDER_METADATA)[number]["id"];
|
|
473
510
|
|
|
474
511
|
/**
|
|
475
|
-
* The Buddy family: CodeBuddy
|
|
512
|
+
* The Buddy family: CodeBuddy and its China tenant.
|
|
476
513
|
*
|
|
477
514
|
* They share one billing and credit system, so several layers need to ask "is
|
|
478
515
|
* this a Buddy provider" — the health recorder, the request preparer, and the
|
|
@@ -480,7 +517,7 @@ export type BundledProviderId = (typeof RAW_BUNDLED_PROVIDER_METADATA)[number]["
|
|
|
480
517
|
* get added in one place and silently missed in the others. Declared here
|
|
481
518
|
* beside the identities it names; the layers keep their own *policies*.
|
|
482
519
|
*/
|
|
483
|
-
export const BUDDY_PROVIDER_IDS: ReadonlySet<string> = new Set(["cb", "cbcn"
|
|
520
|
+
export const BUDDY_PROVIDER_IDS: ReadonlySet<string> = new Set(["cb", "cbcn"]);
|
|
484
521
|
|
|
485
522
|
/**
|
|
486
523
|
* Per-provider JWT verification defaults for issued OAuth access tokens. An
|
|
@@ -650,6 +687,24 @@ export function providerServiceKinds(providerId: string): readonly ServiceKind[]
|
|
|
650
687
|
return SERVICE_KINDS;
|
|
651
688
|
}
|
|
652
689
|
|
|
690
|
+
/**
|
|
691
|
+
* Resolves the service kinds for a custom (BYOK) provider row.
|
|
692
|
+
*
|
|
693
|
+
* The operator may narrow the surface set via `compatibilityProfile.service_kinds`
|
|
694
|
+
* (the Advanced Capabilities checkboxes in the Add Custom Provider modal). Absent,
|
|
695
|
+
* every kind is allowed — the historical default. A present list never drops
|
|
696
|
+
* `llm` (agentic is the floor of a custom provider).
|
|
697
|
+
*/
|
|
698
|
+
export function customProviderServiceKinds(
|
|
699
|
+
compatibilityProfile: CompatibilityProfile | null | undefined,
|
|
700
|
+
): readonly ServiceKind[] {
|
|
701
|
+
const kinds = compatibilityProfile?.service_kinds;
|
|
702
|
+
if (kinds === undefined || kinds.length === 0) return SERVICE_KINDS;
|
|
703
|
+
const set = new Set<ServiceKind>(SERVICE_KINDS.filter((kind) => kinds.includes(kind)));
|
|
704
|
+
set.add("llm");
|
|
705
|
+
return SERVICE_KINDS.filter((kind) => set.has(kind));
|
|
706
|
+
}
|
|
707
|
+
|
|
653
708
|
|
|
654
709
|
/** Canonical builtin base URL used by adapters, discovery, and quota clients. */
|
|
655
710
|
export function providerBaseUrl(providerId: string): string {
|
|
@@ -33,6 +33,37 @@ export const FREE_TIER_COST: ModelCostDefinition = {
|
|
|
33
33
|
pricing_model: "pay-per-use",
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Severity of one display-only model warning. `warn` is the default so a
|
|
38
|
+
* catalog that omits severity does not silently render a critical banner.
|
|
39
|
+
*/
|
|
40
|
+
export type ModelWarningSeverity = "info" | "warn" | "critical";
|
|
41
|
+
|
|
42
|
+
/** Severity values accepted on the wire, as a runtime tuple. */
|
|
43
|
+
export const MODEL_WARNING_SEVERITIES = ["info", "warn", "critical"] as const;
|
|
44
|
+
|
|
45
|
+
/** Longest accepted warning `kind` / `message`, in UTF-16 code units. */
|
|
46
|
+
export const MODEL_WARNING_KIND_MAX_LENGTH = 64;
|
|
47
|
+
export const MODEL_WARNING_MESSAGE_MAX_LENGTH = 500;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* One display-only warning attached to a model row. `kind` is an open string
|
|
51
|
+
* with three defined values (`fallback_model`, `cache_reliability`,
|
|
52
|
+
* `model_refusal`) so a new category needs no migration. Persisted as
|
|
53
|
+
* `{ warnings: ModelWarning[] }` in `models.warning_metadata`; never read by
|
|
54
|
+
* routing.
|
|
55
|
+
*/
|
|
56
|
+
export interface ModelWarning {
|
|
57
|
+
/** Stable machine-readable category; open vocabulary. */
|
|
58
|
+
readonly kind: string;
|
|
59
|
+
/** Operator-facing text. Provider-level claims use "may". */
|
|
60
|
+
readonly message: string;
|
|
61
|
+
/** Defaults to `warn` when absent. */
|
|
62
|
+
readonly severity?: ModelWarningSeverity;
|
|
63
|
+
/** ISO timestamp of the last edit, when known. */
|
|
64
|
+
readonly updatedAt?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
36
67
|
/** Canonical static metadata for one provider model. */
|
|
37
68
|
export interface ModelDefinition {
|
|
38
69
|
readonly modelId: string;
|
|
@@ -118,6 +149,8 @@ export interface ModelDefinition {
|
|
|
118
149
|
* turns a gap in our catalog into a claim about someone else's model.
|
|
119
150
|
*/
|
|
120
151
|
readonly limitsEstimated?: boolean;
|
|
152
|
+
/** Display-only warnings carried from a static catalog into the seeded row. */
|
|
153
|
+
readonly warnings?: readonly ModelWarning[];
|
|
121
154
|
}
|
|
122
155
|
|
|
123
156
|
/**
|
|
@@ -205,6 +238,8 @@ export interface ModelHelperRow {
|
|
|
205
238
|
readonly proOnly?: boolean;
|
|
206
239
|
/** Explicit wire dialect override for this row. See `ModelDefinition.dialect`. */
|
|
207
240
|
readonly dialect?: DialectId;
|
|
241
|
+
/** Display-only warnings to carry onto the built row. */
|
|
242
|
+
readonly warnings?: readonly ModelWarning[];
|
|
208
243
|
}
|
|
209
244
|
|
|
210
245
|
function defaultEndpoint(wireFamily: ModelDefinition["wireFamily"]): string {
|
|
@@ -309,6 +344,7 @@ export function defineModel(row: ModelHelperRow): ModelDefinition {
|
|
|
309
344
|
...(row.proOnly === undefined ? {} : { proOnly: row.proOnly }),
|
|
310
345
|
...(row.dialect === undefined ? {} : { dialect: row.dialect }),
|
|
311
346
|
...(limitsEstimated ? { limitsEstimated: true } : {}),
|
|
347
|
+
...(row.warnings === undefined ? {} : { warnings: row.warnings }),
|
|
312
348
|
};
|
|
313
349
|
}
|
|
314
350
|
|
|
@@ -11,6 +11,7 @@ import type { CartethyiaDatabase } from "../../persistence/db-handle";
|
|
|
11
11
|
import { providerAccounts } from "../../persistence/schema";
|
|
12
12
|
import type { RedisClient } from "../../persistence/redis";
|
|
13
13
|
import { log } from "../../observability/logger";
|
|
14
|
+
import { pushStructuredConsoleLog } from "../../observability/log-ring";
|
|
14
15
|
import { fetchProviderQuota } from "./quota-support";
|
|
15
16
|
import { type FetchLike, type ProviderQuotaResult } from "./quota-contracts";
|
|
16
17
|
import type { ProviderId } from "../provider-contracts";
|
|
@@ -261,6 +262,12 @@ const TARGET_COLUMNS = {
|
|
|
261
262
|
* disabled account is not necessarily revoked — it may have been parked for a
|
|
262
263
|
* reason unrelated to its credential — and skipping it would let a credential
|
|
263
264
|
* die silently while the row still reads healthy.
|
|
265
|
+
*
|
|
266
|
+
* Invariant (area 04, D21): this sweep **never flips status**. The refresh it
|
|
267
|
+
* kicks off rewrites only credential/expiry columns; `recordAccountCheck` is
|
|
268
|
+
* active-only, so `disabled` rows are identical after the pass to before. The
|
|
269
|
+
* `origin: "sweep:quota"` tag on the token-refresh log lines (D26) is what
|
|
270
|
+
* lets an operator verify that in the Console Log.
|
|
264
271
|
*/
|
|
265
272
|
export async function listQuotaRefreshTargets(
|
|
266
273
|
db: CartethyiaDatabase,
|
|
@@ -343,6 +350,20 @@ async function runQuotaRefresh(
|
|
|
343
350
|
let resolveError: string | undefined;
|
|
344
351
|
try {
|
|
345
352
|
credential = await deps.resolveCredential(target.providerId as ProviderId, target.accountId);
|
|
353
|
+
// D26: every OAuth credential this sweep resolves may have been refreshed
|
|
354
|
+
// inside the refresh-aware resolver. Label that refresh with its origin so
|
|
355
|
+
// the Console Log can prove the sweep touches disabled accounts without
|
|
356
|
+
// ever flipping their status. The refresh service itself also logs the
|
|
357
|
+
// actual token refresh under the `sweep` origin family; this line names
|
|
358
|
+
// the sweep here, at the source of the trigger.
|
|
359
|
+
if (credential !== "" && target.credentialKind === "oauth") {
|
|
360
|
+
pushStructuredConsoleLog("debug", "Quota sweep resolved OAuth credential; token may have been refreshed", {
|
|
361
|
+
event: "token_refresh",
|
|
362
|
+
accountId: target.accountId,
|
|
363
|
+
providerId: target.providerId,
|
|
364
|
+
details: { origin: "sweep:quota" },
|
|
365
|
+
});
|
|
366
|
+
}
|
|
346
367
|
} catch (error) {
|
|
347
368
|
resolveError = error instanceof Error ? error.message : "Failed to resolve credential";
|
|
348
369
|
}
|
package/src/security/abuse.ts
CHANGED
|
@@ -113,6 +113,15 @@ export interface IpAbuseStore {
|
|
|
113
113
|
* bounded `admission_unavailable` with zero dispatches.
|
|
114
114
|
*/
|
|
115
115
|
checkAndRecord(attempt: IpAbuseAttempt): Promise<IpAbuseOutcome>;
|
|
116
|
+
/**
|
|
117
|
+
* Manually bans an identity for `durationMs` (0/negative = permanent).
|
|
118
|
+
* Throws when the store is unreachable.
|
|
119
|
+
*/
|
|
120
|
+
ban(identity: string, durationMs: number): Promise<void>;
|
|
121
|
+
/** Lifts a manual (or any) ban on an identity. No-op when not banned. */
|
|
122
|
+
unban(identity: string): Promise<void>;
|
|
123
|
+
/** Lists currently active bans with their expiry (`null` = permanent). */
|
|
124
|
+
listBans(): Promise<readonly { identity: string; bannedUntil: number | null }[]>;
|
|
116
125
|
}
|
|
117
126
|
|
|
118
127
|
interface IpAbuseProtectionServiceOptions {
|
|
@@ -293,6 +302,34 @@ export class InMemoryIpAbuseStore implements IpAbuseStore {
|
|
|
293
302
|
return true;
|
|
294
303
|
}
|
|
295
304
|
|
|
305
|
+
async ban(identity: string, durationMs: number): Promise<void> {
|
|
306
|
+
this.assertAvailable();
|
|
307
|
+
this.evictOldest(this.bans);
|
|
308
|
+
this.bans.set(identity, durationMs > 0 ? this.clock() + durationMs : Number.POSITIVE_INFINITY);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async unban(identity: string): Promise<void> {
|
|
312
|
+
this.assertAvailable();
|
|
313
|
+
this.bans.delete(identity);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async listBans(): Promise<readonly { identity: string; bannedUntil: number | null }[]> {
|
|
317
|
+
this.assertAvailable();
|
|
318
|
+
const now = this.clock();
|
|
319
|
+
const out: { identity: string; bannedUntil: number | null }[] = [];
|
|
320
|
+
for (const [identity, until] of this.bans) {
|
|
321
|
+
if (until <= now) {
|
|
322
|
+
this.bans.delete(identity);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
out.push({
|
|
326
|
+
identity,
|
|
327
|
+
bannedUntil: Number.isFinite(until) ? until : null,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
return out;
|
|
331
|
+
}
|
|
332
|
+
|
|
296
333
|
async checkAndRecord(attempt: IpAbuseAttempt): Promise<IpAbuseOutcome> {
|
|
297
334
|
this.assertAvailable();
|
|
298
335
|
const { identity, route, now, limit, banThreshold, banDurationMs } = attempt;
|
|
@@ -582,5 +619,39 @@ export class RedisIpAbuseStore implements IpAbuseStore {
|
|
|
582
619
|
);
|
|
583
620
|
return decodeAbuseOutcome(raw);
|
|
584
621
|
}
|
|
622
|
+
|
|
623
|
+
async ban(identity: string, durationMs: number): Promise<void> {
|
|
624
|
+
const marker = this.banMarkerKey(identity);
|
|
625
|
+
const now = Date.now();
|
|
626
|
+
if (durationMs > 0) {
|
|
627
|
+
await this.redis.set(marker, String(now + durationMs), "PX", durationMs);
|
|
628
|
+
} else {
|
|
629
|
+
// Permanent: no TTL; a large sentinel far in the future.
|
|
630
|
+
await this.redis.set(marker, String(Number.MAX_SAFE_INTEGER));
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async unban(identity: string): Promise<void> {
|
|
635
|
+
await this.redis.del(this.banMarkerKey(identity));
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async listBans(): Promise<readonly { identity: string; bannedUntil: number | null }[]> {
|
|
639
|
+
const keys = await this.redis.keys("cartethyia:ip:ban:*");
|
|
640
|
+
const now = Date.now();
|
|
641
|
+
const out: { identity: string; bannedUntil: number | null }[] = [];
|
|
642
|
+
for (const key of keys) {
|
|
643
|
+
const value = await this.redis.get(key);
|
|
644
|
+
const until = Number(value ?? 0);
|
|
645
|
+
if (!Number.isFinite(until) || until <= now) {
|
|
646
|
+
await this.redis.del(key);
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
out.push({
|
|
650
|
+
identity: key.slice("cartethyia:ip:ban:".length),
|
|
651
|
+
bannedUntil: until >= Number.MAX_SAFE_INTEGER - 1 ? null : until,
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
return out;
|
|
655
|
+
}
|
|
585
656
|
}
|
|
586
657
|
|
|
@@ -263,6 +263,7 @@ export async function handleProviderProxyRequest(
|
|
|
263
263
|
...(conversationAffinity ? { conversationAffinity } : {}),
|
|
264
264
|
...(tenantSystemPromptText === undefined ? {} : { tenantSystemPrompt: tenantSystemPromptText }),
|
|
265
265
|
...(candidate.user_agent === undefined ? {} : { userAgent: candidate.user_agent }),
|
|
266
|
+
...(providerCapture ? { providerCapture } : {}),
|
|
266
267
|
...(deps.networkBindingFactory
|
|
267
268
|
? {
|
|
268
269
|
outboundFetch: captureProviderExchange(
|
|
@@ -249,6 +249,7 @@ export async function dispatchStreamingAttempt(input: StreamingDispatchInput): P
|
|
|
249
249
|
: {}),
|
|
250
250
|
...(leaseOwnership === undefined ? {} : { leaseOwnership }),
|
|
251
251
|
...(continuationScope === undefined ? {} : { continuationScope }),
|
|
252
|
+
...(providerCapture ? { providerCapture } : {}),
|
|
252
253
|
});
|
|
253
254
|
// `ProviderAdapter.dispatch` is declared to return a non-nullable
|
|
254
255
|
// `AsyncIterable`, so an adapter that returned `undefined` would be a
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CanonicalEvent } from "../../protocol/canonical-model";
|
|
2
2
|
import type { AttemptLeaseOwnership, ValidatedOutboundFetch, ValidatedOutboundHttp2Bidi } from "../../providers/provider-contracts";
|
|
3
|
-
import type { ProviderContinuationScope } from "../../providers/provider-contracts";
|
|
3
|
+
import type { ProviderCaptureExchange, ProviderContinuationScope } from "../../providers/provider-contracts";
|
|
4
4
|
import { GATEWAY_SECURITY_HEADERS } from "../../security/outbound-headers";
|
|
5
5
|
|
|
6
6
|
const FORWARDED_REQUEST_HEADERS = new Set([
|
|
@@ -42,6 +42,8 @@ export function buildUpstreamDispatchContext(input: {
|
|
|
42
42
|
readonly requestId?: string;
|
|
43
43
|
readonly userAgent?: string;
|
|
44
44
|
readonly outboundFetch?: ValidatedOutboundFetch;
|
|
45
|
+
/** Capture sink for provider exchanges not visible to the HTTP fetch wrapper. */
|
|
46
|
+
readonly providerCapture?: ProviderCaptureExchange;
|
|
45
47
|
/** Validated attachment egress that must not be wrapped by provider payload capture. */
|
|
46
48
|
readonly outboundAttachmentFetch?: ValidatedOutboundFetch;
|
|
47
49
|
/** Validated bidirectional HTTP/2 egress bound to the same selected pool. */
|
|
@@ -100,6 +102,7 @@ export function buildUpstreamDispatchContext(input: {
|
|
|
100
102
|
...(userAgent ? { user_agent: userAgent } : {}),
|
|
101
103
|
...(Object.keys(input.headers).length > 0 ? { request_headers: input.headers } : {}),
|
|
102
104
|
...(outboundFetch ? { outbound_fetch: outboundFetch } : {}),
|
|
105
|
+
...(input.providerCapture ? { provider_capture: input.providerCapture } : {}),
|
|
103
106
|
...(input.outboundAttachmentFetch
|
|
104
107
|
? { outbound_attachment_fetch: input.outboundAttachmentFetch }
|
|
105
108
|
: {}),
|
|
@@ -14,6 +14,7 @@ import { resolveAccountQuotaCooldownMs } from "../config/cooldowns";
|
|
|
14
14
|
* runs a few KB; 4000 keeps the whole thing while bounding a hostile body.
|
|
15
15
|
*/
|
|
16
16
|
const MAX_UPSTREAM_ERROR_BYTES = 4000;
|
|
17
|
+
export { MAX_UPSTREAM_ERROR_BYTES };
|
|
17
18
|
export {
|
|
18
19
|
parseRetryAfter,
|
|
19
20
|
parseRateLimitReset,
|
|
@@ -22,6 +23,32 @@ export {
|
|
|
22
23
|
parseProviderResetDuration,
|
|
23
24
|
parseAbsoluteResetTimestamp,
|
|
24
25
|
} from "../protocol/upstream-parsing";
|
|
26
|
+
/**
|
|
27
|
+
* Extracts only the human-readable text from an upstream error body —
|
|
28
|
+
* `message`/`msg`/`error` fields — deliberately skipping `code`/identifier
|
|
29
|
+
* values. Used by `upstreamMessageAsIs` to decide whether the upstream
|
|
30
|
+
* actually supplied a message: a symbolic code like `"permission_error"` is
|
|
31
|
+
* an identifier, not prose, so it must not count as passthrough text.
|
|
32
|
+
*/
|
|
33
|
+
export function extractUpstreamMessageText(body: unknown): string {
|
|
34
|
+
let raw = "";
|
|
35
|
+
if (typeof body === "string") raw = body;
|
|
36
|
+
else if (isRecord(body)) {
|
|
37
|
+
if (typeof body.error === "string" && body.error.trim()) raw = body.error;
|
|
38
|
+
else {
|
|
39
|
+
const envelope = isRecord(body.error) ? body.error : body;
|
|
40
|
+
const message = (envelope as Record<string, unknown>).message;
|
|
41
|
+
if (typeof message === "string" && message.trim()) raw = message;
|
|
42
|
+
else if (typeof body.msg === "string" && body.msg.trim()) raw = body.msg;
|
|
43
|
+
else if (typeof (envelope as Record<string, unknown>).msg === "string" && String((envelope as Record<string, unknown>).msg).trim())
|
|
44
|
+
raw = String((envelope as Record<string, unknown>).msg);
|
|
45
|
+
else if (typeof (envelope as Record<string, unknown>).error === "string" && String((envelope as Record<string, unknown>).error).trim())
|
|
46
|
+
raw = String((envelope as Record<string, unknown>).error);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return raw.replace(/[\r\n]+/g, " ").trim();
|
|
50
|
+
}
|
|
51
|
+
|
|
25
52
|
/** Extracts a bounded, newline-free provider message from any error envelope. */
|
|
26
53
|
export function extractUpstreamMessage(body: unknown): string {
|
|
27
54
|
let raw = "";
|
|
@@ -696,6 +723,25 @@ const AUTH_ERROR_IDENTIFIERS: ReadonlySet<string> = new Set([
|
|
|
696
723
|
"insufficient_key_scope",
|
|
697
724
|
"model_not_allowed_for_key",
|
|
698
725
|
]);
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Qoder job-token verdicts that are NOT credential evidence.
|
|
729
|
+
*
|
|
730
|
+
* A PAT (`pt-…`) is exchanged for a short-lived job token (`jt-…`). When the
|
|
731
|
+
* job token expires mid-flight the upstream answers `{"code":"105","message":
|
|
732
|
+
* "Login expired"}` (403). The PAT is still valid — re-exchanging it mints a
|
|
733
|
+
* fresh job token — so treating this as `auth_invalidated` parked a healthy
|
|
734
|
+
* account as "Re-login required". It stays `authentication_failed` (retryable;
|
|
735
|
+
* the next attempt re-runs the PAT exchange) but without `credentialEvidence`,
|
|
736
|
+
* so the account is never disabled or cooled on it.
|
|
737
|
+
*/
|
|
738
|
+
const QODER_JOB_TOKEN_EXPIRED: ReadonlySet<string> = new Set([
|
|
739
|
+
"105",
|
|
740
|
+
"login expired",
|
|
741
|
+
"login_expired",
|
|
742
|
+
"job token expired",
|
|
743
|
+
"jobtoken expired",
|
|
744
|
+
]);
|
|
699
745
|
const PLATFORM_ERROR_IDENTIFIERS: ReadonlySet<string> = new Set([
|
|
700
746
|
"server_error",
|
|
701
747
|
"internal_error",
|
|
@@ -809,6 +855,16 @@ export function classifyUpstreamError(
|
|
|
809
855
|
origin: "upstream",
|
|
810
856
|
rateLimitScope: "provider",
|
|
811
857
|
};
|
|
858
|
+
// Qoder job-token expiry (`105` / "Login expired") is recoverable by
|
|
859
|
+
// re-exchanging the PAT — never credential evidence (see
|
|
860
|
+
// `QODER_JOB_TOKEN_EXPIRED`).
|
|
861
|
+
if (normalized !== undefined && QODER_JOB_TOKEN_EXPIRED.has(normalized)) {
|
|
862
|
+
return {
|
|
863
|
+
code: "authentication_failed",
|
|
864
|
+
status: status === 403 || status === 401 ? status : 403,
|
|
865
|
+
origin: "upstream",
|
|
866
|
+
};
|
|
867
|
+
}
|
|
812
868
|
if (normalized !== undefined && AUTH_ERROR_IDENTIFIERS.has(normalized)) {
|
|
813
869
|
let authStatus: number;
|
|
814
870
|
if (status === 401 || status === 403) authStatus = status;
|
|
@@ -919,17 +975,15 @@ export async function mapUpstreamHttpError(response: Response, providerId: strin
|
|
|
919
975
|
: isContextLengthFailure(parsedBody)
|
|
920
976
|
? { code: "context_length_exceeded" as const, status: 413, origin: "upstream" as const }
|
|
921
977
|
: classifyUpstreamError(response.status, identifier);
|
|
922
|
-
const
|
|
923
|
-
let message =
|
|
924
|
-
extracted.length > 0
|
|
925
|
-
? extracted
|
|
926
|
-
: text.trim() || `Provider returned HTTP ${response.status}.`;
|
|
978
|
+
const upstreamText = upstreamMessageAsIs(parsedBody ?? text, "");
|
|
979
|
+
let message = upstreamText.message || `Provider returned HTTP ${response.status}.`;
|
|
927
980
|
// A bare "Provider returned HTTP <5xx>" tells the operator nothing actionable;
|
|
928
981
|
// platform-unavailable fallbacks get the same human sign-off every other
|
|
929
|
-
// gateway error carries
|
|
930
|
-
// diagnostics either way.
|
|
982
|
+
// gateway error carries — but only when the upstream supplied no text of its
|
|
983
|
+
// own. The raw upstream body stays in `details.raw` for diagnostics either way.
|
|
931
984
|
if (
|
|
932
985
|
classification.code === "platform_unavailable" &&
|
|
986
|
+
!upstreamText.hadUpstreamText &&
|
|
933
987
|
message === `Provider returned HTTP ${response.status}.`
|
|
934
988
|
) {
|
|
935
989
|
message = `The provider's service is temporarily unavailable (HTTP ${response.status}) (request {requestId}). Your request did not go through — wait a moment and try again. Hold tight ☁️`;
|
|
@@ -949,6 +1003,7 @@ export async function mapUpstreamHttpError(response: Response, providerId: strin
|
|
|
949
1003
|
...(reportedProviderCode ? { providerCode: reportedProviderCode } : {}),
|
|
950
1004
|
...(requestId ? { upstreamRequestId: requestId } : {}),
|
|
951
1005
|
raw: text.slice(0, MAX_UPSTREAM_ERROR_BYTES),
|
|
1006
|
+
...(upstreamText.hadUpstreamText ? { upstreamMessageAsIs: true } : {}),
|
|
952
1007
|
...(retryAfterMs === null || retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
953
1008
|
...(queuedWaitMs !== undefined
|
|
954
1009
|
? { reason: "upstream-queued", cooldownMs: queuedWaitMs }
|
|
@@ -971,6 +1026,57 @@ export function extractHtmlTitle(html: string): string {
|
|
|
971
1026
|
return html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim().slice(0, 160);
|
|
972
1027
|
}
|
|
973
1028
|
|
|
1029
|
+
/** The public bound on an upstream message surface. */
|
|
1030
|
+
export const MAX_UPSTREAM_MESSAGE_LENGTH = 500;
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* Extracts the upstream's own text from an error body, "as-is".
|
|
1034
|
+
*
|
|
1035
|
+
* The upstream error passthrough contract: when a failure is unknown or
|
|
1036
|
+
* unparseable, the client should see whatever the upstream actually said,
|
|
1037
|
+
* not a generalized 502 sentence. Structured JSON envelopes already flow
|
|
1038
|
+
* through `extractUpstreamMessage`; this helper extends the same idea to the
|
|
1039
|
+
* shapes that used to lose the text entirely — HTML error pages (their
|
|
1040
|
+
* `<title>` is often the only readable thing) and plain-text bodies.
|
|
1041
|
+
*
|
|
1042
|
+
* The returned message is bounded to 500 chars (D44) and newline-collapsed,
|
|
1043
|
+
* matching `sanitizePublicDetail` per-string bound; `details.raw` keeps the
|
|
1044
|
+
* full 4000. `hadUpstreamText` distinguishes "the upstream said something"
|
|
1045
|
+
* from "we synthesized a fallback", so callers can keep their human sign-off
|
|
1046
|
+
* only when there is genuinely no upstream text.
|
|
1047
|
+
*/
|
|
1048
|
+
export function upstreamMessageAsIs(
|
|
1049
|
+
body: unknown,
|
|
1050
|
+
fallback: string,
|
|
1051
|
+
): { message: string; hadUpstreamText: boolean } {
|
|
1052
|
+
const raw = typeof body === "string" ? body : "";
|
|
1053
|
+
const looksLikeHtml = raw.includes("<") && raw.includes(">");
|
|
1054
|
+
// HTML error pages first: the structured extractor passes a string through
|
|
1055
|
+
// verbatim, which would surface raw markup instead of the readable `<title>`
|
|
1056
|
+
// (or a tag-stripped excerpt when the page has no title).
|
|
1057
|
+
if (looksLikeHtml) {
|
|
1058
|
+
const title = extractHtmlTitle(raw);
|
|
1059
|
+
if (title.length > 0) {
|
|
1060
|
+
return {
|
|
1061
|
+
message: title.replace(/[\r\n]+/g, " ").trim().slice(0, MAX_UPSTREAM_MESSAGE_LENGTH),
|
|
1062
|
+
hadUpstreamText: true,
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
const structured = extractUpstreamMessageText(body);
|
|
1067
|
+
if (structured.length > 0) {
|
|
1068
|
+
return { message: structured.slice(0, MAX_UPSTREAM_MESSAGE_LENGTH), hadUpstreamText: true };
|
|
1069
|
+
}
|
|
1070
|
+
const text = raw.trim();
|
|
1071
|
+
if (text.length > 0) {
|
|
1072
|
+
return {
|
|
1073
|
+
message: text.replace(/[\r\n]+/g, " ").trim().slice(0, MAX_UPSTREAM_MESSAGE_LENGTH),
|
|
1074
|
+
hadUpstreamText: true,
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
return { message: fallback, hadUpstreamText: false };
|
|
1078
|
+
}
|
|
1079
|
+
|
|
974
1080
|
/** Throws a typed transport error when an upstream returns an HTML error page. */
|
|
975
1081
|
export async function throwIfHtmlResponse(response: Response): Promise<void> {
|
|
976
1082
|
const contentType = response.headers.get("content-type") ?? "";
|