cartethyia 2.7.8 → 2.8.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 +3 -3
- package/README.md +29 -59
- package/dist/dashboard/assets/BannedPage-Cc0vKpMG.js +1 -0
- package/dist/dashboard/assets/BannedPage-Cc0vKpMG.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-BRWKPQPf.js → CliToolDetailPage-Bqj2aa1T.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-Bqj2aa1T.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-BP-Y5iHN.js → CliToolsPage-CXsaOAO2.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-CXsaOAO2.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-BeLpJ4ma.js +1 -0
- package/dist/dashboard/assets/CombosPage-BeLpJ4ma.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-DnV0uZ3m.js +1 -0
- package/dist/dashboard/assets/ConsoleLogPage-DnV0uZ3m.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-CBxAjo63.js +1 -0
- package/dist/dashboard/assets/CustomizationPage-CBxAjo63.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js.gz +0 -0
- package/dist/dashboard/assets/{InjectSystemPromptPanel-Cv-2Ol_K.js → InjectSystemPromptPanel-BWFLECmz.js} +2 -2
- package/dist/dashboard/assets/InjectSystemPromptPanel-BWFLECmz.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-bKfz_b1K.js → LoginPage-CgoPc5B9.js} +1 -1
- package/dist/dashboard/assets/LoginPage-CgoPc5B9.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-B-WQxx_H.js → ModelPicker-D9bFQern.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-D9bFQern.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-B_p4tL8k.js +1 -0
- package/dist/dashboard/assets/OverviewPage-B_p4tL8k.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js +8 -0
- package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-D_0PiZtg.js +1 -0
- package/dist/dashboard/assets/ProviderIcon-D_0PiZtg.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js.gz +0 -0
- package/dist/dashboard/assets/{ProxyPage-emlTuxz6.js → ProxyPage-BwNC_-Cx.js} +1 -1
- package/dist/dashboard/assets/ProxyPage-BwNC_-Cx.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js +1 -0
- package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CctTga2C.js +1 -0
- package/dist/dashboard/assets/SettingsPage-CctTga2C.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-CUZrKs9r.js → SetupPage-febLU4r1.js} +1 -1
- package/dist/dashboard/assets/SetupPage-febLU4r1.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-jWmUdDpy.js → StudioPage-M6fvR5Wz.js} +1 -1
- package/dist/dashboard/assets/StudioPage-M6fvR5Wz.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-nyeuRqz9.js +56 -0
- package/dist/dashboard/assets/UsagePage-nyeuRqz9.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-BFOlvrJC.js +1 -0
- package/dist/dashboard/assets/api-keys-BFOlvrJC.js.gz +0 -0
- package/dist/dashboard/assets/app-Dx2QgsNj.css +2 -0
- package/dist/dashboard/assets/app-Dx2QgsNj.css.gz +0 -0
- package/dist/dashboard/assets/{assert-CidQVoNo.js → assert-CqTLw5e7.js} +1 -1
- package/dist/dashboard/assets/assert-CqTLw5e7.js.gz +0 -0
- package/dist/dashboard/assets/{badge-DyI_mJfi.js → badge-C83EqLB0.js} +1 -1
- package/dist/dashboard/assets/badge-C83EqLB0.js.gz +0 -0
- package/dist/dashboard/assets/ban-durations-B58FyRxC.js +1 -0
- package/dist/dashboard/assets/ban-durations-B58FyRxC.js.gz +0 -0
- package/dist/dashboard/assets/{banner-B-yXtCqb.js → banner-D1xn7pIe.js} +1 -1
- package/dist/dashboard/assets/banner-D1xn7pIe.js.gz +0 -0
- package/dist/dashboard/assets/brand/bansos_page.webp +0 -0
- package/dist/dashboard/assets/brand/denia_bansos.webp +0 -0
- package/dist/dashboard/assets/brand/favicon.webp +0 -0
- package/dist/dashboard/assets/brand/login_register.webp +0 -0
- package/dist/dashboard/assets/brand/login_register_initial.webp +0 -0
- package/dist/dashboard/assets/brand/og_bansos.webp +0 -0
- package/dist/dashboard/assets/brand/og_image.webp +0 -0
- package/dist/dashboard/assets/brand/sidebar_ico.webp +0 -0
- package/dist/dashboard/assets/{card-_HwAEqFF.js → card-DU3Lq5Lm.js} +1 -1
- package/dist/dashboard/assets/card-DU3Lq5Lm.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-Dw1U_drK.js +1 -0
- package/dist/dashboard/assets/cli-tools-Dw1U_drK.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-ByjCk0o9.js → clipboard-button-CEL5Sg_J.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-CEL5Sg_J.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-BZhOlfz-.js → dialog-BjFR5UHy.js} +1 -1
- package/dist/dashboard/assets/dialog-BjFR5UHy.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-BADqIRYP.js → drawer-HvBW0AjI.js} +1 -1
- package/dist/dashboard/assets/drawer-HvBW0AjI.js.gz +0 -0
- package/dist/dashboard/assets/entry-BSu3gFlt.js +4 -0
- package/dist/dashboard/assets/entry-BSu3gFlt.js.gz +0 -0
- package/dist/dashboard/assets/{entry-COaC-irN.js → entry-CECVELyy.js} +1 -1
- package/dist/dashboard/assets/entry-CECVELyy.js.gz +0 -0
- package/dist/dashboard/assets/entry-DYYofQ7W.js +1 -0
- package/dist/dashboard/assets/entry-DYYofQ7W.js.gz +0 -0
- package/dist/dashboard/assets/{formatters-DcHJyRiu.js → formatters-DLW4i8zK.js} +1 -1
- package/dist/dashboard/assets/formatters-DLW4i8zK.js.gz +0 -0
- package/dist/dashboard/assets/{index-CgKmFngv.js → index-1-fCqe64.js} +2 -2
- package/dist/dashboard/assets/index-1-fCqe64.js.gz +0 -0
- package/dist/dashboard/assets/{inline-B2BirKry.js → inline-DGvU2oLK.js} +1 -1
- package/dist/dashboard/assets/inline-DGvU2oLK.js.gz +0 -0
- package/dist/dashboard/assets/{input-D-qV9dKY.js → input-iNnO1X2K.js} +1 -1
- package/dist/dashboard/assets/input-iNnO1X2K.js.gz +0 -0
- package/dist/dashboard/assets/{layout-B7tVWhtV.js → layout-0q8UGypb.js} +1 -1
- package/dist/dashboard/assets/layout-0q8UGypb.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-CtkGE3bF.js → list-row-C0w2bQjJ.js} +1 -1
- package/dist/dashboard/assets/list-row-C0w2bQjJ.js.gz +0 -0
- package/dist/dashboard/assets/{live-DGizF06-.js → live-M5aJ_xCa.js} +1 -1
- package/dist/dashboard/assets/live-M5aJ_xCa.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-GU7XdEjz.js → mutation-BQ0ccH_A.js} +1 -1
- package/dist/dashboard/assets/mutation-BQ0ccH_A.js.gz +0 -0
- package/dist/dashboard/assets/{network-C9UE-bOr.js → network-BKeQGT3K.js} +1 -1
- package/dist/dashboard/assets/network-BKeQGT3K.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-BnAd22a_.js → numbers-CivAk3da.js} +1 -1
- package/dist/dashboard/assets/numbers-CivAk3da.js.gz +0 -0
- package/dist/dashboard/assets/{pagination-BAywyMzS.js → pagination-CpzvI5ep.js} +1 -1
- package/dist/dashboard/assets/pagination-CpzvI5ep.js.gz +0 -0
- package/dist/dashboard/assets/providers/devin.svg +9 -0
- package/dist/dashboard/assets/providers/devin.svg.gz +0 -0
- package/dist/dashboard/assets/providers/windsurf.svg +10 -0
- package/dist/dashboard/assets/providers/windsurf.svg.gz +0 -0
- package/dist/dashboard/assets/providers/zro.svg +3 -0
- package/dist/dashboard/assets/providers/zro.svg.gz +0 -0
- package/dist/dashboard/assets/{query-keys-YSoQOUh6.js → query-keys-IKLh0YFd.js} +1 -1
- package/dist/dashboard/assets/query-keys-IKLh0YFd.js.gz +0 -0
- package/dist/dashboard/assets/quota-DiPD6UKH.js +1 -0
- package/dist/dashboard/assets/quota-DiPD6UKH.js.gz +0 -0
- package/dist/dashboard/assets/routing-BFQX2VBc.js +1 -0
- package/dist/dashboard/assets/routing-BFQX2VBc.js.gz +0 -0
- package/dist/dashboard/assets/{select-BCOcv8WK.js → select-CXbck0jG.js} +2 -2
- package/dist/dashboard/assets/select-CXbck0jG.js.gz +0 -0
- package/dist/dashboard/assets/settings-C_XvMHh9.js +1 -0
- package/dist/dashboard/assets/settings-C_XvMHh9.js.gz +0 -0
- package/dist/dashboard/assets/{stack-ByKt3Q1g.js → stack-DggRiCxE.js} +1 -1
- package/dist/dashboard/assets/stack-DggRiCxE.js.gz +0 -0
- package/dist/dashboard/assets/{state-DXbGTx7N.js → state-ClFzfbYp.js} +1 -1
- package/dist/dashboard/assets/state-ClFzfbYp.js.gz +0 -0
- package/dist/dashboard/assets/story/25817331.webp +0 -0
- package/dist/dashboard/assets/story/Shorekeeper.webp +0 -0
- package/dist/dashboard/assets/story/cartethyia-god.webp +0 -0
- package/dist/dashboard/assets/story/fleurdelys_plus.webp +0 -0
- package/dist/dashboard/assets/story/jinhsi-blossom.webp +0 -0
- package/dist/dashboard/assets/story/phrolova-a.webp +0 -0
- package/dist/dashboard/assets/story/requestdeniawokkjpg.webp +0 -0
- package/dist/dashboard/assets/story/wuthering-waves-hiyuki-aemeath-hiyuki-rover.webp +0 -0
- package/dist/dashboard/assets/{switch-ZeTsW623.js → switch-CTIFxKqS.js} +1 -1
- package/dist/dashboard/assets/switch-CTIFxKqS.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-CbCP4w1o.js → useMutation-BClG3yWj.js} +1 -1
- package/dist/dashboard/assets/useMutation-BClG3yWj.js.gz +0 -0
- package/dist/dashboard/console-backgrounds.webp +0 -0
- package/dist/dashboard/console-backgrounds_initial.webp +0 -0
- package/dist/dashboard/donation/qris-risun-marke.webp +0 -0
- package/dist/dashboard/index.html +3 -3
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +59 -56
- package/migrations/full-postgres/0044_add_models_warning_metadata.sql +10 -0
- package/migrations/full-postgres/0045_migrate_workbuddy_to_codebuddy.sql +156 -0
- package/migrations/full-postgres/0046_add_accounts_quota_plan_fetched_at.sql +7 -0
- package/migrations/lite-sqlite3/0044_add_models_warning_metadata.sql +11 -0
- package/migrations/lite-sqlite3/0045_migrate_workbuddy_to_codebuddy.sql +156 -0
- package/migrations/lite-sqlite3/0046_add_accounts_quota_plan_fetched_at.sql +4 -0
- package/package.json +1 -1
- package/src/boot/app.ts +10 -13
- package/src/boot/dependencies.ts +12 -4
- package/src/boot/flush.ts +20 -0
- package/src/boot/lifecycle.ts +7 -5
- package/src/config/cooldowns.ts +1 -1
- package/src/config/routing.ts +26 -0
- package/src/config/spec-metadata.ts +72 -0
- package/src/config/spec.ts +20 -6
- package/src/console/api-keys/contracts.ts +12 -1
- package/src/console/api-keys/routes.ts +50 -31
- 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/cli-tools/service.ts +6 -12
- package/src/console/console-router-guards.ts +23 -16
- package/src/console/console-router.ts +7 -0
- package/src/console/dashboard-assets.ts +5 -5
- package/src/console/domain-registration.ts +27 -2
- package/src/console/inferhub/normalize.ts +24 -28
- package/src/console/inferhub/types.ts +3 -0
- package/src/console/ip-bans/routes.ts +93 -0
- package/src/console/observability/store.ts +0 -53
- package/src/console/providers/catalog/catalog-projections.ts +5 -2
- package/src/console/providers/catalog/contracts.ts +123 -0
- package/src/console/providers/catalog/discover-constants.ts +6 -0
- package/src/console/providers/catalog/model-operations.ts +122 -10
- package/src/console/providers/catalog/provider-operations.ts +116 -8
- package/src/console/providers/catalog/public-model-store.ts +0 -0
- package/src/console/providers/catalog/routes.ts +65 -4
- package/src/console/providers/catalog/store-helpers.ts +30 -1
- package/src/console/providers/catalog/store.ts +249 -4
- package/src/console/quota/account-quota-global.ts +97 -19
- package/src/console/quota/account-quota-shared.ts +49 -0
- package/src/console/quota/account-quota-tenant.ts +6 -18
- package/src/console/quota/native-identity.ts +2 -0
- package/src/console/quota/quota-view.ts +19 -3
- package/src/console/routing/pools/operations.ts +31 -46
- package/src/console/settings/contracts.ts +18 -0
- package/src/console/settings/store.ts +32 -0
- package/src/console/shared/errors.ts +33 -6
- package/src/console/system-prompt/contracts.ts +0 -1
- package/src/main.ts +3 -3
- package/src/network/pool/byte-accounting.ts +3 -2
- package/src/network/pool/selector.ts +182 -39
- package/src/network/pool-cooldown-store.ts +89 -14
- package/src/network/pool-health.ts +49 -61
- package/src/network/rate-limit-verdict.ts +1 -1
- package/src/network/ssrf.ts +6 -6
- package/src/observability/event-log.ts +7 -361
- package/src/observability/event-query.ts +18 -4
- package/src/observability/performance-metrics.ts +9 -0
- package/src/observability/runtime-metrics.ts +14 -9
- package/src/observability/telemetry-buffer.ts +15 -25
- 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 +30 -0
- package/src/persistence/migrate.ts +6 -3
- package/src/persistence/schema.ts +28 -2
- package/src/persistence/ttl-cache.ts +20 -2
- package/src/protocol/codecs.ts +1 -1
- package/src/protocol/dsml.ts +35 -87
- package/src/protocol/gateway-error.ts +1 -0
- package/src/protocol/markup-stream-parser.ts +195 -0
- package/src/protocol/reasoning-text.ts +19 -0
- package/src/protocol/reasoning.ts +1 -1
- package/src/protocol/request/chat.ts +14 -14
- package/src/protocol/request/gemini.ts +8 -2
- package/src/protocol/request/messages.ts +4 -5
- package/src/protocol/response/chat.ts +12 -20
- package/src/protocol/response/gemini.ts +6 -1
- package/src/protocol/response/omp-tool-call-markup.ts +43 -86
- package/src/protocol/response/responses.ts +18 -2
- package/src/protocol/sse-decoder.ts +25 -8
- package/src/protocol/stop-reason.ts +23 -1
- package/src/protocol/stream-error-frames.ts +13 -3
- package/src/protocol/think-split.ts +112 -0
- package/src/protocol/translation/dialect/qwen.ts +26 -58
- package/src/protocol/translation/quirks.ts +1 -1
- package/src/protocol/translation/tool-repair.ts +2 -2
- package/src/protocol/upstream/send-claude-messages.ts +15 -4
- package/src/protocol/upstream-parsing.ts +1 -1
- package/src/providers/adapters/compatible-adapter.ts +78 -17
- package/src/providers/authentication/oauth-client.ts +1 -1
- package/src/providers/authentication/oauth-device-flow.ts +98 -8
- package/src/providers/authentication/oauth-refresh-service.ts +153 -59
- package/src/providers/configured-provider.ts +148 -0
- package/src/providers/default-registry.ts +117 -27
- package/src/providers/discovery/discovery-types.ts +12 -0
- package/src/providers/discovery/probe-phases.ts +74 -33
- package/src/providers/discovery/probing-service.ts +293 -165
- 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 +121 -2
- package/src/providers/integrations/antigravity/antigravity.ts +13 -2
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +72 -34
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +191 -35
- package/src/providers/integrations/buddy/buddy-checkin.ts +17 -39
- package/src/providers/integrations/buddy/buddy-conversation-headers.ts +2 -2
- package/src/providers/integrations/buddy/buddy-discovery-shared.ts +5 -5
- package/src/providers/integrations/buddy/buddy-identity.ts +89 -6
- package/src/providers/integrations/buddy/buddy-media.ts +4 -5
- package/src/providers/integrations/buddy/buddy-oauth-shared.ts +2 -2
- package/src/providers/integrations/buddy/buddy-quota-shared.ts +1 -1
- package/src/providers/integrations/buddy/buddy-usage-shared.ts +1 -3
- package/src/providers/integrations/buddy/capabilities.ts +6 -43
- package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +2 -1
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +23 -1
- package/src/providers/integrations/buddy/codebuddy/provider.ts +6 -2
- package/src/providers/integrations/buddy/codebuddy/shared.ts +38 -27
- package/src/providers/integrations/claude/claude-quota.ts +14 -17
- package/src/providers/integrations/cline/cline.ts +2 -2
- package/src/providers/integrations/commandcode/quota.ts +22 -28
- package/src/providers/integrations/cursor/cursor-auth.ts +0 -8
- package/src/providers/integrations/cursor/cursor-catalog.ts +2 -0
- package/src/providers/integrations/cursor/cursor-exec.ts +745 -0
- package/src/providers/integrations/cursor/cursor-http2.ts +2 -0
- package/src/providers/integrations/cursor/cursor-request.ts +35 -20
- package/src/providers/integrations/cursor/cursor-shared.ts +26 -5
- package/src/providers/integrations/cursor/cursor.ts +139 -70
- 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/cursor/protocol/tool-canonical.ts +26 -100
- package/src/providers/integrations/devin/devin-auth.ts +353 -0
- package/src/providers/integrations/devin/devin-catalog.ts +207 -0
- package/src/providers/integrations/devin/devin-http.ts +91 -0
- package/src/providers/integrations/devin/devin-quota.ts +214 -0
- package/src/providers/integrations/devin/devin-shared.ts +111 -0
- package/src/providers/integrations/devin/devin.ts +539 -0
- package/src/providers/integrations/devin/protocol/devin-proto.ts +558 -0
- package/src/providers/integrations/devin/protocol/framing.ts +124 -0
- package/src/providers/integrations/factory/factory-oauth.ts +17 -44
- package/src/providers/integrations/factory/factory.ts +2 -2
- 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 +60 -0
- package/src/providers/integrations/freepi/freepi-discovery.ts +82 -0
- package/src/providers/integrations/freepi/freepi-headers.ts +21 -0
- package/src/providers/integrations/freepi/freepi-oauth.ts +142 -0
- package/src/providers/integrations/freepi/freepi-protocol.ts +242 -0
- package/src/providers/integrations/freepi/freepi-quota.ts +126 -0
- package/src/providers/integrations/freepi/freepi-realm.ts +35 -0
- package/src/providers/integrations/freepi/freepi.ts +292 -0
- package/src/providers/integrations/gemini.ts +2 -2
- package/src/providers/integrations/grok/grok-oauth.ts +48 -45
- 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 +94 -0
- package/src/providers/integrations/{hermes.ts → hermes/hermes.ts} +13 -21
- package/src/providers/integrations/hermes/index.ts +2 -0
- 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-quota.ts +8 -4
- package/src/providers/integrations/kiro/kiro.ts +15 -19
- 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/perch/perch-catalog.ts +105 -0
- package/src/providers/integrations/perch/perch-credential.ts +25 -0
- package/src/providers/integrations/perch/perch-oauth.ts +170 -0
- package/src/providers/integrations/perch/perch-protocol.ts +208 -0
- package/src/providers/integrations/perch/perch-quota.ts +123 -0
- package/src/providers/integrations/perch/perch-realm.ts +82 -0
- package/src/providers/integrations/perch/perch-system-prompt.ts +13 -0
- package/src/providers/integrations/perch/perch.ts +305 -0
- package/src/providers/integrations/perch/saffron-system-prompt.txt +8 -0
- package/src/providers/integrations/qoder/chat-proto.ts +328 -328
- package/src/providers/integrations/qoder/claim-qoder-credits.ts +19 -60
- package/src/providers/integrations/qoder/qoder-adapter.ts +90 -40
- package/src/providers/integrations/qoder/qoder-auth.ts +101 -29
- package/src/providers/integrations/qoder/qoder-cache.ts +32 -0
- package/src/providers/integrations/qoder/qoder-catalog.ts +26 -6
- package/src/providers/integrations/qoder/qoder-chat.ts +7 -21
- package/src/providers/integrations/qoder/qoder-checkin.ts +6 -6
- package/src/providers/integrations/qoder/qoder-grpc.ts +210 -7
- package/src/providers/integrations/qoder/qoder-ide.ts +5 -108
- package/src/providers/integrations/qoder/qoder-identity-cache.ts +184 -0
- package/src/providers/integrations/qoder/qoder-native-identity-store.ts +83 -18
- package/src/providers/integrations/qoder/qoder-realm.ts +9 -6
- package/src/providers/integrations/qoder/qoder-umid.ts +109 -342
- package/src/providers/integrations/qoder/qoder-virtual.ts +33 -1
- package/src/providers/integrations/qoder/qoder.ts +3 -7
- package/src/providers/integrations/trae/trae-catalog.ts +9 -14
- package/src/providers/integrations/trae/trae-headers.ts +36 -11
- package/src/providers/integrations/trae/trae-oauth.ts +14 -17
- package/src/providers/integrations/trae/trae-protocol.ts +12 -11
- package/src/providers/integrations/trae/trae-quota.ts +96 -36
- package/src/providers/integrations/trae/trae-realm.ts +74 -15
- package/src/providers/integrations/trae/trae.ts +99 -26
- 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 +204 -0
- package/src/providers/operations/account-error-classification.ts +235 -278
- package/src/providers/operations/account-health-service.ts +273 -26
- package/src/providers/operations/account-refresh-status.ts +269 -0
- package/src/providers/operations/account-tier.ts +66 -0
- package/src/providers/operations/client-version-resolver.ts +1 -1
- package/src/providers/operations/client-versions.ts +42 -150
- package/src/providers/operations/cooldown-rules.ts +56 -0
- package/src/providers/operations/{model-catalog-cache.ts → provider-caches.ts} +68 -16
- package/src/providers/operations/provider-catalog-seeder.ts +4 -0
- package/src/providers/operations/provider-catalog-service.ts +1 -1
- package/src/providers/operations/provider-credential-service.ts +102 -32
- 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/provider-contracts.ts +33 -2
- package/src/providers/{provider-credential-resolver.ts → provider-credentials.ts} +40 -3
- package/src/providers/provider-metadata.ts +99 -14
- package/src/providers/provider-model-definition.ts +36 -0
- package/src/providers/provider-probe.ts +15 -1
- package/src/providers/provider-registry.ts +42 -33
- 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 +96 -18
- package/src/security/abuse.ts +71 -0
- package/src/security/api-key-auth-cache.ts +1 -1
- package/src/security/api-key-auth.ts +6 -2
- package/src/transport/dispatch/attempt-finalize.ts +14 -13
- package/src/transport/dispatch/attempt-loop.ts +71 -17
- package/src/transport/dispatch/generation.ts +2 -8
- package/src/transport/dispatch/native-shared.ts +25 -0
- package/src/transport/dispatch/proxy-request.ts +9 -46
- package/src/transport/dispatch/responses-compact.ts +2 -8
- package/src/transport/dispatch/streaming/finalize.ts +9 -6
- package/src/transport/dispatch/streaming/stream.ts +9 -36
- package/src/transport/dispatch/systemone.ts +2 -8
- package/src/transport/dispatch/token-count.ts +2 -6
- package/src/transport/dispatch/upstream.ts +67 -1
- package/src/transport/drain-endpoint.ts +31 -4
- package/src/transport/failure-policy.ts +129 -37
- 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 +418 -33
- 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 +9 -2
- 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 +34 -6
- package/src/transport/surface/responses/parse.ts +3 -11
- package/src/workers/daily-checkin.ts +1 -1
- package/src/workers/quota-refresh-sweep.ts +1 -1
- package/src/workers/scheduler.ts +22 -4
- package/dist/dashboard/assets/BannedPage-DeIbIiBU.js +0 -1
- package/dist/dashboard/assets/BannedPage-DeIbIiBU.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-BRWKPQPf.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-BP-Y5iHN.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-B7JHozVa.js +0 -1
- package/dist/dashboard/assets/CombosPage-B7JHozVa.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-GX7iGGcu.js +0 -1
- package/dist/dashboard/assets/ConsoleLogPage-GX7iGGcu.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-HVonbGEa.js +0 -1
- package/dist/dashboard/assets/CustomizationPage-HVonbGEa.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-xaut4OJ9.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-xaut4OJ9.js.gz +0 -0
- package/dist/dashboard/assets/InjectSystemPromptPanel-Cv-2Ol_K.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-bKfz_b1K.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-B-WQxx_H.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-Z9pJZFo8.js +0 -1
- package/dist/dashboard/assets/OverviewPage-Z9pJZFo8.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CMeDBxR1.js +0 -7
- package/dist/dashboard/assets/ProviderDetailPage-CMeDBxR1.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js +0 -1
- package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-BXPdOpG4.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-BXPdOpG4.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-emlTuxz6.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-CMJKNYWb.js +0 -1
- package/dist/dashboard/assets/QuotaPage-CMJKNYWb.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-BSy7EwGo.js +0 -1
- package/dist/dashboard/assets/SettingsPage-BSy7EwGo.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-CUZrKs9r.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-jWmUdDpy.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-5F9tGVGG.js +0 -56
- package/dist/dashboard/assets/UsagePage-5F9tGVGG.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-KvX83mRe.js +0 -1
- package/dist/dashboard/assets/api-keys-KvX83mRe.js.gz +0 -0
- package/dist/dashboard/assets/app-BEsU8hKs.css +0 -2
- package/dist/dashboard/assets/app-BEsU8hKs.css.gz +0 -0
- package/dist/dashboard/assets/assert-CidQVoNo.js.gz +0 -0
- package/dist/dashboard/assets/badge-DyI_mJfi.js.gz +0 -0
- package/dist/dashboard/assets/banner-B-yXtCqb.js.gz +0 -0
- package/dist/dashboard/assets/brand/bansos_page.png +0 -0
- package/dist/dashboard/assets/brand/denia_bansos.jpg +0 -0
- package/dist/dashboard/assets/brand/favicon.jpg +0 -0
- package/dist/dashboard/assets/brand/login_register.jpg +0 -0
- package/dist/dashboard/assets/brand/login_register_initial.jpg +0 -0
- package/dist/dashboard/assets/brand/og_bansos.jpg +0 -0
- package/dist/dashboard/assets/brand/og_image.jpg +0 -0
- package/dist/dashboard/assets/brand/sidebar_ico.jpg +0 -0
- package/dist/dashboard/assets/card-_HwAEqFF.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js +0 -1
- package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-ByjCk0o9.js.gz +0 -0
- package/dist/dashboard/assets/dialog-BZhOlfz-.js.gz +0 -0
- package/dist/dashboard/assets/drawer-BADqIRYP.js.gz +0 -0
- package/dist/dashboard/assets/entry-COaC-irN.js.gz +0 -0
- package/dist/dashboard/assets/entry-Ca7LSCet.js +0 -4
- package/dist/dashboard/assets/entry-Ca7LSCet.js.gz +0 -0
- package/dist/dashboard/assets/entry-PDd27uCd.js +0 -1
- package/dist/dashboard/assets/entry-PDd27uCd.js.gz +0 -0
- package/dist/dashboard/assets/formatters-DcHJyRiu.js.gz +0 -0
- package/dist/dashboard/assets/index-CgKmFngv.js.gz +0 -0
- package/dist/dashboard/assets/inline-B2BirKry.js.gz +0 -0
- package/dist/dashboard/assets/input-D-qV9dKY.js.gz +0 -0
- package/dist/dashboard/assets/layout-B7tVWhtV.js.gz +0 -0
- package/dist/dashboard/assets/list-row-CtkGE3bF.js.gz +0 -0
- package/dist/dashboard/assets/live-DGizF06-.js.gz +0 -0
- package/dist/dashboard/assets/minimize-2-BTmMTK4o.js +0 -1
- package/dist/dashboard/assets/minimize-2-BTmMTK4o.js.gz +0 -0
- package/dist/dashboard/assets/mutation-GU7XdEjz.js.gz +0 -0
- package/dist/dashboard/assets/network-C9UE-bOr.js.gz +0 -0
- package/dist/dashboard/assets/numbers-BnAd22a_.js.gz +0 -0
- package/dist/dashboard/assets/pagination-BAywyMzS.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-YSoQOUh6.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-fnCsxgNO.js +0 -1
- package/dist/dashboard/assets/routing-fnCsxgNO.js.gz +0 -0
- package/dist/dashboard/assets/select-BCOcv8WK.js.gz +0 -0
- package/dist/dashboard/assets/settings-BxslsErn.js +0 -1
- package/dist/dashboard/assets/settings-BxslsErn.js.gz +0 -0
- package/dist/dashboard/assets/stack-ByKt3Q1g.js.gz +0 -0
- package/dist/dashboard/assets/state-DXbGTx7N.js.gz +0 -0
- package/dist/dashboard/assets/switch-ZeTsW623.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-CbCP4w1o.js.gz +0 -0
- package/dist/dashboard/console-backgrounds.jpg +0 -0
- package/dist/dashboard/console-backgrounds_initial.jpg +0 -0
- package/dist/dashboard/donation/qris-risun-marke.png +0 -0
- package/src/providers/bundled-discovery-providers.ts +0 -98
- package/src/providers/configured-openai-providers.ts +0 -120
- package/src/providers/integrations/buddy/turing-device-token.ts +0 -262
- package/src/providers/integrations/buddy/workbuddy/cn-oauth.ts +0 -30
- package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +0 -58
- package/src/providers/integrations/buddy/workbuddy/oauth.ts +0 -31
- package/src/providers/integrations/buddy/workbuddy/provider.ts +0 -209
- package/src/providers/integrations/buddy/workbuddy/quota.ts +0 -161
- package/src/providers/integrations/buddy/workbuddy/shared.ts +0 -285
- package/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-CTxoqyYB.js → app-DIXLjojg.js} +0 -0
- /package/dist/dashboard/assets/{app-CTxoqyYB.js.gz → app-DIXLjojg.js.gz} +0 -0
|
@@ -23,7 +23,7 @@ import type { AdmissionLease } from "../../../security/admission/contracts";
|
|
|
23
23
|
import type { Reservation } from "../../routing/route-model";
|
|
24
24
|
import { repriceUsage } from "../../../providers/provider-usage";
|
|
25
25
|
import { metrics } from "../../../observability/metrics";
|
|
26
|
-
import {
|
|
26
|
+
import { flagPoolOnEgressFailure } from "../../../network/pool-health";
|
|
27
27
|
import { classifyEgressFailure } from "../../../network/rate-limit-verdict";
|
|
28
28
|
import { releaseAttemptLeases } from "../leases";
|
|
29
29
|
import type { AttemptLeaseOwnership } from "../leases";
|
|
@@ -581,11 +581,14 @@ export class StreamFinalizer {
|
|
|
581
581
|
this.streamSettled = true;
|
|
582
582
|
if (candidate.networkPoolId && !cancelled && deps.poolSelector && err instanceof GatewayError) {
|
|
583
583
|
const egressVerdict = classifyEgressFailure(err, candidate.providerId);
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
584
|
+
flagPoolOnEgressFailure(
|
|
585
|
+
deps.poolSelector,
|
|
586
|
+
deps.db,
|
|
587
|
+
candidate.networkPoolId,
|
|
588
|
+
candidate.providerId,
|
|
589
|
+
err,
|
|
590
|
+
egressVerdict,
|
|
591
|
+
);
|
|
589
592
|
}
|
|
590
593
|
if (!cancelled) metrics.proxy_requests_total.inc(1, { status: "failed" });
|
|
591
594
|
else metrics.proxy_requests_total.inc(1, { status: "cancelled" });
|
|
@@ -43,17 +43,11 @@ 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 {
|
|
47
|
-
import {
|
|
48
|
-
providerCaptureMaxBytes,
|
|
49
|
-
resolveCaptureDepthForRequest,
|
|
50
|
-
resolveCaptureModeForRequest,
|
|
51
|
-
shouldCaptureProviderBodyForRequest,
|
|
52
|
-
} from "../capture-policy";
|
|
46
|
+
import { resolveCaptureModeForRequest } from "../capture-policy";
|
|
53
47
|
import { resolveTenantImageClampMaxBytes, readTenantSystemPromptsForDispatch } from "../../request/tenant-preferences";
|
|
54
48
|
import { resolveTenantSystemPrompt } from "../../request/system-prompt-injection";
|
|
55
49
|
import { isWrappedInjectionProvider } from "../../../console/system-prompt/contracts";
|
|
56
|
-
import { proxySuccessHeaders, buildUpstreamDispatchContext } from "../upstream";
|
|
50
|
+
import { proxySuccessHeaders, buildUpstreamDispatchContext, buildDispatchNetworkBindings } from "../upstream";
|
|
57
51
|
import type { AttemptLeaseOwnership } from "../leases";
|
|
58
52
|
import type { ProviderContinuationScope } from "../../../providers/provider-contracts";
|
|
59
53
|
import { resolveStreamFirstChunkTimeoutMs, resolveStreamStallTimeoutMs, resolveUpstreamTimeoutMs } from "../../../config/timeouts";
|
|
@@ -170,18 +164,13 @@ export async function dispatchStreamingAttempt(input: StreamingDispatchInput): P
|
|
|
170
164
|
continuationScope,
|
|
171
165
|
} = input;
|
|
172
166
|
|
|
173
|
-
const
|
|
167
|
+
const bindings = await buildDispatchNetworkBindings({
|
|
168
|
+
deps,
|
|
174
169
|
networkPoolId,
|
|
175
|
-
prepared.authorization.snapshot.tenant_id,
|
|
176
|
-
|
|
177
|
-
const captureBody = outboundFetch
|
|
178
|
-
? await shouldCaptureProviderBodyForRequest(deps.db, prepared.authorization.snapshot.tenant_id, state)
|
|
179
|
-
: false;
|
|
180
|
-
const captureDepth = await resolveCaptureDepthForRequest(
|
|
181
|
-
deps.db,
|
|
182
|
-
prepared.authorization.snapshot.tenant_id,
|
|
170
|
+
tenantId: prepared.authorization.snapshot.tenant_id,
|
|
171
|
+
providerCapture,
|
|
183
172
|
state,
|
|
184
|
-
);
|
|
173
|
+
});
|
|
185
174
|
// Resolved once, before the pipeline exists: the pipeline's diagnostics are
|
|
186
175
|
// gated on it from construction, so `none`/`metadata` tenants never pay the
|
|
187
176
|
// transcript serialization or the client-response copy for any event.
|
|
@@ -190,14 +179,6 @@ export async function dispatchStreamingAttempt(input: StreamingDispatchInput): P
|
|
|
190
179
|
prepared.authorization.snapshot.tenant_id,
|
|
191
180
|
state,
|
|
192
181
|
);
|
|
193
|
-
const wrappedOutboundFetch = outboundFetch
|
|
194
|
-
? captureProviderExchange(
|
|
195
|
-
outboundFetch,
|
|
196
|
-
providerCapture,
|
|
197
|
-
captureBody,
|
|
198
|
-
providerCaptureMaxBytes(captureDepth),
|
|
199
|
-
)
|
|
200
|
-
: undefined;
|
|
201
182
|
// Same memoized ceiling the non-streaming path resolves, read before the
|
|
202
183
|
// dispatch context is built so a stream gets the same limit as a normal
|
|
203
184
|
// request rather than silently falling back to the deployment default.
|
|
@@ -237,18 +218,10 @@ export async function dispatchStreamingAttempt(input: StreamingDispatchInput): P
|
|
|
237
218
|
return text === undefined ? {} : { tenantSystemPrompt: text };
|
|
238
219
|
})()),
|
|
239
220
|
...(candidate.user_agent === undefined ? {} : { userAgent: candidate.user_agent }),
|
|
240
|
-
...
|
|
241
|
-
...(outboundFetch ? { outboundAttachmentFetch: outboundFetch } : {}),
|
|
242
|
-
...(deps.networkBindingFactory
|
|
243
|
-
? {
|
|
244
|
-
outboundHttp2: deps.networkBindingFactory.http2Bidi(
|
|
245
|
-
networkPoolId,
|
|
246
|
-
prepared.authorization.snapshot.tenant_id,
|
|
247
|
-
),
|
|
248
|
-
}
|
|
249
|
-
: {}),
|
|
221
|
+
...bindings,
|
|
250
222
|
...(leaseOwnership === undefined ? {} : { leaseOwnership }),
|
|
251
223
|
...(continuationScope === undefined ? {} : { continuationScope }),
|
|
224
|
+
...(providerCapture ? { providerCapture } : {}),
|
|
252
225
|
});
|
|
253
226
|
// `ProviderAdapter.dispatch` is declared to return a non-nullable
|
|
254
227
|
// `AsyncIterable`, so an adapter that returned `undefined` would be a
|
|
@@ -4,6 +4,7 @@ import { metrics } from "../../observability/metrics";
|
|
|
4
4
|
import { estimatedUsage, nativeCompletionBase, completeAttempt } from "./attempt-finalize";
|
|
5
5
|
import { runAttemptLoop } from "./attempt-loop";
|
|
6
6
|
import {
|
|
7
|
+
forwardUpstreamResponse,
|
|
7
8
|
networkFetchContext,
|
|
8
9
|
requireRecordBody,
|
|
9
10
|
resolveAttemptCredential,
|
|
@@ -164,14 +165,7 @@ export function createSystemoneHandler(deps: SystemoneHandlerDeps) {
|
|
|
164
165
|
responseBody: null,
|
|
165
166
|
});
|
|
166
167
|
metrics.proxy_requests_total.inc(1, { status: "completed" });
|
|
167
|
-
return
|
|
168
|
-
status: response.status,
|
|
169
|
-
headers: {
|
|
170
|
-
"content-type": response.headers.get("content-type") ?? "application/json",
|
|
171
|
-
"cache-control": "no-store",
|
|
172
|
-
"x-request-id": state.requestId,
|
|
173
|
-
},
|
|
174
|
-
});
|
|
168
|
+
return forwardUpstreamResponse(response, state.requestId);
|
|
175
169
|
},
|
|
176
170
|
});
|
|
177
171
|
};
|
|
@@ -43,7 +43,7 @@ import type { PreparedNativeRequest } from "../request/preparer";
|
|
|
43
43
|
import { completeAttempt, estimatedUsage, nativeCompletionBase } from "./attempt-finalize";
|
|
44
44
|
import { finalizeRequestTelemetry } from "../guards/error-lifecycle";
|
|
45
45
|
import { runAttemptLoop } from "./attempt-loop";
|
|
46
|
-
import { hasScope, networkFetchContext, requireRecordBody, resolveAttemptCredential } from "./native-shared";
|
|
46
|
+
import { forwardUpstreamResponse, hasScope, networkFetchContext, requireRecordBody, resolveAttemptCredential } from "./native-shared";
|
|
47
47
|
|
|
48
48
|
import type { GatewayHandlerDeps } from "./handler-deps";
|
|
49
49
|
|
|
@@ -189,11 +189,7 @@ export function createTokenCountHandler(deps: TokenCountHandlerDeps) {
|
|
|
189
189
|
|
|
190
190
|
// Verbatim: the client reads the provider's own number, and the
|
|
191
191
|
// provider's own error when it refuses to produce one.
|
|
192
|
-
|
|
193
|
-
return new Response(upstream.body, {
|
|
194
|
-
status: upstream.status,
|
|
195
|
-
headers: { "content-type": contentType, "cache-control": "no-store", "x-request-id": state.requestId },
|
|
196
|
-
});
|
|
192
|
+
return forwardUpstreamResponse(upstream, state.requestId);
|
|
197
193
|
},
|
|
198
194
|
});
|
|
199
195
|
};
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import type { CanonicalEvent } from "../../protocol/canonical-model";
|
|
2
2
|
import type { AttemptLeaseOwnership, ValidatedOutboundFetch, ValidatedOutboundHttp2Bidi } from "../../providers/provider-contracts";
|
|
3
|
-
import type { ProviderContinuationScope } from "../../providers/provider-contracts";
|
|
3
|
+
import type { ProviderCaptureExchange, ProviderContinuationScope } from "../../providers/provider-contracts";
|
|
4
4
|
import { GATEWAY_SECURITY_HEADERS } from "../../security/outbound-headers";
|
|
5
|
+
import type { ValidatedNetworkBindingFactory } from "../../network/pool/resolver";
|
|
6
|
+
import type { CartethyiaDatabase } from "../../persistence/db-handle";
|
|
7
|
+
import { captureProviderExchange, type ProviderExchangeCapture } from "./attempt-finalize";
|
|
8
|
+
import {
|
|
9
|
+
providerCaptureMaxBytes,
|
|
10
|
+
resolveCaptureDepthForRequest,
|
|
11
|
+
shouldCaptureProviderBodyForRequest,
|
|
12
|
+
type CapturePreferenceState,
|
|
13
|
+
} from "./capture-policy";
|
|
5
14
|
|
|
6
15
|
const FORWARDED_REQUEST_HEADERS = new Set([
|
|
7
16
|
"user-agent",
|
|
@@ -34,6 +43,60 @@ export function proxySuccessHeaders(state: { requestId: string }): Record<string
|
|
|
34
43
|
};
|
|
35
44
|
}
|
|
36
45
|
|
|
46
|
+
/** The three validated egress bindings shared by every dispatch attempt. */
|
|
47
|
+
export interface DispatchNetworkBindings {
|
|
48
|
+
readonly outboundFetch?: ValidatedOutboundFetch;
|
|
49
|
+
readonly outboundAttachmentFetch?: ValidatedOutboundFetch;
|
|
50
|
+
readonly outboundHttp2?: ValidatedOutboundHttp2Bidi;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Builds the outbound network bindings for one attempt's dispatch context,
|
|
55
|
+
* shared by the non-streaming proxy path and the streaming branch.
|
|
56
|
+
*
|
|
57
|
+
* Both previously assembled the same three objects by hand (wrapped fetch,
|
|
58
|
+
* attachment fetch, HTTP/2 bidi), which drifted in the capture wrap. The
|
|
59
|
+
* capture-body gating is preserved exactly: the DB-backed capture-preference
|
|
60
|
+
* read is skipped when no network binding exists (`outboundFetch ? ... :
|
|
61
|
+
* false`), so a request that can never tee a provider exchange does not pay
|
|
62
|
+
* for the preference read. The reads are memoized on the request state, so
|
|
63
|
+
* resolving them here costs no extra DB reads.
|
|
64
|
+
*/
|
|
65
|
+
export async function buildDispatchNetworkBindings(input: {
|
|
66
|
+
readonly deps: {
|
|
67
|
+
readonly networkBindingFactory?: ValidatedNetworkBindingFactory | undefined;
|
|
68
|
+
readonly db: CartethyiaDatabase;
|
|
69
|
+
};
|
|
70
|
+
readonly networkPoolId: string | undefined;
|
|
71
|
+
readonly tenantId: string | null;
|
|
72
|
+
readonly providerCapture: ProviderExchangeCapture;
|
|
73
|
+
readonly state: CapturePreferenceState;
|
|
74
|
+
}): Promise<DispatchNetworkBindings> {
|
|
75
|
+
const factory = input.deps.networkBindingFactory;
|
|
76
|
+
if (factory === undefined) return {};
|
|
77
|
+
const outboundFetch = factory.fetch(input.networkPoolId, input.tenantId ?? undefined);
|
|
78
|
+
const captureBody = await shouldCaptureProviderBodyForRequest(
|
|
79
|
+
input.deps.db,
|
|
80
|
+
input.tenantId,
|
|
81
|
+
input.state,
|
|
82
|
+
);
|
|
83
|
+
const captureDepth = await resolveCaptureDepthForRequest(
|
|
84
|
+
input.deps.db,
|
|
85
|
+
input.tenantId,
|
|
86
|
+
input.state,
|
|
87
|
+
);
|
|
88
|
+
return {
|
|
89
|
+
outboundFetch: captureProviderExchange(
|
|
90
|
+
outboundFetch,
|
|
91
|
+
input.providerCapture,
|
|
92
|
+
captureBody,
|
|
93
|
+
providerCaptureMaxBytes(captureDepth),
|
|
94
|
+
),
|
|
95
|
+
outboundAttachmentFetch: factory.fetch(input.networkPoolId, input.tenantId ?? undefined),
|
|
96
|
+
outboundHttp2: factory.http2Bidi(input.networkPoolId, input.tenantId ?? undefined),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
37
100
|
export function buildUpstreamDispatchContext(input: {
|
|
38
101
|
readonly credential: unknown;
|
|
39
102
|
readonly deadline: number;
|
|
@@ -42,6 +105,8 @@ export function buildUpstreamDispatchContext(input: {
|
|
|
42
105
|
readonly requestId?: string;
|
|
43
106
|
readonly userAgent?: string;
|
|
44
107
|
readonly outboundFetch?: ValidatedOutboundFetch;
|
|
108
|
+
/** Capture sink for provider exchanges not visible to the HTTP fetch wrapper. */
|
|
109
|
+
readonly providerCapture?: ProviderCaptureExchange;
|
|
45
110
|
/** Validated attachment egress that must not be wrapped by provider payload capture. */
|
|
46
111
|
readonly outboundAttachmentFetch?: ValidatedOutboundFetch;
|
|
47
112
|
/** Validated bidirectional HTTP/2 egress bound to the same selected pool. */
|
|
@@ -100,6 +165,7 @@ export function buildUpstreamDispatchContext(input: {
|
|
|
100
165
|
...(userAgent ? { user_agent: userAgent } : {}),
|
|
101
166
|
...(Object.keys(input.headers).length > 0 ? { request_headers: input.headers } : {}),
|
|
102
167
|
...(outboundFetch ? { outbound_fetch: outboundFetch } : {}),
|
|
168
|
+
...(input.providerCapture ? { provider_capture: input.providerCapture } : {}),
|
|
103
169
|
...(input.outboundAttachmentFetch
|
|
104
170
|
? { outbound_attachment_fetch: input.outboundAttachmentFetch }
|
|
105
171
|
: {}),
|
|
@@ -50,14 +50,41 @@ export function tokenMatches(provided: string | null, expected: string): boolean
|
|
|
50
50
|
return a.length === b.length && timingSafeEqual(a, b);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
interface LoopbackAdminGate {
|
|
54
|
+
/** Peer address of the request, resolved by the composition root. */
|
|
55
|
+
readonly peer: string | null;
|
|
56
|
+
/** Shared secret; the route is registered only when this is set. */
|
|
57
|
+
readonly token: string;
|
|
58
|
+
/** `policy_rejected` (403) message, preserved per surface. */
|
|
59
|
+
readonly policyRejectedMessage: string;
|
|
60
|
+
/** `authentication_failed` (401) message, preserved per surface. */
|
|
61
|
+
readonly authenticationFailedMessage: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The shared security gate for host-local admin operations: the request must
|
|
66
|
+
* come from loopback, then it must carry the drain token compared in constant
|
|
67
|
+
* time. The peer check is independent of the secret, so a leaked token alone
|
|
68
|
+
* cannot reach a remote surface. The peer refusal comes first, then the token,
|
|
69
|
+
* with the codes and messages preserved verbatim per caller.
|
|
70
|
+
*/
|
|
71
|
+
export function requireLoopbackAdmin(request: Request, gate: LoopbackAdminGate): void {
|
|
72
|
+
if (!isLoopbackPeer(gate.peer))
|
|
73
|
+
throw new GatewayError("policy_rejected", 403, gate.policyRejectedMessage);
|
|
74
|
+
if (!tokenMatches(request.headers.get("x-drain-token"), gate.token))
|
|
75
|
+
throw new GatewayError("authentication_failed", 401, gate.authenticationFailedMessage);
|
|
76
|
+
}
|
|
77
|
+
|
|
53
78
|
export function createDrainHandler(
|
|
54
79
|
deps: DrainEndpointDeps,
|
|
55
80
|
): (context: { request: Request }) => Promise<Response> {
|
|
56
81
|
return async ({ request }) => {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
82
|
+
requireLoopbackAdmin(request, {
|
|
83
|
+
peer: deps.resolvePeerAddress(request),
|
|
84
|
+
token: deps.token,
|
|
85
|
+
policyRejectedMessage: "drain is only available from loopback (request {requestId}). This operation is only allowed from a trusted loopback connection — no hard feelings 🫂",
|
|
86
|
+
authenticationFailedMessage: "invalid drain token (request {requestId}). Wrong key, right door — check the token 🔑",
|
|
87
|
+
});
|
|
61
88
|
// Acknowledge first, then drain: the response must be on the wire before the
|
|
62
89
|
// shutdown path starts closing sockets, or the caller sees a dropped
|
|
63
90
|
// connection instead of a 202.
|
|
@@ -2,7 +2,6 @@ import { GatewayError, type GatewayErrorCode, type GatewayErrorOrigin } from "..
|
|
|
2
2
|
import { isRecord } from "../protocol/codecs";
|
|
3
3
|
import { resolveFallbackRetryBaseMs, resolveFallbackRetryCapMs } from "../config/timeouts";
|
|
4
4
|
import { parseUpstreamBackoff } from "../protocol/upstream-parsing";
|
|
5
|
-
import { resolveAccountQuotaCooldownMs } from "../config/cooldowns";
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* How much of the upstream's error body is kept and surfaced.
|
|
@@ -14,6 +13,7 @@ import { resolveAccountQuotaCooldownMs } from "../config/cooldowns";
|
|
|
14
13
|
* runs a few KB; 4000 keeps the whole thing while bounding a hostile body.
|
|
15
14
|
*/
|
|
16
15
|
const MAX_UPSTREAM_ERROR_BYTES = 4000;
|
|
16
|
+
export { MAX_UPSTREAM_ERROR_BYTES };
|
|
17
17
|
export {
|
|
18
18
|
parseRetryAfter,
|
|
19
19
|
parseRateLimitReset,
|
|
@@ -22,6 +22,32 @@ export {
|
|
|
22
22
|
parseProviderResetDuration,
|
|
23
23
|
parseAbsoluteResetTimestamp,
|
|
24
24
|
} from "../protocol/upstream-parsing";
|
|
25
|
+
/**
|
|
26
|
+
* Extracts only the human-readable text from an upstream error body —
|
|
27
|
+
* `message`/`msg`/`error` fields — deliberately skipping `code`/identifier
|
|
28
|
+
* values. Used by `upstreamMessageAsIs` to decide whether the upstream
|
|
29
|
+
* actually supplied a message: a symbolic code like `"permission_error"` is
|
|
30
|
+
* an identifier, not prose, so it must not count as passthrough text.
|
|
31
|
+
*/
|
|
32
|
+
export function extractUpstreamMessageText(body: unknown): string {
|
|
33
|
+
let raw = "";
|
|
34
|
+
if (typeof body === "string") raw = body;
|
|
35
|
+
else if (isRecord(body)) {
|
|
36
|
+
if (typeof body.error === "string" && body.error.trim()) raw = body.error;
|
|
37
|
+
else {
|
|
38
|
+
const envelope = isRecord(body.error) ? body.error : body;
|
|
39
|
+
const message = (envelope as Record<string, unknown>).message;
|
|
40
|
+
if (typeof message === "string" && message.trim()) raw = message;
|
|
41
|
+
else if (typeof body.msg === "string" && body.msg.trim()) raw = body.msg;
|
|
42
|
+
else if (typeof (envelope as Record<string, unknown>).msg === "string" && String((envelope as Record<string, unknown>).msg).trim())
|
|
43
|
+
raw = String((envelope as Record<string, unknown>).msg);
|
|
44
|
+
else if (typeof (envelope as Record<string, unknown>).error === "string" && String((envelope as Record<string, unknown>).error).trim())
|
|
45
|
+
raw = String((envelope as Record<string, unknown>).error);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return raw.replace(/[\r\n]+/g, " ").trim();
|
|
49
|
+
}
|
|
50
|
+
|
|
25
51
|
/** Extracts a bounded, newline-free provider message from any error envelope. */
|
|
26
52
|
export function extractUpstreamMessage(body: unknown): string {
|
|
27
53
|
let raw = "";
|
|
@@ -233,31 +259,6 @@ export function classifyUpstreamFailure(error: unknown): UpstreamFailurePolicy {
|
|
|
233
259
|
// DNS failure. Failing the client request instead of failing over is the wrong
|
|
234
260
|
// outcome for a whole class the table already names as platform-wide.
|
|
235
261
|
const statusCode = error.status;
|
|
236
|
-
// Qoder answers an exhausted quota by closing the stream with an upstream 499
|
|
237
|
-
// (nginx client-close style). That is a refusal from the upstream — the
|
|
238
|
-
// gateway's own aborts are `transport_closed`, produced locally, and never
|
|
239
|
-
// carry a qoder providerId — so a 499 scoped to qoder is quota, not a
|
|
240
|
-
// malformed request. Without this it fell through to `invalid_request`
|
|
241
|
-
// (non-retryable, no cooldown) and the account was re-dialled on every
|
|
242
|
-
// request inside the exhausted window until a late 429 finally classified it.
|
|
243
|
-
if (
|
|
244
|
-
error.origin === "upstream" &&
|
|
245
|
-
statusCode === 499 &&
|
|
246
|
-
(providerId === "qoder" || providerId === "qodercn")
|
|
247
|
-
) {
|
|
248
|
-
const cooldownMs = resolveAccountQuotaCooldownMs();
|
|
249
|
-
return {
|
|
250
|
-
retryable: true,
|
|
251
|
-
mutatesAccount: true,
|
|
252
|
-
category: "quota_exceeded",
|
|
253
|
-
scope: "account",
|
|
254
|
-
origin: error.origin,
|
|
255
|
-
...(providerCode ? { providerCode } : {}),
|
|
256
|
-
...(providerId ? { providerId } : {}),
|
|
257
|
-
cooldownMs,
|
|
258
|
-
statusCode,
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
262
|
const retryableByStatus =
|
|
262
263
|
statusCode === 401 ||
|
|
263
264
|
statusCode === 403 ||
|
|
@@ -657,10 +658,8 @@ const QUOTA_ERROR_IDENTIFIERS: ReadonlySet<string> = new Set([
|
|
|
657
658
|
// quota (a cooldown) instead of `auth_invalidated`.
|
|
658
659
|
"110",
|
|
659
660
|
// Qoder also reports quota exhaustion as `{"code":116,"error":"quota
|
|
660
|
-
// exceeded"}
|
|
661
|
-
//
|
|
662
|
-
// naming the code here makes any status that carries it classify as quota
|
|
663
|
-
// instead of `invalid_request`/`authentication_failed`.
|
|
661
|
+
// exceeded"}`. Naming the code here makes any status that carries it classify
|
|
662
|
+
// as quota instead of `invalid_request`/`authentication_failed`.
|
|
664
663
|
"116",
|
|
665
664
|
// InferHub publishes its own vocabulary in `error.code`. The two 403s below
|
|
666
665
|
// are deliberate absences: `account_blocked` means the account was blocked
|
|
@@ -696,6 +695,25 @@ const AUTH_ERROR_IDENTIFIERS: ReadonlySet<string> = new Set([
|
|
|
696
695
|
"insufficient_key_scope",
|
|
697
696
|
"model_not_allowed_for_key",
|
|
698
697
|
]);
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Qoder job-token verdicts that are NOT credential evidence.
|
|
701
|
+
*
|
|
702
|
+
* A PAT (`pt-…`) is exchanged for a short-lived job token (`jt-…`). When the
|
|
703
|
+
* job token expires mid-flight the upstream answers `{"code":"105","message":
|
|
704
|
+
* "Login expired"}` (403). The PAT is still valid — re-exchanging it mints a
|
|
705
|
+
* fresh job token — so treating this as `auth_invalidated` parked a healthy
|
|
706
|
+
* account as "Re-login required". It stays `authentication_failed` (retryable;
|
|
707
|
+
* the next attempt re-runs the PAT exchange) but without `credentialEvidence`,
|
|
708
|
+
* so the account is never disabled or cooled on it.
|
|
709
|
+
*/
|
|
710
|
+
const QODER_JOB_TOKEN_EXPIRED: ReadonlySet<string> = new Set([
|
|
711
|
+
"105",
|
|
712
|
+
"login expired",
|
|
713
|
+
"login_expired",
|
|
714
|
+
"job token expired",
|
|
715
|
+
"jobtoken expired",
|
|
716
|
+
]);
|
|
699
717
|
const PLATFORM_ERROR_IDENTIFIERS: ReadonlySet<string> = new Set([
|
|
700
718
|
"server_error",
|
|
701
719
|
"internal_error",
|
|
@@ -800,6 +818,20 @@ export function classifyUpstreamError(
|
|
|
800
818
|
return { code: "proxy_auth_required", status: 407, origin: "network" };
|
|
801
819
|
if (normalized === "11140")
|
|
802
820
|
return { code: "policy_rejected", status: 403, origin: "upstream" };
|
|
821
|
+
// ZCode plan-lane envelope codes (verified live 2026-10-30 by the donor and
|
|
822
|
+
// mirrored by the zcode-start provider). `3009` is model concurrency
|
|
823
|
+
// (transient, retried in place), `3008` user concurrency (the desktop client
|
|
824
|
+
// is busy on that account — switchable), `3012` is the identity/prompt-shape
|
|
825
|
+
// gate, `1113` the paid-lane "insufficient balance or no resource package"
|
|
826
|
+
// (a request that was not recognized as start-plan traffic).
|
|
827
|
+
if (normalized === "3009")
|
|
828
|
+
return { code: "rate_limited", status: 429, origin: "upstream" };
|
|
829
|
+
if (normalized === "3008")
|
|
830
|
+
return { code: "accounts_rate_limited", status: 429, origin: "upstream" };
|
|
831
|
+
if (normalized === "3012")
|
|
832
|
+
return { code: "policy_rejected", status: 405, origin: "upstream" };
|
|
833
|
+
if (normalized === "1113")
|
|
834
|
+
return { code: "quota_exceeded", status: 429, origin: "upstream", rateLimitScope: "provider" };
|
|
803
835
|
if (normalized !== undefined && CONTEXT_LENGTH_CODES.has(normalized))
|
|
804
836
|
return { code: "context_length_exceeded", status: 413, origin: "upstream" };
|
|
805
837
|
if (normalized !== undefined && QUOTA_ERROR_IDENTIFIERS.has(normalized))
|
|
@@ -809,6 +841,16 @@ export function classifyUpstreamError(
|
|
|
809
841
|
origin: "upstream",
|
|
810
842
|
rateLimitScope: "provider",
|
|
811
843
|
};
|
|
844
|
+
// Qoder job-token expiry (`105` / "Login expired") is recoverable by
|
|
845
|
+
// re-exchanging the PAT — never credential evidence (see
|
|
846
|
+
// `QODER_JOB_TOKEN_EXPIRED`).
|
|
847
|
+
if (normalized !== undefined && QODER_JOB_TOKEN_EXPIRED.has(normalized)) {
|
|
848
|
+
return {
|
|
849
|
+
code: "authentication_failed",
|
|
850
|
+
status: status === 403 || status === 401 ? status : 403,
|
|
851
|
+
origin: "upstream",
|
|
852
|
+
};
|
|
853
|
+
}
|
|
812
854
|
if (normalized !== undefined && AUTH_ERROR_IDENTIFIERS.has(normalized)) {
|
|
813
855
|
let authStatus: number;
|
|
814
856
|
if (status === 401 || status === 403) authStatus = status;
|
|
@@ -919,17 +961,15 @@ export async function mapUpstreamHttpError(response: Response, providerId: strin
|
|
|
919
961
|
: isContextLengthFailure(parsedBody)
|
|
920
962
|
? { code: "context_length_exceeded" as const, status: 413, origin: "upstream" as const }
|
|
921
963
|
: classifyUpstreamError(response.status, identifier);
|
|
922
|
-
const
|
|
923
|
-
let message =
|
|
924
|
-
extracted.length > 0
|
|
925
|
-
? extracted
|
|
926
|
-
: text.trim() || `Provider returned HTTP ${response.status}.`;
|
|
964
|
+
const upstreamText = upstreamMessageAsIs(parsedBody ?? text, "");
|
|
965
|
+
let message = upstreamText.message || `Provider returned HTTP ${response.status}.`;
|
|
927
966
|
// A bare "Provider returned HTTP <5xx>" tells the operator nothing actionable;
|
|
928
967
|
// platform-unavailable fallbacks get the same human sign-off every other
|
|
929
|
-
// gateway error carries
|
|
930
|
-
// diagnostics either way.
|
|
968
|
+
// gateway error carries — but only when the upstream supplied no text of its
|
|
969
|
+
// own. The raw upstream body stays in `details.raw` for diagnostics either way.
|
|
931
970
|
if (
|
|
932
971
|
classification.code === "platform_unavailable" &&
|
|
972
|
+
!upstreamText.hadUpstreamText &&
|
|
933
973
|
message === `Provider returned HTTP ${response.status}.`
|
|
934
974
|
) {
|
|
935
975
|
message = `The provider's service is temporarily unavailable (HTTP ${response.status}) (request {requestId}). Your request did not go through — wait a moment and try again. Hold tight ☁️`;
|
|
@@ -949,6 +989,7 @@ export async function mapUpstreamHttpError(response: Response, providerId: strin
|
|
|
949
989
|
...(reportedProviderCode ? { providerCode: reportedProviderCode } : {}),
|
|
950
990
|
...(requestId ? { upstreamRequestId: requestId } : {}),
|
|
951
991
|
raw: text.slice(0, MAX_UPSTREAM_ERROR_BYTES),
|
|
992
|
+
...(upstreamText.hadUpstreamText ? { upstreamMessageAsIs: true } : {}),
|
|
952
993
|
...(retryAfterMs === null || retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
953
994
|
...(queuedWaitMs !== undefined
|
|
954
995
|
? { reason: "upstream-queued", cooldownMs: queuedWaitMs }
|
|
@@ -971,6 +1012,57 @@ export function extractHtmlTitle(html: string): string {
|
|
|
971
1012
|
return html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim().slice(0, 160);
|
|
972
1013
|
}
|
|
973
1014
|
|
|
1015
|
+
/** The public bound on an upstream message surface. */
|
|
1016
|
+
export const MAX_UPSTREAM_MESSAGE_LENGTH = 500;
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* Extracts the upstream's own text from an error body, "as-is".
|
|
1020
|
+
*
|
|
1021
|
+
* The upstream error passthrough contract: when a failure is unknown or
|
|
1022
|
+
* unparseable, the client should see whatever the upstream actually said,
|
|
1023
|
+
* not a generalized 502 sentence. Structured JSON envelopes already flow
|
|
1024
|
+
* through `extractUpstreamMessage`; this helper extends the same idea to the
|
|
1025
|
+
* shapes that used to lose the text entirely — HTML error pages (their
|
|
1026
|
+
* `<title>` is often the only readable thing) and plain-text bodies.
|
|
1027
|
+
*
|
|
1028
|
+
* The returned message is bounded to 500 chars (D44) and newline-collapsed,
|
|
1029
|
+
* matching `sanitizePublicDetail` per-string bound; `details.raw` keeps the
|
|
1030
|
+
* full 4000. `hadUpstreamText` distinguishes "the upstream said something"
|
|
1031
|
+
* from "we synthesized a fallback", so callers can keep their human sign-off
|
|
1032
|
+
* only when there is genuinely no upstream text.
|
|
1033
|
+
*/
|
|
1034
|
+
export function upstreamMessageAsIs(
|
|
1035
|
+
body: unknown,
|
|
1036
|
+
fallback: string,
|
|
1037
|
+
): { message: string; hadUpstreamText: boolean } {
|
|
1038
|
+
const raw = typeof body === "string" ? body : "";
|
|
1039
|
+
const looksLikeHtml = raw.includes("<") && raw.includes(">");
|
|
1040
|
+
// HTML error pages first: the structured extractor passes a string through
|
|
1041
|
+
// verbatim, which would surface raw markup instead of the readable `<title>`
|
|
1042
|
+
// (or a tag-stripped excerpt when the page has no title).
|
|
1043
|
+
if (looksLikeHtml) {
|
|
1044
|
+
const title = extractHtmlTitle(raw);
|
|
1045
|
+
if (title.length > 0) {
|
|
1046
|
+
return {
|
|
1047
|
+
message: title.replace(/[\r\n]+/g, " ").trim().slice(0, MAX_UPSTREAM_MESSAGE_LENGTH),
|
|
1048
|
+
hadUpstreamText: true,
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
const structured = extractUpstreamMessageText(body);
|
|
1053
|
+
if (structured.length > 0) {
|
|
1054
|
+
return { message: structured.slice(0, MAX_UPSTREAM_MESSAGE_LENGTH), hadUpstreamText: true };
|
|
1055
|
+
}
|
|
1056
|
+
const text = raw.trim();
|
|
1057
|
+
if (text.length > 0) {
|
|
1058
|
+
return {
|
|
1059
|
+
message: text.replace(/[\r\n]+/g, " ").trim().slice(0, MAX_UPSTREAM_MESSAGE_LENGTH),
|
|
1060
|
+
hadUpstreamText: true,
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
return { message: fallback, hadUpstreamText: false };
|
|
1064
|
+
}
|
|
1065
|
+
|
|
974
1066
|
/** Throws a typed transport error when an upstream returns an HTML error page. */
|
|
975
1067
|
export async function throwIfHtmlResponse(response: Response): Promise<void> {
|
|
976
1068
|
const contentType = response.headers.get("content-type") ?? "";
|
|
@@ -58,8 +58,14 @@ export function isProxyDispatchRoute(path: string | undefined): path is string {
|
|
|
58
58
|
return path !== undefined && isJsonProxyRoute(path);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* The same canonical JSON-route membership as `isProxyDispatchRoute`, kept as
|
|
63
|
+
* a non-narrowing alias for callers that already hold a string path. One
|
|
64
|
+
* predicate, two spellings, so a route-table change cannot diverge between
|
|
65
|
+
* the body-read policy and the canonical-parse gate.
|
|
66
|
+
*/
|
|
61
67
|
export function isJsonProxyRoutePath(path: string): boolean {
|
|
62
|
-
return
|
|
68
|
+
return isProxyDispatchRoute(path);
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
interface IngressPolicyOptions {
|
|
@@ -77,8 +77,20 @@ export function createErrorNormalizationMiddleware(deps: {
|
|
|
77
77
|
.error(({ request, error, set }) => {
|
|
78
78
|
const state = deps.stateStore.get(request as Request);
|
|
79
79
|
const gateway = error instanceof GatewayError ? error : undefined;
|
|
80
|
+
// Elysia's built-in errors (NotFound, ValidationError, ParseError,
|
|
81
|
+
// InternalServerError) always carry a numeric `status` **and** a code.
|
|
82
|
+
// A raw runtime error can carry a `.code` on its own — Bun's fetch
|
|
83
|
+
// surfaces `TypeError` with `code: "ECONNRESET"` when an upstream socket
|
|
84
|
+
// dies mid-response — so a `code` alone is not evidence of an Elysia
|
|
85
|
+
// built-in. Requiring the numeric status keeps that raw error from
|
|
86
|
+
// leaking into the public envelope as a fake code/400; the dispatch loop
|
|
87
|
+
// already normalizes socket drops to `transport_unavailable`/502 before
|
|
88
|
+
// they reach here.
|
|
80
89
|
const builtin =
|
|
81
|
-
!gateway &&
|
|
90
|
+
!gateway &&
|
|
91
|
+
typeof error === "object" &&
|
|
92
|
+
error !== null &&
|
|
93
|
+
typeof (error as ElysiaBuiltinError).status === "number"
|
|
82
94
|
? (error as ElysiaBuiltinError)
|
|
83
95
|
: undefined;
|
|
84
96
|
const builtinStatus = typeof builtin?.status === "number" ? builtin.status : undefined;
|
|
@@ -12,7 +12,7 @@ import { resolveClientIdentity } from "../../security/ip-boundary";
|
|
|
12
12
|
import { GATEWAY_SECURITY_HEADERS } from "../../security/outbound-headers";
|
|
13
13
|
import { pushStructuredConsoleLog } from "../../observability/log-ring";
|
|
14
14
|
import { log } from "../../observability/logger";
|
|
15
|
-
import {
|
|
15
|
+
import { isProxyDispatchRoute } from "./body-policy";
|
|
16
16
|
import { isOpaqueBodyRoute } from "../contracts";
|
|
17
17
|
import { peerAddressUnavailableError } from "./guard-errors";
|
|
18
18
|
|
|
@@ -24,6 +24,24 @@ interface RequestApp {
|
|
|
24
24
|
): RequestApp;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Whether this request falls outside the canonical JSON-proxy pipeline the
|
|
29
|
+
* canonical-parse and route-preparation stages serve.
|
|
30
|
+
*
|
|
31
|
+
* Shared gate, applied in the exact order both middlewares historically
|
|
32
|
+
* checked: non-`/v1/` paths, GET/HEAD (which carry no body to parse), then
|
|
33
|
+
* the JSON route table with opaque-body routes excluded — `/v1/completions`
|
|
34
|
+
* needs no separate arm because it is already in the JSON routes table, and
|
|
35
|
+
* native routes (compact, System One, token count) read a body but are never
|
|
36
|
+
* parsed into a canonical request, their bodies being opaque by contract.
|
|
37
|
+
*/
|
|
38
|
+
function skipNonCanonicalProxyRequest(request: Request): boolean {
|
|
39
|
+
const path = fastPathname(request.url);
|
|
40
|
+
if (!path.startsWith("/v1/")) return true;
|
|
41
|
+
if (request.method === "GET" || request.method === "HEAD") return true;
|
|
42
|
+
return !isProxyDispatchRoute(path) || isOpaqueBodyRoute(path);
|
|
43
|
+
}
|
|
44
|
+
|
|
27
45
|
interface RequestContextDeps {
|
|
28
46
|
readonly stateStore: ProxyRequestStateStore;
|
|
29
47
|
readonly clock?: () => number;
|
|
@@ -141,15 +159,9 @@ export function createCanonicalRequestMiddleware(deps: {
|
|
|
141
159
|
}): Elysia {
|
|
142
160
|
return new Elysia()
|
|
143
161
|
.beforeHandle(async ({ request }) => {
|
|
144
|
-
const path = fastPathname(request.url);
|
|
145
|
-
if (!path.startsWith("/v1/")) return;
|
|
146
|
-
if (request.method === "GET" || request.method === "HEAD") return;
|
|
147
|
-
// `/v1/completions` needs no separate arm: it is already in
|
|
148
|
-
// the JSON routes table, so the body-policy predicate covers it.
|
|
149
|
-
// Native routes (compact, System One) read a body but are never parsed
|
|
150
|
-
// into a canonical request — their bodies are opaque by contract.
|
|
151
|
-
if (!isJsonProxyRoutePath(path) || isOpaqueBodyRoute(path)) return;
|
|
152
162
|
// Model discovery and other GET/multimodal routes must not go through canonical parsing.
|
|
163
|
+
if (skipNonCanonicalProxyRequest(request)) return;
|
|
164
|
+
const path = fastPathname(request.url);
|
|
153
165
|
const state = deps.stateStore.require(request);
|
|
154
166
|
// Single-read invariant: the ingress policy middleware already decoded the
|
|
155
167
|
// body once into `state.ingressBody`. Never re-read `request.body` here —
|
|
@@ -216,11 +228,7 @@ export function createProxyRoutePreparationMiddleware(deps: {
|
|
|
216
228
|
}): Elysia {
|
|
217
229
|
return new Elysia()
|
|
218
230
|
.beforeHandle(async ({ request }) => {
|
|
219
|
-
|
|
220
|
-
if (!path.startsWith("/v1/")) return;
|
|
221
|
-
if (request.method === "GET" || request.method === "HEAD") return;
|
|
222
|
-
const isJsonRoute = isJsonProxyRoutePath(path);
|
|
223
|
-
if (!isJsonRoute || isOpaqueBodyRoute(path)) return;
|
|
231
|
+
if (skipNonCanonicalProxyRequest(request)) return;
|
|
224
232
|
const state = deps.stateStore.require(request);
|
|
225
233
|
if (!state.authorization || !state.canonicalRequest)
|
|
226
234
|
throw new GatewayError("route_unavailable", 503, "The request was not authenticated and parsed, so this route cannot be served (request {requestId}). Resend the request. The bouncer stepped out — resend shortly 🚪");
|