cartethyia 2.4.5 → 2.5.0
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 +4 -1
- package/README.md +63 -13
- package/dist/dashboard/assets/AccountCooldown-ChpdzxRH.js.gz +0 -0
- package/dist/dashboard/assets/BannedPage-9igfctfZ.js +1 -0
- package/dist/dashboard/assets/BannedPage-9igfctfZ.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-BzGevuql.js → CliToolDetailPage-ZRr2K2sF.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-ZRr2K2sF.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-BaRM_7We.js → CliToolsPage-D3xqZbN-.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-D3xqZbN-.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-CVnQPBBz.js +1 -0
- package/dist/dashboard/assets/CombosPage-CVnQPBBz.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-u6W1UXSc.js +1 -0
- package/dist/dashboard/assets/ConsoleLogPage-u6W1UXSc.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-DqUGcoH0.js → CustomizationPage-CTTpCb25.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-CTTpCb25.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-C5XpWlvy.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-C5XpWlvy.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-DbhTGfhb.js → LoginPage-DHoB3Agn.js} +1 -1
- package/dist/dashboard/assets/LoginPage-DHoB3Agn.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js +1 -0
- package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-BrY4SgIt.js +1 -0
- package/dist/dashboard/assets/OverviewPage-BrY4SgIt.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-D9VSt7T_.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage-D9VSt7T_.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-Df77V5I5.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-Df77V5I5.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-CDfxP7AC.js +7 -0
- package/dist/dashboard/assets/ProxyPage-CDfxP7AC.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-CY4o2t4p.js +1 -0
- package/dist/dashboard/assets/QuotaPage-CY4o2t4p.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-YxRml8AT.js → SettingsPage-CvjZxHE4.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-CvjZxHE4.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-D3nza7LH.js +1 -0
- package/dist/dashboard/assets/SetupPage-D3nza7LH.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-Bol1ASmI.js → SortableList-Dtt12WT1.js} +1 -1
- package/dist/dashboard/assets/SortableList-Dtt12WT1.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-CbK3hXvU.js +31 -0
- package/dist/dashboard/assets/StudioPage-CbK3hXvU.js.gz +0 -0
- package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-BQlWbWg2.js +56 -0
- package/dist/dashboard/assets/UsagePage-BQlWbWg2.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-BmDbsior.js +1 -0
- 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/{app-BDJWMThu.js → app-C-Y4oZT0.js} +3 -3
- package/dist/dashboard/assets/app-C-Y4oZT0.js.gz +0 -0
- package/dist/dashboard/assets/arrow-left-CpBxkoxI.js.gz +0 -0
- package/dist/dashboard/assets/arrow-right-Dl5yKnp8.js.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/backup-file-gdKIlg8U.js.gz +0 -0
- package/dist/dashboard/assets/{badge-BwmwEOgA.js → badge-DpXT4Ixt.js} +1 -1
- package/dist/dashboard/assets/badge-DpXT4Ixt.js.gz +0 -0
- package/dist/dashboard/assets/{banner-BKBfKTqn.js → banner-CSAAWLJv.js} +1 -1
- package/dist/dashboard/assets/banner-CSAAWLJv.js.gz +0 -0
- package/dist/dashboard/assets/brain-Cwed_72z.js.gz +0 -0
- package/dist/dashboard/assets/{card-BTdbRFFg.js → card-DEhRXXw4.js} +1 -1
- package/dist/dashboard/assets/card-DEhRXXw4.js.gz +0 -0
- package/dist/dashboard/assets/check-D69FzdvC.js.gz +0 -0
- package/dist/dashboard/assets/chevron-right-BXIy5l3h.js.gz +0 -0
- package/dist/dashboard/assets/circle-alert-CntYHV5v.js.gz +0 -0
- package/dist/dashboard/assets/circle-check-BU3v_mFW.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-B0wBq-RL.js +1 -0
- package/dist/dashboard/assets/cli-tools-B0wBq-RL.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js +1 -0
- package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js.gz +0 -0
- package/dist/dashboard/assets/cog-CdWhsajs.js.gz +0 -0
- package/dist/dashboard/assets/copy-ngmQ9zcZ.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-iocKA3jr.js → dialog-BvQeBMfP.js} +1 -1
- package/dist/dashboard/assets/dialog-BvQeBMfP.js.gz +0 -0
- package/dist/dashboard/assets/dialog-actions-B1axpEg7.js.gz +0 -0
- package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
- package/dist/dashboard/assets/download-dRFo5_Wu.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-CDKOTZdY.js → drawer-CMwPXwQB.js} +1 -1
- package/dist/dashboard/assets/drawer-CMwPXwQB.js.gz +0 -0
- package/dist/dashboard/assets/entry-D4O2229X.js +1 -0
- package/dist/dashboard/assets/entry-D4O2229X.js.gz +0 -0
- package/dist/dashboard/assets/entry-DKvr5Q9K.js +1 -0
- package/dist/dashboard/assets/entry-DKvr5Q9K.js.gz +0 -0
- package/dist/dashboard/assets/entry-FQCejoDY.js +4 -0
- package/dist/dashboard/assets/entry-FQCejoDY.js.gz +0 -0
- package/dist/dashboard/assets/eye-C_hawfH4.js.gz +0 -0
- package/dist/dashboard/assets/eye-off-Bk5g2luF.js.gz +0 -0
- package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
- package/dist/dashboard/assets/gauge-BbIMy7MB.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-zliLvBwr.js +1 -0
- package/dist/dashboard/assets/github-badge-zliLvBwr.js.gz +0 -0
- package/dist/dashboard/assets/image-BPaouViG.js.gz +0 -0
- package/dist/dashboard/assets/{index-C4hXfAMs.js → index-CXm3FIyt.js} +2 -2
- package/dist/dashboard/assets/index-CXm3FIyt.js.gz +0 -0
- package/dist/dashboard/assets/{inline-DCejSLlA.js → inline-D1IlZZ8H.js} +1 -1
- package/dist/dashboard/assets/inline-D1IlZZ8H.js.gz +0 -0
- package/dist/dashboard/assets/{input-Bn4_kNav.js → input-BpVZdrbc.js} +1 -1
- package/dist/dashboard/assets/input-BpVZdrbc.js.gz +0 -0
- package/dist/dashboard/assets/jsx-runtime-CXUMngI_.js.gz +0 -0
- package/dist/dashboard/assets/key-round-UkavsGxp.js.gz +0 -0
- package/dist/dashboard/assets/layers-2-glBbjl.js.gz +0 -0
- package/dist/dashboard/assets/{layout-Bv-NxqZ5.js → layout-BioLW7gN.js} +1 -1
- package/dist/dashboard/assets/layout-BioLW7gN.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-BLA0sldD.js → list-row-CwVcGAIc.js} +1 -1
- package/dist/dashboard/assets/list-row-CwVcGAIc.js.gz +0 -0
- package/dist/dashboard/assets/{live-YlifPYox.js → live-yfEJFF9T.js} +1 -1
- package/dist/dashboard/assets/live-yfEJFF9T.js.gz +0 -0
- package/dist/dashboard/assets/log-in-BmRjy60c.js.gz +0 -0
- package/dist/dashboard/assets/message-circle-CCxGwRvv.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-CT-TxSNX.js +1 -0
- package/dist/dashboard/assets/model-bans-CT-TxSNX.js.gz +0 -0
- package/dist/dashboard/assets/mutation-BkoLM5Ba.js +1 -0
- package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
- package/dist/dashboard/assets/numbers-CFL4_Dqc.js.gz +0 -0
- package/dist/dashboard/assets/pencil-768ysRAr.js.gz +0 -0
- package/dist/dashboard/assets/plus-DIdX_g-x.js.gz +0 -0
- package/dist/dashboard/assets/power-off-Yjw9GMJh.js +1 -0
- package/dist/dashboard/assets/power-off-Yjw9GMJh.js.gz +0 -0
- package/dist/dashboard/assets/privacy-mode-GM8bej85.js +1 -0
- package/dist/dashboard/assets/privacy-mode-GM8bej85.js.gz +0 -0
- package/dist/dashboard/assets/providers/agentrouter.svg.gz +0 -0
- package/dist/dashboard/assets/providers/aihubmix.svg.gz +0 -0
- package/dist/dashboard/assets/providers/alibaba-coding-plan.svg.gz +0 -0
- package/dist/dashboard/assets/providers/alibaba.svg.gz +0 -0
- package/dist/dashboard/assets/providers/antigravity.svg.gz +0 -0
- package/dist/dashboard/assets/providers/bai.svg.gz +0 -0
- package/dist/dashboard/assets/providers/blackbox.svg.gz +0 -0
- package/dist/dashboard/assets/providers/claude-code.svg.gz +0 -0
- package/dist/dashboard/assets/providers/cursor.webp +0 -0
- package/dist/dashboard/assets/providers/exa.svg.gz +0 -0
- package/dist/dashboard/assets/providers/gmi-cloud.svg.gz +0 -0
- package/dist/dashboard/assets/providers/inferhub.svg.gz +0 -0
- package/dist/dashboard/assets/providers/muse.svg.gz +0 -0
- package/dist/dashboard/assets/providers/nous-research.svg.gz +0 -0
- package/dist/dashboard/assets/providers/openai-light.svg.gz +0 -0
- package/dist/dashboard/assets/providers/parallel.svg.gz +0 -0
- package/dist/dashboard/assets/providers/tokenharbor.svg.gz +0 -0
- package/dist/dashboard/assets/providers/trae.webp +0 -0
- package/dist/dashboard/assets/providers/zai-coding-plan.svg.gz +0 -0
- package/dist/dashboard/assets/{query-keys-ROf-g1Wi.js → query-keys-D7wX2vM5.js} +1 -1
- package/dist/dashboard/assets/query-keys-D7wX2vM5.js.gz +0 -0
- package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
- package/dist/dashboard/assets/refresh-cw-BqJFk4sU.js.gz +0 -0
- package/dist/dashboard/assets/rotate-ccw-CufcqITi.js.gz +0 -0
- package/dist/dashboard/assets/route-CNSNj9Zm.js.gz +0 -0
- package/dist/dashboard/assets/routing-Cs6jl49L.js +1 -0
- package/dist/dashboard/assets/routing-Cs6jl49L.js.gz +0 -0
- package/dist/dashboard/assets/scaling-nWE4Cx8f.js.gz +0 -0
- package/dist/dashboard/assets/search-JOKus9rb.js.gz +0 -0
- package/dist/dashboard/assets/select-C0Rer3gF.js +41 -0
- package/dist/dashboard/assets/select-C0Rer3gF.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/shield-check-DVS0Foqh.js.gz +0 -0
- package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
- package/dist/dashboard/assets/{stack-C1kTcOmP.js → stack-C9_uUjaA.js} +1 -1
- package/dist/dashboard/assets/stack-C9_uUjaA.js.gz +0 -0
- package/dist/dashboard/assets/{state-DHKdg-pm.js → state-DA0B95UQ.js} +1 -1
- package/dist/dashboard/assets/state-DA0B95UQ.js.gz +0 -0
- package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
- package/dist/dashboard/assets/{switch-DIDeAiz9.js → switch-Cittxy-x.js} +1 -1
- package/dist/dashboard/assets/switch-Cittxy-x.js.gz +0 -0
- package/dist/dashboard/assets/terminal-Bd4ee8TA.js +1 -0
- package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
- package/dist/dashboard/assets/toast-CNt_jzO4.js +2 -0
- package/dist/dashboard/assets/toast-CNt_jzO4.js.gz +0 -0
- package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
- package/dist/dashboard/assets/trash-Cppn5c3R.js.gz +0 -0
- package/dist/dashboard/assets/use-clipboard-B0Sy03fE.js.gz +0 -0
- package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
- package/dist/dashboard/assets/use-reduced-motion-C4B0R6Sz.js.gz +0 -0
- package/dist/dashboard/assets/use-timeout-BwcBBRrT.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-DVmwOKU-.js → useMutation-BQKMcJ1H.js} +1 -1
- package/dist/dashboard/assets/useMutation-BQKMcJ1H.js.gz +0 -0
- package/dist/dashboard/assets/wrench-B6E1oEuY.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 +122 -0
- package/migrations/full-postgres/0013_add_providers_tenant_id_index.sql +22 -0
- package/migrations/full-postgres/0014_add_provider_accounts_tenant_id_index.sql +21 -0
- 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/0000_baseline.sql +4 -2
- package/migrations/lite-sqlite3/0013_add_providers_tenant_id_index.sql +4 -0
- package/migrations/lite-sqlite3/0014_add_provider_accounts_tenant_id_index.sql +4 -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 +26 -30
- package/scripts/cli/{carte-launcher.cjs → bin/carte.cjs} +8 -8
- package/scripts/cli/{cli-launcher.cjs → bin/cartethyia.cjs} +167 -134
- package/scripts/cli/command/admin.ts +65 -0
- package/scripts/cli/command/console-menu.ts +246 -0
- package/scripts/cli/{doctor.ts → command/doctor.ts} +257 -258
- package/scripts/cli/{setup.ts → command/setup.ts} +8 -12
- package/scripts/cli/command/start.ts +77 -0
- package/scripts/cli/command/tray.ts +165 -0
- package/scripts/cli/command/update.ts +23 -0
- package/scripts/cli/{runtime-state.ts → gateway/drain-token.ts} +78 -78
- package/scripts/cli/gateway/instance-lock.ts +105 -0
- package/scripts/cli/gateway/spawn.ts +173 -0
- package/scripts/cli/gateway/stop.ts +51 -0
- package/scripts/cli/main.ts +94 -0
- package/scripts/cli/support/bun.ts +27 -0
- package/scripts/cli/support/env.ts +99 -0
- package/scripts/cli/support/platform.ts +64 -0
- package/scripts/cli/support/services.ts +88 -0
- package/scripts/cli/support/startup-signal.ts +23 -0
- package/scripts/cli/{version.ts → support/version.ts} +1 -1
- package/scripts/cli/tray/hidden-window.vbs +28 -0
- package/scripts/cli/{tray.ts → tray/index.ts} +3 -3
- package/scripts/cli/{tray.ps1 → tray/windows.ps1} +107 -98
- package/scripts/cli/ui/background.ts +110 -0
- package/src/boot/app.ts +23 -7
- package/src/boot/dependencies.ts +55 -13
- 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 +31 -5
- package/src/config/runtime.ts +19 -3
- package/src/config/spec.ts +20 -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/backup/routes.ts +8 -0
- package/src/console/cli-tools/contracts.ts +6 -6
- package/src/console/console-router.ts +5 -3
- package/src/console/dashboard-assets.ts +81 -11
- package/src/console/domain-registration.ts +9 -2
- package/src/console/observability/contracts.ts +475 -423
- package/src/console/observability/routes.ts +31 -6
- package/src/console/observability/store.ts +1015 -912
- package/src/console/providers/catalog/contracts.ts +31 -0
- package/src/console/providers/catalog/provider-operations.ts +8 -0
- package/src/console/providers/catalog/public-model-store.ts +91 -60
- package/src/console/providers/catalog/store.ts +1532 -1521
- package/src/console/providers/oauth/callback-listener.ts +18 -3
- package/src/console/providers/oauth/routes.ts +38 -15
- package/src/console/quota/account-quota-tenant.ts +9 -6
- package/src/console/quota/refresh.ts +19 -0
- package/src/console/response-compression.ts +69 -2
- package/src/console/routing/pools/probe-verdict.ts +72 -0
- package/src/console/share/share-router.ts +7 -4
- package/src/console/share/share-stats.ts +12 -5
- package/src/console/shared/read-through-cache.ts +133 -7
- package/src/console/updates/repository-updates.ts +39 -34
- 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 +87 -24
- 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/event-log.ts +29 -8
- package/src/observability/metrics.ts +40 -13
- package/src/observability/payload-capture.ts +344 -324
- package/src/observability/runtime-metrics.ts +7 -0
- 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 -1257
- 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/codex.ts +9 -75
- package/src/protocol/response/omp-tool-call-markup.ts +221 -0
- package/src/protocol/tool-identity.ts +43 -9
- package/src/providers/authentication/jwt-validator.ts +130 -187
- package/src/providers/authentication/oauth-flow-store.ts +16 -9
- package/src/providers/default-registry.ts +177 -1
- package/src/providers/discovery/catalog/catalog-format.ts +20 -0
- package/src/providers/discovery/models-dev-catalog.ts +39 -22
- package/src/providers/discovery/openai-model-discovery.ts +39 -3
- package/src/providers/discovery/openrouter-free-roster.ts +98 -0
- package/src/providers/discovery/probe-wire.ts +29 -5
- package/src/providers/discovery/probing-service.ts +1022 -1001
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +42 -2
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +913 -914
- package/src/providers/integrations/buddy/buddy-cli-headers.ts +88 -0
- package/src/providers/integrations/buddy/buddy-conversation-headers.ts +22 -5
- package/src/providers/integrations/buddy/buddy-discovery-shared.ts +33 -1
- package/src/providers/integrations/buddy/buddy-identity.ts +46 -0
- package/src/providers/integrations/buddy/buddy-oauth-shared.ts +37 -1
- package/src/providers/integrations/buddy/buddy-usage-store.ts +40 -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 +115 -73
- package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +5 -2
- package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -204
- package/src/providers/integrations/buddy/workbuddy/quota.ts +172 -159
- package/src/providers/integrations/buddy/workbuddy/shared.ts +200 -171
- 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/codex/codex.ts +5 -8
- 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/factory/factory-oauth.ts +7 -11
- 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/opencode-go-quota.ts +130 -0
- package/src/providers/integrations/opencode/provider.ts +532 -499
- 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 -263
- 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/shared/install-id.ts +29 -2
- 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 +143 -6
- package/src/providers/operations/model-catalog-cache.ts +18 -3
- package/src/providers/operations/provider-catalog-seeder.ts +95 -86
- package/src/providers/operations/provider-catalog-service.ts +76 -0
- 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 +39 -2
- package/src/transport/dispatch/attempt-loop.ts +530 -513
- package/src/transport/dispatch/empty-stream.ts +17 -26
- 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/failure-policy.ts +20 -0
- 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 -738
- package/src/transport/routing/route-model.ts +52 -1
- package/src/transport/routing/router.ts +51 -0
- package/src/transport/surface/chat/encode.ts +18 -31
- package/src/transport/surface/messages/stream.ts +3 -2
- package/src/transport/translation/tool-repair.ts +37 -16
- package/src/workers/account-identity-sweep.ts +14 -12
- package/src/workers/daily-checkin.ts +62 -0
- package/src/workers/quota-refresh-sweep.ts +27 -2
- package/dist/dashboard/assets/BannedPage-DUrXpp-X.js +0 -1
- package/dist/dashboard/assets/CombosPage-B7ETkfVH.js +0 -1
- package/dist/dashboard/assets/ConsoleLogPage-AisQzCI1.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-BNflDm_f.js +0 -1
- package/dist/dashboard/assets/ModelPicker-CAiPR5Vd.js +0 -1
- package/dist/dashboard/assets/OverviewPage-C3NIzsDx.js +0 -1
- package/dist/dashboard/assets/ProviderDetailPage-BK8i5B5X.js +0 -3
- package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-CvCVgm1e.js +0 -1
- package/dist/dashboard/assets/ProxyPage-DjabkuzP.js +0 -7
- package/dist/dashboard/assets/QuotaPage-Dxe0B0iu.js +0 -1
- package/dist/dashboard/assets/SetupPage-BaD4P_jW.js +0 -1
- package/dist/dashboard/assets/StudioPage-CLP4le3U.js +0 -31
- package/dist/dashboard/assets/UsagePage-Djes-Epy.js +0 -56
- package/dist/dashboard/assets/api-keys-CjIGQhe7.js +0 -1
- package/dist/dashboard/assets/app-Cjgo4-Lf.css +0 -2
- package/dist/dashboard/assets/assert-Bwh6DJTK.js +0 -1
- package/dist/dashboard/assets/cli-tools-DccaOfhX.js +0 -1
- package/dist/dashboard/assets/clipboard-button-DQmMikNQ.js +0 -1
- package/dist/dashboard/assets/entry-Cpi0rph1.js +0 -4
- package/dist/dashboard/assets/entry-Dm3PpDCO.js +0 -1
- package/dist/dashboard/assets/entry-keNDZ_Wz.js +0 -1
- package/dist/dashboard/assets/github-badge-BuAJr8oU.js +0 -1
- package/dist/dashboard/assets/model-bans-CVvHZ-qz.js +0 -1
- package/dist/dashboard/assets/mutation-BIizFZav.js +0 -1
- package/dist/dashboard/assets/privacy-mode-CQjbZ3xk.js +0 -1
- package/dist/dashboard/assets/routing-jntw9-Ui.js +0 -1
- package/dist/dashboard/assets/select-CGUuTQmj.js +0 -41
- package/dist/dashboard/assets/settings-BAS6QbmJ.js +0 -1
- package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js +0 -1
- package/dist/dashboard/assets/terminal-C9mZ4Ugm.js +0 -1
- package/dist/dashboard/assets/toast-DKAdP2mo.js +0 -2
- package/scripts/cli/cli.ts +0 -771
- package/scripts/operations/probe-egress.ts +0 -335
- package/scripts/operations/restart.ts +0 -120
- package/scripts/shared/env.ts +0 -269
- /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.ico +0 -0
- /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.png +0 -0
- /package/scripts/cli/{menu.ts → ui/prompt.ts} +0 -0
- /package/scripts/cli/{wordmark.ts → ui/wordmark.ts} +0 -0
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
* rather than surfacing an empty body to the client.
|
|
19
19
|
*/
|
|
20
20
|
import { GatewayError } from "../../protocol/gateway-error";
|
|
21
|
+
import {
|
|
22
|
+
fallbackRetryDelayMs,
|
|
23
|
+
MIN_RETRY_BUDGET_MS,
|
|
24
|
+
sleep as abortableSleep,
|
|
25
|
+
} from "../failure-policy";
|
|
21
26
|
|
|
22
27
|
// ─── Public types ──────────────────────────────────────────────────────────
|
|
23
28
|
|
|
@@ -38,9 +43,17 @@ export type EmptyStreamFetch = (
|
|
|
38
43
|
init: { headers: Record<string, string>; body: string; signal: AbortSignal },
|
|
39
44
|
) => Promise<Response>;
|
|
40
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Retry-delay policy.
|
|
48
|
+
*
|
|
49
|
+
* Delays come from `fallbackRetryDelayMs` (capped exponential with full
|
|
50
|
+
* jitter), whose base and cap are operator-tunable. There is deliberately no
|
|
51
|
+
* per-caller base: the local formula this replaced was uncapped and
|
|
52
|
+
* unjittered, so a provider returning 200-with-no-events could drive an
|
|
53
|
+
* unbounded wait, and concurrent clients could resynchronize their retries.
|
|
54
|
+
*/
|
|
41
55
|
export interface EmptyStreamPolicy {
|
|
42
56
|
readonly maxEmptyReplays?: number;
|
|
43
|
-
readonly baseDelayMs?: number;
|
|
44
57
|
readonly firstEventTimeoutMs?: number;
|
|
45
58
|
readonly sleep?: (ms: number, signal: AbortSignal) => Promise<void>;
|
|
46
59
|
}
|
|
@@ -55,9 +68,8 @@ export interface EmptyStreamResult {
|
|
|
55
68
|
// ─── Defaults ──────────────────────────────────────────────────────────────
|
|
56
69
|
|
|
57
70
|
const DEFAULT_MAX_REPLAYS = 2;
|
|
58
|
-
const DEFAULT_BASE_DELAY_MS = 500;
|
|
59
71
|
const DEFAULT_FIRST_EVENT_TIMEOUT_MS = 200_000; // 200s, matches CARTETHYIA_STREAM_FIRST_CHUNK_TIMEOUT_MS default
|
|
60
|
-
|
|
72
|
+
|
|
61
73
|
|
|
62
74
|
// ─── Helpers ───────────────────────────────────────────────────────────────
|
|
63
75
|
|
|
@@ -66,27 +78,7 @@ export function isEmptyStream(tally: StreamTally): boolean {
|
|
|
66
78
|
return tally.dataEvents === 0 || (!tally.meaningful && !tally.thoughtOnly);
|
|
67
79
|
}
|
|
68
80
|
|
|
69
|
-
function defaultSleep(ms: number, signal: AbortSignal): Promise<void> {
|
|
70
|
-
return new Promise((resolve, reject) => {
|
|
71
|
-
if (signal.aborted) {
|
|
72
|
-
reject(new GatewayError("transport_closed", 499, "Aborted"));
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
const timer = setTimeout(() => {
|
|
76
|
-
signal.removeEventListener("abort", onAbort);
|
|
77
|
-
resolve();
|
|
78
|
-
}, ms);
|
|
79
|
-
const onAbort = () => {
|
|
80
|
-
clearTimeout(timer);
|
|
81
|
-
reject(new GatewayError("transport_closed", 499, "Aborted"));
|
|
82
|
-
};
|
|
83
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
81
|
|
|
87
|
-
function backoffMs(attempt: number, base: number): number {
|
|
88
|
-
return base * 2 ** attempt;
|
|
89
|
-
}
|
|
90
82
|
|
|
91
83
|
// ─── Main function ─────────────────────────────────────────────────────────
|
|
92
84
|
|
|
@@ -109,9 +101,8 @@ export async function fetchWithEmptyStreamRecovery(
|
|
|
109
101
|
},
|
|
110
102
|
): Promise<EmptyStreamResult> {
|
|
111
103
|
const maxReplays = deps.policy?.maxEmptyReplays ?? DEFAULT_MAX_REPLAYS;
|
|
112
|
-
const baseDelay = deps.policy?.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
|
|
113
104
|
const firstEventTimeout = deps.policy?.firstEventTimeoutMs ?? DEFAULT_FIRST_EVENT_TIMEOUT_MS;
|
|
114
|
-
const sleep = deps.policy?.sleep ??
|
|
105
|
+
const sleep = deps.policy?.sleep ?? abortableSleep;
|
|
115
106
|
|
|
116
107
|
if (endpoints.length === 0) {
|
|
117
108
|
throw new GatewayError("platform_unavailable", 502, "No endpoints available", {}, "upstream");
|
|
@@ -173,7 +164,7 @@ export async function fetchWithEmptyStreamRecovery(
|
|
|
173
164
|
|
|
174
165
|
// Empty — replay if we haven't exhausted this endpoint's budget
|
|
175
166
|
if (replay < replaysAllowed) {
|
|
176
|
-
const delay =
|
|
167
|
+
const delay = fallbackRetryDelayMs(replay);
|
|
177
168
|
try {
|
|
178
169
|
await sleep(delay, deps.signal);
|
|
179
170
|
} catch (error) {
|
|
@@ -52,6 +52,37 @@ export interface GenerationHandlerDeps {
|
|
|
52
52
|
/** Which surface a handler serves. Drives candidate filtering and error copy. */
|
|
53
53
|
export type GenerationKind = Extract<ServiceKind, "imageGeneration" | "videoGeneration">;
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Surfaces the gateway routes but does not model.
|
|
57
|
+
*
|
|
58
|
+
* Embeddings and audio have no adapter method and no media shape — there is
|
|
59
|
+
* nothing for this repo to translate — so they exist purely so a caller can
|
|
60
|
+
* reach a compatible upstream through the normal pipeline. Opening the route
|
|
61
|
+
* is the whole feature: routing, admission, failover, and accounting still
|
|
62
|
+
* apply, and the upstream's own bytes come back untouched.
|
|
63
|
+
*/
|
|
64
|
+
export type PassthroughKind = Extract<ServiceKind, "embedding" | "audioTranscription" | "audioSpeech">;
|
|
65
|
+
|
|
66
|
+
/** Error-copy noun per surface, so a refusal names what the caller asked for. */
|
|
67
|
+
const SURFACE_NOUNS: Record<GenerationKind | PassthroughKind, string> = {
|
|
68
|
+
imageGeneration: "image",
|
|
69
|
+
videoGeneration: "video",
|
|
70
|
+
embedding: "embedding",
|
|
71
|
+
audioTranscription: "transcription",
|
|
72
|
+
audioSpeech: "speech",
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Only the two media surfaces carry `prompt`/`n`.
|
|
77
|
+
*
|
|
78
|
+
* An embedding body is `{model, input}` and a transcription is multipart —
|
|
79
|
+
* neither has a prompt, so validating one here would reject every legitimate
|
|
80
|
+
* request to a surface the gateway just opened.
|
|
81
|
+
*/
|
|
82
|
+
function isMediaKind(kind: GenerationKind | PassthroughKind): kind is GenerationKind {
|
|
83
|
+
return kind === "imageGeneration" || kind === "videoGeneration";
|
|
84
|
+
}
|
|
85
|
+
|
|
55
86
|
/**
|
|
56
87
|
* Maximum items one request may ask for.
|
|
57
88
|
*
|
|
@@ -126,7 +157,15 @@ async function passThrough(
|
|
|
126
157
|
|
|
127
158
|
/** Builds the handler for one generation surface. */
|
|
128
159
|
export function createGenerationHandler(kind: GenerationKind, deps: GenerationHandlerDeps) {
|
|
129
|
-
|
|
160
|
+
return createNativeSurfaceHandler(kind, deps);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Builds the handler for one routed surface, media or passthrough. */
|
|
164
|
+
export function createNativeSurfaceHandler(
|
|
165
|
+
kind: GenerationKind | PassthroughKind,
|
|
166
|
+
deps: GenerationHandlerDeps,
|
|
167
|
+
) {
|
|
168
|
+
const noun = SURFACE_NOUNS[kind];
|
|
130
169
|
return async ({ request }: { request: Request }): Promise<Response> => {
|
|
131
170
|
const state = deps.stateStore.require(request);
|
|
132
171
|
const authorization = state.authorization;
|
|
@@ -151,19 +190,21 @@ export function createGenerationHandler(kind: GenerationKind, deps: GenerationHa
|
|
|
151
190
|
}
|
|
152
191
|
// Trust boundary only. Keep accepting `messages` for chat-backed providers
|
|
153
192
|
// while native image/video endpoints can use the simpler `prompt` field.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
193
|
+
if (isMediaKind(kind)) {
|
|
194
|
+
const hasPrompt = typeof generation.prompt === "string" && generation.prompt.trim().length > 0;
|
|
195
|
+
const hasMessages = Array.isArray(generation.messages) && generation.messages.length > 0;
|
|
196
|
+
if (!hasPrompt && !hasMessages) {
|
|
197
|
+
throw new GatewayError("invalid_request", 400, "prompt is required");
|
|
198
|
+
}
|
|
199
|
+
if (generation.n !== undefined) {
|
|
200
|
+
const n = generation.n;
|
|
201
|
+
if (typeof n !== "number" || !Number.isInteger(n) || n < 1 || n > MAX_ITEMS_PER_REQUEST) {
|
|
202
|
+
throw new GatewayError(
|
|
203
|
+
"invalid_request",
|
|
204
|
+
400,
|
|
205
|
+
`n must be an integer between 1 and ${MAX_ITEMS_PER_REQUEST}`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
167
208
|
}
|
|
168
209
|
}
|
|
169
210
|
|
|
@@ -293,14 +334,20 @@ export function createGenerationHandler(kind: GenerationKind, deps: GenerationHa
|
|
|
293
334
|
// body to the candidate's endpoint through the adapter, which owns the
|
|
294
335
|
// upstream base URL. Either way the gateway does not interpret the
|
|
295
336
|
// media — it returns what the upstream produced.
|
|
337
|
+
//
|
|
338
|
+
// A passthrough surface has no media method to prefer: the whole point
|
|
339
|
+
// is that this repo does not model it, so it goes straight to the
|
|
340
|
+
// adapter's native dispatch, which is plain forwarding.
|
|
296
341
|
const media =
|
|
297
|
-
kind
|
|
298
|
-
?
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
:
|
|
342
|
+
!isMediaKind(kind)
|
|
343
|
+
? undefined
|
|
344
|
+
: kind === "videoGeneration"
|
|
345
|
+
? typeof adapter.generateVideo === "function"
|
|
346
|
+
? adapter.generateVideo(upstreamBody, target, mediaContext).then((outcome) => finish(outcome))
|
|
347
|
+
: undefined
|
|
348
|
+
: typeof adapter.generateImage === "function"
|
|
349
|
+
? adapter.generateImage(upstreamBody, target, mediaContext).then((outcome) => finish(outcome))
|
|
350
|
+
: undefined;
|
|
304
351
|
const upstream =
|
|
305
352
|
media !== undefined
|
|
306
353
|
? await media
|
|
@@ -348,3 +395,23 @@ export function createImageGenerationHandler(deps: GenerationHandlerDeps) {
|
|
|
348
395
|
export function createVideoGenerationHandler(deps: GenerationHandlerDeps) {
|
|
349
396
|
return createGenerationHandler("videoGeneration", deps);
|
|
350
397
|
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* `POST /v1/embeddings`.
|
|
401
|
+
*
|
|
402
|
+
* Routed with no provider implementation behind it: a model row carrying the
|
|
403
|
+
* `embedding` kind is enough, and the adapter forwards the body as-is.
|
|
404
|
+
*/
|
|
405
|
+
export function createEmbeddingHandler(deps: GenerationHandlerDeps) {
|
|
406
|
+
return createNativeSurfaceHandler("embedding", deps);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** `POST /v1/audio/transcriptions`. Same passthrough shape as embeddings. */
|
|
410
|
+
export function createAudioTranscriptionHandler(deps: GenerationHandlerDeps) {
|
|
411
|
+
return createNativeSurfaceHandler("audioTranscription", deps);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** `POST /v1/audio/speech`. Same passthrough shape as embeddings. */
|
|
415
|
+
export function createAudioSpeechHandler(deps: GenerationHandlerDeps) {
|
|
416
|
+
return createNativeSurfaceHandler("audioSpeech", deps);
|
|
417
|
+
}
|
|
@@ -14,6 +14,63 @@ export interface AttemptLeases {
|
|
|
14
14
|
readonly reservation: Reservation;
|
|
15
15
|
readonly proxySlot?: { poolId: string; release: () => void };
|
|
16
16
|
readonly networkPoolId?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Ownership of this attempt's account/network-pool release.
|
|
19
|
+
*
|
|
20
|
+
* A provider session that outlives the HTTP request — Cursor's Run, held open
|
|
21
|
+
* for the client's next tool-result request — takes the release over instead
|
|
22
|
+
* of having the request release it when the response is flushed. Request-scoped
|
|
23
|
+
* accounting (admission, per-request telemetry) still settles per request;
|
|
24
|
+
* only the *slot and pool inflight* move to the session's lifetime.
|
|
25
|
+
*/
|
|
26
|
+
readonly ownership?: AttemptLeaseOwnership;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Request-scoped accounting versus session-scoped ownership.
|
|
31
|
+
*
|
|
32
|
+
* One HTTP request = one admission lease, one reservation, one pool slot. One
|
|
33
|
+
* Cursor Run = one upstream turn that can span several HTTP requests, and it
|
|
34
|
+
* must keep the slot for the whole turn. Transferring ownership is the only way
|
|
35
|
+
* to satisfy both: the request settles its own accounting and stops owning the
|
|
36
|
+
* release; the session owns it until the Run terminates.
|
|
37
|
+
*
|
|
38
|
+
* Idempotent by construction — the release is handed over at most once, and a
|
|
39
|
+
* second transfer attempt gets `undefined` so nothing is released twice.
|
|
40
|
+
*/
|
|
41
|
+
export class AttemptLeaseOwnership {
|
|
42
|
+
#transferred = false;
|
|
43
|
+
|
|
44
|
+
constructor(private readonly release: () => void) {}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Hands the release to a provider session. Returns the release to call
|
|
48
|
+
* exactly once when the session terminates, or `undefined` when this attempt
|
|
49
|
+
* already transferred it (or released it itself).
|
|
50
|
+
*/
|
|
51
|
+
transferToProviderSession(): (() => void) | undefined {
|
|
52
|
+
if (this.#transferred) return undefined;
|
|
53
|
+
this.#transferred = true;
|
|
54
|
+
const release = this.release;
|
|
55
|
+
let released = false;
|
|
56
|
+
return () => {
|
|
57
|
+
if (released) return;
|
|
58
|
+
released = true;
|
|
59
|
+
release();
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Releases only when ownership was never transferred — the ordinary
|
|
65
|
+
* end-of-request path. Returns false when a session owns it, so the caller
|
|
66
|
+
* can see that the slot outlives this request.
|
|
67
|
+
*/
|
|
68
|
+
releaseByResponse(): boolean {
|
|
69
|
+
if (this.#transferred) return false;
|
|
70
|
+
this.#transferred = true;
|
|
71
|
+
this.release();
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
17
74
|
}
|
|
18
75
|
|
|
19
76
|
export interface AttemptLeaseSource {
|
|
@@ -23,6 +80,21 @@ export interface AttemptLeaseSource {
|
|
|
23
80
|
readonly estimatedInputTokens: number;
|
|
24
81
|
readonly estimatedOutputTokens: number;
|
|
25
82
|
readonly authorizationSnapshot: ApiKeyAuthorizationSnapshot;
|
|
83
|
+
/**
|
|
84
|
+
* True when this request continues a provider session the gateway already
|
|
85
|
+
* holds, so its account and network-pool slot are already occupied by that
|
|
86
|
+
* session.
|
|
87
|
+
*
|
|
88
|
+
* A Cursor tool-result request borrows the held Run: it talks to the socket
|
|
89
|
+
* that is already open and does not open another connection. Acquiring a
|
|
90
|
+
* second pool slot would charge one logical upstream turn two concurrency
|
|
91
|
+
* slots — and, with a pool at its limit, would make the continuation fail
|
|
92
|
+
* with `proxy_pool_capacity_exceeded` for a turn that is already inside it.
|
|
93
|
+
*
|
|
94
|
+
* Admission (API-key authorization) and the routing reservation still run:
|
|
95
|
+
* per-request accounting is per request.
|
|
96
|
+
*/
|
|
97
|
+
readonly borrowsHeldSession?: boolean;
|
|
26
98
|
}
|
|
27
99
|
|
|
28
100
|
async function acquirePoolSlot(
|
|
@@ -196,7 +268,10 @@ export async function acquireAttemptLeases(
|
|
|
196
268
|
// address per request, so storing it here would only pin a stale answer.
|
|
197
269
|
if (input.networkBindingFactory && input.host)
|
|
198
270
|
await input.networkBindingFactory.resolve(input.host.hostname, input.signal, input.host.port);
|
|
199
|
-
|
|
271
|
+
// A continuation borrows the session's slot; see `borrowsHeldSession`.
|
|
272
|
+
const slot = source.borrowsHeldSession === true
|
|
273
|
+
? undefined
|
|
274
|
+
: await acquirePoolSlot(candidate, source, input.poolSelector, input.strictPoolSelection);
|
|
200
275
|
if (slot) partial.proxySlot = slot;
|
|
201
276
|
if (input.strictPoolSelection && input.signal.aborted)
|
|
202
277
|
throw new GatewayError("transport_closed", 499, "request was cancelled");
|
|
@@ -209,6 +284,14 @@ export async function acquireAttemptLeases(
|
|
|
209
284
|
reservation: partial.reservation,
|
|
210
285
|
...(partial.proxySlot ? { proxySlot: partial.proxySlot } : {}),
|
|
211
286
|
...(partial.proxySlot ? { networkPoolId: partial.proxySlot.poolId } : {}),
|
|
287
|
+
// The pool slot is the one resource a provider session can outlive, so it
|
|
288
|
+
// is the one wrapped in a transferable owner. The admission lease and the
|
|
289
|
+
// routing reservation stay request-scoped: the account's concurrency slot
|
|
290
|
+
// is held for the Run's duration, while per-request usage settles per
|
|
291
|
+
// request.
|
|
292
|
+
...(partial.proxySlot
|
|
293
|
+
? { ownership: new AttemptLeaseOwnership(partial.proxySlot.release) }
|
|
294
|
+
: {}),
|
|
212
295
|
};
|
|
213
296
|
} catch (error) {
|
|
214
297
|
await releaseAttemptLeases(partial, source.routingEngine);
|
|
@@ -221,14 +304,28 @@ export async function releaseAttemptLeases(
|
|
|
221
304
|
readonly lease?: AdmissionLease | undefined;
|
|
222
305
|
readonly reservation?: Reservation | undefined;
|
|
223
306
|
readonly proxySlot?: { release: () => void } | undefined;
|
|
307
|
+
/**
|
|
308
|
+
* When present, the pool slot's release is owned rather than called
|
|
309
|
+
* directly: `releaseByResponse()` releases only if a provider session did
|
|
310
|
+
* not take it over. A slot a live Cursor Run is holding must not be freed
|
|
311
|
+
* at response time — the Run's next request needs it, and releasing early
|
|
312
|
+
* is what lets another tenant's request occupy a slot that is still in use.
|
|
313
|
+
*/
|
|
314
|
+
readonly ownership?: AttemptLeaseOwnership | undefined;
|
|
224
315
|
},
|
|
225
316
|
routingEngine: Pick<RoutingEngine, "release">,
|
|
226
317
|
): Promise<void> {
|
|
227
318
|
// Every release runs inside the settled set: a throwing pool-slot release
|
|
228
319
|
// must not skip the admission lease or routing reservation (slot + inflight
|
|
229
320
|
// leak). Sync release is wrapped so it participates in the same guarantee.
|
|
321
|
+
// When ownership exists it supersedes the raw slot release: calling both
|
|
322
|
+
// would release twice, and `releaseByResponse` is the single-shot gate.
|
|
230
323
|
await Promise.allSettled([
|
|
231
|
-
...(leases.
|
|
324
|
+
...(leases.ownership
|
|
325
|
+
? [Promise.resolve().then(() => leases.ownership?.releaseByResponse())]
|
|
326
|
+
: leases.proxySlot
|
|
327
|
+
? [Promise.resolve().then(() => leases.proxySlot?.release())]
|
|
328
|
+
: []),
|
|
232
329
|
...(leases.lease ? [leases.lease.release()] : []),
|
|
233
330
|
...(leases.reservation ? [routingEngine.release(leases.reservation)] : []),
|
|
234
331
|
]);
|
|
@@ -28,12 +28,14 @@ import {
|
|
|
28
28
|
completionContext,
|
|
29
29
|
estimatedUsage,
|
|
30
30
|
parseCapturedBody,
|
|
31
|
-
|
|
31
|
+
shouldCaptureProviderBodyForRequest,
|
|
32
32
|
terminalFailure,
|
|
33
33
|
} from "./attempt-finalize";
|
|
34
34
|
import { runAttemptLoop } from "./attempt-loop";
|
|
35
35
|
import { projectForRoute, routeCapabilitiesFor } from "../translation/capabilities";
|
|
36
36
|
import { dispatchStreamingAttempt } from "./streaming/stream";
|
|
37
|
+
import type { ProviderContinuationScope } from "../../providers/provider-contracts";
|
|
38
|
+
import type { PreparedProxyRequest } from "../request/preparer";
|
|
37
39
|
|
|
38
40
|
export interface ProviderProxyHandlerDeps {
|
|
39
41
|
readonly db: CartethyiaDatabase;
|
|
@@ -52,6 +54,24 @@ export interface ProviderProxyHandlerDeps {
|
|
|
52
54
|
readonly oauthRefreshService?: OAuthRefreshService;
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
/**
|
|
58
|
+
* The continuation scope one dispatch carries.
|
|
59
|
+
*
|
|
60
|
+
* Present only for a route that has an account: a provider session held across
|
|
61
|
+
* requests is authorized by tenant + API key + provider + account, and a route
|
|
62
|
+
* with no account cannot be scoped — so it must not hold a session open at all.
|
|
63
|
+
*/
|
|
64
|
+
function continuationScopeFor(prepared: PreparedProxyRequest): ProviderContinuationScope | undefined {
|
|
65
|
+
const accountId = prepared.candidate.provider_account_id;
|
|
66
|
+
if (accountId === undefined) return undefined;
|
|
67
|
+
return {
|
|
68
|
+
tenantId: prepared.authorization.tenantId,
|
|
69
|
+
apiKeyId: prepared.authorization.id,
|
|
70
|
+
providerId: prepared.candidate.provider_id,
|
|
71
|
+
providerAccountId: accountId,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
55
75
|
/**
|
|
56
76
|
* First-token latency for one attempt, in the spread shape `completeAttempt`
|
|
57
77
|
* takes: `{}` when there is nothing to measure — no first byte arrived (a
|
|
@@ -127,6 +147,9 @@ export async function handleProviderProxyRequest(
|
|
|
127
147
|
estimatedInputTokens: prepared.estimatedInputTokens,
|
|
128
148
|
estimatedOutputTokens: prepared.estimatedOutputTokens,
|
|
129
149
|
authorizationSnapshot: prepared.authorization.snapshot,
|
|
150
|
+
// A continuation borrows the held Cursor Run: one upstream turn must not
|
|
151
|
+
// occupy two account/pool concurrency slots.
|
|
152
|
+
...(prepared.continuationAffinity === undefined ? {} : { borrowsHeldSession: true }),
|
|
130
153
|
},
|
|
131
154
|
candidates: attemptCandidates,
|
|
132
155
|
tenantId: prepared.authorization.tenantId,
|
|
@@ -175,6 +198,8 @@ export async function handleProviderProxyRequest(
|
|
|
175
198
|
const lease = context.leases.lease;
|
|
176
199
|
const reservation = context.leases.reservation;
|
|
177
200
|
const proxySlot = context.leases.proxySlot;
|
|
201
|
+
const leaseOwnership = context.leases.ownership;
|
|
202
|
+
const continuationScope = continuationScopeFor(prepared);
|
|
178
203
|
const providerRouteCandidate = {
|
|
179
204
|
provider_id: candidate.provider_id,
|
|
180
205
|
model_id: candidate.model_id,
|
|
@@ -209,13 +234,35 @@ export async function handleProviderProxyRequest(
|
|
|
209
234
|
prepared.authorization.snapshot.tenant_id,
|
|
210
235
|
),
|
|
211
236
|
providerCapture,
|
|
212
|
-
await
|
|
237
|
+
await shouldCaptureProviderBodyForRequest(
|
|
238
|
+
deps.db,
|
|
239
|
+
prepared.authorization.snapshot.tenant_id,
|
|
240
|
+
state,
|
|
241
|
+
),
|
|
242
|
+
),
|
|
243
|
+
}
|
|
244
|
+
: {}),
|
|
245
|
+
...(deps.networkBindingFactory
|
|
246
|
+
? {
|
|
247
|
+
outboundAttachmentFetch: deps.networkBindingFactory.fetch(
|
|
248
|
+
networkPoolId,
|
|
249
|
+
prepared.authorization.snapshot.tenant_id,
|
|
213
250
|
),
|
|
214
251
|
}
|
|
215
252
|
: {}),
|
|
216
253
|
...(deps.networkBindingFactory
|
|
217
254
|
? { outboundWebSocket: deps.networkBindingFactory.webSocket(networkPoolId, prepared.authorization.snapshot.tenant_id) }
|
|
218
255
|
: {}),
|
|
256
|
+
...(deps.networkBindingFactory
|
|
257
|
+
? {
|
|
258
|
+
outboundHttp2: deps.networkBindingFactory.http2Bidi(
|
|
259
|
+
networkPoolId,
|
|
260
|
+
prepared.authorization.snapshot.tenant_id,
|
|
261
|
+
),
|
|
262
|
+
}
|
|
263
|
+
: {}),
|
|
264
|
+
...(leaseOwnership === undefined ? {} : { leaseOwnership }),
|
|
265
|
+
...(continuationScope === undefined ? {} : { continuationScope }),
|
|
219
266
|
});
|
|
220
267
|
if (dispatchRequest.stream) {
|
|
221
268
|
return dispatchStreamingAttempt({
|
|
@@ -237,6 +284,8 @@ export async function handleProviderProxyRequest(
|
|
|
237
284
|
providerCapture,
|
|
238
285
|
retainLeases: context.retainLeases,
|
|
239
286
|
ttfbFields,
|
|
287
|
+
...(leaseOwnership === undefined ? {} : { leaseOwnership }),
|
|
288
|
+
...(continuationScope === undefined ? {} : { continuationScope }),
|
|
240
289
|
});
|
|
241
290
|
}
|
|
242
291
|
// Pool slot ownership belongs to the candidate attempt: the slot was
|
|
@@ -269,6 +318,11 @@ export async function handleProviderProxyRequest(
|
|
|
269
318
|
if (terminal === undefined) throw terminalFailure(undefined);
|
|
270
319
|
const terminalError = terminalFailure(terminal);
|
|
271
320
|
if (terminalError !== undefined) throw terminalError;
|
|
321
|
+
// An interim `tool_use` terminal defers usage: the provider's turn is not
|
|
322
|
+
// over, so its authoritative usage arrives with the *next* request that
|
|
323
|
+
// continues the same upstream turn. Charging an estimate here would bill
|
|
324
|
+
// the turn twice — the estimate now and the real usage later.
|
|
325
|
+
const deferredUsage = terminal.usage_deferred === true;
|
|
272
326
|
const usage =
|
|
273
327
|
terminal.usage ??
|
|
274
328
|
estimatedUsage(prepared.estimatedInputTokens, prepared.estimatedOutputTokens);
|
|
@@ -316,7 +370,10 @@ export async function handleProviderProxyRequest(
|
|
|
316
370
|
return reported === undefined ? {} : { reportedModel: reported };
|
|
317
371
|
})(),
|
|
318
372
|
usage: pricedUsage,
|
|
319
|
-
|
|
373
|
+
// Deferred: the request consumed upstream resources (it is mid-turn), so
|
|
374
|
+
// the *recorded* usage stays for the row, but nothing is committed —
|
|
375
|
+
// the Run's own `turn_ended` commits once, on the final response.
|
|
376
|
+
...(deferredUsage ? {} : { commitUsage: pricedUsage }),
|
|
320
377
|
// Decode once: `output.bytes` is a `Uint8Array`, which
|
|
321
378
|
// `parseCapturedBody` returns untouched — the Server panel would store
|
|
322
379
|
// a byte-index map (`{"0":123,…}`) after redaction instead of the
|
|
@@ -26,6 +26,7 @@ import { metrics } from "../../../observability/metrics";
|
|
|
26
26
|
import { flagPoolBlocked, flagPoolCooldown } from "../../../network/pool-health";
|
|
27
27
|
import { isFreeTierIpBlock, shouldCooldownPool } from "../retry-policy";
|
|
28
28
|
import { releaseAttemptLeases } from "../leases";
|
|
29
|
+
import type { AttemptLeaseOwnership } from "../leases";
|
|
29
30
|
import { finalizeRequestTelemetry } from "../../guards/error-lifecycle";
|
|
30
31
|
import { drainAbortReason } from "../../shutdown-notice";
|
|
31
32
|
import {
|
|
@@ -69,6 +70,12 @@ export interface StreamFinalizeInput {
|
|
|
69
70
|
* single pre-content Responses retry rebinds it mid-stream.
|
|
70
71
|
*/
|
|
71
72
|
readonly getIterator: () => AsyncIterator<CanonicalEvent>;
|
|
73
|
+
/**
|
|
74
|
+
* Ownership of this attempt's account/network-pool release. A provider
|
|
75
|
+
* session that outlived this request already took it over, so
|
|
76
|
+
* `releaseByResponse()` is a no-op and the slot stays held for the session.
|
|
77
|
+
*/
|
|
78
|
+
readonly leaseOwnership?: AttemptLeaseOwnership | undefined;
|
|
72
79
|
/** First-token latency in the spread shape `completeAttempt` accepts. */
|
|
73
80
|
readonly ttfbFields: (
|
|
74
81
|
firstByteAt: number | undefined,
|
|
@@ -338,6 +345,9 @@ export class StreamFinalizer {
|
|
|
338
345
|
lease,
|
|
339
346
|
reservation,
|
|
340
347
|
proxySlot,
|
|
348
|
+
...(this.input.leaseOwnership === undefined
|
|
349
|
+
? {}
|
|
350
|
+
: { ownership: this.input.leaseOwnership }),
|
|
341
351
|
},
|
|
342
352
|
prepared.routingEngine,
|
|
343
353
|
);
|
|
@@ -392,6 +402,11 @@ export class StreamFinalizer {
|
|
|
392
402
|
candidate.providerId,
|
|
393
403
|
candidate.modelId,
|
|
394
404
|
);
|
|
405
|
+
// A deferred terminal means the provider's turn is not over: the stream
|
|
406
|
+
// ended because the model asked for a tool the client must answer, and the
|
|
407
|
+
// authoritative usage arrives with the request that continues the same
|
|
408
|
+
// upstream turn. Committing an estimate here would bill that turn twice.
|
|
409
|
+
const deferredUsage = pipeline.terminal.usage_deferred === true;
|
|
395
410
|
await completeAttempt(state, {
|
|
396
411
|
status: upstreamTruncated ? "truncated" : "completed",
|
|
397
412
|
...this.streamCompletionContext(),
|
|
@@ -404,7 +419,7 @@ export class StreamFinalizer {
|
|
|
404
419
|
}
|
|
405
420
|
: {}),
|
|
406
421
|
usage: finalUsage,
|
|
407
|
-
commitUsage: finalUsage,
|
|
422
|
+
...(deferredUsage ? {} : { commitUsage: finalUsage }),
|
|
408
423
|
responseBody: pipeline.telemetryEventTranscript,
|
|
409
424
|
...(pipeline.clientResponseText ? { clientResponseText: pipeline.clientResponseText } : {}),
|
|
410
425
|
...(pipeline.firstContentDeltaAtMs !== undefined ? { firstContentDeltaAtMs: pipeline.firstContentDeltaAtMs } : {}),
|
|
@@ -43,8 +43,10 @@ import type { AdmissionLease } from "../../../security/admission/contracts";
|
|
|
43
43
|
import type { NetworkPoolSelector } from "../../../network/pool/selector";
|
|
44
44
|
import type { TelemetryBatchBuffer } from "../../../observability/telemetry-buffer";
|
|
45
45
|
import type { ProviderExchangeCapture } from "../attempt-finalize";
|
|
46
|
-
import { captureProviderExchange,
|
|
46
|
+
import { captureProviderExchange, shouldCaptureProviderBodyForRequest } from "../attempt-finalize";
|
|
47
47
|
import { proxySuccessHeaders, buildUpstreamDispatchContext } from "../upstream";
|
|
48
|
+
import type { AttemptLeaseOwnership } from "../leases";
|
|
49
|
+
import type { ProviderContinuationScope } from "../../../providers/provider-contracts";
|
|
48
50
|
import { resolveStreamFirstChunkTimeoutMs, resolveStreamStallTimeoutMs, resolveUpstreamTimeoutMs } from "../../../config/timeouts";
|
|
49
51
|
import type { ProxyRequestState } from "../../request/state";
|
|
50
52
|
import type { PreparedProxyRequest } from "../../request/preparer";
|
|
@@ -81,6 +83,10 @@ export interface StreamingDispatchInput {
|
|
|
81
83
|
readonly proxySlot: { poolId: string; release: () => void } | undefined;
|
|
82
84
|
readonly networkPoolId: string | undefined;
|
|
83
85
|
readonly providerCapture: ProviderExchangeCapture;
|
|
86
|
+
/** Ownership of this attempt's account/network-pool release, when one was acquired. */
|
|
87
|
+
readonly leaseOwnership?: AttemptLeaseOwnership | undefined;
|
|
88
|
+
/** Caller identity authorizing a provider session held across requests. */
|
|
89
|
+
readonly continuationScope?: ProviderContinuationScope | undefined;
|
|
84
90
|
/** Hands lease ownership to the returned `Response`'s async lifetime. */
|
|
85
91
|
retainLeases: () => void;
|
|
86
92
|
/** First-token latency in the spread shape `completeAttempt` accepts. */
|
|
@@ -151,6 +157,8 @@ export async function dispatchStreamingAttempt(input: StreamingDispatchInput): P
|
|
|
151
157
|
providerCapture,
|
|
152
158
|
retainLeases,
|
|
153
159
|
ttfbFields,
|
|
160
|
+
leaseOwnership,
|
|
161
|
+
continuationScope,
|
|
154
162
|
} = input;
|
|
155
163
|
|
|
156
164
|
const outboundFetch = deps.networkBindingFactory?.fetch(
|
|
@@ -161,7 +169,7 @@ export async function dispatchStreamingAttempt(input: StreamingDispatchInput): P
|
|
|
161
169
|
? captureProviderExchange(
|
|
162
170
|
outboundFetch,
|
|
163
171
|
providerCapture,
|
|
164
|
-
await
|
|
172
|
+
await shouldCaptureProviderBodyForRequest(deps.db, prepared.authorization.snapshot.tenant_id, state),
|
|
165
173
|
)
|
|
166
174
|
: undefined;
|
|
167
175
|
const dispatchContext = buildUpstreamDispatchContext({
|
|
@@ -174,6 +182,7 @@ export async function dispatchStreamingAttempt(input: StreamingDispatchInput): P
|
|
|
174
182
|
...(conversationAffinity ? { conversationAffinity } : {}),
|
|
175
183
|
...(candidate.user_agent === undefined ? {} : { userAgent: candidate.user_agent }),
|
|
176
184
|
...(wrappedOutboundFetch ? { outboundFetch: wrappedOutboundFetch } : {}),
|
|
185
|
+
...(outboundFetch ? { outboundAttachmentFetch: outboundFetch } : {}),
|
|
177
186
|
...(deps.networkBindingFactory
|
|
178
187
|
? {
|
|
179
188
|
outboundWebSocket: deps.networkBindingFactory.webSocket(
|
|
@@ -182,6 +191,16 @@ export async function dispatchStreamingAttempt(input: StreamingDispatchInput): P
|
|
|
182
191
|
),
|
|
183
192
|
}
|
|
184
193
|
: {}),
|
|
194
|
+
...(deps.networkBindingFactory
|
|
195
|
+
? {
|
|
196
|
+
outboundHttp2: deps.networkBindingFactory.http2Bidi(
|
|
197
|
+
networkPoolId,
|
|
198
|
+
prepared.authorization.snapshot.tenant_id,
|
|
199
|
+
),
|
|
200
|
+
}
|
|
201
|
+
: {}),
|
|
202
|
+
...(leaseOwnership === undefined ? {} : { leaseOwnership }),
|
|
203
|
+
...(continuationScope === undefined ? {} : { continuationScope }),
|
|
185
204
|
});
|
|
186
205
|
// `ProviderAdapter.dispatch` is declared to return a non-nullable
|
|
187
206
|
// `AsyncIterable`, so an adapter that returned `undefined` would be a
|
|
@@ -248,6 +267,7 @@ export async function dispatchStreamingAttempt(input: StreamingDispatchInput): P
|
|
|
248
267
|
providerCapture,
|
|
249
268
|
getIterator: () => iterator,
|
|
250
269
|
ttfbFields,
|
|
270
|
+
...(leaseOwnership === undefined ? {} : { leaseOwnership }),
|
|
251
271
|
},
|
|
252
272
|
pipeline,
|
|
253
273
|
{
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CanonicalEvent } from "../../protocol/canonical-model";
|
|
2
|
-
import type { ValidatedOutboundFetch, ValidatedOutboundWebSocket } from "../../providers/provider-contracts";
|
|
2
|
+
import type { AttemptLeaseOwnership, ValidatedOutboundFetch, ValidatedOutboundHttp2Bidi, ValidatedOutboundWebSocket } from "../../providers/provider-contracts";
|
|
3
|
+
import type { ProviderContinuationScope } from "../../providers/provider-contracts";
|
|
3
4
|
import { GATEWAY_SECURITY_HEADERS } from "../../security/outbound-headers";
|
|
4
5
|
|
|
5
6
|
const FORWARDED_REQUEST_HEADERS = new Set([
|
|
@@ -41,7 +42,18 @@ export function buildUpstreamDispatchContext(input: {
|
|
|
41
42
|
readonly requestId?: string;
|
|
42
43
|
readonly userAgent?: string;
|
|
43
44
|
readonly outboundFetch?: ValidatedOutboundFetch;
|
|
45
|
+
/** Validated attachment egress that must not be wrapped by provider payload capture. */
|
|
46
|
+
readonly outboundAttachmentFetch?: ValidatedOutboundFetch;
|
|
44
47
|
readonly outboundWebSocket?: ValidatedOutboundWebSocket;
|
|
48
|
+
/** Validated bidirectional HTTP/2 egress bound to the same selected pool. */
|
|
49
|
+
readonly outboundHttp2?: ValidatedOutboundHttp2Bidi;
|
|
50
|
+
/**
|
|
51
|
+
* Ownership of this attempt's account/network-pool lease; a provider session
|
|
52
|
+
* that outlives the request takes it over.
|
|
53
|
+
*/
|
|
54
|
+
readonly leaseOwnership?: AttemptLeaseOwnership;
|
|
55
|
+
/** Caller identity authorizing a provider session held across requests. */
|
|
56
|
+
readonly continuationScope?: ProviderContinuationScope;
|
|
45
57
|
/** Stable per-conversation cache affinity (preferred over random ids). */
|
|
46
58
|
readonly conversationAffinity?: string;
|
|
47
59
|
/** Opaque identity shared by retries of one logical gateway request. */
|
|
@@ -74,7 +86,13 @@ export function buildUpstreamDispatchContext(input: {
|
|
|
74
86
|
...(userAgent ? { user_agent: userAgent } : {}),
|
|
75
87
|
...(Object.keys(input.headers).length > 0 ? { request_headers: input.headers } : {}),
|
|
76
88
|
...(outboundFetch ? { outbound_fetch: outboundFetch } : {}),
|
|
89
|
+
...(input.outboundAttachmentFetch
|
|
90
|
+
? { outbound_attachment_fetch: input.outboundAttachmentFetch }
|
|
91
|
+
: {}),
|
|
77
92
|
...(input.outboundWebSocket ? { outbound_websocket: input.outboundWebSocket } : {}),
|
|
93
|
+
...(input.outboundHttp2 ? { outbound_http2: input.outboundHttp2 } : {}),
|
|
94
|
+
...(input.leaseOwnership ? { lease_ownership: input.leaseOwnership } : {}),
|
|
95
|
+
...(input.continuationScope ? { continuation_scope: input.continuationScope } : {}),
|
|
78
96
|
...(input.conversationAffinity ? { conversation_affinity: input.conversationAffinity } : {}),
|
|
79
97
|
...(input.requestIdentity ? { request_identity: input.requestIdentity } : {}),
|
|
80
98
|
} as Record<string, unknown>;
|