cartethyia 2.7.9 → 2.8.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/.env.example +6 -5
- package/dist/dashboard/assets/{BannedPage-BxoG1MrL.js → BannedPage-Dq530awD.js} +1 -1
- package/dist/dashboard/assets/BannedPage-Dq530awD.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-ixwgKk53.js → CliToolDetailPage-DZzaS-7S.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-DZzaS-7S.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-BmttNV3U.js → CliToolsPage-DaZZ3cqP.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-DaZZ3cqP.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-BE3qwwfC.js → CombosPage-BlC_xdkX.js} +1 -1
- package/dist/dashboard/assets/CombosPage-BlC_xdkX.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-CRZdmMIX.js → ConsoleLogPage-D-rqeVtG.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-D-rqeVtG.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-Dv_FJ6Er.js → CustomizationPage-7xT5yg2E.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-7xT5yg2E.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-Clq8E9cW.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-Clq8E9cW.js.gz +0 -0
- package/dist/dashboard/assets/{InjectSystemPromptPanel-BOoO8rpz.js → InjectSystemPromptPanel-6Teoq9Se.js} +2 -2
- package/dist/dashboard/assets/InjectSystemPromptPanel-6Teoq9Se.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-ChJuNNVN.js → LoginPage-CZuI0MAR.js} +1 -1
- package/dist/dashboard/assets/LoginPage-CZuI0MAR.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-BZAJtPg1.js → ModelPicker-C1hNTlkr.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-C1hNTlkr.js.gz +0 -0
- package/dist/dashboard/assets/{OverviewPage-BE40s3u5.js → OverviewPage-0yfhSI1C.js} +1 -1
- package/dist/dashboard/assets/OverviewPage-0yfhSI1C.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js +8 -0
- package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js.gz +0 -0
- package/dist/dashboard/assets/{ProviderIcon-CQ6bPUf_.js → ProviderIcon-Ddhm1InL.js} +1 -1
- package/dist/dashboard/assets/ProviderIcon-Ddhm1InL.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-C3fYuu6i.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-C3fYuu6i.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js +9 -0
- package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-D1RV_2BX.js +1 -0
- package/dist/dashboard/assets/QuotaPage-D1RV_2BX.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-C5HPWqEv.js → SettingsPage-BzZAvYSX.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-BzZAvYSX.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-CFNcATJJ.js → SetupPage--s-EfEIL.js} +1 -1
- package/dist/dashboard/assets/SetupPage--s-EfEIL.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-YoD4tY5l.js → StudioPage-BjXFGSK4.js} +1 -1
- package/dist/dashboard/assets/StudioPage-BjXFGSK4.js.gz +0 -0
- package/dist/dashboard/assets/{UsagePage-DxjaAhb_.js → UsagePage-CP8F8BN7.js} +1 -1
- package/dist/dashboard/assets/UsagePage-CP8F8BN7.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-BTlkq_Uk.js → api-keys-2hc12Uz2.js} +1 -1
- package/dist/dashboard/assets/api-keys-2hc12Uz2.js.gz +0 -0
- package/dist/dashboard/assets/app-DGXbNfAR.css +2 -0
- package/dist/dashboard/assets/app-DGXbNfAR.css.gz +0 -0
- package/dist/dashboard/assets/assert-BMcj1xZE.js +1 -0
- package/dist/dashboard/assets/assert-BMcj1xZE.js.gz +0 -0
- package/dist/dashboard/assets/{badge-DdbT_7Am.js → badge-DkRUuVfz.js} +1 -1
- package/dist/dashboard/assets/badge-DkRUuVfz.js.gz +0 -0
- package/dist/dashboard/assets/{banner-CsGN-nbm.js → banner-CDjW841Q.js} +1 -1
- package/dist/dashboard/assets/banner-CDjW841Q.js.gz +0 -0
- package/dist/dashboard/assets/{card-mgZ_TLLZ.js → card-Bto5UlL9.js} +1 -1
- package/dist/dashboard/assets/card-Bto5UlL9.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-A1-v8-03.js → cli-tools-DaH6g9hL.js} +1 -1
- package/dist/dashboard/assets/cli-tools-DaH6g9hL.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-CJ4Zty89.js → clipboard-button-BYKBCzjk.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-BYKBCzjk.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-B5L2jPQ_.js → dialog-DL0Zftwv.js} +1 -1
- package/dist/dashboard/assets/dialog-DL0Zftwv.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-DkjUouAj.js → drawer-Dpx_UXaS.js} +1 -1
- package/dist/dashboard/assets/drawer-Dpx_UXaS.js.gz +0 -0
- package/dist/dashboard/assets/{entry-B-LVLV6o.js → entry-BV7-1NoM.js} +1 -1
- package/dist/dashboard/assets/entry-BV7-1NoM.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CAhdYl3B.js → entry-CGvdNU6r.js} +1 -1
- package/dist/dashboard/assets/entry-CGvdNU6r.js.gz +0 -0
- package/dist/dashboard/assets/entry-DScKYvZj.js +4 -0
- package/dist/dashboard/assets/entry-DScKYvZj.js.gz +0 -0
- package/dist/dashboard/assets/{formatters-CrsdLVsn.js → formatters-BS3DZlZj.js} +1 -1
- package/dist/dashboard/assets/formatters-BS3DZlZj.js.gz +0 -0
- package/dist/dashboard/assets/{index-BU64Lqvp.js → index-SQnvvTWX.js} +2 -2
- package/dist/dashboard/assets/index-SQnvvTWX.js.gz +0 -0
- package/dist/dashboard/assets/{inline-B2aDLyyg.js → inline-Dl-cuLK0.js} +1 -1
- package/dist/dashboard/assets/inline-Dl-cuLK0.js.gz +0 -0
- package/dist/dashboard/assets/{input-BCVkdR9d.js → input-Bfx0EzRc.js} +1 -1
- package/dist/dashboard/assets/input-Bfx0EzRc.js.gz +0 -0
- package/dist/dashboard/assets/{layout-DNcAdMK-.js → layout-BxhbQ_JS.js} +1 -1
- package/dist/dashboard/assets/layout-BxhbQ_JS.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-DgrIH0hw.js → list-row-D49Y4ptn.js} +1 -1
- package/dist/dashboard/assets/list-row-D49Y4ptn.js.gz +0 -0
- package/dist/dashboard/assets/{live-Cae01iu8.js → live-CbVTdX9V.js} +1 -1
- package/dist/dashboard/assets/live-CbVTdX9V.js.gz +0 -0
- package/dist/dashboard/assets/{network-CRib7H_2.js → network-JGvDveof.js} +1 -1
- package/dist/dashboard/assets/network-JGvDveof.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-BKQE8Xpz.js → numbers-Dw6kiew-.js} +1 -1
- package/dist/dashboard/assets/numbers-Dw6kiew-.js.gz +0 -0
- package/dist/dashboard/assets/{pagination-ecPvG_8g.js → pagination-iUoI8GeE.js} +1 -1
- package/dist/dashboard/assets/pagination-iUoI8GeE.js.gz +0 -0
- package/dist/dashboard/assets/providers/sensenova-intl.png +0 -0
- package/dist/dashboard/assets/providers/sensenova.png +0 -0
- package/dist/dashboard/assets/providers/zro.svg +3 -0
- package/dist/dashboard/assets/providers/zro.svg.gz +0 -0
- package/dist/dashboard/assets/quota-DiPD6UKH.js +1 -0
- package/dist/dashboard/assets/quota-DiPD6UKH.js.gz +0 -0
- package/dist/dashboard/assets/{routing-Ckm1UXuC.js → routing-8jZr-3WE.js} +1 -1
- package/dist/dashboard/assets/routing-8jZr-3WE.js.gz +0 -0
- package/dist/dashboard/assets/{select-ChoFWyKz.js → select-oO1FxMVb.js} +2 -2
- package/dist/dashboard/assets/select-oO1FxMVb.js.gz +0 -0
- package/dist/dashboard/assets/{settings-CKjFPlKW.js → settings-CLr3kdIx.js} +1 -1
- package/dist/dashboard/assets/settings-CLr3kdIx.js.gz +0 -0
- package/dist/dashboard/assets/{stack-DWCwU3fh.js → stack-Y3pDteZr.js} +1 -1
- package/dist/dashboard/assets/stack-Y3pDteZr.js.gz +0 -0
- package/dist/dashboard/assets/{state-Cg3VL5J_.js → state-CArX_Iak.js} +1 -1
- package/dist/dashboard/assets/state-CArX_Iak.js.gz +0 -0
- package/dist/dashboard/assets/{switch-Db70BMix.js → switch-DwlwmDO-.js} +1 -1
- package/dist/dashboard/assets/switch-DwlwmDO-.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-BqdE0LuM.js → useMutation-DJ9GYEgf.js} +1 -1
- package/dist/dashboard/assets/useMutation-DJ9GYEgf.js.gz +0 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +54 -53
- package/migrations/full-postgres/0046_add_accounts_quota_plan_fetched_at.sql +7 -0
- package/migrations/full-postgres/0047_add_provider_device_flow_proxy.sql +17 -0
- package/migrations/lite-sqlite3/0046_add_accounts_quota_plan_fetched_at.sql +4 -0
- package/migrations/lite-sqlite3/0047_add_provider_device_flow_proxy.sql +19 -0
- package/package.json +1 -1
- package/src/boot/app.ts +11 -14
- package/src/boot/dependencies.ts +5 -7
- package/src/boot/flush.ts +20 -0
- package/src/boot/lifecycle.ts +7 -5
- package/src/config/providers.ts +6 -5
- package/src/config/routing.ts +26 -0
- package/src/config/spec-metadata.ts +72 -0
- package/src/config/spec.ts +28 -8
- package/src/console/api-keys/contracts.ts +12 -1
- package/src/console/api-keys/routes.ts +40 -28
- package/src/console/auth/account-reset.ts +7 -9
- package/src/console/backup/contracts.ts +4 -0
- package/src/console/backup/export-endpoint.ts +7 -5
- package/src/console/backup/nine-router.ts +71 -10
- package/src/console/backup/service.ts +8 -1
- package/src/console/cli-tools/service.ts +6 -12
- package/src/console/console-router-guards.ts +23 -16
- package/src/console/console-router.ts +1 -1
- package/src/console/domain-registration.ts +7 -4
- package/src/console/inferhub/client.ts +1 -1
- package/src/console/inferhub/normalize.ts +24 -28
- package/src/console/inferhub/types.ts +3 -0
- package/src/console/ip-bans/routes.ts +7 -13
- package/src/console/observability/live.ts +2 -2
- package/src/console/observability/store.ts +0 -53
- package/src/console/providers/catalog/contracts.ts +9 -3
- package/src/console/providers/catalog/model-operations.ts +2 -3
- package/src/console/providers/catalog/provider-operations.ts +1 -1
- package/src/console/providers/catalog/routes.ts +3 -2
- package/src/console/providers/catalog/store.ts +1914 -1902
- package/src/console/providers/detail/routes.ts +1 -0
- package/src/console/providers/detail/store.ts +4 -0
- package/src/console/providers/oauth/callback-listener.ts +30 -11
- package/src/console/providers/oauth/device-egress.ts +123 -0
- package/src/console/providers/oauth/routes.ts +123 -16
- package/src/console/quota/account-quota-global.ts +14 -24
- package/src/console/quota/account-quota-shared.ts +49 -0
- package/src/console/quota/account-quota-tenant.ts +11 -23
- package/src/console/quota/account-test-egress.ts +2 -2
- package/src/console/quota/daily-growth-job.ts +1 -1
- package/src/console/quota/quota-view.ts +19 -3
- package/src/console/routing/pools/contracts.ts +1 -1
- package/src/console/routing/pools/operations.ts +32 -47
- package/src/console/routing/pools/probe-verdict.ts +2 -2
- package/src/console/routing/pools/relay-deploy.ts +1 -1
- package/src/console/routing/pools/store.ts +4 -4
- package/src/console/shared/errors.ts +33 -6
- package/src/main.ts +2 -3
- package/src/network/client.ts +67 -0
- package/src/network/egress/bidi-session.ts +322 -0
- package/src/network/egress/dial.ts +201 -0
- package/src/network/egress/factory.ts +191 -0
- package/src/network/{outbound-fetch.ts → egress/fetch.ts} +195 -29
- package/src/network/egress/tls-profile.ts +123 -0
- package/src/network/egress/ws.ts +175 -0
- package/src/network/pool/{byte-accounting.ts → accounting.ts} +3 -2
- package/src/network/{types.ts → pool/agent-config.ts} +1 -1
- package/src/network/pool/agent.ts +2 -2
- package/src/network/{pool-cooldown-store.ts → pool/cooldown-store.ts} +95 -20
- package/src/network/{pool-health-machine.ts → pool/health.ts} +205 -6
- package/src/network/pool/resolver.ts +10 -849
- package/src/network/pool/selector.ts +184 -41
- package/src/network/ssrf.ts +7 -7
- package/src/network/{retry.ts → util/retry.ts} +1 -1
- package/src/network/{rate-limit-verdict.ts → verdict.ts} +1 -1
- package/src/observability/event-log.ts +7 -361
- package/src/observability/performance-metrics.ts +9 -0
- package/src/observability/runtime-metrics.ts +14 -9
- package/src/observability/telemetry-buffer.ts +16 -26
- package/src/observability/telemetry-reconciliation.ts +9 -37
- package/src/observability/telemetry-row.ts +125 -0
- package/src/persistence/db-mode.ts +4 -1
- package/src/persistence/dialect.ts +18 -0
- package/src/persistence/migrate.ts +6 -3
- package/src/persistence/schema.ts +17 -0
- package/src/persistence/ttl-cache.ts +21 -3
- package/src/protocol/codecs.ts +59 -2
- package/src/protocol/dsml.ts +34 -86
- package/src/protocol/markup-stream-parser.ts +195 -0
- package/src/protocol/reasoning-text.ts +19 -0
- package/src/protocol/request/chat.ts +10 -10
- package/src/protocol/request/gemini.ts +8 -2
- package/src/protocol/request/messages.ts +34 -22
- package/src/protocol/response/chat.ts +12 -20
- package/src/protocol/response/messages.ts +34 -7
- package/src/protocol/response/omp-tool-call-markup.ts +43 -86
- package/src/protocol/response/responses.ts +18 -2
- package/src/protocol/stop-reason.ts +23 -1
- package/src/protocol/think-split.ts +112 -0
- package/src/protocol/translation/dialect/qwen.ts +26 -58
- package/src/protocol/translation/quirks.ts +15 -0
- package/src/protocol/upstream/send-claude-messages.ts +2 -2
- package/src/providers/adapters/compatible-adapter.ts +76 -12
- package/src/providers/authentication/oauth-client.ts +40 -6
- package/src/providers/authentication/oauth-device-flow.ts +105 -11
- package/src/providers/authentication/oauth-flow-store.ts +82 -1
- package/src/providers/authentication/oauth-refresh-service.ts +766 -758
- package/src/providers/configured-provider.ts +148 -0
- package/src/providers/default-registry.ts +137 -36
- package/src/providers/discovery/discovery-types.ts +7 -0
- package/src/providers/discovery/probe-phases.ts +20 -6
- package/src/providers/discovery/probing-service.ts +22 -2
- package/src/providers/integrations/alibabacp/alibabacp-models.ts +1 -2
- package/src/providers/integrations/{dashscope-models.ts → alibabacp/dashscope-models.ts} +2 -2
- package/src/providers/integrations/antigravity/antigravity-protocol.ts +16 -1
- package/src/providers/integrations/buddy/account.ts +969 -0
- package/src/providers/integrations/buddy/capabilities.ts +12 -12
- package/src/providers/integrations/buddy/{buddy-catalog-shared.ts → catalog.ts} +198 -136
- package/src/providers/integrations/buddy/{buddy-chat-shared.ts → chat.ts} +176 -20
- package/src/providers/integrations/buddy/codebuddy/index.ts +429 -0
- package/src/providers/integrations/buddy/codebuddy/shared.ts +41 -26
- package/src/providers/integrations/buddy/headers.ts +311 -0
- package/src/providers/integrations/buddy/lifecycle.ts +1018 -0
- package/src/providers/integrations/buddy/{buddy-media.ts → media.ts} +1 -1
- package/src/providers/integrations/claude/claude-quota.ts +14 -17
- package/src/providers/integrations/cline/cline-oauth.ts +10 -5
- package/src/providers/integrations/cline/cline.ts +5 -6
- package/src/providers/integrations/clixad/clixad-oauth.ts +98 -0
- package/src/providers/integrations/clixad/clixad-quota.ts +132 -0
- package/src/providers/integrations/clixad/provider.ts +145 -0
- package/src/providers/integrations/codex/codex-oauth.ts +8 -5
- package/src/providers/integrations/commandcode/quota.ts +22 -28
- package/src/providers/integrations/cursor/cursor-auth.ts +1 -9
- package/src/providers/integrations/cursor/cursor-exec.ts +9 -2
- package/src/providers/integrations/cursor/cursor-hardening.ts +279 -0
- package/src/providers/integrations/cursor/cursor-http2.ts +605 -6
- package/src/providers/integrations/cursor/cursor-request.ts +26 -12
- package/src/providers/integrations/cursor/cursor-shared.ts +8 -5
- package/src/providers/integrations/cursor/cursor.ts +574 -110
- package/src/providers/integrations/cursor/protocol/messages.ts +22 -29
- package/src/providers/integrations/cursor/protocol/protobuf-schema.ts +37 -0
- package/src/providers/integrations/deepai/deepai-catalog.ts +149 -0
- package/src/providers/integrations/deepai/deepai-common.ts +145 -0
- package/src/providers/integrations/deepai/deepai-oauth.ts +129 -0
- package/src/providers/integrations/deepai/deepai-quota.ts +97 -0
- package/src/providers/integrations/deepai/provider.ts +42 -0
- package/src/providers/integrations/devin/devin-auth.ts +7 -0
- package/src/providers/integrations/devin/protocol/devin-proto.ts +1 -27
- package/src/providers/integrations/factory/factory-oauth.ts +33 -51
- package/src/providers/integrations/freebuff/assets/official-system-manager.txt +88 -0
- package/src/providers/integrations/freebuff/assets/official-system-worker.txt +57 -0
- package/src/providers/integrations/freebuff/assets/official-tools.json +1070 -0
- package/src/providers/integrations/freebuff/freebuff-catalog.ts +120 -0
- package/src/providers/integrations/freebuff/freebuff-constants.ts +1233 -0
- package/src/providers/integrations/freebuff/freebuff-discovery.ts +108 -0
- package/src/providers/integrations/freebuff/freebuff-oauth.ts +240 -0
- package/src/providers/integrations/freebuff/freebuff-quota.ts +260 -0
- package/src/providers/integrations/freebuff/freebuff-virtual.ts +89 -0
- package/src/providers/integrations/freebuff/freebuff.ts +682 -0
- package/src/providers/integrations/freepi/freepi-catalog.ts +5 -0
- package/src/providers/integrations/freepi/freepi-discovery.ts +2 -2
- package/src/providers/integrations/freepi/freepi-headers.ts +2 -2
- package/src/providers/integrations/freepi/freepi-protocol.ts +11 -2
- package/src/providers/integrations/freepi/freepi-realm.ts +0 -3
- package/src/providers/integrations/freepi/freepi-session.ts +158 -0
- package/src/providers/integrations/freepi/freepi.ts +80 -54
- package/src/providers/integrations/github/github-discovery.ts +30 -13
- package/src/providers/integrations/github/github-oauth.ts +217 -44
- package/src/providers/integrations/github/github-quota.ts +22 -5
- package/src/providers/integrations/github/github.ts +40 -32
- package/src/providers/integrations/grok/grok-oauth.ts +52 -49
- package/src/providers/integrations/grok/grok-ratelimit-store.ts +10 -17
- package/src/providers/integrations/grok/grok-turn-index.ts +11 -16
- package/src/providers/integrations/hermes/hermes-oauth.ts +3 -3
- package/src/providers/integrations/{hermes.ts → hermes/hermes.ts} +42 -13
- package/src/providers/integrations/hermes/index.ts +2 -0
- package/src/providers/integrations/kilo/kilo-oauth.ts +4 -4
- package/src/providers/integrations/kilo/kilo.ts +1 -1
- package/src/providers/integrations/kimi/kimi-oauth.ts +3 -3
- package/src/providers/integrations/kiro/kiro-catalog.ts +17 -1
- package/src/providers/integrations/kiro/kiro-discovery.ts +10 -12
- package/src/providers/integrations/kiro/kiro-oauth.ts +2 -2
- package/src/providers/integrations/kiro/kiro-quota.ts +8 -4
- package/src/providers/integrations/kiro/kiro.ts +15 -19
- package/src/providers/integrations/muse/muse-oauth.ts +5 -5
- package/src/providers/integrations/ollamacloud/index.ts +2 -0
- package/src/providers/integrations/{ollamacloud-models.ts → ollamacloud/models.ts} +2 -2
- package/src/providers/integrations/{ollamacloud-quota.ts → ollamacloud/quota.ts} +3 -3
- package/src/providers/integrations/opencode/fingerprint.ts +1 -1
- package/src/providers/integrations/opencode/opencode-go-quota.ts +2 -8
- package/src/providers/integrations/openrouter/provider.ts +1 -1
- package/src/providers/integrations/perch/perch-credential.ts +25 -0
- package/src/providers/integrations/perch/perch-protocol.ts +0 -6
- package/src/providers/integrations/perch/perch-quota.ts +2 -16
- package/src/providers/integrations/perch/perch.ts +6 -16
- package/src/providers/integrations/qoder/claim-qoder-credits.ts +19 -60
- package/src/providers/integrations/qoder/qoder-adapter.ts +20 -125
- package/src/providers/integrations/qoder/qoder-auth.ts +109 -30
- package/src/providers/integrations/qoder/qoder-catalog.ts +21 -6
- package/src/providers/integrations/qoder/qoder-chat.ts +32 -29
- package/src/providers/integrations/qoder/qoder-checkin.ts +5 -5
- package/src/providers/integrations/qoder/qoder-cosy.ts +37 -9
- package/src/providers/integrations/qoder/qoder-oauth.ts +9 -2
- package/src/providers/integrations/qoder/qoder-realm.ts +13 -36
- package/src/providers/integrations/qoder/qoder-sse.ts +447 -68
- package/src/providers/integrations/qoder/qoder-types.ts +9 -0
- package/src/providers/integrations/qoder/qoder.ts +3 -26
- package/src/providers/integrations/sensenova/provider.ts +144 -0
- package/src/providers/integrations/trae/trae-protocol.ts +9 -5
- package/src/providers/integrations/trae/trae-quota.ts +21 -29
- package/src/providers/integrations/trae/trae-realm.ts +38 -0
- package/src/providers/integrations/trae/trae.ts +7 -24
- package/src/providers/integrations/vercel/provider.ts +117 -0
- package/src/providers/integrations/vercel/vercel-quota.ts +96 -0
- package/src/providers/integrations/xai/xai-oauth.ts +5 -5
- package/src/providers/integrations/zcode-start-plan/gifts.ts +1 -1
- package/src/providers/integrations/zcode-start-plan/oauth.ts +3 -3
- package/src/providers/integrations/zcode-start-plan/provider.ts +226 -23
- package/src/providers/integrations/zcode-start-plan/quota.ts +19 -2
- package/src/providers/integrations/zro/zro.ts +205 -0
- package/src/providers/operations/account-error-classification.ts +199 -307
- package/src/providers/operations/account-health-service.ts +82 -1
- package/src/providers/operations/account-refresh-status.ts +36 -37
- package/src/providers/operations/account-tier.ts +15 -16
- package/src/providers/operations/client-version-resolver.ts +1 -1
- package/src/providers/operations/client-versions.ts +100 -81
- package/src/providers/operations/cooldown-rules.ts +11 -16
- package/src/providers/operations/oauth-refresh-lead.ts +46 -22
- package/src/providers/operations/{model-catalog-cache.ts → provider-caches.ts} +68 -16
- package/src/providers/operations/provider-catalog-service.ts +1 -1
- package/src/providers/operations/provider-credential-service.ts +118 -51
- package/src/providers/operations/provider-rules/github.ts +48 -0
- package/src/providers/operations/provider-rules/grok.ts +33 -0
- package/src/providers/operations/provider-rules/inception.ts +49 -0
- package/src/providers/operations/provider-rules/index.ts +32 -0
- package/src/providers/operations/provider-rules/perch.ts +37 -0
- package/src/providers/operations/provider-rules/qoder.ts +70 -0
- package/src/providers/operations/provider-rules/trae.ts +81 -0
- package/src/providers/operations/session-resolution.ts +17 -1
- package/src/providers/provider-contracts.ts +6 -4
- package/src/providers/{provider-credential-resolver.ts → provider-credentials.ts} +40 -3
- package/src/providers/provider-metadata.ts +70 -10
- package/src/providers/provider-probe.ts +15 -1
- package/src/providers/provider-registry.ts +42 -33
- package/src/providers/quota/quota-contracts.ts +2 -0
- package/src/providers/quota/quota-limit-windows.ts +16 -5
- package/src/providers/quota/quota-support.ts +1 -1
- package/src/providers/quota/quota-window-parser.ts +90 -18
- package/src/providers/quota/refresh.ts +100 -21
- package/src/security/api-key-auth-cache.ts +1 -1
- package/src/security/api-key-auth.ts +6 -2
- package/src/security/ip-boundary.ts +1 -1
- package/src/transport/dispatch/attempt-finalize.ts +15 -14
- package/src/transport/dispatch/attempt-loop.ts +73 -19
- package/src/transport/dispatch/generation.ts +2 -8
- package/src/transport/dispatch/handler-deps.ts +1 -1
- package/src/transport/dispatch/leases.ts +1 -1
- package/src/transport/dispatch/native-shared.ts +25 -0
- package/src/transport/dispatch/proxy-request.ts +8 -46
- package/src/transport/dispatch/responses-compact.ts +2 -8
- package/src/transport/dispatch/retry-policy.ts +1 -1
- package/src/transport/dispatch/streaming/finalize.ts +10 -7
- package/src/transport/dispatch/streaming/stream.ts +9 -37
- package/src/transport/dispatch/systemone.ts +2 -8
- package/src/transport/dispatch/token-count.ts +2 -6
- package/src/transport/dispatch/upstream.ts +63 -0
- package/src/transport/drain-endpoint.ts +31 -4
- package/src/transport/failure-policy.ts +16 -30
- package/src/transport/guards/body-policy.ts +7 -1
- package/src/transport/guards/error-lifecycle.ts +13 -1
- package/src/transport/guards/request-context.ts +22 -14
- package/src/transport/request/preparer.ts +81 -71
- package/src/transport/routing/bounded-lru.ts +14 -0
- package/src/transport/routing/drain-reserve.ts +129 -0
- package/src/transport/routing/plan-gate.ts +7 -1
- package/src/transport/routing/round-robin.ts +60 -0
- package/src/transport/routing/route-catalog.ts +123 -111
- package/src/transport/routing/route-model.ts +51 -9
- package/src/transport/routing/router.ts +393 -32
- package/src/transport/shutdown-notice.ts +1 -1
- package/src/transport/surface/adapters.ts +11 -2
- package/src/transport/surface/chat/adapter.ts +2 -2
- package/src/transport/surface/chat/encode.ts +47 -4
- package/src/transport/surface/chat/parse.ts +2 -12
- package/src/transport/surface/completion.ts +44 -33
- package/src/transport/surface/content-parts.ts +21 -0
- package/src/transport/surface/messages/adapter.ts +7 -1
- package/src/transport/surface/messages/encode.ts +11 -3
- package/src/transport/surface/messages/parse.ts +2 -1
- package/src/transport/surface/messages/stream.ts +33 -17
- package/src/transport/surface/responses/encode.ts +22 -5
- package/src/transport/surface/responses/parse.ts +3 -11
- package/src/workers/buddy-growth-tasks.ts +8 -8
- package/src/workers/daily-checkin.ts +4 -4
- package/src/workers/quota-refresh-sweep.ts +1 -1
- package/src/workers/zcode-gift-claim.ts +1 -1
- package/dist/dashboard/assets/BannedPage-BxoG1MrL.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-ixwgKk53.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-BmttNV3U.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-BE3qwwfC.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-CRZdmMIX.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-Dv_FJ6Er.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js.gz +0 -0
- package/dist/dashboard/assets/InjectSystemPromptPanel-BOoO8rpz.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-ChJuNNVN.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-BZAJtPg1.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-BE40s3u5.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js +0 -8
- package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-CQ6bPUf_.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js +0 -9
- package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js +0 -1
- package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-C5HPWqEv.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-CFNcATJJ.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-YoD4tY5l.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-DxjaAhb_.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-BTlkq_Uk.js.gz +0 -0
- package/dist/dashboard/assets/app-BX2nnKqs.css +0 -2
- package/dist/dashboard/assets/app-BX2nnKqs.css.gz +0 -0
- package/dist/dashboard/assets/assert-CndthXFQ.js +0 -1
- package/dist/dashboard/assets/assert-CndthXFQ.js.gz +0 -0
- package/dist/dashboard/assets/badge-DdbT_7Am.js.gz +0 -0
- package/dist/dashboard/assets/banner-CsGN-nbm.js.gz +0 -0
- package/dist/dashboard/assets/card-mgZ_TLLZ.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-A1-v8-03.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-CJ4Zty89.js.gz +0 -0
- package/dist/dashboard/assets/dialog-B5L2jPQ_.js.gz +0 -0
- package/dist/dashboard/assets/drawer-DkjUouAj.js.gz +0 -0
- package/dist/dashboard/assets/entry-B-LVLV6o.js.gz +0 -0
- package/dist/dashboard/assets/entry-CAhdYl3B.js.gz +0 -0
- package/dist/dashboard/assets/entry-D2xD0Tmd.js +0 -4
- package/dist/dashboard/assets/entry-D2xD0Tmd.js.gz +0 -0
- package/dist/dashboard/assets/formatters-CrsdLVsn.js.gz +0 -0
- package/dist/dashboard/assets/index-BU64Lqvp.js.gz +0 -0
- package/dist/dashboard/assets/inline-B2aDLyyg.js.gz +0 -0
- package/dist/dashboard/assets/input-BCVkdR9d.js.gz +0 -0
- package/dist/dashboard/assets/layout-DNcAdMK-.js.gz +0 -0
- package/dist/dashboard/assets/list-row-DgrIH0hw.js.gz +0 -0
- package/dist/dashboard/assets/live-Cae01iu8.js.gz +0 -0
- package/dist/dashboard/assets/network-CRib7H_2.js.gz +0 -0
- package/dist/dashboard/assets/numbers-BKQE8Xpz.js.gz +0 -0
- package/dist/dashboard/assets/pagination-ecPvG_8g.js.gz +0 -0
- package/dist/dashboard/assets/quota-DJ1QlAOq.js +0 -1
- package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
- package/dist/dashboard/assets/routing-Ckm1UXuC.js.gz +0 -0
- package/dist/dashboard/assets/select-ChoFWyKz.js.gz +0 -0
- package/dist/dashboard/assets/settings-CKjFPlKW.js.gz +0 -0
- package/dist/dashboard/assets/stack-DWCwU3fh.js.gz +0 -0
- package/dist/dashboard/assets/state-Cg3VL5J_.js.gz +0 -0
- package/dist/dashboard/assets/switch-Db70BMix.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-BqdE0LuM.js.gz +0 -0
- package/src/network/pool-health.ts +0 -210
- package/src/providers/bundled-discovery-providers.ts +0 -98
- package/src/providers/configured-openai-providers.ts +0 -120
- package/src/providers/integrations/buddy/buddy-checkin.ts +0 -521
- package/src/providers/integrations/buddy/buddy-cli-headers.ts +0 -76
- package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -130
- package/src/providers/integrations/buddy/buddy-discovery-shared.ts +0 -234
- package/src/providers/integrations/buddy/buddy-growth-shared.ts +0 -496
- package/src/providers/integrations/buddy/buddy-identity.ts +0 -300
- package/src/providers/integrations/buddy/buddy-oauth-shared.ts +0 -341
- package/src/providers/integrations/buddy/buddy-quota-shared.ts +0 -417
- package/src/providers/integrations/buddy/buddy-server-tools.ts +0 -240
- package/src/providers/integrations/buddy/buddy-usage-shared.ts +0 -221
- package/src/providers/integrations/buddy/buddy-usage-store.ts +0 -181
- package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +0 -37
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +0 -185
- package/src/providers/integrations/buddy/codebuddy/oauth.ts +0 -47
- package/src/providers/integrations/buddy/codebuddy/provider.ts +0 -175
- package/src/providers/integrations/buddy/codebuddy/quota.ts +0 -154
- package/src/providers/integrations/qoder/chat-proto.ts +0 -328
- package/src/providers/integrations/qoder/qoder-grpc.ts +0 -871
- package/src/providers/integrations/qoder/qoder-ide.ts +0 -588
- package/src/providers/operations/model-discovery-cache.ts +0 -34
- package/src/providers/operations/probe-fetch.ts +0 -15
- package/src/providers/operations/provider-version-cache.ts +0 -30
- package/src/providers/provider-capabilities.ts +0 -33
- package/src/providers/provider-credential-envelope.ts +0 -43
- /package/dist/dashboard/assets/{app-B7kIWFm5.js → app-C0gTXEoN.js} +0 -0
- /package/dist/dashboard/assets/{app-B7kIWFm5.js.gz → app-C0gTXEoN.js.gz} +0 -0
- /package/src/network/{http2-bidi.ts → bidi.ts} +0 -0
- /package/src/network/pool/{pool-scripts.ts → scripts.ts} +0 -0
- /package/src/network/pool/{pool-state.ts → state.ts} +0 -0
- /package/src/network/{backoff.ts → util/backoff.ts} +0 -0
- /package/src/network/{deduplication.ts → util/dedupe.ts} +0 -0
- /package/src/network/{ip-utils.ts → util/ip.ts} +0 -0
|
@@ -3,7 +3,7 @@ import { cleanError, unsupportedQuota } from "./quota-contracts";
|
|
|
3
3
|
import type { ProviderRegistry } from "../provider-registry";
|
|
4
4
|
import { parseProviderId } from "../provider-contracts";
|
|
5
5
|
import { providerBaseUrl } from "../provider-metadata";
|
|
6
|
-
import { createProbeFetch } from "../
|
|
6
|
+
import { createProbeFetch } from "../provider-probe";
|
|
7
7
|
/**
|
|
8
8
|
* Verifies an API key against the provider's OpenAI-compatible `/models`
|
|
9
9
|
* endpoint. For providers that expose no billing/quota surface, a reachable
|
|
@@ -98,6 +98,75 @@ function findReset(root: unknown, mapping: QuotaWindowMapping): string | null {
|
|
|
98
98
|
return mapping.resetFallback ?? null;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/**
|
|
102
|
+
* The raw per-window candidates the shared emission core resolves.
|
|
103
|
+
*
|
|
104
|
+
* Both quota walkers (`parseQuotaWindows` and `parseLimitWindows`) feed their
|
|
105
|
+
* independently-read fields into one resolution so the percent fallback chain
|
|
106
|
+
* and the skip rule cannot drift between them.
|
|
107
|
+
*/
|
|
108
|
+
export interface WindowFieldCandidates {
|
|
109
|
+
/** Provider-preferred percent (e.g. a `derivePercent` result). */
|
|
110
|
+
readonly explicitPercent: number | null;
|
|
111
|
+
/** Raw used-percent read from the payload. */
|
|
112
|
+
readonly usedPercent: number | null;
|
|
113
|
+
/** Raw remaining-percent read from the payload. */
|
|
114
|
+
readonly remainingPercent: number | null;
|
|
115
|
+
/** Absolute used value. */
|
|
116
|
+
readonly used: number | null;
|
|
117
|
+
/** Absolute limit value. */
|
|
118
|
+
readonly limit: number | null;
|
|
119
|
+
/** Reset timestamp; required to emit a reset-only window. */
|
|
120
|
+
readonly reset: string | null;
|
|
121
|
+
/**
|
|
122
|
+
* Whether percent may be derived from absolute used/limit when no percentage
|
|
123
|
+
* was reported. Only `parseQuotaWindows` does this — `parseLimitWindows`
|
|
124
|
+
* reports percentages and resets the upstream gave it, never ones it
|
|
125
|
+
* invented, so Cline entries stay reset-only instead of gaining a derived
|
|
126
|
+
* percent.
|
|
127
|
+
*/
|
|
128
|
+
readonly deriveFromUsedLimit: boolean;
|
|
129
|
+
/**
|
|
130
|
+
* When true, a reset-only window (percent null, reset present) is still
|
|
131
|
+
* emitted. When false/absent, an entry with neither percent nor reset is
|
|
132
|
+
* dropped — never rendered as 0% — and a reset-only window is dropped too.
|
|
133
|
+
*/
|
|
134
|
+
readonly emitWithoutPercent?: boolean;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Shared per-window field resolution: the percent fallback chain plus the
|
|
139
|
+
* "emit or skip" decision.
|
|
140
|
+
*
|
|
141
|
+
* Percent resolution order is exact: explicit percent, then used-percent, then
|
|
142
|
+
* remaining-percent inverted (100 − remaining), then derived from absolute
|
|
143
|
+
* used/limit (only when the limit is positive and `deriveFromUsedLimit` is
|
|
144
|
+
* set). The result is never coerced to 0: a missing percentage stays `null`,
|
|
145
|
+
* so a window can be omitted or rendered as a countdown depending on
|
|
146
|
+
* `emitWithoutPercent`.
|
|
147
|
+
*/
|
|
148
|
+
export function resolveWindowEmission(candidates: WindowFieldCandidates): {
|
|
149
|
+
readonly emit: boolean;
|
|
150
|
+
readonly percent: number | null;
|
|
151
|
+
} {
|
|
152
|
+
let percent = candidates.explicitPercent;
|
|
153
|
+
if (percent === null && candidates.usedPercent !== null) percent = candidates.usedPercent;
|
|
154
|
+
if (percent === null && candidates.remainingPercent !== null) {
|
|
155
|
+
percent = Math.max(0, 100 - candidates.remainingPercent);
|
|
156
|
+
}
|
|
157
|
+
if (
|
|
158
|
+
percent === null &&
|
|
159
|
+
candidates.deriveFromUsedLimit &&
|
|
160
|
+
candidates.used !== null &&
|
|
161
|
+
candidates.limit !== null &&
|
|
162
|
+
candidates.limit > 0
|
|
163
|
+
) {
|
|
164
|
+
percent = (candidates.used / candidates.limit) * 100;
|
|
165
|
+
}
|
|
166
|
+
const emit = percent !== null || (candidates.emitWithoutPercent === true && candidates.reset !== null);
|
|
167
|
+
return { emit, percent };
|
|
168
|
+
}
|
|
169
|
+
|
|
101
170
|
/** Parses a generic quota payload using the provided window mappings. */
|
|
102
171
|
export function parseQuotaWindows(
|
|
103
172
|
body: unknown,
|
|
@@ -110,27 +179,30 @@ export function parseQuotaWindows(
|
|
|
110
179
|
for (const mapping of windowMappings) {
|
|
111
180
|
const used = findNumber(payload, mapping.usedPaths);
|
|
112
181
|
const limit = findNumber(payload, mapping.limitPaths);
|
|
182
|
+
const reset = findReset(payload, mapping);
|
|
113
183
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
if (percent !== null && mapping.valueMultiplier !== undefined && mapping.valueMultiplier !== 0) {
|
|
127
|
-
percent = Math.min(100, Math.max(0, percent * mapping.valueMultiplier));
|
|
128
|
-
}
|
|
184
|
+
const { emit, percent } = resolveWindowEmission({
|
|
185
|
+
explicitPercent: mapping.derivePercent ? mapping.derivePercent(payload) : null,
|
|
186
|
+
usedPercent: findNumber(payload, mapping.usedPercentPaths),
|
|
187
|
+
remainingPercent: findNumber(payload, mapping.remainingPercentPaths),
|
|
188
|
+
used,
|
|
189
|
+
limit,
|
|
190
|
+
reset,
|
|
191
|
+
deriveFromUsedLimit: true,
|
|
192
|
+
...(mapping.emitWithoutPercent === undefined ? {} : { emitWithoutPercent: mapping.emitWithoutPercent }),
|
|
193
|
+
});
|
|
194
|
+
if (!emit) continue;
|
|
129
195
|
|
|
130
|
-
|
|
131
|
-
if (
|
|
196
|
+
let resolvedPercent = percent;
|
|
197
|
+
if (
|
|
198
|
+
resolvedPercent !== null &&
|
|
199
|
+
mapping.valueMultiplier !== undefined &&
|
|
200
|
+
mapping.valueMultiplier !== 0
|
|
201
|
+
) {
|
|
202
|
+
resolvedPercent = Math.min(100, Math.max(0, resolvedPercent * mapping.valueMultiplier));
|
|
203
|
+
}
|
|
132
204
|
|
|
133
|
-
windows.push(percentWindow(mapping.kind, mapping.label,
|
|
205
|
+
windows.push(percentWindow(mapping.kind, mapping.label, resolvedPercent, reset, used, limit, "independent"));
|
|
134
206
|
}
|
|
135
207
|
|
|
136
208
|
if (options.extraWindows) windows.push(...options.extraWindows(payload));
|
|
@@ -18,8 +18,10 @@ import type { ProviderId } from "../provider-contracts";
|
|
|
18
18
|
import type { ProviderRegistry } from "../provider-registry";
|
|
19
19
|
import { quotaLens, setCachedQuota } from "./cache";
|
|
20
20
|
import { persistQuotaSnapshot } from "./snapshot";
|
|
21
|
+
import { applyAccountVerdict } from "../operations/account-health-service";
|
|
21
22
|
import { asJsonbRecord } from "../../shared/coerce";
|
|
22
23
|
import { KIRO_DEFAULT_LABELS } from "../integrations/kiro/kiro-oauth";
|
|
24
|
+
import { invalidateCredentialCache } from "../operations/provider-credential-service";
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
* The stock Kiro fallback labels as a SQL literal list. Built once from the
|
|
@@ -33,7 +35,7 @@ const KIRO_DEFAULT_LABEL_IN = sql.raw(
|
|
|
33
35
|
);
|
|
34
36
|
|
|
35
37
|
/** Why an account quota refresh failed; `null` means the fetch succeeded. */
|
|
36
|
-
export type QuotaRefreshFailureCode = "missing_credential" | "quota_fetch_failed";
|
|
38
|
+
export type QuotaRefreshFailureCode = "missing_credential" | "quota_fetch_failed" | "auth_invalidated";
|
|
37
39
|
|
|
38
40
|
export interface QuotaRefreshOutcome {
|
|
39
41
|
readonly quota: ProviderQuotaResult;
|
|
@@ -149,13 +151,69 @@ function isTransientTimeout(error: string | null | undefined): boolean {
|
|
|
149
151
|
export async function recordAccountCheck(
|
|
150
152
|
db: CartethyiaDatabase,
|
|
151
153
|
accountId: string,
|
|
152
|
-
outcome: { ok: boolean; error?: string | null; category?: string | null },
|
|
154
|
+
outcome: { ok: boolean; error?: string | null; category?: string | null; authInvalidated?: boolean },
|
|
155
|
+
snapshotInvalidator?: { invalidate(): unknown },
|
|
153
156
|
): Promise<void> {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
157
|
+
if (outcome.authInvalidated === true) {
|
|
158
|
+
const disabled = await applyAccountVerdict(db, {
|
|
159
|
+
accountId,
|
|
160
|
+
status: "disabled",
|
|
161
|
+
reason: outcome.error ?? "Provider rejected account authentication",
|
|
162
|
+
category: "auth_invalidated",
|
|
163
|
+
// No deadline: a credential rejected as invalid cannot heal by waiting,
|
|
164
|
+
// and the OAuth refresh sweep must not keep spending refresh attempts.
|
|
165
|
+
cooldownUntil: null,
|
|
166
|
+
});
|
|
167
|
+
if (disabled) {
|
|
168
|
+
invalidateCredentialCache(accountId);
|
|
169
|
+
await snapshotInvalidator?.invalidate();
|
|
170
|
+
}
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
158
173
|
if (outcome.ok) {
|
|
174
|
+
// A successful quota refresh is the freshest evidence about the account's
|
|
175
|
+
// health: heal a cooldown that the refresh shows is no longer justified, so
|
|
176
|
+
// the account returns to rotation immediately instead of waiting out the
|
|
177
|
+
// remainder of a stale fallback window. Only healable scopes heal — an
|
|
178
|
+
// auth/policy cooldown must never be lifted by a quota report (the
|
|
179
|
+
// credential is still refused). `lastErrorCategory` carries the category
|
|
180
|
+
// that parked the account, so the heal is scoped by the same verdict the
|
|
181
|
+
// health machine wrote.
|
|
182
|
+
const healableCategories = new Set([
|
|
183
|
+
"quota_exhausted",
|
|
184
|
+
"rate_limit_transient",
|
|
185
|
+
"model_capacity",
|
|
186
|
+
"model_unreachable",
|
|
187
|
+
"unknown",
|
|
188
|
+
"check_failed",
|
|
189
|
+
]);
|
|
190
|
+
const rows = await db
|
|
191
|
+
.select({ status: providerAccounts.status, lastErrorCategory: providerAccounts.lastErrorCategory })
|
|
192
|
+
.from(providerAccounts)
|
|
193
|
+
.where(eq(providerAccounts.id, accountId))
|
|
194
|
+
.limit(1);
|
|
195
|
+
const account = rows[0];
|
|
196
|
+
const healable =
|
|
197
|
+
account?.status === "cooldown" &&
|
|
198
|
+
(account.lastErrorCategory === null || healableCategories.has(account.lastErrorCategory));
|
|
199
|
+
if (healable) {
|
|
200
|
+
const healed = await db
|
|
201
|
+
.update(providerAccounts)
|
|
202
|
+
.set({
|
|
203
|
+
status: "active",
|
|
204
|
+
lastSuccessAt: new Date(),
|
|
205
|
+
lastError: null,
|
|
206
|
+
lastErrorCategory: null,
|
|
207
|
+
lastErrorAt: null,
|
|
208
|
+
consecutiveFailures: 0,
|
|
209
|
+
cooldownUntil: null,
|
|
210
|
+
lastRecoveredAt: new Date(),
|
|
211
|
+
})
|
|
212
|
+
.where(eq(providerAccounts.id, accountId))
|
|
213
|
+
.returning({ id: providerAccounts.id });
|
|
214
|
+
if (healed.length > 0) await snapshotInvalidator?.invalidate();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
159
217
|
await db
|
|
160
218
|
.update(providerAccounts)
|
|
161
219
|
.set({
|
|
@@ -165,7 +223,7 @@ export async function recordAccountCheck(
|
|
|
165
223
|
lastErrorAt: null,
|
|
166
224
|
consecutiveFailures: 0,
|
|
167
225
|
})
|
|
168
|
-
.where(
|
|
226
|
+
.where(and(eq(providerAccounts.id, accountId), eq(providerAccounts.status, "active")));
|
|
169
227
|
return;
|
|
170
228
|
}
|
|
171
229
|
const transient = isTransientTimeout(outcome.error);
|
|
@@ -186,7 +244,7 @@ export async function recordAccountCheck(
|
|
|
186
244
|
lastErrorAt: new Date(),
|
|
187
245
|
...(transient ? {} : { consecutiveFailures: sql`${providerAccounts.consecutiveFailures} + 1` }),
|
|
188
246
|
})
|
|
189
|
-
.where(
|
|
247
|
+
.where(and(eq(providerAccounts.id, accountId), eq(providerAccounts.status, "active")))
|
|
190
248
|
.returning({ consecutiveFailures: providerAccounts.consecutiveFailures });
|
|
191
249
|
if (transient) return;
|
|
192
250
|
const failures = failing[0]?.consecutiveFailures ?? 0;
|
|
@@ -196,13 +254,18 @@ export async function recordAccountCheck(
|
|
|
196
254
|
// pass. The cooldown clears itself; a success before it elapses resets the
|
|
197
255
|
// count above.
|
|
198
256
|
if (failures >= QUOTA_FAILURE_PARK_THRESHOLD) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
.
|
|
257
|
+
// Converge on the health application seam (proposal §3.5): quota refresh
|
|
258
|
+
// parking is a producer into `applyAccountVerdict`, not a separate state
|
|
259
|
+
// machine.
|
|
260
|
+
const parked = await applyAccountVerdict(db, {
|
|
261
|
+
accountId,
|
|
262
|
+
status: "cooldown",
|
|
263
|
+
reason: outcome.error ?? "Account check failed",
|
|
264
|
+
category: outcome.category ?? "quota_fetch_failed",
|
|
265
|
+
cooldownUntil: new Date(Date.now() + QUOTA_FAILURE_COOLDOWN_MS),
|
|
266
|
+
fromStatus: "active",
|
|
267
|
+
});
|
|
268
|
+
if (parked) await snapshotInvalidator?.invalidate();
|
|
206
269
|
}
|
|
207
270
|
}
|
|
208
271
|
|
|
@@ -407,7 +470,11 @@ async function runQuotaRefresh(
|
|
|
407
470
|
error: error instanceof Error ? error.message : "Failed to refresh quota",
|
|
408
471
|
};
|
|
409
472
|
}
|
|
410
|
-
failure = quota.error === null
|
|
473
|
+
failure = quota.error === null
|
|
474
|
+
? null
|
|
475
|
+
: quota.authInvalidated === true
|
|
476
|
+
? "auth_invalidated"
|
|
477
|
+
: "quota_fetch_failed";
|
|
411
478
|
}
|
|
412
479
|
|
|
413
480
|
// Cache even a failed fetch: the error view ("credential expired", "quota
|
|
@@ -427,7 +494,13 @@ async function runQuotaRefresh(
|
|
|
427
494
|
try {
|
|
428
495
|
await deps.db
|
|
429
496
|
.update(providerAccounts)
|
|
430
|
-
.set({
|
|
497
|
+
.set({
|
|
498
|
+
quotaPlan: quota.plan,
|
|
499
|
+
// Freshness stamp for the plan column: routing uses it to downgrade
|
|
500
|
+
// a plan older than the freshness horizon to `unknown` instead of
|
|
501
|
+
// trusting a weeks-old string.
|
|
502
|
+
quotaPlanFetchedAt: new Date(),
|
|
503
|
+
})
|
|
431
504
|
.where(eq(providerAccounts.id, target.accountId));
|
|
432
505
|
} catch (error) {
|
|
433
506
|
log.warn(`[quota] failed to record plan for account=${target.accountId}`, error as Error);
|
|
@@ -469,10 +542,16 @@ async function runQuotaRefresh(
|
|
|
469
542
|
}
|
|
470
543
|
}
|
|
471
544
|
try {
|
|
472
|
-
await recordAccountCheck(
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
545
|
+
await recordAccountCheck(
|
|
546
|
+
deps.db,
|
|
547
|
+
target.accountId,
|
|
548
|
+
{
|
|
549
|
+
ok: quota.error === null,
|
|
550
|
+
...(quota.error === null ? {} : { error: quota.error, category: failure ?? "quota_fetch_failed" }),
|
|
551
|
+
...(quota.authInvalidated === true ? { authInvalidated: true } : {}),
|
|
552
|
+
},
|
|
553
|
+
deps.snapshotInvalidator,
|
|
554
|
+
);
|
|
476
555
|
} catch (error) {
|
|
477
556
|
// A stamp failure must not lose the freshly fetched quota.
|
|
478
557
|
log.warn(`[quota] failed to stamp check outcome for account=${target.accountId}`, error as Error);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Bounded process-local authorization cache; shared-cache I/O stays in the auth service. */
|
|
2
2
|
export const AUTH_CACHE_TTL_MS = 3_000;
|
|
3
3
|
const AUTH_CACHE_MAX_ENTRIES = 10_000;
|
|
4
|
-
const AUTH_NEGATIVE_CACHE_TTL_MS = 1_500;
|
|
4
|
+
export const AUTH_NEGATIVE_CACHE_TTL_MS = 1_500;
|
|
5
5
|
const AUTH_FAILURE_COOLDOWN_MS = 1_000;
|
|
6
6
|
|
|
7
7
|
interface CacheEntry<T> {
|
|
@@ -21,8 +21,12 @@ import type { ApiKeyModelAccessMode } from "../persistence/schema";
|
|
|
21
21
|
* snapshot's consumed value is a reference point, so a brief stale window
|
|
22
22
|
* cannot meaningfully over-consume a budget.
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
* a
|
|
24
|
+
* Unknown or revoked tokens are negative-cached for
|
|
25
|
+
* `AUTH_NEGATIVE_CACHE_TTL_MS` (1.5s): a token rejected within that window
|
|
26
|
+
* stays refused even if its key is minted or un-revoked in between. A newly
|
|
27
|
+
* minted key is usable immediately only because `invalidateApiKeyCache`
|
|
28
|
+
* clears the negative entries when a mutation lands — not because negatives
|
|
29
|
+
* are absent.
|
|
26
30
|
*/
|
|
27
31
|
const authorizationCache = createApiKeyAuthorizationCache<ResolvedApiKey>();
|
|
28
32
|
|
|
@@ -20,7 +20,7 @@ import { modelsDevCatalog } from "../../providers/discovery/models-dev-catalog";
|
|
|
20
20
|
import {
|
|
21
21
|
disablePoolForProxyHttpStatus,
|
|
22
22
|
recordPoolDispatchOutcome,
|
|
23
|
-
} from "../../network/pool
|
|
23
|
+
} from "../../network/pool/health";
|
|
24
24
|
import { ProxyConnectRefusalError } from "../../network/pool/agent";
|
|
25
25
|
import { captureDepthMaxBytes, providerCaptureMaxBytes } from "./capture-policy";
|
|
26
26
|
|
|
@@ -622,19 +622,20 @@ export function nativeCompletionBase(input: {
|
|
|
622
622
|
| "tokensPerSec"
|
|
623
623
|
> {
|
|
624
624
|
return {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
625
|
+
...completionContext({
|
|
626
|
+
providerId: input.providerId,
|
|
627
|
+
modelId: input.modelId,
|
|
628
|
+
tenantId: input.tenantId,
|
|
629
|
+
ingressBody: input.ingressBody,
|
|
630
|
+
...(input.providerCapture === undefined ? {} : { providerCapture: input.providerCapture }),
|
|
631
|
+
db: input.db,
|
|
632
|
+
...(input.accountId === undefined ? {} : { accountId: input.accountId }),
|
|
633
|
+
...(input.accountLabel === undefined ? {} : { accountLabel: input.accountLabel }),
|
|
634
|
+
...(input.networkPoolId === undefined ? {} : { networkPoolId: input.networkPoolId }),
|
|
635
|
+
...(input.lease === undefined ? {} : { lease: input.lease }),
|
|
636
|
+
...(input.telemetryBuffer === undefined ? {} : { telemetryBuffer: input.telemetryBuffer }),
|
|
637
|
+
...(input.snapshotService === undefined ? {} : { snapshotService: input.snapshotService }),
|
|
638
|
+
}),
|
|
638
639
|
...(input.usage === undefined ? {} : { usage: input.usage }),
|
|
639
640
|
...(input.commitUsage === undefined ? {} : { commitUsage: input.commitUsage }),
|
|
640
641
|
};
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
import { resolveRouteMaxAttempts } from "../../config/timeouts";
|
|
21
21
|
import type { OAuthTokenRefresher } from "../../providers/authentication/oauth-refresh-service";
|
|
22
22
|
import type { OAuthRefreshService } from "../../providers/authentication/oauth-refresh-service";
|
|
23
|
-
import type { ValidatedNetworkBindingFactory } from "../../network/
|
|
23
|
+
import type { ValidatedNetworkBindingFactory } from "../../network/egress/factory";
|
|
24
24
|
import type { ByokUpstreamHost } from "../../providers/operations/provider-catalog-service";
|
|
25
25
|
import type { AdmissionLease } from "../../security/admission/contracts";
|
|
26
26
|
import type { RouteCandidate, Reservation, RouteSnapshotService } from "../routing/route-model";
|
|
@@ -32,7 +32,7 @@ import { metrics } from "../../observability/metrics";
|
|
|
32
32
|
import type { NetworkPoolSelector } from "../../network/pool/selector";
|
|
33
33
|
import type { ProxyRequestState } from "../request/state";
|
|
34
34
|
import type { TelemetryBatchBuffer } from "../../observability/telemetry-buffer";
|
|
35
|
-
import {
|
|
35
|
+
import { flagPoolOnEgressFailure } from "../../network/pool/health";
|
|
36
36
|
import {
|
|
37
37
|
completeAttempt,
|
|
38
38
|
errorClientResponseBody,
|
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
type ProviderExchangeCapture,
|
|
42
42
|
} from "./attempt-finalize";
|
|
43
43
|
import { repriceUsage } from "../../providers/provider-usage";
|
|
44
|
-
import { classifyEgressFailure } from "../../network/
|
|
44
|
+
import { classifyEgressFailure } from "../../network/verdict";
|
|
45
45
|
import { isOAuthCredentialInvalidated } from "./retry-policy";
|
|
46
46
|
import { drainAbortReason } from "../shutdown-notice";
|
|
47
47
|
|
|
@@ -61,6 +61,41 @@ function comboCandidateKey(candidate: RouteCandidate): string {
|
|
|
61
61
|
].join(":");
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Extra dials allowed for a mid-response upstream socket drop when the
|
|
66
|
+
* candidate list is exhausted (single-account providers). One extra dial
|
|
67
|
+
* recovers the transient reset; more would stack latency on a dying origin.
|
|
68
|
+
*/
|
|
69
|
+
const MAX_SOCKET_DROP_RETRIES = 1;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Normalizes a raw (non-GatewayError) failure before it escapes the loop.
|
|
73
|
+
*
|
|
74
|
+
* A transport-level throw — e.g. Bun's `TypeError` with
|
|
75
|
+
* `code: "ECONNRESET"` when the upstream socket dies mid-response — carries a
|
|
76
|
+
* `.code` of its own. The error middleware reads that `.code` for Elysia
|
|
77
|
+
* built-in errors, so a raw socket drop leaked to the client as
|
|
78
|
+
* `{"code":"ECONNRESET","status":400}` with origin `cartethyia`: the wrong
|
|
79
|
+
* code, the wrong status, and the wrong blame. The classifier already knows
|
|
80
|
+
* what this is (`transport_unavailable`, 502, upstream) — materialize it as a
|
|
81
|
+
* real GatewayError so the client and the captured Client Response see the
|
|
82
|
+
* same stable envelope every other upstream transport failure gets.
|
|
83
|
+
*/
|
|
84
|
+
function normalizeEscapeError(error: unknown, signal: AbortSignal): unknown {
|
|
85
|
+
if (error instanceof GatewayError) return error;
|
|
86
|
+
const terminal = classifyTerminalOutcome(error, signal);
|
|
87
|
+
if (terminal.errorCategory === "transport_unavailable") {
|
|
88
|
+
return new GatewayError(
|
|
89
|
+
"transport_unavailable",
|
|
90
|
+
502,
|
|
91
|
+
"The upstream closed the connection mid-response (request {requestId}). Retry; if it keeps happening, the account or pool is unstable. The connection ghosted us — come back in a moment 👻",
|
|
92
|
+
{},
|
|
93
|
+
"upstream",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return error;
|
|
97
|
+
}
|
|
98
|
+
|
|
64
99
|
/**
|
|
65
100
|
* True when the rejection is about the API key or the gateway's own policy,
|
|
66
101
|
* not about the provider account that was dialled.
|
|
@@ -321,7 +356,7 @@ export async function runAttemptLoop<TResult, TAdapter>(
|
|
|
321
356
|
status: terminal.status === "cancelled" ? "cancelled" : "failed",
|
|
322
357
|
});
|
|
323
358
|
}
|
|
324
|
-
throw lastError;
|
|
359
|
+
throw normalizeEscapeError(lastError, state.abortController.signal);
|
|
325
360
|
}
|
|
326
361
|
}
|
|
327
362
|
let lease: AdmissionLease | undefined;
|
|
@@ -381,6 +416,13 @@ export async function runAttemptLoop<TResult, TAdapter>(
|
|
|
381
416
|
// names the real cause.
|
|
382
417
|
const drain = drainAbortReason(state.abortController.signal.reason);
|
|
383
418
|
if (drain !== undefined) error = drain;
|
|
419
|
+
// Normalize a raw transport throw (e.g. Bun's `TypeError` with
|
|
420
|
+
// `code: "ECONNRESET"` when the upstream socket dies mid-response) into
|
|
421
|
+
// the stable `transport_unavailable`/502/upstream GatewayError **before**
|
|
422
|
+
// classification, so the telemetry row, the captured Client Response, and
|
|
423
|
+
// the escaped error all agree — and the raw `.code` never leaks to the
|
|
424
|
+
// client as a fake Elysia built-in code (see `normalizeEscapeError`).
|
|
425
|
+
error = normalizeEscapeError(error, state.abortController.signal);
|
|
384
426
|
lastError = error;
|
|
385
427
|
// A drain is a *server* close, not a client cancel: it must be recorded
|
|
386
428
|
// as a failure with the shutdown code, and it is never retryable.
|
|
@@ -411,21 +453,33 @@ export async function runAttemptLoop<TResult, TAdapter>(
|
|
|
411
453
|
// the pool excluded. Both flag helpers write the local entry
|
|
412
454
|
// synchronously, so the ordering holds even though the Redis mirror is
|
|
413
455
|
// fire-and-forget.
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
456
|
+
flagPoolOnEgressFailure(
|
|
457
|
+
deps.poolSelector,
|
|
458
|
+
deps.db,
|
|
459
|
+
networkPoolId,
|
|
460
|
+
candidate.provider_id,
|
|
461
|
+
error,
|
|
462
|
+
egressVerdict,
|
|
463
|
+
);
|
|
464
|
+
// A mid-response upstream socket drop (`transport_unavailable`) is the one
|
|
465
|
+
// retryable failure that receives an extra dial even when the candidate
|
|
466
|
+
// list is exhausted: an origin that closes the connection before the
|
|
467
|
+
// response is often transient (Trae's edge resets under concurrency),
|
|
468
|
+
// and the fresh dial recovers it — verified live 2026-11-09, 7/7
|
|
469
|
+
// recovered on the first retry. Without this, a single-account provider
|
|
470
|
+
// (the common case) tripped `index === candidates.length - 1` on the very
|
|
471
|
+
// first attempt and the client saw the raw socket error. The retry is
|
|
472
|
+
// bounded to one extra dial per request, never unbounded.
|
|
473
|
+
const memberAttempts = attemptsByMember.get(memberKey) ?? 0;
|
|
474
|
+
const socketDropRetryLeft =
|
|
475
|
+
terminal.errorCategory === "transport_unavailable" &&
|
|
476
|
+
!cancelled &&
|
|
477
|
+
memberAttempts < MAX_SOCKET_DROP_RETRIES;
|
|
424
478
|
const terminalAttempt =
|
|
425
479
|
cancelled ||
|
|
426
480
|
isClientScopedRejection(error) ||
|
|
427
481
|
!isRetryableFailure(error) ||
|
|
428
|
-
(index === candidates.length - 1 && !poolScopedFailure);
|
|
482
|
+
((index === candidates.length - 1 && !poolScopedFailure) && !socketDropRetryLeft);
|
|
429
483
|
// Track only the most recent fired exchange; the array this replaced
|
|
430
484
|
// accumulated every attempt's capture until the loop returned.
|
|
431
485
|
if (providerCapture.request !== null) lastFiredCapture = providerCapture;
|
|
@@ -474,7 +528,7 @@ export async function runAttemptLoop<TResult, TAdapter>(
|
|
|
474
528
|
// A 403 can be a deterministic provider policy rejection, not an
|
|
475
529
|
// invalid OAuth credential. Only the evidence-based classifier above
|
|
476
530
|
// may trigger a refresh.
|
|
477
|
-
const candidateKey =
|
|
531
|
+
const candidateKey = comboCandidateKey(candidate);
|
|
478
532
|
const authInvalidated = isOAuthCredentialInvalidated(error);
|
|
479
533
|
if (
|
|
480
534
|
!cancelled &&
|
|
@@ -499,7 +553,7 @@ export async function runAttemptLoop<TResult, TAdapter>(
|
|
|
499
553
|
}
|
|
500
554
|
if (terminalAttempt) {
|
|
501
555
|
metrics.proxy_requests_total.inc(1, { status: cancelled ? "cancelled" : "failed" });
|
|
502
|
-
throw error;
|
|
556
|
+
throw normalizeEscapeError(error, state.abortController.signal);
|
|
503
557
|
}
|
|
504
558
|
// A failed combo member stays flagged for this request. Move it behind
|
|
505
559
|
// every candidate that has not failed yet, so the next attempt cannot
|
|
@@ -542,7 +596,7 @@ export async function runAttemptLoop<TResult, TAdapter>(
|
|
|
542
596
|
// as the sleep's own error would leave `proxy_requests_total` missing
|
|
543
597
|
// the request.
|
|
544
598
|
metrics.proxy_requests_total.inc(1, { status: "cancelled" });
|
|
545
|
-
throw lastError;
|
|
599
|
+
throw normalizeEscapeError(lastError, state.abortController.signal);
|
|
546
600
|
}
|
|
547
601
|
}
|
|
548
602
|
}
|
|
@@ -577,5 +631,5 @@ export async function runAttemptLoop<TResult, TAdapter>(
|
|
|
577
631
|
}));
|
|
578
632
|
}
|
|
579
633
|
metrics.proxy_requests_total.inc(1, { status: "failed" });
|
|
580
|
-
throw lastError;
|
|
634
|
+
throw normalizeEscapeError(lastError, state.abortController.signal);
|
|
581
635
|
}
|
|
@@ -27,6 +27,7 @@ import { completeAttempt, estimatedUsage, nativeCompletionBase } from "./attempt
|
|
|
27
27
|
import { runAttemptLoop } from "./attempt-loop";
|
|
28
28
|
import { captureProviderRateLimit } from "./ratelimit-capture";
|
|
29
29
|
import {
|
|
30
|
+
forwardUpstreamResponse,
|
|
30
31
|
hasScope,
|
|
31
32
|
jsonResponse,
|
|
32
33
|
networkFetchContext,
|
|
@@ -133,14 +134,7 @@ async function passThrough(
|
|
|
133
134
|
responseBody: null,
|
|
134
135
|
});
|
|
135
136
|
metrics.proxy_requests_total.inc(1, { status: "completed" });
|
|
136
|
-
return
|
|
137
|
-
status: upstream.status,
|
|
138
|
-
headers: {
|
|
139
|
-
"content-type": upstream.headers.get("content-type") ?? "text/event-stream",
|
|
140
|
-
"cache-control": "no-store",
|
|
141
|
-
"x-request-id": state.requestId,
|
|
142
|
-
},
|
|
143
|
-
});
|
|
137
|
+
return forwardUpstreamResponse(upstream, state.requestId, "text/event-stream");
|
|
144
138
|
}
|
|
145
139
|
|
|
146
140
|
/** Builds the handler for one generation surface. */
|
|
@@ -11,7 +11,7 @@ import type { CartethyiaDatabase } from "../../persistence/db-handle";
|
|
|
11
11
|
import type { ProviderAdapter } from "../../providers/provider-contracts";
|
|
12
12
|
import type { OAuthRefreshService, OAuthTokenRefresher } from "../../providers/authentication/oauth-refresh-service";
|
|
13
13
|
import type { NetworkPoolSelector } from "../../network/pool/selector";
|
|
14
|
-
import type { ValidatedNetworkBindingFactory } from "../../network/
|
|
14
|
+
import type { ValidatedNetworkBindingFactory } from "../../network/egress/factory";
|
|
15
15
|
import type { TelemetryBatchBuffer } from "../../observability/telemetry-buffer";
|
|
16
16
|
import type { ProxyRequestPreparer } from "../request/preparer";
|
|
17
17
|
import type { ProxyRequestStateStore } from "../request/state";
|
|
@@ -5,7 +5,7 @@ import type { ApiKeyAuthorizationSnapshot } from "../../security/api-key-auth";
|
|
|
5
5
|
import type { ByokUpstreamHost } from "../../providers/operations/provider-catalog-service";
|
|
6
6
|
import type { PoolRotation, PoolSelectionFailure } from "../../network/pool/selector";
|
|
7
7
|
import type { NetworkPoolSelector } from "../../network/pool/selector";
|
|
8
|
-
import type { ValidatedNetworkBindingFactory } from "../../network/
|
|
8
|
+
import type { ValidatedNetworkBindingFactory } from "../../network/egress/factory";
|
|
9
9
|
import type { RouteCandidate as RouteCandidate, Reservation, RoutePlan } from "../routing/route-model";
|
|
10
10
|
import type { RoutingEngine } from "../routing/router";
|
|
11
11
|
|
|
@@ -71,6 +71,31 @@ export function jsonResponse(payload: unknown, status: number, requestId: string
|
|
|
71
71
|
});
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Forwards an upstream response body verbatim with the shared native-response
|
|
76
|
+
* header set.
|
|
77
|
+
*
|
|
78
|
+
* The native handlers (System One, responses-compact, token-count,
|
|
79
|
+
* generation's passThrough) each spelled the same three headers by hand; the
|
|
80
|
+
* copies drifted at least once (generation's JSON builder lost `cache-control`).
|
|
81
|
+
* Status and body come from the upstream untouched; only the content-type
|
|
82
|
+
* default differs per route (SSE vs JSON) and is passed per call.
|
|
83
|
+
*/
|
|
84
|
+
export function forwardUpstreamResponse(
|
|
85
|
+
response: Response,
|
|
86
|
+
requestId: string,
|
|
87
|
+
defaultContentType = "application/json",
|
|
88
|
+
): Response {
|
|
89
|
+
return new Response(response.body, {
|
|
90
|
+
status: response.status,
|
|
91
|
+
headers: {
|
|
92
|
+
"content-type": response.headers.get("content-type") ?? defaultContentType,
|
|
93
|
+
"cache-control": "no-store",
|
|
94
|
+
"x-request-id": requestId,
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
74
99
|
/**
|
|
75
100
|
* Strips a thinking suffix (`model(high)`) from the model name and rebuilds
|
|
76
101
|
* the body with the bare model when it changed. Returns the bare model and
|