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
|
@@ -1,22 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Per-provider proactive OAuth refresh lead.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* where re-minting every few minutes is pointless traffic. And some providers
|
|
8
|
-
* (Google-backed) rate-limit the token endpoint aggressively, so refreshing
|
|
9
|
-
* only when genuinely close to expiry matters more than reacting fast.
|
|
4
|
+
* The lead is how long *before* expiry a token becomes eligible for the
|
|
5
|
+
* scheduled refresh sweep. It exists so the sweep re-mints a token before it
|
|
6
|
+
* dies — never to push refreshes any more often than that.
|
|
10
7
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
8
|
+
* Buckets are deliberately coarse. A single global "5 minutes before expiry"
|
|
9
|
+
* window is wrong: some providers issue short-lived tokens that a 5-minute
|
|
10
|
+
* lead can miss under a loaded sweep, while others issue very long-lived
|
|
11
|
+
* tokens where re-minting days early is pointless traffic. Three buckets cover
|
|
12
|
+
* both ends without a hand-tuned value per provider:
|
|
14
13
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* - daily (default): tokens that live hours to days — refresh once a day at
|
|
15
|
+
* most, or when the sweep sees the token enter its final 24h.
|
|
16
|
+
* - 3 days: multi-day tokens — refresh at most ~3 days before expiry.
|
|
17
|
+
* - 7 weeks: very long-lived tokens — refresh ~7 weeks before expiry, so a
|
|
18
|
+
* ~6-month token is touched roughly once a quarter.
|
|
19
|
+
*
|
|
20
|
+
* Short-lived tokens (below the daily bucket) keep tighter leads because the
|
|
21
|
+
* invariant is "never let the token expire": a lead larger than the token
|
|
22
|
+
* lifetime would make the token permanently due and the sweep would re-mint it
|
|
23
|
+
* on every tick, which is the wasteful behavior these buckets exist to remove.
|
|
24
|
+
*
|
|
25
|
+
* Real expiries (from the first-party clients these providers model):
|
|
26
|
+
* - Google-backed (Antigravity): access tokens live ~1h; 5 min lead.
|
|
27
|
+
* - Claude Code: CLI session tokens live ~8h; 4h lead keeps one work
|
|
28
|
+
* session on a single token.
|
|
29
|
+
* - xAI/Grok CLI and Kimi: short-lived tokens; 5 min lead.
|
|
30
|
+
* - ChatGPT/Codex: access tokens live ~5 days; 3-day lead re-mints ~2 days
|
|
31
|
+
* before expiry.
|
|
32
|
+
* - iFlow: very long-lived tokens; 7-week lead.
|
|
18
33
|
*/
|
|
19
|
-
export const DEFAULT_REFRESH_LEAD_MS =
|
|
34
|
+
export const DEFAULT_REFRESH_LEAD_MS = 24 * 60 * 60 * 1000;
|
|
35
|
+
|
|
36
|
+
/** 3-day lead for multi-day tokens. */
|
|
37
|
+
export const REFRESH_LEAD_3DAY_MS = 3 * 24 * 60 * 60 * 1000;
|
|
38
|
+
|
|
39
|
+
/** 7-week lead for very long-lived tokens. */
|
|
40
|
+
export const REFRESH_LEAD_7WEEK_MS = 7 * 7 * 24 * 60 * 60 * 1000;
|
|
20
41
|
|
|
21
42
|
/**
|
|
22
43
|
* Per-provider refresh lead in milliseconds. Providers absent here use
|
|
@@ -24,19 +45,22 @@ export const DEFAULT_REFRESH_LEAD_MS = 5 * 60 * 1000;
|
|
|
24
45
|
* `provider_accounts.provider_id`.
|
|
25
46
|
*/
|
|
26
47
|
export const REFRESH_LEAD_MS: Readonly<Record<string, number>> = Object.freeze({
|
|
27
|
-
// Google-backed token endpoints throttle
|
|
28
|
-
//
|
|
48
|
+
// Google-backed token endpoints throttle, and the access token itself lives
|
|
49
|
+
// only ~1h — a tight lead (below the token lifetime) is what keeps the
|
|
50
|
+
// sweep from re-minting on every tick.
|
|
29
51
|
antigravity: 5 * 60 * 1000,
|
|
30
|
-
// Claude Code access tokens live ~8h
|
|
31
|
-
//
|
|
52
|
+
// Claude Code access tokens live ~8h; 4h keeps a full work session on one
|
|
53
|
+
// token without permanent due-state.
|
|
32
54
|
claude: 4 * 60 * 60 * 1000,
|
|
33
|
-
//
|
|
34
|
-
codex: 5 * 24 * 60 * 60 * 1000,
|
|
35
|
-
// xAI/Grok CLI and Kimi both re-mint on a 5-minute lead.
|
|
55
|
+
// xAI/Grok CLI and Kimi re-mint on a 5-minute lead (short-lived tokens).
|
|
36
56
|
grok: 5 * 60 * 1000,
|
|
37
57
|
kimi: 5 * 60 * 1000,
|
|
38
|
-
//
|
|
39
|
-
|
|
58
|
+
// ChatGPT/Codex access tokens live ~5 days; a 3-day lead re-mints ~2 days
|
|
59
|
+
// before expiry — one refresh per weekend, not per request.
|
|
60
|
+
codex: REFRESH_LEAD_3DAY_MS,
|
|
61
|
+
// iFlow tokens are very long-lived; a 7-week lead avoids pointless churn
|
|
62
|
+
// while still re-minting well before expiry.
|
|
63
|
+
iflow: REFRESH_LEAD_7WEEK_MS,
|
|
40
64
|
});
|
|
41
65
|
|
|
42
66
|
/** The refresh lead for `providerId`, falling back to the shared default. */
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* `bundledModelCatalog` is called during startup seeding and again whenever a
|
|
5
|
-
* caller rebuilds the routing/console catalog. Caching the loader *promise*
|
|
6
|
-
* (not just the resolved value) makes concurrent and repeated calls for one
|
|
7
|
-
* provider share a single load, and lets the dynamic catalog imports run once
|
|
8
|
-
* per process. A rejected load is evicted so a later call can retry.
|
|
2
|
+
* In-process caches for model catalogs, model discovery, and client/provider versions.
|
|
9
3
|
*/
|
|
4
|
+
|
|
10
5
|
import type { ModelDefinition } from "../provider-model-definition";
|
|
11
6
|
import { trackModelCatalogLoad } from "../../observability/performance-metrics";
|
|
12
7
|
import { resolveRedisClient } from "../../persistence/redis";
|
|
13
8
|
import { cachedRead } from "../../console/shared/read-through-cache";
|
|
9
|
+
import { TtlCacheFamily } from "../../persistence/ttl-cache";
|
|
10
|
+
import { retryWithBackoff } from "../../network/util/retry";
|
|
11
|
+
|
|
12
|
+
/* -------------------------------------------------------------------------- */
|
|
13
|
+
/* 1. Model Catalog Cache */
|
|
14
|
+
/* -------------------------------------------------------------------------- */
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Provider entries retained before the least-recently-used catalog is evicted.
|
|
@@ -37,8 +38,7 @@ const catalogCache = new Map<string, Promise<readonly ModelDefinition[]>>();
|
|
|
37
38
|
/**
|
|
38
39
|
* Evicts the least-recently-used provider before inserting a new one once the
|
|
39
40
|
* cache is at its bound. `Map` preserves insertion order and every cache hit
|
|
40
|
-
* re-inserts its entry at the tail, so the head is the LRU key.
|
|
41
|
-
* bound the process-lifetime cache pinned one catalog per provider forever.
|
|
41
|
+
* re-inserts its entry at the tail, so the head is the LRU key.
|
|
42
42
|
*/
|
|
43
43
|
function evictLeastRecentlyUsedCatalog(maxEntries: number): void {
|
|
44
44
|
if (catalogCache.size < maxEntries) return;
|
|
@@ -87,13 +87,65 @@ export function resetModelCatalogCacheForTesting(): void {
|
|
|
87
87
|
catalogCache.clear();
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
/**
|
|
91
|
-
* Live entry count, for the `/metrics` gauge.
|
|
92
|
-
*
|
|
93
|
-
* Exported so a rebuild that saturates the cap is visible from outside the
|
|
94
|
-
* process: before this, every in-process cache except the quota tier was
|
|
95
|
-
* invisible, and growth could only be found with a heap dump.
|
|
96
|
-
*/
|
|
90
|
+
/** Live entry count, for the `/metrics` gauge. */
|
|
97
91
|
export function modelCatalogCacheSize(): number {
|
|
98
92
|
return catalogCache.size;
|
|
99
93
|
}
|
|
94
|
+
|
|
95
|
+
/* -------------------------------------------------------------------------- */
|
|
96
|
+
/* 2. Model Discovery Cache */
|
|
97
|
+
/* -------------------------------------------------------------------------- */
|
|
98
|
+
|
|
99
|
+
const MODEL_DISCOVERY_CACHE_TTL_MS = 10 * 60 * 1000;
|
|
100
|
+
const discoveryCaches = new TtlCacheFamily<readonly ModelDefinition[]>({
|
|
101
|
+
defaultTtlMs: MODEL_DISCOVERY_CACHE_TTL_MS,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
export interface CachedModelDiscoveryOptions {
|
|
105
|
+
readonly ttlMs?: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Returns cached models, retrying null/empty loads on the next call. */
|
|
109
|
+
export async function getCachedModelDiscovery(
|
|
110
|
+
key: string,
|
|
111
|
+
loader: () => Promise<readonly ModelDefinition[] | null>,
|
|
112
|
+
options: CachedModelDiscoveryOptions = {},
|
|
113
|
+
): Promise<readonly ModelDefinition[] | null> {
|
|
114
|
+
return discoveryCaches.for(options.ttlMs).load(key, loader, {
|
|
115
|
+
namespace: "model-discovery",
|
|
116
|
+
shouldCache: (models) => models.length > 0,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Drops all cached discoveries. Test-only. */
|
|
121
|
+
export function resetModelDiscoveryCacheForTesting(): void {
|
|
122
|
+
discoveryCaches.clear();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* -------------------------------------------------------------------------- */
|
|
126
|
+
/* 3. Provider Version Cache */
|
|
127
|
+
/* -------------------------------------------------------------------------- */
|
|
128
|
+
|
|
129
|
+
const VERSION_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
130
|
+
const versionCaches = new TtlCacheFamily<string>({ defaultTtlMs: VERSION_CACHE_TTL_MS });
|
|
131
|
+
|
|
132
|
+
export interface CachedVersionOptions {
|
|
133
|
+
readonly ttlMs?: number;
|
|
134
|
+
readonly maxRetries?: number;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function getCachedVersion(
|
|
138
|
+
key: string,
|
|
139
|
+
loader: () => Promise<string | null>,
|
|
140
|
+
options: CachedVersionOptions = {},
|
|
141
|
+
): Promise<string | null> {
|
|
142
|
+
const loadWithRetry = () => retryWithBackoff(loader, { maxRetries: options.maxRetries ?? 2 });
|
|
143
|
+
return versionCaches.for(options.ttlMs).load(key, loadWithRetry, {
|
|
144
|
+
namespace: "provider-version",
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Drops all cached versions. Test-only. */
|
|
149
|
+
export function resetVersionCacheForTesting(): void {
|
|
150
|
+
versionCaches.clear();
|
|
151
|
+
}
|
|
@@ -10,7 +10,7 @@ import { ProviderRegistry } from "../provider-registry";
|
|
|
10
10
|
import { OpenAICompatibleAdapter } from "../adapters/compatible-adapter";
|
|
11
11
|
import { resolveByokWireProfile } from "./byok-wire-profile";
|
|
12
12
|
import { BUNDLED_PROVIDER_MODULES } from "../default-registry";
|
|
13
|
-
import { getCachedModels } from "./
|
|
13
|
+
import { getCachedModels } from "./provider-caches";
|
|
14
14
|
import { seedBundledModels } from "./provider-catalog-seeder";
|
|
15
15
|
import type { CartethyiaDatabase } from "../../persistence/db-handle";
|
|
16
16
|
import { providers } from "../../persistence/schema";
|
|
@@ -6,7 +6,7 @@ import { providerAccounts, providerOauthStates } from "../../persistence/schema"
|
|
|
6
6
|
import { decryptCredentialToString } from "../../security/crypto";
|
|
7
7
|
import { GatewayError } from "../../protocol/gateway-error";
|
|
8
8
|
import type { OAuthRefreshService, OAuthTokenRefresher } from "../authentication/oauth-refresh-service";
|
|
9
|
-
import { CredentialResolver, type CredentialAlternative } from "../provider-
|
|
9
|
+
import { CredentialResolver, type CredentialAlternative } from "../provider-credentials";
|
|
10
10
|
import { parseProviderId, type CredentialKind } from "../provider-contracts";
|
|
11
11
|
import type { ResolvedCredential } from "../provider-contracts";
|
|
12
12
|
import { refreshLeadMs } from "./oauth-refresh-lead";
|
|
@@ -145,19 +145,62 @@ export interface AccountWithFreshness {
|
|
|
145
145
|
readonly dueAt: number | undefined;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
/** Columns every account+OAuth-state reader selects; narrower readers pick from it. */
|
|
149
|
+
export interface AccountWithOauthStateRow {
|
|
150
|
+
readonly id: string;
|
|
151
|
+
readonly providerId: string;
|
|
152
|
+
readonly credentialKind: CredentialKind;
|
|
153
|
+
readonly credentialCiphertext: Buffer | null;
|
|
154
|
+
readonly refreshCiphertext: Buffer | null;
|
|
155
|
+
readonly authState: unknown;
|
|
156
|
+
readonly clientSecretCiphertext: Buffer | null;
|
|
157
|
+
readonly staticToken: boolean;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Which optional columns a reader wants on top of the shared base. */
|
|
161
|
+
export interface LoadAccountWithOauthStateOptions {
|
|
162
|
+
/** Select `status` from providerAccounts. */
|
|
163
|
+
readonly includeStatus?: boolean;
|
|
164
|
+
/** Select the last health category from providerAccounts. */
|
|
165
|
+
readonly includeLastErrorCategory?: boolean;
|
|
166
|
+
/** Select `expiresAt` from providerOauthStates. */
|
|
167
|
+
readonly includeExpiry?: boolean;
|
|
168
|
+
/** Also compute the skew-adjusted OAuth due time (see `loadAccountWithFreshness`). */
|
|
169
|
+
readonly includeDueAt?: boolean;
|
|
170
|
+
/** Refresh lead override for the `dueAt` skew; defaults to `refreshLeadMs(providerId)`. */
|
|
171
|
+
readonly skewMs?: number;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** One loaded account row plus whatever optional columns were requested. */
|
|
175
|
+
export interface LoadedAccountWithOauthState {
|
|
176
|
+
readonly id: string;
|
|
177
|
+
readonly providerId: string;
|
|
178
|
+
readonly credentialKind: CredentialKind;
|
|
179
|
+
readonly lastErrorCategory?: string | null;
|
|
180
|
+
readonly credentialCiphertext: Buffer | null;
|
|
181
|
+
readonly refreshCiphertext: Buffer | null;
|
|
182
|
+
readonly authState: unknown;
|
|
183
|
+
readonly clientSecretCiphertext: Buffer | null;
|
|
184
|
+
readonly staticToken: boolean;
|
|
185
|
+
readonly status?: string;
|
|
186
|
+
readonly expiresAt?: Date | null;
|
|
187
|
+
readonly dueAt?: number;
|
|
188
|
+
}
|
|
189
|
+
|
|
148
190
|
/**
|
|
149
|
-
*
|
|
191
|
+
* Shared account+OAuth-state loader behind every status/health reader.
|
|
150
192
|
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
193
|
+
* Builds the same `select(...).from(providerAccounts)` with the conditional
|
|
194
|
+
* `leftJoin` on `providerOauthStates` (the memory/Postgres dialect guard), so
|
|
195
|
+
* a schema or dialect change is fixed once instead of in four copies. The
|
|
196
|
+
* `dueAt` skew logic lives here too; `loadAccountWithFreshness` is a thin
|
|
197
|
+
* wrapper that returns it beside the row.
|
|
155
198
|
*/
|
|
156
|
-
export async function
|
|
199
|
+
export async function loadAccountWithOauthState(
|
|
157
200
|
db: CartethyiaDatabase,
|
|
158
201
|
accountId: string,
|
|
159
|
-
|
|
160
|
-
): Promise<
|
|
202
|
+
options: LoadAccountWithOauthStateOptions = {},
|
|
203
|
+
): Promise<LoadedAccountWithOauthState | undefined> {
|
|
161
204
|
const selectBuilder = db
|
|
162
205
|
.select({
|
|
163
206
|
id: providerAccounts.id,
|
|
@@ -167,8 +210,14 @@ export async function loadAccountWithFreshness(
|
|
|
167
210
|
refreshCiphertext: providerOauthStates.refreshCiphertext,
|
|
168
211
|
authState: providerAccounts.authState,
|
|
169
212
|
clientSecretCiphertext: providerOauthStates.clientSecretCiphertext,
|
|
170
|
-
expiresAt: providerOauthStates.expiresAt,
|
|
171
213
|
staticToken: providerAccounts.staticToken,
|
|
214
|
+
...(options.includeStatus === true ? { status: providerAccounts.status } : {}),
|
|
215
|
+
...(options.includeLastErrorCategory === true
|
|
216
|
+
? { lastErrorCategory: providerAccounts.lastErrorCategory }
|
|
217
|
+
: {}),
|
|
218
|
+
...(options.includeExpiry === true || options.includeDueAt === true
|
|
219
|
+
? { expiresAt: providerOauthStates.expiresAt }
|
|
220
|
+
: {}),
|
|
172
221
|
})
|
|
173
222
|
.from(providerAccounts);
|
|
174
223
|
const query = "leftJoin" in selectBuilder
|
|
@@ -180,37 +229,60 @@ export async function loadAccountWithFreshness(
|
|
|
180
229
|
const rows = await query.where(eq(providerAccounts.id, accountId)).limit(1);
|
|
181
230
|
const row = rows[0];
|
|
182
231
|
if (!row) return undefined;
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
// refresh
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
232
|
+
if (options.includeDueAt !== true) return row;
|
|
233
|
+
const lead = options.skewMs ?? refreshLeadMs(row.providerId);
|
|
234
|
+
// A refresh-capable OAuth account with no stored expiry has an unknown
|
|
235
|
+
// liveness, but the scheduled sweep is the only refresher: marking it due
|
|
236
|
+
// here would refresh on every load, and the dispatch path deliberately no
|
|
237
|
+
// longer refreshes inline. Unknown expiry therefore means "not due yet" —
|
|
238
|
+
// if the token actually died, the upstream's 401 triggers the dispatch
|
|
239
|
+
// retry refresh (see `attempt-loop.ts`).
|
|
240
|
+
// A static token is never "due": it is used as issued and there is no
|
|
241
|
+
// refresh to run, so the dispatch path must not treat it as stale.
|
|
242
|
+
const dueAt =
|
|
243
|
+
row.staticToken
|
|
244
|
+
? undefined
|
|
245
|
+
: row.expiresAt != null
|
|
246
|
+
? row.expiresAt.getTime() - lead
|
|
247
|
+
: undefined;
|
|
199
248
|
return {
|
|
200
|
-
row,
|
|
201
|
-
|
|
202
|
-
// refresh to run, so the dispatch path must not treat it as stale.
|
|
203
|
-
dueAt:
|
|
204
|
-
row.staticToken
|
|
205
|
-
? undefined
|
|
206
|
-
: row.expiresAt != null
|
|
207
|
-
? row.expiresAt.getTime() - lead
|
|
208
|
-
: unknownExpiryButRefreshable
|
|
209
|
-
? 0
|
|
210
|
-
: undefined,
|
|
249
|
+
...row,
|
|
250
|
+
...(dueAt === undefined ? {} : { dueAt }),
|
|
211
251
|
};
|
|
212
252
|
}
|
|
213
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Loads an account and its optional OAuth state in one query.
|
|
256
|
+
*
|
|
257
|
+
* `dueAt` is the OAuth expiry minus the provider's refresh lead
|
|
258
|
+
* (`refreshLeadMs(providerId)`), so a token becomes due at the point that
|
|
259
|
+
* provider re-mints its own (Claude ~4h, Codex ~5 days, Antigravity ~5 min).
|
|
260
|
+
* Non-OAuth accounts and accounts without OAuth state have no due time.
|
|
261
|
+
*/
|
|
262
|
+
export async function loadAccountWithFreshness(
|
|
263
|
+
db: CartethyiaDatabase,
|
|
264
|
+
accountId: string,
|
|
265
|
+
skewMs?: number,
|
|
266
|
+
): Promise<AccountWithFreshness | undefined> {
|
|
267
|
+
const loaded = await loadAccountWithOauthState(db, accountId, {
|
|
268
|
+
includeDueAt: true,
|
|
269
|
+
...(skewMs === undefined ? {} : { skewMs }),
|
|
270
|
+
});
|
|
271
|
+
if (!loaded) return undefined;
|
|
272
|
+
const row: AccountWithFreshnessRow = {
|
|
273
|
+
id: loaded.id,
|
|
274
|
+
providerId: loaded.providerId,
|
|
275
|
+
credentialKind: loaded.credentialKind,
|
|
276
|
+
credentialCiphertext: loaded.credentialCiphertext,
|
|
277
|
+
refreshCiphertext: loaded.refreshCiphertext,
|
|
278
|
+
authState: loaded.authState,
|
|
279
|
+
clientSecretCiphertext: loaded.clientSecretCiphertext,
|
|
280
|
+
expiresAt: loaded.expiresAt ?? null,
|
|
281
|
+
staticToken: loaded.staticToken,
|
|
282
|
+
};
|
|
283
|
+
return { row, dueAt: loaded.dueAt };
|
|
284
|
+
}
|
|
285
|
+
|
|
214
286
|
/** OAuth refresh collaborators, injected by the composition root. */
|
|
215
287
|
export interface ResolveCredentialOAuth {
|
|
216
288
|
/** Registry-backed refresher lookup; providers without a refresher are skipped. */
|
|
@@ -223,7 +295,7 @@ export async function resolveCredentialForAccount(
|
|
|
223
295
|
db: CartethyiaDatabase,
|
|
224
296
|
providerId: string,
|
|
225
297
|
accountId: string,
|
|
226
|
-
|
|
298
|
+
_oauth?: ResolveCredentialOAuth,
|
|
227
299
|
): Promise<ResolvedCredential> {
|
|
228
300
|
// Cache hit: skip DB round-trip + AES-GCM decrypt.
|
|
229
301
|
// Don't cache OAuth credentials whose token is due for refresh soon —
|
|
@@ -268,19 +340,14 @@ export async function resolveCredentialForAccount(
|
|
|
268
340
|
);
|
|
269
341
|
}
|
|
270
342
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
// worse than letting the server say so.
|
|
280
|
-
const refreshed = await oauth.refreshService.ensureFreshAccessToken(accountId, refresher);
|
|
281
|
-
if (refreshed) secret = refreshed;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
343
|
+
// No inline refresh on the request path. OAuth tokens are kept fresh by the
|
|
344
|
+
// scheduled refresh sweep (`oauth-refresh-sweep`, due by `refreshLeadMs`),
|
|
345
|
+
// and a token that still expired before its sweep turn is answered by the
|
|
346
|
+
// upstream's own 401, which the dispatch attempt-loop turns into a forced
|
|
347
|
+
// refresh (see `attempt-loop.ts`). Refreshing here blocked a client request
|
|
348
|
+
// on the token endpoint — a hung endpoint held the request for the full 30 s
|
|
349
|
+
// refresh timeout — and re-minted tokens on every expired request instead of
|
|
350
|
+
// once per schedule.
|
|
284
351
|
|
|
285
352
|
const authState = jsonbRecord(row.authState);
|
|
286
353
|
const alternative: CredentialAlternative = {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AccountErrorRule,
|
|
3
|
+
} from "../account-error-classification";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Checks whether an upstream body indicates Copilot is not licensed.
|
|
7
|
+
* Copilot answers 401 for a bad credential and 403 for a
|
|
8
|
+
* license/entitlement refusal, so forbidden/unauthorized on this surface
|
|
9
|
+
* is the same verdict.
|
|
10
|
+
*/
|
|
11
|
+
export function copilotNotLicensed(lower: string): boolean {
|
|
12
|
+
return (
|
|
13
|
+
lower.includes("not licensed") ||
|
|
14
|
+
lower.includes("forbidden") ||
|
|
15
|
+
lower.includes("unauthorized") ||
|
|
16
|
+
lower.includes("license")
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const githubRules: readonly AccountErrorRule[] = [
|
|
21
|
+
{
|
|
22
|
+
// GitHub Copilot answers a license/entitlement refusal with 403 and
|
|
23
|
+
// "not licensed to use Copilot" / "forbidden" / "unauthorized". That is a
|
|
24
|
+
// verdict on the account: the subscription cannot serve this credential,
|
|
25
|
+
// and no amount of waiting or refreshing changes it. Cooldown cycling
|
|
26
|
+
// (auth_invalidated + 5m backoff) kept a dead account in rotation where a
|
|
27
|
+
// client's retry budget burned sibling accounts — the same failure the
|
|
28
|
+
// buddy ban exists to prevent. Disable immediately. Must run before
|
|
29
|
+
// `credential-rejected`, which would claim the bare 403 as a refreshable
|
|
30
|
+
// auth rejection.
|
|
31
|
+
name: "github-copilot-not-licensed",
|
|
32
|
+
match: (s) =>
|
|
33
|
+
s.origin === "upstream" &&
|
|
34
|
+
s.providerId === "github" &&
|
|
35
|
+
copilotNotLicensed(s.lower),
|
|
36
|
+
decide: (s) =>
|
|
37
|
+
s.result(
|
|
38
|
+
"policy_blocked_banned",
|
|
39
|
+
"disabled",
|
|
40
|
+
0,
|
|
41
|
+
null,
|
|
42
|
+
s.reason("GitHub Copilot account not licensed"),
|
|
43
|
+
// A license refusal is evidence about the account even at provider
|
|
44
|
+
// scope, for the same reason a named revocation or a buddy ban is.
|
|
45
|
+
true,
|
|
46
|
+
),
|
|
47
|
+
},
|
|
48
|
+
];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { AccountErrorSignals } from "../account-error-classification";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* xAI Grok Build's free tier resets on a rolling 24-hour window, so its
|
|
5
|
+
* exhaustion is a full-day quota cooldown — never the generic 1h fallback,
|
|
6
|
+
* which let the account re-enter rotation inside the window.
|
|
7
|
+
*/
|
|
8
|
+
export const GROK_QUOTA_COOLDOWN_MS = 24 * 60 * 60 * 1000;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Detects whether a quota failure belongs to Grok's free tier or spending limit.
|
|
12
|
+
*/
|
|
13
|
+
export function isGrokFreeTierExhaustion(s: AccountErrorSignals): boolean {
|
|
14
|
+
if (s.providerId !== undefined && s.providerId !== "grok") {
|
|
15
|
+
// If providerId is known and not grok, only match if the providerCode is explicitly grok subscription
|
|
16
|
+
if (!s.providerCode.startsWith("subscription:")) return false;
|
|
17
|
+
}
|
|
18
|
+
return (
|
|
19
|
+
s.providerCode === "subscription:free-usage-exhausted" ||
|
|
20
|
+
s.providerCode.startsWith("subscription:") ||
|
|
21
|
+
s.providerCode.includes("spending") ||
|
|
22
|
+
s.providerCode.includes("credits") ||
|
|
23
|
+
s.lower.includes("free-usage") ||
|
|
24
|
+
s.lower.includes("free usage") ||
|
|
25
|
+
s.lower.includes("included free usage") ||
|
|
26
|
+
s.lower.includes("rolling 24-hour") ||
|
|
27
|
+
s.lower.includes("rolling 24 hour") ||
|
|
28
|
+
s.lower.includes("spending limit") ||
|
|
29
|
+
s.lower.includes("out of credits") ||
|
|
30
|
+
s.lower.includes("run out of credits") ||
|
|
31
|
+
s.lower.includes("need a grok subscription")
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AccountErrorRule,
|
|
3
|
+
} from "../account-error-classification";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Inception's per-minute limit window (1 minute).
|
|
7
|
+
*
|
|
8
|
+
* Inception's Free tier throttles per minute (1 000 requests / 1M input
|
|
9
|
+
* tokens / 100K output tokens) and the window resets every minute. The generic
|
|
10
|
+
* 15-minute rate-limit fallback would park a healthy key long after the window
|
|
11
|
+
* refilled; 1 minute is the provider's own cadence, hardcoded because it is a
|
|
12
|
+
* fact about the upstream, not a deployment knob.
|
|
13
|
+
*/
|
|
14
|
+
export const INCEPTION_RATE_LIMIT_COOLDOWN_MS = 60_000;
|
|
15
|
+
|
|
16
|
+
export const inceptionRules: readonly AccountErrorRule[] = [
|
|
17
|
+
{
|
|
18
|
+
// Inception's Free tier throttles per minute (1 000 requests / 1M input
|
|
19
|
+
// tokens / 100K output tokens) and the window resets every minute. A
|
|
20
|
+
// generic 429 would cool the account for the 15-minute default, leaving a
|
|
21
|
+
// healthy key parked long after the window refilled — the source of
|
|
22
|
+
// "limitasiya lumayan keras". This arm must run before `rate-limit`: it
|
|
23
|
+
// matches the same signals and shortens the window to 1 minute so routing
|
|
24
|
+
// moves to the next account and the flagged one returns almost at once.
|
|
25
|
+
name: "inception-per-minute-limit",
|
|
26
|
+
match: (s) =>
|
|
27
|
+
s.origin === "upstream" &&
|
|
28
|
+
s.providerId === "inception" &&
|
|
29
|
+
(s.statusCode === 429 ||
|
|
30
|
+
s.providerCode === "rate_limit_reached" ||
|
|
31
|
+
s.providerCode === "rate_limit_exceeded" ||
|
|
32
|
+
s.lower.includes("rate_limit") ||
|
|
33
|
+
s.lower.includes("rate limit") ||
|
|
34
|
+
s.lower.includes("too many requests") ||
|
|
35
|
+
s.lower.includes("tokens per minute") ||
|
|
36
|
+
s.lower.includes("input token")),
|
|
37
|
+
decide: (s) => {
|
|
38
|
+
const cooldownMs = s.statedCooldownMs ?? INCEPTION_RATE_LIMIT_COOLDOWN_MS;
|
|
39
|
+
return s.result(
|
|
40
|
+
"rate_limit_transient",
|
|
41
|
+
"cooldown",
|
|
42
|
+
cooldownMs,
|
|
43
|
+
new Date(Date.now() + cooldownMs),
|
|
44
|
+
s.reason("Inception per-minute input-token limit"),
|
|
45
|
+
s.origin === "upstream",
|
|
46
|
+
);
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export {
|
|
2
|
+
TRAE_REGION_COOLDOWN_MS,
|
|
3
|
+
TRAE_QUOTA_COOLDOWN_MS,
|
|
4
|
+
traeRules,
|
|
5
|
+
} from "./trae";
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
QODER_QUOTA_COOLDOWN_MS,
|
|
9
|
+
qoderPlanGatedRule,
|
|
10
|
+
qoderModelQuotaRule,
|
|
11
|
+
qoderRules,
|
|
12
|
+
} from "./qoder";
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
githubRules,
|
|
16
|
+
copilotNotLicensed,
|
|
17
|
+
} from "./github";
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
INCEPTION_RATE_LIMIT_COOLDOWN_MS,
|
|
21
|
+
inceptionRules,
|
|
22
|
+
} from "./inception";
|
|
23
|
+
|
|
24
|
+
export {
|
|
25
|
+
PERCH_TURN_RATE_LIMIT_COOLDOWN_MS,
|
|
26
|
+
perchRules,
|
|
27
|
+
} from "./perch";
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
GROK_QUOTA_COOLDOWN_MS,
|
|
31
|
+
isGrokFreeTierExhaustion,
|
|
32
|
+
} from "./grok";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AccountErrorRule,
|
|
3
|
+
} from "../account-error-classification";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Perch's `turn_rate_limited` window. The upstream does not state a reset and
|
|
7
|
+
* answers the same on every ticket mint while the window is exhausted, so a
|
|
8
|
+
* day matches the practical cadence and stops the redial loop.
|
|
9
|
+
*/
|
|
10
|
+
export const PERCH_TURN_RATE_LIMIT_COOLDOWN_MS = 24 * 60 * 60 * 1000;
|
|
11
|
+
|
|
12
|
+
export const perchRules: readonly AccountErrorRule[] = [
|
|
13
|
+
{
|
|
14
|
+
// Perch's `turn_rate_limited` is a per-turn API window, not a per-request
|
|
15
|
+
// 429: the account keeps answering but every call mints another ticket
|
|
16
|
+
// into the same exhausted window. The stated remedy ("contact us for
|
|
17
|
+
// direct API access") implies the window is long — park the account for a
|
|
18
|
+
// day so routing picks a sibling instead of hammering the same key.
|
|
19
|
+
name: "perch-turn-rate-limit",
|
|
20
|
+
match: (s) =>
|
|
21
|
+
s.origin === "upstream" &&
|
|
22
|
+
s.providerId === "perch" &&
|
|
23
|
+
(s.providerCode === "turn_rate_limited" ||
|
|
24
|
+
(s.statusCode === 429 && s.lower.includes("turn rate"))),
|
|
25
|
+
decide: (s) => {
|
|
26
|
+
const cooldownMs = s.statedCooldownMs ?? PERCH_TURN_RATE_LIMIT_COOLDOWN_MS;
|
|
27
|
+
return s.result(
|
|
28
|
+
"rate_limit_transient",
|
|
29
|
+
"cooldown",
|
|
30
|
+
cooldownMs,
|
|
31
|
+
new Date(Date.now() + cooldownMs),
|
|
32
|
+
s.reason("Perch turn rate limit"),
|
|
33
|
+
s.origin === "upstream",
|
|
34
|
+
);
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
];
|