cartethyia 2.4.6 → 2.5.1
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/README.md +45 -13
- package/dist/dashboard/assets/BannedPage-ChqnTRpZ.js +1 -0
- package/dist/dashboard/assets/BannedPage-ChqnTRpZ.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-Bcy150Jo.js → CliToolDetailPage-Bb9CNuoh.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-Bb9CNuoh.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-DyNXefph.js → CliToolsPage-BXUaRCQQ.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-BXUaRCQQ.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-CisNZOUt.js +1 -0
- package/dist/dashboard/assets/CombosPage-CisNZOUt.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-CATpAEOz.js → ConsoleLogPage-BOpI_LOH.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-BOpI_LOH.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-D6jEU7nL.js → CustomizationPage-DAkdvJ4k.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-DAkdvJ4k.js.gz +0 -0
- package/dist/dashboard/assets/{HealthEventsModal-CZkpn0a7.js → HealthEventsModal-DeT-VXHd.js} +1 -1
- package/dist/dashboard/assets/HealthEventsModal-DeT-VXHd.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-BJlMVN4U.js → LoginPage-CytxSeGP.js} +1 -1
- package/dist/dashboard/assets/LoginPage-CytxSeGP.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-Br3CXEau.js → ModelPicker-oQ-SDXp8.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-CU7S1wJs.js +1 -0
- package/dist/dashboard/assets/OverviewPage-CU7S1wJs.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-DLm2baot.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage-DLm2baot.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js +1 -0
- package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-DOXfuzfL.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-DOXfuzfL.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-wvk0ciQN.js +7 -0
- package/dist/dashboard/assets/ProxyPage-wvk0ciQN.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-Cn_fcUHE.js +1 -0
- package/dist/dashboard/assets/QuotaPage-Cn_fcUHE.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-BHW4Zt06.js → SettingsPage-8-eVadhc.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-8-eVadhc.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-VOYC_lIa.js → SetupPage-uEIXPyGO.js} +1 -1
- package/dist/dashboard/assets/SetupPage-uEIXPyGO.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-DCY0TPTZ.js +1 -0
- package/dist/dashboard/assets/SortableList-DCY0TPTZ.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-BNIDLiU6.js → StudioPage-DqK5E26W.js} +4 -4
- package/dist/dashboard/assets/StudioPage-DqK5E26W.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-38JfiZ0H.js +56 -0
- package/dist/dashboard/assets/UsagePage-38JfiZ0H.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-BETfaMYZ.js → api-keys-BmDbsior.js} +1 -1
- package/dist/dashboard/assets/api-keys-BmDbsior.js.gz +0 -0
- package/dist/dashboard/assets/app-BbpSarVV.css +2 -0
- package/dist/dashboard/assets/app-BbpSarVV.css.gz +0 -0
- package/dist/dashboard/assets/assert-BHfaei-X.js +1 -0
- package/dist/dashboard/assets/assert-BHfaei-X.js.gz +0 -0
- package/dist/dashboard/assets/{badge-BTyGk_DL.js → badge-DpXT4Ixt.js} +1 -1
- package/dist/dashboard/assets/badge-DpXT4Ixt.js.gz +0 -0
- package/dist/dashboard/assets/{banner-bujTWYM4.js → banner-CSAAWLJv.js} +1 -1
- package/dist/dashboard/assets/banner-CSAAWLJv.js.gz +0 -0
- package/dist/dashboard/assets/{card-B0Bnx5CA.js → card-DEhRXXw4.js} +1 -1
- package/dist/dashboard/assets/card-DEhRXXw4.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-DE2ug812.js → cli-tools-B0wBq-RL.js} +1 -1
- package/dist/dashboard/assets/cli-tools-B0wBq-RL.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-R_rArH8S.js → clipboard-button-DOo6EU8O.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-BWIP_EvW.js → dialog-BvQeBMfP.js} +1 -1
- package/dist/dashboard/assets/dialog-BvQeBMfP.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-BKcgVugF.js → drawer-CMwPXwQB.js} +1 -1
- package/dist/dashboard/assets/drawer-CMwPXwQB.js.gz +0 -0
- package/dist/dashboard/assets/entry--yj84zk3.js +4 -0
- package/dist/dashboard/assets/entry--yj84zk3.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CcWkGR4a.js → entry-D4O2229X.js} +1 -1
- package/dist/dashboard/assets/{entry-CcWkGR4a.js.gz → entry-D4O2229X.js.gz} +0 -0
- package/dist/dashboard/assets/{entry-kTiTFrvh.js → entry-DKvr5Q9K.js} +1 -1
- package/dist/dashboard/assets/entry-DKvr5Q9K.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-Booc5mNe.js → github-badge-zliLvBwr.js} +1 -1
- package/dist/dashboard/assets/github-badge-zliLvBwr.js.gz +0 -0
- package/dist/dashboard/assets/{index-D70pChfn.js → index-CcD1eSJ6.js} +2 -2
- package/dist/dashboard/assets/index-CcD1eSJ6.js.gz +0 -0
- package/dist/dashboard/assets/{inline-D1TCqM2D.js → inline-D1IlZZ8H.js} +1 -1
- package/dist/dashboard/assets/inline-D1IlZZ8H.js.gz +0 -0
- package/dist/dashboard/assets/{input-B9oyU93y.js → input-BpVZdrbc.js} +1 -1
- package/dist/dashboard/assets/input-BpVZdrbc.js.gz +0 -0
- package/dist/dashboard/assets/{layout-BArzXirQ.js → layout-BioLW7gN.js} +1 -1
- package/dist/dashboard/assets/layout-BioLW7gN.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-BVZzaoHF.js → list-row-CwVcGAIc.js} +1 -1
- package/dist/dashboard/assets/list-row-CwVcGAIc.js.gz +0 -0
- package/dist/dashboard/assets/{live-EmuIayqp.js → live-yfEJFF9T.js} +1 -1
- package/dist/dashboard/assets/live-yfEJFF9T.js.gz +0 -0
- package/dist/dashboard/assets/{model-bans-CSPQ9xB0.js → model-bans-CT-TxSNX.js} +1 -1
- package/dist/dashboard/assets/model-bans-CT-TxSNX.js.gz +0 -0
- package/dist/dashboard/assets/providers/cursor.webp +0 -0
- package/dist/dashboard/assets/providers/trae.webp +0 -0
- package/dist/dashboard/assets/{routing-BExgxxBU.js → routing-Cs6jl49L.js} +1 -1
- package/dist/dashboard/assets/routing-Cs6jl49L.js.gz +0 -0
- package/dist/dashboard/assets/select-XPJ-dIw3.js +41 -0
- package/dist/dashboard/assets/select-XPJ-dIw3.js.gz +0 -0
- package/dist/dashboard/assets/settings-CfcTcOyU.js +1 -0
- package/dist/dashboard/assets/settings-CfcTcOyU.js.gz +0 -0
- package/dist/dashboard/assets/{stack-Ca5HxI5i.js → stack-C9_uUjaA.js} +1 -1
- package/dist/dashboard/assets/stack-C9_uUjaA.js.gz +0 -0
- package/dist/dashboard/assets/{state-CzUPfLXo.js → state-DA0B95UQ.js} +1 -1
- package/dist/dashboard/assets/state-DA0B95UQ.js.gz +0 -0
- package/dist/dashboard/assets/{switch-DOUIky_i.js → switch-Cittxy-x.js} +1 -1
- package/dist/dashboard/assets/switch-Cittxy-x.js.gz +0 -0
- package/dist/dashboard/assets/{toast-C76WrMhc.js → toast-CNt_jzO4.js} +1 -1
- package/dist/dashboard/assets/toast-CNt_jzO4.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-D87FZwH0.js → useMutation-BQKMcJ1H.js} +1 -1
- package/dist/dashboard/assets/useMutation-BQKMcJ1H.js.gz +0 -0
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +51 -52
- package/migrations/full-postgres/0015_add_payload_stages.sql +18 -0
- package/migrations/full-postgres/0016_add_share_link_slug.sql +20 -0
- package/migrations/full-postgres/0017_add_models_pro_only.sql +18 -0
- package/migrations/full-postgres/0018_add_accounts_quota_plan.sql +18 -0
- package/migrations/full-postgres/0019_add_models_free_tier.sql +15 -0
- package/migrations/lite-sqlite3/0015_add_payload_stages.sql +18 -0
- package/migrations/lite-sqlite3/0016_add_share_link_slug.sql +20 -0
- package/migrations/lite-sqlite3/0017_add_models_pro_only.sql +18 -0
- package/migrations/lite-sqlite3/0018_add_accounts_quota_plan.sql +18 -0
- package/migrations/lite-sqlite3/0019_add_models_free_tier.sql +15 -0
- package/package.json +3 -2
- package/scripts/cli/command/console-menu.ts +30 -11
- package/scripts/cli/command/start.ts +77 -13
- package/scripts/cli/command/tray.ts +47 -8
- package/scripts/cli/command/update.ts +1 -1
- package/scripts/cli/gateway/instance-lock.ts +105 -0
- package/scripts/cli/gateway/spawn.ts +173 -101
- package/scripts/cli/ui/background.ts +110 -49
- package/src/boot/app.ts +20 -6
- package/src/boot/dependencies.ts +19 -0
- package/src/boot/elysia-typebox-bridge.ts +37 -0
- package/src/boot/lifecycle.ts +2 -0
- package/src/config/cooldowns.ts +138 -138
- package/src/config/providers.ts +5 -5
- package/src/config/runtime.ts +19 -3
- package/src/config/spec.ts +9 -0
- package/src/console/api-keys/contracts.ts +414 -391
- package/src/console/api-keys/routes.ts +880 -870
- package/src/console/auth/account-reset-store.ts +14 -0
- package/src/console/auth/account-reset.ts +3 -2
- package/src/console/auth/setup-service.ts +6 -3
- package/src/console/domain-registration.ts +9 -2
- package/src/console/observability/contracts.ts +475 -455
- package/src/console/observability/store.ts +1015 -952
- package/src/console/providers/catalog/contracts.ts +31 -0
- package/src/console/providers/catalog/provider-operations.ts +8 -0
- package/src/console/providers/catalog/store.ts +1532 -1525
- package/src/console/providers/oauth/callback-listener.ts +18 -3
- package/src/console/providers/oauth/routes.ts +38 -15
- package/src/console/quota/refresh.ts +19 -0
- package/src/console/updates/routes.ts +162 -4
- package/src/main.ts +3 -0
- package/src/network/http2-bidi.ts +85 -0
- package/src/network/outbound-fetch.ts +1 -26
- package/src/network/pool/agent.ts +478 -460
- package/src/network/pool/resolver.ts +1104 -544
- package/src/network/pool-health-machine.ts +12 -5
- package/src/observability/payload-capture.ts +344 -324
- package/src/persistence/db-pg.ts +9 -3
- package/src/persistence/db-sqlite.ts +497 -495
- package/src/persistence/health-event-flush.ts +137 -0
- package/src/persistence/migrate.ts +181 -129
- package/src/persistence/schema.ts +1360 -1295
- package/src/persistence/settings-revision.ts +25 -0
- package/src/persistence/shared-cache.ts +217 -0
- package/src/persistence/stores/share-store.ts +556 -528
- package/src/persistence/usage-flush.ts +61 -1
- package/src/protocol/canonical-model.ts +23 -1
- package/src/protocol/response/chat.ts +143 -10
- package/src/protocol/response/omp-tool-call-markup.ts +221 -0
- package/src/providers/default-registry.ts +173 -1
- package/src/providers/discovery/openai-model-discovery.ts +39 -3
- package/src/providers/discovery/openrouter-free-roster.ts +98 -0
- package/src/providers/discovery/probing-service.ts +1022 -1001
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +39 -0
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +913 -914
- package/src/providers/integrations/buddy/buddy-conversation-headers.ts +18 -5
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +4 -0
- package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -175
- package/src/providers/integrations/buddy/codebuddy/shared.ts +15 -6
- package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -204
- package/src/providers/integrations/buddy/workbuddy/shared.ts +3 -3
- package/src/providers/integrations/claude/claude.ts +606 -582
- package/src/providers/integrations/cline/cline-quota.ts +18 -16
- package/src/providers/integrations/cline/cline.ts +539 -468
- package/src/providers/integrations/commandcode/commandcode.ts +675 -541
- package/src/providers/integrations/commandcode/discovery.ts +116 -0
- package/src/providers/integrations/commandcode/quota.ts +160 -0
- package/src/providers/integrations/cursor/cursor-auth.ts +418 -0
- package/src/providers/integrations/cursor/cursor-catalog.ts +368 -0
- package/src/providers/integrations/cursor/cursor-http2.ts +300 -0
- package/src/providers/integrations/cursor/cursor-identity.ts +92 -0
- package/src/providers/integrations/cursor/cursor-request.ts +471 -0
- package/src/providers/integrations/cursor/cursor-session-manager.ts +281 -0
- package/src/providers/integrations/cursor/cursor-shared.ts +108 -0
- package/src/providers/integrations/cursor/cursor.ts +960 -0
- package/src/providers/integrations/cursor/protocol/framing.ts +129 -0
- package/src/providers/integrations/cursor/protocol/messages.ts +2355 -0
- package/src/providers/integrations/cursor/protocol/struct.ts +305 -0
- package/src/providers/integrations/github/github.ts +4 -4
- package/src/providers/integrations/hermes.ts +79 -8
- package/src/providers/integrations/kilo/kilo-discovery.ts +10 -1
- package/src/providers/integrations/kilo/kilo.ts +153 -146
- package/src/providers/integrations/opencode/provider.ts +532 -538
- package/src/providers/integrations/openrouter/provider.ts +139 -281
- package/src/providers/integrations/qoder/qoder-catalog.ts +450 -0
- package/src/providers/integrations/qoder/qoder-oauth.ts +310 -257
- package/src/providers/integrations/qoder/qoder-realm.ts +106 -0
- package/src/providers/integrations/qoder/qoder-virtual.ts +77 -0
- package/src/providers/integrations/qoder/qoder.ts +865 -730
- package/src/providers/integrations/trae/trae-catalog.ts +190 -0
- package/src/providers/integrations/trae/trae-headers.ts +149 -0
- package/src/providers/integrations/trae/trae-oauth.ts +773 -0
- package/src/providers/integrations/trae/trae-protocol.ts +400 -0
- package/src/providers/integrations/trae/trae-realm.ts +220 -0
- package/src/providers/integrations/trae/trae.ts +458 -0
- package/src/providers/operations/account-health-service.ts +107 -10
- package/src/providers/operations/client-versions.ts +133 -6
- package/src/providers/operations/provider-catalog-seeder.ts +95 -86
- package/src/providers/operations/provider-credential-service.ts +46 -7
- package/src/providers/provider-contracts.ts +76 -0
- package/src/providers/provider-metadata.ts +78 -6
- package/src/providers/provider-model-definition.ts +373 -349
- package/src/security/api-key-auth.ts +126 -8
- package/src/transport/contracts.ts +3 -0
- package/src/transport/dispatch/attempt-finalize.ts +32 -1
- package/src/transport/dispatch/attempt-loop.ts +530 -515
- package/src/transport/dispatch/generation.ts +88 -21
- package/src/transport/dispatch/leases.ts +99 -2
- package/src/transport/dispatch/proxy-request.ts +60 -3
- package/src/transport/dispatch/streaming/finalize.ts +16 -1
- package/src/transport/dispatch/streaming/stream.ts +22 -2
- package/src/transport/dispatch/upstream.ts +19 -1
- package/src/transport/guards/auth-guard.ts +26 -1
- package/src/transport/guards/body-policy.ts +5 -4
- package/src/transport/request/preparer.ts +103 -0
- package/src/transport/request/state.ts +14 -0
- package/src/transport/routing/plan-gate.ts +45 -0
- package/src/transport/routing/route-catalog.ts +799 -777
- package/src/transport/routing/route-model.ts +52 -1
- package/src/transport/routing/router.ts +51 -0
- package/dist/dashboard/assets/BannedPage-QhvbJB9o.js +0 -1
- package/dist/dashboard/assets/BannedPage-QhvbJB9o.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-Bcy150Jo.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-DyNXefph.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-Cak0ljnO.js +0 -1
- package/dist/dashboard/assets/CombosPage-Cak0ljnO.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-CATpAEOz.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-D6jEU7nL.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-CZkpn0a7.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-BJlMVN4U.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-Br3CXEau.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-CQxXbo2g.js +0 -1
- package/dist/dashboard/assets/OverviewPage-CQxXbo2g.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js +0 -1
- package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-CXiumA53.js +0 -7
- package/dist/dashboard/assets/ProxyPage-CXiumA53.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js +0 -1
- package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-BHW4Zt06.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-VOYC_lIa.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-DpOTnGuH.js +0 -1
- package/dist/dashboard/assets/SortableList-DpOTnGuH.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-BNIDLiU6.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-BAFaSDQV.js +0 -56
- package/dist/dashboard/assets/UsagePage-BAFaSDQV.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-BETfaMYZ.js.gz +0 -0
- package/dist/dashboard/assets/app-BLCvGdzb.css +0 -2
- package/dist/dashboard/assets/app-BLCvGdzb.css.gz +0 -0
- package/dist/dashboard/assets/assert-7J6_LVD3.js +0 -1
- package/dist/dashboard/assets/assert-7J6_LVD3.js.gz +0 -0
- package/dist/dashboard/assets/badge-BTyGk_DL.js.gz +0 -0
- package/dist/dashboard/assets/banner-bujTWYM4.js.gz +0 -0
- package/dist/dashboard/assets/card-B0Bnx5CA.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-DE2ug812.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-R_rArH8S.js.gz +0 -0
- package/dist/dashboard/assets/dialog-BWIP_EvW.js.gz +0 -0
- package/dist/dashboard/assets/drawer-BKcgVugF.js.gz +0 -0
- package/dist/dashboard/assets/entry-7dpgvpJ3.js +0 -4
- package/dist/dashboard/assets/entry-7dpgvpJ3.js.gz +0 -0
- package/dist/dashboard/assets/entry-kTiTFrvh.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-Booc5mNe.js.gz +0 -0
- package/dist/dashboard/assets/index-D70pChfn.js.gz +0 -0
- package/dist/dashboard/assets/inline-D1TCqM2D.js.gz +0 -0
- package/dist/dashboard/assets/input-B9oyU93y.js.gz +0 -0
- package/dist/dashboard/assets/layout-BArzXirQ.js.gz +0 -0
- package/dist/dashboard/assets/list-row-BVZzaoHF.js.gz +0 -0
- package/dist/dashboard/assets/live-EmuIayqp.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-CSPQ9xB0.js.gz +0 -0
- package/dist/dashboard/assets/routing-BExgxxBU.js.gz +0 -0
- package/dist/dashboard/assets/select-C9mMoEpJ.js +0 -41
- package/dist/dashboard/assets/select-C9mMoEpJ.js.gz +0 -0
- package/dist/dashboard/assets/settings-C4_Gn_wq.js +0 -1
- package/dist/dashboard/assets/settings-C4_Gn_wq.js.gz +0 -0
- package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js +0 -1
- package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js.gz +0 -0
- package/dist/dashboard/assets/stack-Ca5HxI5i.js.gz +0 -0
- package/dist/dashboard/assets/state-CzUPfLXo.js.gz +0 -0
- package/dist/dashboard/assets/switch-DOUIky_i.js.gz +0 -0
- package/dist/dashboard/assets/toast-C76WrMhc.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-D87FZwH0.js.gz +0 -0
- package/src/network/tls/bun-client-hello.ts +0 -112
- /package/dist/dashboard/assets/{app-Cjq7nrwu.js → app-C-Y4oZT0.js} +0 -0
- /package/dist/dashboard/assets/{app-Cjq7nrwu.js.gz → app-C-Y4oZT0.js.gz} +0 -0
|
@@ -1,515 +1,530 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared per-candidate attempt lifecycle for every dispatch route.
|
|
3
|
-
*
|
|
4
|
-
* Admission lease → pool slot → routing reservation, the route-specific
|
|
5
|
-
* upstream operation, then one `completeAttempt` for the attempt's accounting.
|
|
6
|
-
* Retry, backoff, pool-cooldown flagging, and reactive OAuth refresh are one
|
|
7
|
-
* policy here, so the canonical proxy routes and the native Responses-compact
|
|
8
|
-
* route cannot drift. Route-specific behavior arrives as the `prepare` and
|
|
9
|
-
* `attempt` closures; this module never forks retry policy.
|
|
10
|
-
*/
|
|
11
|
-
import { GatewayError } from "../../protocol/gateway-error";
|
|
12
|
-
import type { ResolvedCredential } from "../../providers/provider-contracts";
|
|
13
|
-
import {
|
|
14
|
-
classifyTerminalOutcome,
|
|
15
|
-
fallbackRetryDelayMs,
|
|
16
|
-
isRetryableFailure,
|
|
17
|
-
MIN_RETRY_BUDGET_MS,
|
|
18
|
-
sleep,
|
|
19
|
-
} from "../failure-policy";
|
|
20
|
-
import { resolveRouteMaxAttempts } from "../../config/timeouts";
|
|
21
|
-
import type { OAuthTokenRefresher } from "../../providers/authentication/oauth-refresh-service";
|
|
22
|
-
import type { OAuthRefreshService } from "../../providers/authentication/oauth-refresh-service";
|
|
23
|
-
import type { ValidatedNetworkBindingFactory } from "../../network/pool/resolver";
|
|
24
|
-
import type { ByokUpstreamHost } from "../../providers/operations/provider-catalog-service";
|
|
25
|
-
import type { AdmissionLease } from "../../security/admission/contracts";
|
|
26
|
-
import type { RouteCandidate, Reservation, RouteSnapshotService } from "../routing/route-model";
|
|
27
|
-
import type { CartethyiaDatabase } from "../../persistence/db-handle";
|
|
28
|
-
import { acquireAttemptLeases, releaseAttemptLeases } from "./leases";
|
|
29
|
-
import type { AttemptLeaseSource, AttemptLeases } from "./leases";
|
|
30
|
-
import {
|
|
31
|
-
import
|
|
32
|
-
import type {
|
|
33
|
-
import type {
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
candidate.
|
|
51
|
-
candidate.
|
|
52
|
-
candidate.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* member's
|
|
76
|
-
*
|
|
77
|
-
* `
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
readonly
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
readonly
|
|
104
|
-
readonly
|
|
105
|
-
readonly
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
readonly
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
readonly
|
|
130
|
-
readonly
|
|
131
|
-
readonly
|
|
132
|
-
readonly
|
|
133
|
-
readonly
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
readonly
|
|
139
|
-
readonly
|
|
140
|
-
|
|
141
|
-
readonly
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
lease
|
|
280
|
-
reservation
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
// one
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
//
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
//
|
|
332
|
-
// the
|
|
333
|
-
//
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
(
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
deps.
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Shared per-candidate attempt lifecycle for every dispatch route.
|
|
3
|
+
*
|
|
4
|
+
* Admission lease → pool slot → routing reservation, the route-specific
|
|
5
|
+
* upstream operation, then one `completeAttempt` for the attempt's accounting.
|
|
6
|
+
* Retry, backoff, pool-cooldown flagging, and reactive OAuth refresh are one
|
|
7
|
+
* policy here, so the canonical proxy routes and the native Responses-compact
|
|
8
|
+
* route cannot drift. Route-specific behavior arrives as the `prepare` and
|
|
9
|
+
* `attempt` closures; this module never forks retry policy.
|
|
10
|
+
*/
|
|
11
|
+
import { GatewayError } from "../../protocol/gateway-error";
|
|
12
|
+
import type { ResolvedCredential } from "../../providers/provider-contracts";
|
|
13
|
+
import {
|
|
14
|
+
classifyTerminalOutcome,
|
|
15
|
+
fallbackRetryDelayMs,
|
|
16
|
+
isRetryableFailure,
|
|
17
|
+
MIN_RETRY_BUDGET_MS,
|
|
18
|
+
sleep,
|
|
19
|
+
} from "../failure-policy";
|
|
20
|
+
import { resolveRouteMaxAttempts } from "../../config/timeouts";
|
|
21
|
+
import type { OAuthTokenRefresher } from "../../providers/authentication/oauth-refresh-service";
|
|
22
|
+
import type { OAuthRefreshService } from "../../providers/authentication/oauth-refresh-service";
|
|
23
|
+
import type { ValidatedNetworkBindingFactory } from "../../network/pool/resolver";
|
|
24
|
+
import type { ByokUpstreamHost } from "../../providers/operations/provider-catalog-service";
|
|
25
|
+
import type { AdmissionLease } from "../../security/admission/contracts";
|
|
26
|
+
import type { RouteCandidate, Reservation, RouteSnapshotService } from "../routing/route-model";
|
|
27
|
+
import type { CartethyiaDatabase } from "../../persistence/db-handle";
|
|
28
|
+
import { acquireAttemptLeases, releaseAttemptLeases } from "./leases";
|
|
29
|
+
import type { AttemptLeaseSource, AttemptLeases } from "./leases";
|
|
30
|
+
import type { AttemptLeaseOwnership } from "./leases";
|
|
31
|
+
import { metrics } from "../../observability/metrics";
|
|
32
|
+
import type { NetworkPoolSelector } from "../../network/pool/selector";
|
|
33
|
+
import type { ProxyRequestState } from "../request/state";
|
|
34
|
+
import type { TelemetryBatchBuffer } from "../../observability/telemetry-buffer";
|
|
35
|
+
import { flagPoolBlocked, flagPoolCooldown } from "../../network/pool-health";
|
|
36
|
+
import { completeAttempt, errorClientResponseBody, estimatedUsage, type ProviderExchangeCapture } from "./attempt-finalize";
|
|
37
|
+
import { repriceUsage } from "../../providers/provider-usage";
|
|
38
|
+
import { isFreeTierIpBlock, isOAuthCredentialInvalidated, shouldCooldownPool } from "./retry-policy";
|
|
39
|
+
import { drainAbortReason } from "../shutdown-notice";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Identity of one combo member for the request-scoped failure flag.
|
|
43
|
+
*
|
|
44
|
+
* Account, model, and endpoint distinguish siblings that share a provider.
|
|
45
|
+
* The flag lives only for this request: it stops a failed member from being
|
|
46
|
+
* retried while another member remains, without writing a durable cooldown.
|
|
47
|
+
*/
|
|
48
|
+
function comboCandidateKey(candidate: RouteCandidate): string {
|
|
49
|
+
return [
|
|
50
|
+
candidate.provider_id,
|
|
51
|
+
candidate.provider_account_id ?? "global",
|
|
52
|
+
candidate.model_id,
|
|
53
|
+
candidate.endpoint,
|
|
54
|
+
].join(":");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Which combo member a candidate belongs to.
|
|
59
|
+
*
|
|
60
|
+
* A combo's members are distinct models (usually one per provider), so the
|
|
61
|
+
* model id is what separates them — the accounts of one member are siblings
|
|
62
|
+
* that share it. Used only to give each member its own share of the attempt
|
|
63
|
+
* budget; a candidate list that is not a combo collapses to one member, which
|
|
64
|
+
* keeps the ordinary single-model path untouched.
|
|
65
|
+
*/
|
|
66
|
+
function comboMemberKey(candidate: RouteCandidate): string {
|
|
67
|
+
return `${candidate.provider_id}:${candidate.model_id}:${candidate.endpoint}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* One member's ordinal, resolved by *identity* rather than by position.
|
|
72
|
+
*
|
|
73
|
+
* Deliberately not a run-length scan. `plan()` reorders the eligible list — its
|
|
74
|
+
* cooling partition emits every healthy candidate before every cooling one — so
|
|
75
|
+
* one member's accounts can be non-contiguous and even interleaved with another
|
|
76
|
+
* member's. A run-length scan then reports more "members" than actually exist:
|
|
77
|
+
* `[cb, wb, cb, wb]` counts four, which pushed each member's skip threshold to
|
|
78
|
+
* `cap * 4` — well past the whole budget — and silently disabled the cap on
|
|
79
|
+
* exactly the degraded lists it exists for.
|
|
80
|
+
*
|
|
81
|
+
* Keyed off the same string the member count is derived from, so the two can
|
|
82
|
+
* never disagree.
|
|
83
|
+
*/
|
|
84
|
+
function buildMemberOrdinals(
|
|
85
|
+
candidates: readonly RouteCandidate[],
|
|
86
|
+
): ReadonlyMap<string, number> {
|
|
87
|
+
const ordinals = new Map<string, number>();
|
|
88
|
+
for (const candidate of candidates) {
|
|
89
|
+
const key = comboMemberKey(candidate);
|
|
90
|
+
if (!ordinals.has(key)) ordinals.set(key, ordinals.size);
|
|
91
|
+
}
|
|
92
|
+
return ordinals;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Route-specific preconditions resolved for one candidate before its leases are taken. */
|
|
96
|
+
interface PreparedAttempt<TAdapter> {
|
|
97
|
+
readonly credential: ResolvedCredential;
|
|
98
|
+
readonly adapter: TAdapter;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** One candidate attempt with its admission lease, pool slot, and reservation held. */
|
|
102
|
+
interface AttemptContext<TAdapter> {
|
|
103
|
+
readonly candidate: RouteCandidate;
|
|
104
|
+
readonly credential: ResolvedCredential;
|
|
105
|
+
readonly adapter: TAdapter;
|
|
106
|
+
readonly leases: AttemptLeases;
|
|
107
|
+
/** Zero-based candidate index; also this attempt's failover count. */
|
|
108
|
+
readonly attemptIndex: number;
|
|
109
|
+
readonly providerCapture: ProviderExchangeCapture;
|
|
110
|
+
/**
|
|
111
|
+
* Ownership of this attempt's account/network-pool release, when the route
|
|
112
|
+
* acquired a pool slot. A provider session that outlives this request (a
|
|
113
|
+
* Cursor Run held open for the client's tool result) calls
|
|
114
|
+
* `transferToProviderSession()` and takes it over; otherwise the request
|
|
115
|
+
* releases it through `releaseByResponse()`.
|
|
116
|
+
*/
|
|
117
|
+
readonly leaseOwnership?: AttemptLeaseOwnership | undefined;
|
|
118
|
+
/**
|
|
119
|
+
* Hands lease ownership to the value this attempt returns (a streaming
|
|
120
|
+
* response releases its own lease on stream completion/cancel). Without it
|
|
121
|
+
* the loop releases the leases in `finally`.
|
|
122
|
+
*/
|
|
123
|
+
retainLeases(): void;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Collaborators shared by every dispatch route. */
|
|
127
|
+
interface AttemptLoopDeps {
|
|
128
|
+
readonly db: CartethyiaDatabase;
|
|
129
|
+
readonly poolSelector?: NetworkPoolSelector;
|
|
130
|
+
readonly networkBindingFactory?: ValidatedNetworkBindingFactory;
|
|
131
|
+
readonly snapshotService?: RouteSnapshotService;
|
|
132
|
+
readonly telemetryBuffer?: TelemetryBatchBuffer;
|
|
133
|
+
readonly resolveOAuthRefresher?: (providerId: string) => Promise<OAuthTokenRefresher | undefined>;
|
|
134
|
+
readonly oauthRefreshService?: OAuthRefreshService;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface AttemptLoopInput<TResult, TAdapter> {
|
|
138
|
+
readonly state: ProxyRequestState;
|
|
139
|
+
readonly deps: AttemptLoopDeps;
|
|
140
|
+
readonly leaseSource: AttemptLeaseSource;
|
|
141
|
+
readonly candidates: readonly RouteCandidate[];
|
|
142
|
+
readonly tenantId: string | null;
|
|
143
|
+
/** Strict pool selection rejects direct egress when every pool is at capacity. */
|
|
144
|
+
readonly strictPoolSelection: boolean;
|
|
145
|
+
/** BYOK host for a candidate, when this route validates network bindings. */
|
|
146
|
+
readonly resolveHost?: (candidate: RouteCandidate) => ByokUpstreamHost | undefined;
|
|
147
|
+
readonly prepare: (candidate: RouteCandidate) => Promise<PreparedAttempt<TAdapter>>;
|
|
148
|
+
readonly attempt: (context: AttemptContext<TAdapter>) => Promise<TResult>;
|
|
149
|
+
/** Raised when the candidate list is empty and no attempt ever ran. */
|
|
150
|
+
readonly exhaustedError: GatewayError;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Shared per-candidate attempt lifecycle for the canonical proxy routes and
|
|
155
|
+
* the native Responses-compact route: admission lease → pool slot → routing
|
|
156
|
+
* reservation, the route-specific upstream operation, then one
|
|
157
|
+
* `completeAttempt` for the attempt's accounting. Retry, backoff,
|
|
158
|
+
* pool-cooldown flagging, and reactive OAuth refresh are one policy here, so
|
|
159
|
+
* the two routes cannot drift.
|
|
160
|
+
*/
|
|
161
|
+
export async function runAttemptLoop<TResult, TAdapter>(
|
|
162
|
+
input: AttemptLoopInput<TResult, TAdapter>,
|
|
163
|
+
): Promise<TResult> {
|
|
164
|
+
const { state, deps, leaseSource } = input;
|
|
165
|
+
const candidates = [...input.candidates];
|
|
166
|
+
let lastError: unknown = input.exhaustedError;
|
|
167
|
+
const refreshedCandidates = new Set<string>();
|
|
168
|
+
const failedCandidates = new Set<string>();
|
|
169
|
+
// Every attempt owns a fresh capture sink, but only the terminal attempt's
|
|
170
|
+
// sink reaches the payload store. When the terminal attempt fails before it
|
|
171
|
+
// fires an outbound request (lease/acquire/prepare threw, or failover
|
|
172
|
+
// exhausted), its sink is empty and the drawer reads "—" for a request that
|
|
173
|
+
// DID reach an upstream on an earlier attempt. Keep the fired exchanges so
|
|
174
|
+
// the terminal completion can fall back to the last one that has a request.
|
|
175
|
+
const firedCaptures: ProviderExchangeCapture[] = [];
|
|
176
|
+
const maxAttempts = resolveRouteMaxAttempts();
|
|
177
|
+
// One combo member must not be able to spend the whole budget.
|
|
178
|
+
//
|
|
179
|
+
// A combo's member list is flattened into this candidate list, so the first
|
|
180
|
+
// member's accounts occupy its head. With one member holding more accounts
|
|
181
|
+
// than `maxAttempts` (default 8), every attempt was spent inside that member
|
|
182
|
+
// and a later, perfectly healthy member was never dialed — a `deepseek` combo
|
|
183
|
+
// of [cb, wb] stayed stuck on cb while wb sat idle, because cb's twelve
|
|
184
|
+
// accounts exhausted all eight attempts before the list reached wb.
|
|
185
|
+
//
|
|
186
|
+
// Each member gets an equal share of the budget, counted **per member** — not
|
|
187
|
+
// as a cumulative watermark over total attempts, or one member that over-runs
|
|
188
|
+
// silently steals the tail's share. The share is a ceiling, not a reservation:
|
|
189
|
+
// a member that used less than its share simply never reaches it, and the
|
|
190
|
+
// unused budget stays available to the members behind it.
|
|
191
|
+
const memberOrdinals = buildMemberOrdinals(candidates);
|
|
192
|
+
const memberCount = memberOrdinals.size;
|
|
193
|
+
const perMemberCap =
|
|
194
|
+
memberCount > 1 ? Math.max(1, Math.ceil(maxAttempts / memberCount)) : maxAttempts;
|
|
195
|
+
const attemptsByMember = new Map<string, number>();
|
|
196
|
+
// The budget itself is owned by failure-policy, so this loop, the
|
|
197
|
+
// empty-stream replay, and the Codex retry budget cannot drift apart.
|
|
198
|
+
let index = 0;
|
|
199
|
+
let attempts = 0;
|
|
200
|
+
while (index < candidates.length && attempts < maxAttempts) {
|
|
201
|
+
const candidate = candidates[index];
|
|
202
|
+
if (!candidate) break;
|
|
203
|
+
const memberKey = comboMemberKey(candidate);
|
|
204
|
+
// A member that has spent its share is skipped: the loop advances past the
|
|
205
|
+
// rest of its accounts to the next member instead of burning the budget
|
|
206
|
+
// they would have consumed.
|
|
207
|
+
if (
|
|
208
|
+
memberCount > 1 &&
|
|
209
|
+
(attemptsByMember.get(memberKey) ?? 0) >= perMemberCap
|
|
210
|
+
) {
|
|
211
|
+
index += 1;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
// Do not start an attempt that cannot finish. The measured pre-dispatch
|
|
215
|
+
// cost of the previous attempt (`resolveMs` ran 60s+ on a slow upstream)
|
|
216
|
+
// is what a follow-on attempt has to spend again before the upstream is
|
|
217
|
+
// even called, so a retry that begins with less than that left is a retry
|
|
218
|
+
// the deadline will kill mid-flight. Stopping here surfaces the first
|
|
219
|
+
// attempt's own error instead of a second one's timeout.
|
|
220
|
+
if (attempts > 0) {
|
|
221
|
+
const remainingMs = state.deadlineMs - Date.now();
|
|
222
|
+
if (remainingMs < MIN_RETRY_BUDGET_MS) {
|
|
223
|
+
if (lastError !== undefined && !state.completed) {
|
|
224
|
+
const terminal = classifyTerminalOutcome(lastError, state.abortController.signal);
|
|
225
|
+
const lastFired = [...firedCaptures]
|
|
226
|
+
.reverse()
|
|
227
|
+
.find((capture) => capture.request !== null);
|
|
228
|
+
await completeAttempt(state, {
|
|
229
|
+
status: terminal.status,
|
|
230
|
+
providerId: candidate.provider_id,
|
|
231
|
+
...(candidate.provider_account_id
|
|
232
|
+
? { accountId: candidate.provider_account_id }
|
|
233
|
+
: {}),
|
|
234
|
+
...(candidate.provider_account_label
|
|
235
|
+
? { accountLabel: candidate.provider_account_label }
|
|
236
|
+
: {}),
|
|
237
|
+
errorCategory: terminal.errorCategory,
|
|
238
|
+
errorOrigin: terminal.errorOrigin,
|
|
239
|
+
modelId: candidate.model_id,
|
|
240
|
+
error: lastError,
|
|
241
|
+
terminal: true,
|
|
242
|
+
tenantId: input.tenantId,
|
|
243
|
+
ingressBody: state.ingressBody,
|
|
244
|
+
responseBody: null,
|
|
245
|
+
...(terminal.status !== "cancelled"
|
|
246
|
+
? { clientResponseText: errorClientResponseBody(lastError) }
|
|
247
|
+
: {}),
|
|
248
|
+
// Same fallback the terminal catch uses: a request that did fire
|
|
249
|
+
// upstream keeps that exchange in Request Detail.
|
|
250
|
+
...(lastFired ? { providerCapture: lastFired } : {}),
|
|
251
|
+
db: deps.db,
|
|
252
|
+
...(deps.telemetryBuffer ? { telemetryBuffer: deps.telemetryBuffer } : {}),
|
|
253
|
+
...(deps.snapshotService ? { snapshotService: deps.snapshotService } : {}),
|
|
254
|
+
});
|
|
255
|
+
metrics.proxy_requests_total.inc(1, {
|
|
256
|
+
status: terminal.status === "cancelled" ? "cancelled" : "failed",
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
throw lastError;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
let lease: AdmissionLease | undefined;
|
|
263
|
+
let reservation: Reservation | undefined;
|
|
264
|
+
let proxySlot: { poolId: string; release: () => void } | undefined;
|
|
265
|
+
let networkPoolId: string | undefined;
|
|
266
|
+
let leaseOwnership: AttemptLeaseOwnership | undefined;
|
|
267
|
+
let bindingEstablished = false;
|
|
268
|
+
let leasesRetained = false;
|
|
269
|
+
let retryDelayMs = 0;
|
|
270
|
+
const providerCapture: ProviderExchangeCapture = {
|
|
271
|
+
request: null,
|
|
272
|
+
response: Promise.resolve(null),
|
|
273
|
+
};
|
|
274
|
+
try {
|
|
275
|
+
if (state.abortController.signal.aborted)
|
|
276
|
+
throw new GatewayError("transport_closed", 499, "request was cancelled");
|
|
277
|
+
const { credential, adapter } = await input.prepare(candidate);
|
|
278
|
+
const host = input.resolveHost?.(candidate);
|
|
279
|
+
// Single attempt-lease manager owns acquisition order (admission →
|
|
280
|
+
// pool slot → reservation) with reverse-order unwind. Assigned only on
|
|
281
|
+
// success, so a mid-acquire throw leaves these undefined for `finally`.
|
|
282
|
+
const attemptLeases = await acquireAttemptLeases(leaseSource, candidate, {
|
|
283
|
+
signal: state.abortController.signal,
|
|
284
|
+
...(deps.poolSelector ? { poolSelector: deps.poolSelector } : {}),
|
|
285
|
+
...(deps.networkBindingFactory ? { networkBindingFactory: deps.networkBindingFactory } : {}),
|
|
286
|
+
...(host ? { host } : {}),
|
|
287
|
+
strictPoolSelection: input.strictPoolSelection,
|
|
288
|
+
});
|
|
289
|
+
lease = attemptLeases.lease;
|
|
290
|
+
reservation = attemptLeases.reservation;
|
|
291
|
+
proxySlot = attemptLeases.proxySlot;
|
|
292
|
+
networkPoolId = attemptLeases.networkPoolId;
|
|
293
|
+
leaseOwnership = attemptLeases.ownership;
|
|
294
|
+
// Downstream error paths gate `commitUsage(estimated)` on this flag so
|
|
295
|
+
// the estimate is only charged once the request truly consumed
|
|
296
|
+
// upstream resources; the lease and reservation are held from here on.
|
|
297
|
+
bindingEstablished = true;
|
|
298
|
+
state.startProviderFlight();
|
|
299
|
+
return await input.attempt({
|
|
300
|
+
candidate,
|
|
301
|
+
credential,
|
|
302
|
+
adapter,
|
|
303
|
+
leases: attemptLeases,
|
|
304
|
+
attemptIndex: index,
|
|
305
|
+
providerCapture,
|
|
306
|
+
...(leaseOwnership === undefined ? {} : { leaseOwnership }),
|
|
307
|
+
retainLeases: () => {
|
|
308
|
+
leasesRetained = true;
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
} catch (error) {
|
|
312
|
+
// A drain aborts the controller, so a mid-flight attempt unwinds here as
|
|
313
|
+
// an abort. Surface the typed shutdown error (503 shutting_down /
|
|
314
|
+
// restart_for_update) instead of an opaque `transport_closed`, so the
|
|
315
|
+
// client's JSON error — not just the SSE path — names the real cause.
|
|
316
|
+
const drain = drainAbortReason(state.abortController.signal.reason);
|
|
317
|
+
if (drain !== undefined) error = drain;
|
|
318
|
+
lastError = error;
|
|
319
|
+
// A drain is a *server* close, not a client cancel: it must be recorded
|
|
320
|
+
// as a failure with the shutdown code, and it is never retryable.
|
|
321
|
+
// Status, category, and origin come from one classifier so they cannot
|
|
322
|
+
// disagree (a `cancelled` row used to carry `transport_unavailable`/502).
|
|
323
|
+
const terminal = classifyTerminalOutcome(error, state.abortController.signal);
|
|
324
|
+
const cancelled = terminal.status === "cancelled";
|
|
325
|
+
// A failure the *pool* caused retires that pool, not the candidate: the
|
|
326
|
+
// next attempt keeps the same provider/model/account and selects a
|
|
327
|
+
// different egress. That is a genuinely new attempt even though the
|
|
328
|
+
// candidate index does not move, so it must not be judged terminal by
|
|
329
|
+
// "this was the last candidate" — that rule is about candidates, and
|
|
330
|
+
// opencode has exactly one (it is credential-less, so the plan holds a
|
|
331
|
+
// single candidate carrying every pool). Judging it there is what made a
|
|
332
|
+
// refused egress end the request instead of failing over to a healthy
|
|
333
|
+
// one.
|
|
334
|
+
const rateLimitedPool = !cancelled && shouldCooldownPool(error, candidate.provider_id);
|
|
335
|
+
// A free-tier refusal is only ever this provider's: the guard is gated on
|
|
336
|
+
// `ipScopedRateLimit` in the provider metadata, so a 403 from any other
|
|
337
|
+
// upstream still means what it always did and never retires an egress.
|
|
338
|
+
const blockedPool = !cancelled && isFreeTierIpBlock(error, candidate.provider_id);
|
|
339
|
+
const poolScopedFailure =
|
|
340
|
+
(rateLimitedPool || blockedPool) &&
|
|
341
|
+
networkPoolId !== undefined &&
|
|
342
|
+
deps.poolSelector !== undefined;
|
|
343
|
+
// Flag before the decision below, so the retry that follows already sees
|
|
344
|
+
// the pool excluded. Both flag helpers write the local entry
|
|
345
|
+
// synchronously, so the ordering holds even though the Redis mirror is
|
|
346
|
+
// fire-and-forget.
|
|
347
|
+
if (networkPoolId && deps.poolSelector && error instanceof GatewayError) {
|
|
348
|
+
if (rateLimitedPool) {
|
|
349
|
+
flagPoolCooldown(deps.poolSelector, deps.db, networkPoolId, candidate.provider_id, error);
|
|
350
|
+
} else if (blockedPool) {
|
|
351
|
+
// The address itself is refused, not the request: retire the egress
|
|
352
|
+
// so the next attempt leaves by a different one instead of retrying
|
|
353
|
+
// a proxy the upstream has already flagged.
|
|
354
|
+
flagPoolBlocked(deps.poolSelector, deps.db, networkPoolId, candidate.provider_id, error);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const terminalAttempt =
|
|
358
|
+
cancelled ||
|
|
359
|
+
!isRetryableFailure(error) ||
|
|
360
|
+
(index === candidates.length - 1 && !poolScopedFailure);
|
|
361
|
+
if (providerCapture.request !== null) firedCaptures.push(providerCapture);
|
|
362
|
+
// The payload store only keeps the terminal attempt's exchange. When the
|
|
363
|
+
// terminal attempt never fired (failover walked past the attempt that
|
|
364
|
+
// did, or prepare/lease threw first), fall back to the last fired
|
|
365
|
+
// exchange so Request Detail still shows the translated provider
|
|
366
|
+
// request instead of "—" for a request that reached an upstream.
|
|
367
|
+
const terminalCapture =
|
|
368
|
+
providerCapture.request !== null
|
|
369
|
+
? providerCapture
|
|
370
|
+
: [...firedCaptures].reverse().find((capture) => capture.request !== null) ??
|
|
371
|
+
providerCapture;
|
|
372
|
+
await completeAttempt(state, {
|
|
373
|
+
status: terminal.status,
|
|
374
|
+
providerId: candidate.provider_id,
|
|
375
|
+
...(candidate.provider_account_id ? { accountId: candidate.provider_account_id } : {}),
|
|
376
|
+
...(candidate.provider_account_label ? { accountLabel: candidate.provider_account_label } : {}),
|
|
377
|
+
errorCategory: terminal.errorCategory,
|
|
378
|
+
errorOrigin: terminal.errorOrigin,
|
|
379
|
+
modelId: candidate.model_id,
|
|
380
|
+
...(networkPoolId ? { networkPoolId } : {}),
|
|
381
|
+
error,
|
|
382
|
+
...(lease ? { lease } : {}),
|
|
383
|
+
// A failed attempt may have sent bytes upstream, so it is not free —
|
|
384
|
+
// but it must not be charged the full input estimate plus a full
|
|
385
|
+
// output reserve either, because a request that walks N candidates
|
|
386
|
+
// would then leave N estimates on the counters for one client request.
|
|
387
|
+
// Commit the input estimate only: nothing was generated, and the
|
|
388
|
+
// `finally` release then refunds the (uncommitted) output reserve.
|
|
389
|
+
...(!cancelled && bindingEstablished
|
|
390
|
+
? {
|
|
391
|
+
commitUsage: repriceUsage(
|
|
392
|
+
estimatedUsage(leaseSource.estimatedInputTokens, 0),
|
|
393
|
+
candidate.provider_id,
|
|
394
|
+
candidate.model_id,
|
|
395
|
+
),
|
|
396
|
+
}
|
|
397
|
+
: {}),
|
|
398
|
+
terminal: terminalAttempt,
|
|
399
|
+
tenantId: input.tenantId,
|
|
400
|
+
ingressBody: state.ingressBody,
|
|
401
|
+
responseBody: null,
|
|
402
|
+
// The client still receives the public error envelope on the wire —
|
|
403
|
+
// store it so Request Detail's Client Response panel traces failures
|
|
404
|
+
// exactly as the client saw them. Cancelled requests have no client
|
|
405
|
+
// response (the caller is gone).
|
|
406
|
+
...(terminalAttempt && !cancelled
|
|
407
|
+
? { clientResponseText: errorClientResponseBody(error) }
|
|
408
|
+
: {}),
|
|
409
|
+
providerCapture: terminalCapture,
|
|
410
|
+
db: deps.db,
|
|
411
|
+
...(deps.telemetryBuffer ? { telemetryBuffer: deps.telemetryBuffer } : {}),
|
|
412
|
+
...(deps.snapshotService ? { snapshotService: deps.snapshotService } : {}),
|
|
413
|
+
});
|
|
414
|
+
// A 403 can be a deterministic provider policy rejection, not an
|
|
415
|
+
// invalid OAuth credential. Only the evidence-based classifier above
|
|
416
|
+
// may trigger a refresh.
|
|
417
|
+
const candidateKey = `${candidate.provider_id}:${candidate.provider_account_id ?? "global"}:${candidate.model_id}:${candidate.endpoint}`;
|
|
418
|
+
const authInvalidated = isOAuthCredentialInvalidated(error);
|
|
419
|
+
if (
|
|
420
|
+
!cancelled &&
|
|
421
|
+
!refreshedCandidates.has(candidateKey) &&
|
|
422
|
+
authInvalidated &&
|
|
423
|
+
candidate.provider_account_id &&
|
|
424
|
+
deps.oauthRefreshService &&
|
|
425
|
+
deps.resolveOAuthRefresher
|
|
426
|
+
) {
|
|
427
|
+
const refresher = await deps.resolveOAuthRefresher(candidate.provider_id);
|
|
428
|
+
if (refresher) {
|
|
429
|
+
const fresh = await deps.oauthRefreshService.ensureFreshAccessToken(
|
|
430
|
+
candidate.provider_account_id,
|
|
431
|
+
refresher,
|
|
432
|
+
{ force: true },
|
|
433
|
+
);
|
|
434
|
+
if (fresh) {
|
|
435
|
+
refreshedCandidates.add(candidateKey);
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (terminalAttempt) {
|
|
441
|
+
metrics.proxy_requests_total.inc(1, { status: cancelled ? "cancelled" : "failed" });
|
|
442
|
+
throw error;
|
|
443
|
+
}
|
|
444
|
+
// A failed combo member stays flagged for this request. Move it behind
|
|
445
|
+
// every candidate that has not failed yet, so the next attempt cannot
|
|
446
|
+
// stick to it while a sibling remains. It becomes eligible again only
|
|
447
|
+
// after that sibling list is exhausted.
|
|
448
|
+
//
|
|
449
|
+
// A pool-scoped failure is exempt: the candidate never got its turn, so
|
|
450
|
+
// marking it failed would spend its only remaining chance on the pool
|
|
451
|
+
// that just refused it. The candidate stays at the head, and the next
|
|
452
|
+
// attempt selects a different pool for it. The attempt budget still
|
|
453
|
+
// bounds the loop, and once every pool is retired the selection itself
|
|
454
|
+
// fails with `proxy_pool_blocked`, which is terminal.
|
|
455
|
+
const failureKey = comboCandidateKey(candidate);
|
|
456
|
+
if (poolScopedFailure) {
|
|
457
|
+
// Deliberately no index movement and no `failedCandidates` entry.
|
|
458
|
+
} else if (!failedCandidates.has(failureKey)) {
|
|
459
|
+
failedCandidates.add(failureKey);
|
|
460
|
+
candidates.push(candidates.splice(index, 1)[0]!);
|
|
461
|
+
} else {
|
|
462
|
+
index += 1;
|
|
463
|
+
}
|
|
464
|
+
retryDelayMs = fallbackRetryDelayMs(attempts);
|
|
465
|
+
} finally {
|
|
466
|
+
if (!leasesRetained)
|
|
467
|
+
await releaseAttemptLeases(
|
|
468
|
+
{ lease, reservation, proxySlot, ...(leaseOwnership === undefined ? {} : { ownership: leaseOwnership }) },
|
|
469
|
+
leaseSource.routingEngine,
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
attempts += 1;
|
|
473
|
+
// Charged to the member that spent it, so one member's own attempts are
|
|
474
|
+
// what exhaust its share — not the running total across every member.
|
|
475
|
+
attemptsByMember.set(memberKey, (attemptsByMember.get(memberKey) ?? 0) + 1);
|
|
476
|
+
if (retryDelayMs > 0) {
|
|
477
|
+
try {
|
|
478
|
+
await sleep(retryDelayMs, state.abortController.signal);
|
|
479
|
+
} catch {
|
|
480
|
+
// Abort during backoff used to escape as the sleep's own error with
|
|
481
|
+
// no counter increment: attempts already recorded their telemetry,
|
|
482
|
+
// but `proxy_requests_total` missed the request. Fail with the last
|
|
483
|
+
// attempt's error so the client sees the real cause, counted once.
|
|
484
|
+
metrics.proxy_requests_total.inc(1, { status: "cancelled" });
|
|
485
|
+
throw lastError;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
// The loop can fall out with a retryable error still in hand (every attempt
|
|
490
|
+
// was retryable, so the terminal branch in the catch never fired). That used
|
|
491
|
+
// to throw with no completion: no telemetry row, no payload capture, and
|
|
492
|
+
// `state.completed` left clear — so a request that genuinely exhausted its
|
|
493
|
+
// attempts was invisible in Request Detail. Record it as terminal here.
|
|
494
|
+
if (!state.completed) {
|
|
495
|
+
const terminal = classifyTerminalOutcome(lastError, state.abortController.signal);
|
|
496
|
+
const lastCandidate = candidates[Math.min(index, candidates.length - 1)];
|
|
497
|
+
const terminalCapture = [...firedCaptures]
|
|
498
|
+
.reverse()
|
|
499
|
+
.find((capture) => capture.request !== null);
|
|
500
|
+
await completeAttempt(state, {
|
|
501
|
+
status: terminal.status,
|
|
502
|
+
providerId: lastCandidate?.provider_id ?? "",
|
|
503
|
+
...(lastCandidate?.provider_account_id
|
|
504
|
+
? { accountId: lastCandidate.provider_account_id }
|
|
505
|
+
: {}),
|
|
506
|
+
...(lastCandidate?.provider_account_label
|
|
507
|
+
? { accountLabel: lastCandidate.provider_account_label }
|
|
508
|
+
: {}),
|
|
509
|
+
errorCategory: terminal.errorCategory,
|
|
510
|
+
errorOrigin: terminal.errorOrigin,
|
|
511
|
+
...(lastCandidate?.model_id === undefined ? {} : { modelId: lastCandidate.model_id }),
|
|
512
|
+
error: lastError,
|
|
513
|
+
terminal: true,
|
|
514
|
+
tenantId: input.tenantId,
|
|
515
|
+
ingressBody: state.ingressBody,
|
|
516
|
+
responseBody: null,
|
|
517
|
+
// The client receives the public error envelope on the wire — store it
|
|
518
|
+
// so the Client Response panel traces the failure as the client saw it.
|
|
519
|
+
...(terminal.status === "cancelled"
|
|
520
|
+
? {}
|
|
521
|
+
: { clientResponseText: errorClientResponseBody(lastError) }),
|
|
522
|
+
...(terminalCapture === undefined ? {} : { providerCapture: terminalCapture }),
|
|
523
|
+
db: deps.db,
|
|
524
|
+
...(deps.telemetryBuffer ? { telemetryBuffer: deps.telemetryBuffer } : {}),
|
|
525
|
+
...(deps.snapshotService ? { snapshotService: deps.snapshotService } : {}),
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
metrics.proxy_requests_total.inc(1, { status: "failed" });
|
|
529
|
+
throw lastError;
|
|
530
|
+
}
|