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
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validated WebSocket egress — WSS session + the dial that opens one.
|
|
3
|
+
*
|
|
4
|
+
* The dial resolves the target through the SSRF policy (done by the caller,
|
|
5
|
+
* the binding factory) and then either connects through the selected pool
|
|
6
|
+
* agent's tunnel or dials direct with pinned, SSRF-validated address.
|
|
7
|
+
*/
|
|
8
|
+
import * as tls from "node:tls";
|
|
9
|
+
import WebSocket, { type ClientOptions, type RawData } from "ws";
|
|
10
|
+
import { GatewayError } from "../../protocol/gateway-error";
|
|
11
|
+
import { isProxyAgentPair, type PoolAgent } from "../pool/agent";
|
|
12
|
+
import { Agent as HttpsAgent } from "node:https";
|
|
13
|
+
import type { ProviderWebSocketSession } from "../../providers/provider-contracts";
|
|
14
|
+
import type { ValidatedDestination } from "../ssrf";
|
|
15
|
+
|
|
16
|
+
class WsSession implements ProviderWebSocketSession {
|
|
17
|
+
readonly #socket: WebSocket;
|
|
18
|
+
readonly #signal: AbortSignal;
|
|
19
|
+
readonly #messages: string[] = [];
|
|
20
|
+
readonly #waiters: Array<{
|
|
21
|
+
resolve(value: string): void;
|
|
22
|
+
reject(reason: Error): void;
|
|
23
|
+
cleanup(): void;
|
|
24
|
+
}> = [];
|
|
25
|
+
#closed: Error | undefined;
|
|
26
|
+
|
|
27
|
+
constructor(socket: WebSocket, signal: AbortSignal) {
|
|
28
|
+
this.#socket = socket;
|
|
29
|
+
this.#signal = signal;
|
|
30
|
+
socket.on("message", (data: RawData, isBinary: boolean) => {
|
|
31
|
+
if (isBinary) {
|
|
32
|
+
this.#fail(new GatewayError("transport_unavailable", 502, "unexpected binary WebSocket frame (request {requestId}). The connection ghosted us — come back in a moment 👻"));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const message = data.toString();
|
|
36
|
+
const waiter = this.#waiters.shift();
|
|
37
|
+
if (waiter) {
|
|
38
|
+
waiter.cleanup();
|
|
39
|
+
waiter.resolve(message);
|
|
40
|
+
} else {
|
|
41
|
+
this.#messages.push(message);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
socket.on("close", () => this.#fail(new GatewayError("transport_closed", 502, "WebSocket connection closed (request {requestId}). The connection ghosted us — come back in a moment 👻")));
|
|
45
|
+
socket.on("error", () => this.#fail(new GatewayError("transport_unavailable", 502, "WebSocket connection failed (request {requestId}). The connection ghosted us — come back in a moment 👻")));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
send(message: string): void {
|
|
49
|
+
if (this.#closed) throw this.#closed;
|
|
50
|
+
if (this.#socket.readyState !== WebSocket.OPEN) {
|
|
51
|
+
throw new GatewayError("transport_closed", 499, "WebSocket connection is not open (request {requestId}). Send it again whenever ready 🙃");
|
|
52
|
+
}
|
|
53
|
+
this.#socket.send(message, { binary: false });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
receive(signal: AbortSignal): Promise<string> {
|
|
57
|
+
const queued = this.#messages.shift();
|
|
58
|
+
if (queued !== undefined) return Promise.resolve(queued);
|
|
59
|
+
if (this.#closed) return Promise.reject(this.#closed);
|
|
60
|
+
const combined = AbortSignal.any([this.#signal, signal]);
|
|
61
|
+
return new Promise<string>((resolve, reject) => {
|
|
62
|
+
const timeout = setTimeout(() => {
|
|
63
|
+
cleanup();
|
|
64
|
+
reject(new GatewayError("transport_unavailable", 504, "WebSocket receive timed out (request {requestId}). Some things just take longer 🐢"));
|
|
65
|
+
}, 45_000);
|
|
66
|
+
const onAbort = (): void => {
|
|
67
|
+
cleanup();
|
|
68
|
+
reject(new GatewayError("transport_closed", 499, "WebSocket receive was cancelled (request {requestId}). Send it again whenever ready 🙃"));
|
|
69
|
+
};
|
|
70
|
+
const cleanup = (): void => {
|
|
71
|
+
clearTimeout(timeout);
|
|
72
|
+
combined.removeEventListener("abort", onAbort);
|
|
73
|
+
};
|
|
74
|
+
combined.addEventListener("abort", onAbort, { once: true });
|
|
75
|
+
this.#waiters.push({ resolve, reject, cleanup });
|
|
76
|
+
if (combined.aborted) onAbort();
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
close(code?: number, reason?: string): void {
|
|
81
|
+
if (this.#socket.readyState === WebSocket.CLOSED) return;
|
|
82
|
+
this.#socket.close(code, reason);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#fail(error: Error): void {
|
|
86
|
+
if (this.#closed) return;
|
|
87
|
+
this.#closed = error;
|
|
88
|
+
for (const waiter of this.#waiters.splice(0)) {
|
|
89
|
+
waiter.cleanup();
|
|
90
|
+
waiter.reject(error);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function connectValidatedWebSocket(
|
|
96
|
+
url: URL,
|
|
97
|
+
headers: Readonly<Record<string, string>>,
|
|
98
|
+
signal: AbortSignal,
|
|
99
|
+
destination: ValidatedDestination,
|
|
100
|
+
agent?: PoolAgent,
|
|
101
|
+
): Promise<ProviderWebSocketSession> {
|
|
102
|
+
const targetUrl = new URL(url.toString());
|
|
103
|
+
targetUrl.protocol = "wss:";
|
|
104
|
+
targetUrl.port = "";
|
|
105
|
+
targetUrl.hash = "";
|
|
106
|
+
const connectionOptions: ClientOptions = {
|
|
107
|
+
handshakeTimeout: 15_000,
|
|
108
|
+
perMessageDeflate: false,
|
|
109
|
+
followRedirects: false,
|
|
110
|
+
headers: { ...headers },
|
|
111
|
+
...(agent === undefined ? {} : { agent: agentForWebSocket(agent) }),
|
|
112
|
+
...(agent === undefined
|
|
113
|
+
? {
|
|
114
|
+
createConnection: () => {
|
|
115
|
+
const socket = tls.connect({
|
|
116
|
+
host: destination.resolvedAddress,
|
|
117
|
+
port: 443,
|
|
118
|
+
servername: destination.hostname,
|
|
119
|
+
rejectUnauthorized: true,
|
|
120
|
+
ALPNProtocols: ["http/1.1"],
|
|
121
|
+
});
|
|
122
|
+
const onAbort = (): void => {
|
|
123
|
+
socket.destroy(new Error("connection aborted"));
|
|
124
|
+
};
|
|
125
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
126
|
+
socket.once("secureConnect", () => signal.removeEventListener("abort", onAbort));
|
|
127
|
+
return socket;
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
: {}),
|
|
131
|
+
};
|
|
132
|
+
return new Promise<ProviderWebSocketSession>((resolve, reject) => {
|
|
133
|
+
const socket = new WebSocket(targetUrl, connectionOptions);
|
|
134
|
+
let settled = false;
|
|
135
|
+
const timeout = setTimeout(() => {
|
|
136
|
+
if (settled) return;
|
|
137
|
+
settled = true;
|
|
138
|
+
socket.terminate();
|
|
139
|
+
reject(new GatewayError("transport_unavailable", 502, "WebSocket handshake timed out (request {requestId}). Some things just take longer 🐢"));
|
|
140
|
+
}, 15_000);
|
|
141
|
+
const abort = (): void => {
|
|
142
|
+
if (settled) return;
|
|
143
|
+
settled = true;
|
|
144
|
+
clearTimeout(timeout);
|
|
145
|
+
socket.terminate();
|
|
146
|
+
reject(new GatewayError("transport_closed", 499, "WebSocket connection was cancelled (request {requestId}). Send it again whenever ready 🙃"));
|
|
147
|
+
};
|
|
148
|
+
const onError = (): void => {
|
|
149
|
+
if (settled) return;
|
|
150
|
+
settled = true;
|
|
151
|
+
clearTimeout(timeout);
|
|
152
|
+
signal.removeEventListener("abort", abort);
|
|
153
|
+
reject(new GatewayError("transport_unavailable", 502, "WebSocket connection failed (request {requestId}). The connection ghosted us — come back in a moment 👻"));
|
|
154
|
+
};
|
|
155
|
+
socket.once("open", () => {
|
|
156
|
+
if (settled) return;
|
|
157
|
+
settled = true;
|
|
158
|
+
clearTimeout(timeout);
|
|
159
|
+
signal.removeEventListener("abort", abort);
|
|
160
|
+
resolve(new WsSession(socket, signal));
|
|
161
|
+
});
|
|
162
|
+
socket.once("error", onError);
|
|
163
|
+
socket.once("unexpected-response", onError);
|
|
164
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
165
|
+
if (signal.aborted) abort();
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function agentForWebSocket(agent: PoolAgent): import("http").Agent | import("https").Agent {
|
|
170
|
+
if (isProxyAgentPair(agent)) return agent.https;
|
|
171
|
+
if (agent instanceof HttpsAgent) return agent;
|
|
172
|
+
throw new GatewayError("proxy_pool_unhealthy", 503, "configured WebSocket proxy pool does not support WSS (request {requestId}). This bridge doesn't speak WSS — reconfigure the pool 🔧");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export { connectValidatedWebSocket, WsSession };
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
* byte, and nothing is allocated.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import { evictOldestEntry } from "../../transport/routing/bounded-lru";
|
|
17
|
+
|
|
16
18
|
/** Running totals for one pool. */
|
|
17
19
|
interface PoolByteTotals {
|
|
18
20
|
/** Bytes sent to the proxy (uploads, TLS handshake, CONNECT). */
|
|
@@ -33,8 +35,7 @@ function entry(poolId: string): { sent: number; received: number } {
|
|
|
33
35
|
let current = totals.get(poolId);
|
|
34
36
|
if (!current) {
|
|
35
37
|
if (totals.size >= MAX_BYTE_ENTRY_COUNT) {
|
|
36
|
-
|
|
37
|
-
if (oldest !== undefined) totals.delete(oldest);
|
|
38
|
+
evictOldestEntry(totals, MAX_BYTE_ENTRY_COUNT - 1);
|
|
38
39
|
}
|
|
39
40
|
current = { sent: 0, received: 0 };
|
|
40
41
|
totals.set(poolId, current);
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* and the agent resolver, so a malformed row fails loudly instead of producing
|
|
12
12
|
* a silently-wrong agent.
|
|
13
13
|
*/
|
|
14
|
-
import { isRecord } from "
|
|
14
|
+
import { isRecord } from "../../protocol/codecs";
|
|
15
15
|
|
|
16
16
|
/** Thrown when a stored/requested pool config cannot be decoded. */
|
|
17
17
|
export class ProxyConfigError extends Error {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* caches instances of these; the selector owns admission.
|
|
5
5
|
*/
|
|
6
6
|
import { isIP } from "node:net";
|
|
7
|
-
import { accountSocketBytes, accountSocketWrites, recordPoolBytes } from "./
|
|
7
|
+
import { accountSocketBytes, accountSocketWrites, recordPoolBytes } from "./accounting";
|
|
8
8
|
import {
|
|
9
9
|
Agent as HttpAgent,
|
|
10
10
|
request as httpRequest,
|
|
@@ -19,7 +19,7 @@ import { SocksProxyAgent } from "socks-proxy-agent";
|
|
|
19
19
|
import { resolveAllAddresses, validateResolvedAddresses } from "../ssrf";
|
|
20
20
|
import type { SsrfPolicy } from "../../config/network";
|
|
21
21
|
import { resolveProxyMaxFreeSockets, resolveProxyMaxSockets } from "../../config/network";
|
|
22
|
-
import type { AgentConfig } from "
|
|
22
|
+
import type { AgentConfig } from "./agent-config";
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Idle keep-alive socket timeout for every pool agent. Matches the 60s DB pool
|
|
@@ -14,12 +14,13 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { randomUUID } from "node:crypto";
|
|
16
16
|
import { eq } from "drizzle-orm";
|
|
17
|
-
import type { CartethyiaDatabase } from "
|
|
18
|
-
import { networkPools, healthEvents } from "
|
|
19
|
-
import { log } from "
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
17
|
+
import type { CartethyiaDatabase } from "../../persistence/db-handle";
|
|
18
|
+
import { networkPools, healthEvents } from "../../persistence/schema";
|
|
19
|
+
import { log } from "../../observability/logger";
|
|
20
|
+
import { metrics } from "../../observability/metrics";
|
|
21
|
+
import { providerRateLimitIsIpScoped } from "../../providers/provider-metadata";
|
|
22
|
+
import { resolvePoolProviderRateLimitCooldownMs } from "../../config/cooldowns";
|
|
23
|
+
import { PROVIDER_BLOCK_MS, type ProxyCooldownEntry } from "./selector";
|
|
23
24
|
|
|
24
25
|
type PoolProviderCooldownMap = NonNullable<
|
|
25
26
|
(typeof networkPools.$inferSelect)["providerCooldowns"]
|
|
@@ -152,6 +153,88 @@ export async function pruneExpiredProviderCooldownMarkers(
|
|
|
152
153
|
}
|
|
153
154
|
}
|
|
154
155
|
|
|
156
|
+
/** Max simultaneously tracked (pool, provider) coalescing keys. */
|
|
157
|
+
const MAX_TRACKED_PAIRS = 5_000;
|
|
158
|
+
|
|
159
|
+
/** Last coalesced row timestamp per (pool, provider), for the cooldown window only. */
|
|
160
|
+
const lastRecordedByPoolProvider = new Map<string, number>();
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Appends one provider-scoped egress verdict row to `health_events` — the
|
|
164
|
+
* durable audit log the console's pool page reads.
|
|
165
|
+
*
|
|
166
|
+
* Shared by the three writers of this row shape: the dispatch-side 429
|
|
167
|
+
* recorder (`recordPoolCooldownEvent`), the egress-block recorder
|
|
168
|
+
* (`recordPoolBlockEvent`), and the probe verdict recorder
|
|
169
|
+
* (`recordProbeVerdict`). The row is always `entityKind: "pool"`,
|
|
170
|
+
* `fromStatus: null`, `toStatus: "cooldown"`, and the category is chosen by
|
|
171
|
+
* `blocked`; the only divergences are the optional code suffix (block path)
|
|
172
|
+
* and the optional coalescing window (cooldown path).
|
|
173
|
+
*
|
|
174
|
+
* Coalescing: `coalesceWindowMs` suppresses a duplicate row for the same
|
|
175
|
+
* (pool, provider) pair within the window — used by the cooldown path, where
|
|
176
|
+
* dispatch flags the same pair on every throttled request and the audit log
|
|
177
|
+
* needs one row per episode, not one per request. The default `undefined`
|
|
178
|
+
* means no coalescing, so block and probe rows are never deduped; it also
|
|
179
|
+
* means the pair never enters the tracking map, so the eviction sweep below
|
|
180
|
+
* only ever sees coalesced pairs. A failed insert does not stamp the pair, so
|
|
181
|
+
* the next attempt within the window is still tried.
|
|
182
|
+
*
|
|
183
|
+
* Swallow contract: this never throws to dispatch (observability writes are
|
|
184
|
+
* non-fatal); failures bump `pool_cooldown_record_failed` and are logged.
|
|
185
|
+
*/
|
|
186
|
+
export async function appendPoolProviderEgressAuditRow(
|
|
187
|
+
db: CartethyiaDatabase,
|
|
188
|
+
opts: {
|
|
189
|
+
poolId: string;
|
|
190
|
+
providerId: string;
|
|
191
|
+
reason: string;
|
|
192
|
+
blocked: boolean;
|
|
193
|
+
codeSuffix?: string;
|
|
194
|
+
coalesceWindowMs?: number;
|
|
195
|
+
},
|
|
196
|
+
): Promise<void> {
|
|
197
|
+
const { poolId, providerId, reason, blocked, codeSuffix, coalesceWindowMs } = opts;
|
|
198
|
+
let key: string | undefined;
|
|
199
|
+
let now = 0;
|
|
200
|
+
if (coalesceWindowMs !== undefined) {
|
|
201
|
+
key = `${poolId}::${providerId.toLowerCase()}`;
|
|
202
|
+
now = Date.now();
|
|
203
|
+
const last = lastRecordedByPoolProvider.get(key);
|
|
204
|
+
if (last !== undefined && now - last < coalesceWindowMs) return;
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
await db.insert(healthEvents).values({
|
|
208
|
+
id: randomUUID(),
|
|
209
|
+
entityKind: "pool",
|
|
210
|
+
networkPoolId: poolId,
|
|
211
|
+
fromStatus: null,
|
|
212
|
+
toStatus: "cooldown",
|
|
213
|
+
reason: `provider ${providerId}: ${reason}${codeSuffix ?? ""}`,
|
|
214
|
+
errorCategory: blocked ? "egress_blocked" : "rate_limit_transient",
|
|
215
|
+
createdAt: new Date(),
|
|
216
|
+
});
|
|
217
|
+
} catch (error) {
|
|
218
|
+
metrics.pool_cooldown_record_failed.inc(1);
|
|
219
|
+
log.warn("pool provider egress audit row failed", {
|
|
220
|
+
networkPoolId: poolId,
|
|
221
|
+
providerId,
|
|
222
|
+
error: String(error),
|
|
223
|
+
});
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (key !== undefined && coalesceWindowMs !== undefined) {
|
|
227
|
+
lastRecordedByPoolProvider.set(key, now);
|
|
228
|
+
if (lastRecordedByPoolProvider.size > MAX_TRACKED_PAIRS) {
|
|
229
|
+
for (const [trackedKey, recordedAt] of lastRecordedByPoolProvider) {
|
|
230
|
+
if (now - recordedAt >= coalesceWindowMs) {
|
|
231
|
+
lastRecordedByPoolProvider.delete(trackedKey);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
155
238
|
/**
|
|
156
239
|
* Writes a probe-measured verdict the same way a dispatch-side event would be
|
|
157
240
|
* recorded: one mirror marker and one audit row.
|
|
@@ -174,20 +257,12 @@ export async function recordProbeVerdict(
|
|
|
174
257
|
marker: { until: number; reason: string; blocked?: boolean },
|
|
175
258
|
): Promise<void> {
|
|
176
259
|
await upsertProviderCooldownMarker(db, poolId, providerId, marker);
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
toStatus: "cooldown",
|
|
184
|
-
reason: `provider ${providerId}: ${marker.reason}`,
|
|
185
|
-
errorCategory: marker.blocked === true ? "egress_blocked" : "rate_limit_transient",
|
|
186
|
-
createdAt: new Date(),
|
|
187
|
-
});
|
|
188
|
-
} catch (error) {
|
|
189
|
-
log.warn("pool probe event failed", { networkPoolId: poolId, providerId, error: String(error) });
|
|
190
|
-
}
|
|
260
|
+
await appendPoolProviderEgressAuditRow(db, {
|
|
261
|
+
poolId,
|
|
262
|
+
providerId,
|
|
263
|
+
reason: marker.reason,
|
|
264
|
+
blocked: marker.blocked === true,
|
|
265
|
+
});
|
|
191
266
|
}
|
|
192
267
|
|
|
193
268
|
/**
|
|
@@ -1,11 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pool health — flag helpers + state machine.
|
|
3
|
+
*
|
|
4
|
+
* Two halves of one domain, kept in one home:
|
|
5
|
+
*
|
|
6
|
+
* - **Flag helpers** (`flagPoolBlocked`, `flagPoolCooldown`,
|
|
7
|
+
* `flagPoolOnEgressFailure`): durable per-(pool, provider) 429/block
|
|
8
|
+
* history. The volatile flags live in `NetworkPoolSelector` (fast, consulted
|
|
9
|
+
* on every dispatch); these mirror each flag into two durable homes — the
|
|
10
|
+
* append-only `health_events` table (`entity_kind: "pool"`) for audit, and
|
|
11
|
+
* the `provider_cooldowns` column as the state boot restores. They never
|
|
12
|
+
* touch `network_pools.status`: a single provider's 429 must not flip the
|
|
13
|
+
* whole pool to `cooldown`.
|
|
14
|
+
*
|
|
15
|
+
* - **State machine** (`recordPoolDispatchOutcome`, `disablePoolForProxyHttpStatus`,
|
|
16
|
+
* `recoverNetworkPool`, `sweepExpiredPoolCooldowns`): the pool row's health
|
|
17
|
+
* transitions, subscriber feed, and recovery sweep.
|
|
18
|
+
*/
|
|
1
19
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { healthEventBuffer } from "../persistence/health-event-flush";
|
|
3
20
|
import { and, desc, eq, inArray, isNotNull, lte, sql } from "drizzle-orm";
|
|
4
|
-
import type { CartethyiaDatabase } from "
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
21
|
+
import type { CartethyiaDatabase } from "../../persistence/db-handle";
|
|
22
|
+
import { healthEventBuffer } from "../../persistence/health-event-flush";
|
|
23
|
+
import { healthEvents, networkPools } from "../../persistence/schema";
|
|
24
|
+
import { GatewayError } from "../../protocol/gateway-error";
|
|
25
|
+
import { metrics } from "../../observability/metrics";
|
|
26
|
+
import { log } from "../../observability/logger";
|
|
27
|
+
import { resolvePoolProviderRateLimitCooldownMs, resolvePoolCooldownMs } from "../../config/cooldowns";
|
|
28
|
+
import type { NetworkPoolSelector } from "./selector";
|
|
29
|
+
import { appendPoolProviderEgressAuditRow, dropProviderCooldownMarker, pruneExpiredProviderCooldownMarkers, upsertProviderCooldownMarker } from "./cooldown-store";
|
|
30
|
+
import { drainPoolByteDelta } from "./accounting";
|
|
31
|
+
import type { EgressVerdict } from "../verdict";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Coalescing window for repeated per-(pool, provider) 429 events. Dispatch
|
|
35
|
+
* flags the same pair on every throttled request, but the durable audit log
|
|
36
|
+
* needs one row per cooldown episode — not one per throttled request.
|
|
37
|
+
*/
|
|
38
|
+
const POOL_COOLDOWN_EVENT_WINDOW_MS = 15 * 60 * 1000;
|
|
39
|
+
|
|
40
|
+
/** Durable half of `flagPoolCooldown`; not a second public entry point. */
|
|
41
|
+
async function recordPoolCooldownEvent(
|
|
42
|
+
db: CartethyiaDatabase,
|
|
43
|
+
poolId: string,
|
|
44
|
+
providerId: string,
|
|
45
|
+
reason: string,
|
|
46
|
+
): Promise<void> {
|
|
47
|
+
await appendPoolProviderEgressAuditRow(db, {
|
|
48
|
+
poolId,
|
|
49
|
+
providerId,
|
|
50
|
+
reason,
|
|
51
|
+
blocked: false,
|
|
52
|
+
coalesceWindowMs: POOL_COOLDOWN_EVENT_WINDOW_MS,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Retires a pool for one provider because the upstream flagged its egress.
|
|
58
|
+
*
|
|
59
|
+
* Separate from `flagPoolCooldown` on purpose: the two facts are not
|
|
60
|
+
* interchangeable. A 429 is an allowance that refills, so the pool returns to
|
|
61
|
+
* rotation when the window ends. A free-tier refusal means the address itself
|
|
62
|
+
* is retired upstream, and putting it back would spend a real request to learn
|
|
63
|
+
* the same answer. The audit row therefore carries `errorCategory:
|
|
64
|
+
* "egress_blocked"` rather than `rate_limit_transient`, which is what the
|
|
65
|
+
* console reads to draw it as a block instead of a cooldown.
|
|
66
|
+
*/
|
|
67
|
+
export function flagPoolBlocked(
|
|
68
|
+
poolSelector: NetworkPoolSelector,
|
|
69
|
+
db: CartethyiaDatabase,
|
|
70
|
+
poolId: string,
|
|
71
|
+
providerId: string,
|
|
72
|
+
error: GatewayError,
|
|
73
|
+
): void {
|
|
74
|
+
const reason = `Provider ${providerId} refused this egress`;
|
|
75
|
+
void poolSelector
|
|
76
|
+
.flagProviderBlocked(poolId, providerId, reason)
|
|
77
|
+
.then((entry) =>
|
|
78
|
+
// Persisted even when the flag was refused (a credential-keyed
|
|
79
|
+
// provider): the selector returned an inert entry, so the mirror
|
|
80
|
+
// records nothing a reader would act on.
|
|
81
|
+
upsertProviderCooldownMarker(db, poolId, providerId, entry),
|
|
82
|
+
)
|
|
83
|
+
.catch((cause: unknown) => {
|
|
84
|
+
metrics.pool_cooldown_record_failed.inc(1);
|
|
85
|
+
log.warn("pool block flag failed", {
|
|
86
|
+
networkPoolId: poolId,
|
|
87
|
+
providerId,
|
|
88
|
+
error: String(cause),
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
void recordPoolBlockEvent(db, poolId, providerId, reason, error);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Durable half of `flagPoolBlocked`.
|
|
96
|
+
*
|
|
97
|
+
* Not coalesced the way a cooldown is: a block happens once per (pool,
|
|
98
|
+
* provider) for the life of the deployment, so there is no burst to collapse.
|
|
99
|
+
* A repeated event means the operator re-enabled the pool and it was refused
|
|
100
|
+
* again, which is worth a second row.
|
|
101
|
+
*/
|
|
102
|
+
async function recordPoolBlockEvent(
|
|
103
|
+
db: CartethyiaDatabase,
|
|
104
|
+
poolId: string,
|
|
105
|
+
providerId: string,
|
|
106
|
+
reason: string,
|
|
107
|
+
error: GatewayError,
|
|
108
|
+
): Promise<void> {
|
|
109
|
+
await appendPoolProviderEgressAuditRow(db, {
|
|
110
|
+
poolId,
|
|
111
|
+
providerId,
|
|
112
|
+
reason,
|
|
113
|
+
blocked: true,
|
|
114
|
+
codeSuffix: providerCodeSuffix(error),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The upstream's own code, when it named one, so the audit row is actionable. */
|
|
119
|
+
function providerCodeSuffix(error: GatewayError): string {
|
|
120
|
+
const code = error.details.providerCode;
|
|
121
|
+
return typeof code === "string" && code.length > 0 ? ` (${code})` : "";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Cooldown applied when an upstream 429 carries no `retryAfterMs`. */
|
|
125
|
+
const DEFAULT_PROVIDER_COOLDOWN_MS = 15 * 60 * 1000;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The fallback window for a provider-scoped egress cooldown. Upstream `Retry-After`
|
|
129
|
+
* always wins; the configured default exists because a request-counted free
|
|
130
|
+
* tier — the one provider that flags at all — stays refused for hours after
|
|
131
|
+
* the 15-minute guess this function used before.
|
|
132
|
+
*/
|
|
133
|
+
function providerCooldownFallbackMs(): number {
|
|
134
|
+
return Math.max(resolvePoolProviderRateLimitCooldownMs(), DEFAULT_PROVIDER_COOLDOWN_MS);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Records a provider-scoped 429 cooldown: the volatile per-(pool, provider)
|
|
139
|
+
* flag consulted on every dispatch, its durable audit row, and the durable
|
|
140
|
+
* state boot restores from.
|
|
141
|
+
*
|
|
142
|
+
* Both paths that reach this decision — the non-streaming attempt loop and
|
|
143
|
+
* the streaming failure path — call this one function, because each write
|
|
144
|
+
* reports its own failure through the same metric and warn log. Two copies
|
|
145
|
+
* would have to be edited together, and a divergence would surface only as
|
|
146
|
+
* telemetry that silently differs by response mode.
|
|
147
|
+
*
|
|
148
|
+
* Callers gate on `shouldCooldownPool`, which narrows the error to the
|
|
149
|
+
* upstream/provider-scoped 429 this function is written for.
|
|
150
|
+
*/
|
|
151
|
+
export function flagPoolCooldown(
|
|
152
|
+
poolSelector: NetworkPoolSelector,
|
|
153
|
+
db: CartethyiaDatabase,
|
|
154
|
+
poolId: string,
|
|
155
|
+
providerId: string,
|
|
156
|
+
error: GatewayError,
|
|
157
|
+
): void {
|
|
158
|
+
const reason = `Provider ${providerId} rate limit`;
|
|
159
|
+
const retryAfterMs =
|
|
160
|
+
typeof error.details.retryAfterMs === "number"
|
|
161
|
+
? error.details.retryAfterMs
|
|
162
|
+
: providerCooldownFallbackMs();
|
|
163
|
+
void poolSelector
|
|
164
|
+
.flagProviderCooldown(poolId, providerId, retryAfterMs, reason)
|
|
165
|
+
.then((entry) => upsertProviderCooldownMarker(db, poolId, providerId, entry))
|
|
166
|
+
.catch((cause: unknown) => {
|
|
167
|
+
metrics.pool_cooldown_record_failed.inc(1);
|
|
168
|
+
log.warn("pool cooldown flag failed", {
|
|
169
|
+
networkPoolId: poolId,
|
|
170
|
+
providerId,
|
|
171
|
+
error: String(cause),
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
void recordPoolCooldownEvent(db, poolId, providerId, reason);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* One egress-failure verdict → pool-flag dispatch, shared by the attempt loop
|
|
179
|
+
* and the streaming finalizer.
|
|
180
|
+
*
|
|
181
|
+
* Both paths classify the same failure with `classifyEgressFailure`, then must
|
|
182
|
+
* retire the same pool the same way; a policy change — which verdict maps to a
|
|
183
|
+
* cooldown vs a block, or a new verdict — belongs here so the two response
|
|
184
|
+
* modes cannot drift in telemetry.
|
|
185
|
+
*
|
|
186
|
+
* The call sites compute the verdict (the attempt loop also needs it for its
|
|
187
|
+
* retry decision) and pass it in; the gating is identical to what both
|
|
188
|
+
* previously applied by hand: only a real pool id + selector pair, only a
|
|
189
|
+
* `GatewayError`, and never a `"none"` verdict. The flags themselves are
|
|
190
|
+
* fire-and-forget advisory writes that never fail the request.
|
|
191
|
+
*/
|
|
192
|
+
export function flagPoolOnEgressFailure(
|
|
193
|
+
poolSelector: NetworkPoolSelector | undefined,
|
|
194
|
+
db: CartethyiaDatabase,
|
|
195
|
+
poolId: string | undefined,
|
|
196
|
+
providerId: string,
|
|
197
|
+
error: unknown,
|
|
198
|
+
verdict: EgressVerdict,
|
|
199
|
+
): void {
|
|
200
|
+
if (poolId === undefined || poolSelector === undefined || !(error instanceof GatewayError)) return;
|
|
201
|
+
if (verdict.kind === "cooldown") {
|
|
202
|
+
flagPoolCooldown(poolSelector, db, poolId, providerId, error);
|
|
203
|
+
} else if (verdict.kind === "blocked") {
|
|
204
|
+
flagPoolBlocked(poolSelector, db, poolId, providerId, error);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
9
208
|
export interface PoolHealthEvent {
|
|
10
209
|
readonly id: string;
|
|
11
210
|
readonly networkPoolId: string;
|