cartethyia 2.7.8 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +3 -3
- package/README.md +29 -59
- package/dist/dashboard/assets/BannedPage-Cc0vKpMG.js +1 -0
- package/dist/dashboard/assets/BannedPage-Cc0vKpMG.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-BRWKPQPf.js → CliToolDetailPage-Bqj2aa1T.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-Bqj2aa1T.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-BP-Y5iHN.js → CliToolsPage-CXsaOAO2.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-CXsaOAO2.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-BeLpJ4ma.js +1 -0
- package/dist/dashboard/assets/CombosPage-BeLpJ4ma.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-DnV0uZ3m.js +1 -0
- package/dist/dashboard/assets/ConsoleLogPage-DnV0uZ3m.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-CBxAjo63.js +1 -0
- package/dist/dashboard/assets/CustomizationPage-CBxAjo63.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js.gz +0 -0
- package/dist/dashboard/assets/{InjectSystemPromptPanel-Cv-2Ol_K.js → InjectSystemPromptPanel-BWFLECmz.js} +2 -2
- package/dist/dashboard/assets/InjectSystemPromptPanel-BWFLECmz.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-bKfz_b1K.js → LoginPage-CgoPc5B9.js} +1 -1
- package/dist/dashboard/assets/LoginPage-CgoPc5B9.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-B-WQxx_H.js → ModelPicker-D9bFQern.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-D9bFQern.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-B_p4tL8k.js +1 -0
- package/dist/dashboard/assets/OverviewPage-B_p4tL8k.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js +8 -0
- package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-D_0PiZtg.js +1 -0
- package/dist/dashboard/assets/ProviderIcon-D_0PiZtg.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js.gz +0 -0
- package/dist/dashboard/assets/{ProxyPage-emlTuxz6.js → ProxyPage-BwNC_-Cx.js} +1 -1
- package/dist/dashboard/assets/ProxyPage-BwNC_-Cx.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js +1 -0
- package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CctTga2C.js +1 -0
- package/dist/dashboard/assets/SettingsPage-CctTga2C.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-CUZrKs9r.js → SetupPage-febLU4r1.js} +1 -1
- package/dist/dashboard/assets/SetupPage-febLU4r1.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-jWmUdDpy.js → StudioPage-M6fvR5Wz.js} +1 -1
- package/dist/dashboard/assets/StudioPage-M6fvR5Wz.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-nyeuRqz9.js +56 -0
- package/dist/dashboard/assets/UsagePage-nyeuRqz9.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-BFOlvrJC.js +1 -0
- package/dist/dashboard/assets/api-keys-BFOlvrJC.js.gz +0 -0
- package/dist/dashboard/assets/app-Dx2QgsNj.css +2 -0
- package/dist/dashboard/assets/app-Dx2QgsNj.css.gz +0 -0
- package/dist/dashboard/assets/{assert-CidQVoNo.js → assert-CqTLw5e7.js} +1 -1
- package/dist/dashboard/assets/assert-CqTLw5e7.js.gz +0 -0
- package/dist/dashboard/assets/{badge-DyI_mJfi.js → badge-C83EqLB0.js} +1 -1
- package/dist/dashboard/assets/badge-C83EqLB0.js.gz +0 -0
- package/dist/dashboard/assets/ban-durations-B58FyRxC.js +1 -0
- package/dist/dashboard/assets/ban-durations-B58FyRxC.js.gz +0 -0
- package/dist/dashboard/assets/{banner-B-yXtCqb.js → banner-D1xn7pIe.js} +1 -1
- package/dist/dashboard/assets/banner-D1xn7pIe.js.gz +0 -0
- package/dist/dashboard/assets/brand/bansos_page.webp +0 -0
- package/dist/dashboard/assets/brand/denia_bansos.webp +0 -0
- package/dist/dashboard/assets/brand/favicon.webp +0 -0
- package/dist/dashboard/assets/brand/login_register.webp +0 -0
- package/dist/dashboard/assets/brand/login_register_initial.webp +0 -0
- package/dist/dashboard/assets/brand/og_bansos.webp +0 -0
- package/dist/dashboard/assets/brand/og_image.webp +0 -0
- package/dist/dashboard/assets/brand/sidebar_ico.webp +0 -0
- package/dist/dashboard/assets/{card-_HwAEqFF.js → card-DU3Lq5Lm.js} +1 -1
- package/dist/dashboard/assets/card-DU3Lq5Lm.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-Dw1U_drK.js +1 -0
- package/dist/dashboard/assets/cli-tools-Dw1U_drK.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-ByjCk0o9.js → clipboard-button-CEL5Sg_J.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-CEL5Sg_J.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-BZhOlfz-.js → dialog-BjFR5UHy.js} +1 -1
- package/dist/dashboard/assets/dialog-BjFR5UHy.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-BADqIRYP.js → drawer-HvBW0AjI.js} +1 -1
- package/dist/dashboard/assets/drawer-HvBW0AjI.js.gz +0 -0
- package/dist/dashboard/assets/entry-BSu3gFlt.js +4 -0
- package/dist/dashboard/assets/entry-BSu3gFlt.js.gz +0 -0
- package/dist/dashboard/assets/{entry-COaC-irN.js → entry-CECVELyy.js} +1 -1
- package/dist/dashboard/assets/entry-CECVELyy.js.gz +0 -0
- package/dist/dashboard/assets/entry-DYYofQ7W.js +1 -0
- package/dist/dashboard/assets/entry-DYYofQ7W.js.gz +0 -0
- package/dist/dashboard/assets/{formatters-DcHJyRiu.js → formatters-DLW4i8zK.js} +1 -1
- package/dist/dashboard/assets/formatters-DLW4i8zK.js.gz +0 -0
- package/dist/dashboard/assets/{index-CgKmFngv.js → index-1-fCqe64.js} +2 -2
- package/dist/dashboard/assets/index-1-fCqe64.js.gz +0 -0
- package/dist/dashboard/assets/{inline-B2BirKry.js → inline-DGvU2oLK.js} +1 -1
- package/dist/dashboard/assets/inline-DGvU2oLK.js.gz +0 -0
- package/dist/dashboard/assets/{input-D-qV9dKY.js → input-iNnO1X2K.js} +1 -1
- package/dist/dashboard/assets/input-iNnO1X2K.js.gz +0 -0
- package/dist/dashboard/assets/{layout-B7tVWhtV.js → layout-0q8UGypb.js} +1 -1
- package/dist/dashboard/assets/layout-0q8UGypb.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-CtkGE3bF.js → list-row-C0w2bQjJ.js} +1 -1
- package/dist/dashboard/assets/list-row-C0w2bQjJ.js.gz +0 -0
- package/dist/dashboard/assets/{live-DGizF06-.js → live-M5aJ_xCa.js} +1 -1
- package/dist/dashboard/assets/live-M5aJ_xCa.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-GU7XdEjz.js → mutation-BQ0ccH_A.js} +1 -1
- package/dist/dashboard/assets/mutation-BQ0ccH_A.js.gz +0 -0
- package/dist/dashboard/assets/{network-C9UE-bOr.js → network-BKeQGT3K.js} +1 -1
- package/dist/dashboard/assets/network-BKeQGT3K.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-BnAd22a_.js → numbers-CivAk3da.js} +1 -1
- package/dist/dashboard/assets/numbers-CivAk3da.js.gz +0 -0
- package/dist/dashboard/assets/{pagination-BAywyMzS.js → pagination-CpzvI5ep.js} +1 -1
- package/dist/dashboard/assets/pagination-CpzvI5ep.js.gz +0 -0
- package/dist/dashboard/assets/providers/devin.svg +9 -0
- package/dist/dashboard/assets/providers/devin.svg.gz +0 -0
- package/dist/dashboard/assets/providers/windsurf.svg +10 -0
- package/dist/dashboard/assets/providers/windsurf.svg.gz +0 -0
- package/dist/dashboard/assets/providers/zro.svg +3 -0
- package/dist/dashboard/assets/providers/zro.svg.gz +0 -0
- package/dist/dashboard/assets/{query-keys-YSoQOUh6.js → query-keys-IKLh0YFd.js} +1 -1
- package/dist/dashboard/assets/query-keys-IKLh0YFd.js.gz +0 -0
- package/dist/dashboard/assets/quota-DiPD6UKH.js +1 -0
- package/dist/dashboard/assets/quota-DiPD6UKH.js.gz +0 -0
- package/dist/dashboard/assets/routing-BFQX2VBc.js +1 -0
- package/dist/dashboard/assets/routing-BFQX2VBc.js.gz +0 -0
- package/dist/dashboard/assets/{select-BCOcv8WK.js → select-CXbck0jG.js} +2 -2
- package/dist/dashboard/assets/select-CXbck0jG.js.gz +0 -0
- package/dist/dashboard/assets/settings-C_XvMHh9.js +1 -0
- package/dist/dashboard/assets/settings-C_XvMHh9.js.gz +0 -0
- package/dist/dashboard/assets/{stack-ByKt3Q1g.js → stack-DggRiCxE.js} +1 -1
- package/dist/dashboard/assets/stack-DggRiCxE.js.gz +0 -0
- package/dist/dashboard/assets/{state-DXbGTx7N.js → state-ClFzfbYp.js} +1 -1
- package/dist/dashboard/assets/state-ClFzfbYp.js.gz +0 -0
- package/dist/dashboard/assets/story/25817331.webp +0 -0
- package/dist/dashboard/assets/story/Shorekeeper.webp +0 -0
- package/dist/dashboard/assets/story/cartethyia-god.webp +0 -0
- package/dist/dashboard/assets/story/fleurdelys_plus.webp +0 -0
- package/dist/dashboard/assets/story/jinhsi-blossom.webp +0 -0
- package/dist/dashboard/assets/story/phrolova-a.webp +0 -0
- package/dist/dashboard/assets/story/requestdeniawokkjpg.webp +0 -0
- package/dist/dashboard/assets/story/wuthering-waves-hiyuki-aemeath-hiyuki-rover.webp +0 -0
- package/dist/dashboard/assets/{switch-ZeTsW623.js → switch-CTIFxKqS.js} +1 -1
- package/dist/dashboard/assets/switch-CTIFxKqS.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-CbCP4w1o.js → useMutation-BClG3yWj.js} +1 -1
- package/dist/dashboard/assets/useMutation-BClG3yWj.js.gz +0 -0
- package/dist/dashboard/console-backgrounds.webp +0 -0
- package/dist/dashboard/console-backgrounds_initial.webp +0 -0
- package/dist/dashboard/donation/qris-risun-marke.webp +0 -0
- package/dist/dashboard/index.html +3 -3
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +59 -56
- package/migrations/full-postgres/0044_add_models_warning_metadata.sql +10 -0
- package/migrations/full-postgres/0045_migrate_workbuddy_to_codebuddy.sql +156 -0
- package/migrations/full-postgres/0046_add_accounts_quota_plan_fetched_at.sql +7 -0
- package/migrations/lite-sqlite3/0044_add_models_warning_metadata.sql +11 -0
- package/migrations/lite-sqlite3/0045_migrate_workbuddy_to_codebuddy.sql +156 -0
- package/migrations/lite-sqlite3/0046_add_accounts_quota_plan_fetched_at.sql +4 -0
- package/package.json +1 -1
- package/src/boot/app.ts +10 -13
- package/src/boot/dependencies.ts +12 -4
- package/src/boot/flush.ts +20 -0
- package/src/boot/lifecycle.ts +7 -5
- package/src/config/cooldowns.ts +1 -1
- package/src/config/routing.ts +26 -0
- package/src/config/spec-metadata.ts +72 -0
- package/src/config/spec.ts +20 -6
- package/src/console/api-keys/contracts.ts +12 -1
- package/src/console/api-keys/routes.ts +50 -31
- package/src/console/auth/account-reset.ts +7 -9
- package/src/console/backup/contracts.ts +4 -0
- package/src/console/backup/export-endpoint.ts +7 -5
- package/src/console/cli-tools/service.ts +6 -12
- package/src/console/console-router-guards.ts +23 -16
- package/src/console/console-router.ts +7 -0
- package/src/console/dashboard-assets.ts +5 -5
- package/src/console/domain-registration.ts +27 -2
- package/src/console/inferhub/normalize.ts +24 -28
- package/src/console/inferhub/types.ts +3 -0
- package/src/console/ip-bans/routes.ts +93 -0
- package/src/console/observability/store.ts +0 -53
- package/src/console/providers/catalog/catalog-projections.ts +5 -2
- package/src/console/providers/catalog/contracts.ts +123 -0
- package/src/console/providers/catalog/discover-constants.ts +6 -0
- package/src/console/providers/catalog/model-operations.ts +122 -10
- package/src/console/providers/catalog/provider-operations.ts +116 -8
- package/src/console/providers/catalog/public-model-store.ts +0 -0
- package/src/console/providers/catalog/routes.ts +65 -4
- package/src/console/providers/catalog/store-helpers.ts +30 -1
- package/src/console/providers/catalog/store.ts +249 -4
- package/src/console/quota/account-quota-global.ts +97 -19
- package/src/console/quota/account-quota-shared.ts +49 -0
- package/src/console/quota/account-quota-tenant.ts +6 -18
- package/src/console/quota/native-identity.ts +2 -0
- package/src/console/quota/quota-view.ts +19 -3
- package/src/console/routing/pools/operations.ts +31 -46
- package/src/console/settings/contracts.ts +18 -0
- package/src/console/settings/store.ts +32 -0
- package/src/console/shared/errors.ts +33 -6
- package/src/console/system-prompt/contracts.ts +0 -1
- package/src/main.ts +3 -3
- package/src/network/pool/byte-accounting.ts +3 -2
- package/src/network/pool/selector.ts +182 -39
- package/src/network/pool-cooldown-store.ts +89 -14
- package/src/network/pool-health.ts +49 -61
- package/src/network/rate-limit-verdict.ts +1 -1
- package/src/network/ssrf.ts +6 -6
- package/src/observability/event-log.ts +7 -361
- package/src/observability/event-query.ts +18 -4
- package/src/observability/performance-metrics.ts +9 -0
- package/src/observability/runtime-metrics.ts +14 -9
- package/src/observability/telemetry-buffer.ts +15 -25
- package/src/observability/telemetry-reconciliation.ts +9 -37
- package/src/observability/telemetry-row.ts +125 -0
- package/src/persistence/db-mode.ts +4 -1
- package/src/persistence/dialect.ts +30 -0
- package/src/persistence/migrate.ts +6 -3
- package/src/persistence/schema.ts +28 -2
- package/src/persistence/ttl-cache.ts +20 -2
- package/src/protocol/codecs.ts +1 -1
- package/src/protocol/dsml.ts +35 -87
- package/src/protocol/gateway-error.ts +1 -0
- package/src/protocol/markup-stream-parser.ts +195 -0
- package/src/protocol/reasoning-text.ts +19 -0
- package/src/protocol/reasoning.ts +1 -1
- package/src/protocol/request/chat.ts +14 -14
- package/src/protocol/request/gemini.ts +8 -2
- package/src/protocol/request/messages.ts +4 -5
- package/src/protocol/response/chat.ts +12 -20
- package/src/protocol/response/gemini.ts +6 -1
- package/src/protocol/response/omp-tool-call-markup.ts +43 -86
- package/src/protocol/response/responses.ts +18 -2
- package/src/protocol/sse-decoder.ts +25 -8
- package/src/protocol/stop-reason.ts +23 -1
- package/src/protocol/stream-error-frames.ts +13 -3
- package/src/protocol/think-split.ts +112 -0
- package/src/protocol/translation/dialect/qwen.ts +26 -58
- package/src/protocol/translation/quirks.ts +1 -1
- package/src/protocol/translation/tool-repair.ts +2 -2
- package/src/protocol/upstream/send-claude-messages.ts +15 -4
- package/src/protocol/upstream-parsing.ts +1 -1
- package/src/providers/adapters/compatible-adapter.ts +78 -17
- package/src/providers/authentication/oauth-client.ts +1 -1
- package/src/providers/authentication/oauth-device-flow.ts +98 -8
- package/src/providers/authentication/oauth-refresh-service.ts +153 -59
- package/src/providers/configured-provider.ts +148 -0
- package/src/providers/default-registry.ts +117 -27
- package/src/providers/discovery/discovery-types.ts +12 -0
- package/src/providers/discovery/probe-phases.ts +74 -33
- package/src/providers/discovery/probing-service.ts +293 -165
- package/src/providers/integrations/alibabacp/alibabacp-models.ts +1 -2
- package/src/providers/integrations/{dashscope-models.ts → alibabacp/dashscope-models.ts} +2 -2
- package/src/providers/integrations/antigravity/antigravity-protocol.ts +121 -2
- package/src/providers/integrations/antigravity/antigravity.ts +13 -2
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +72 -34
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +191 -35
- package/src/providers/integrations/buddy/buddy-checkin.ts +17 -39
- package/src/providers/integrations/buddy/buddy-conversation-headers.ts +2 -2
- package/src/providers/integrations/buddy/buddy-discovery-shared.ts +5 -5
- package/src/providers/integrations/buddy/buddy-identity.ts +89 -6
- package/src/providers/integrations/buddy/buddy-media.ts +4 -5
- package/src/providers/integrations/buddy/buddy-oauth-shared.ts +2 -2
- package/src/providers/integrations/buddy/buddy-quota-shared.ts +1 -1
- package/src/providers/integrations/buddy/buddy-usage-shared.ts +1 -3
- package/src/providers/integrations/buddy/capabilities.ts +6 -43
- package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +2 -1
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +23 -1
- package/src/providers/integrations/buddy/codebuddy/provider.ts +6 -2
- package/src/providers/integrations/buddy/codebuddy/shared.ts +38 -27
- package/src/providers/integrations/claude/claude-quota.ts +14 -17
- package/src/providers/integrations/cline/cline.ts +2 -2
- package/src/providers/integrations/commandcode/quota.ts +22 -28
- package/src/providers/integrations/cursor/cursor-auth.ts +0 -8
- package/src/providers/integrations/cursor/cursor-catalog.ts +2 -0
- package/src/providers/integrations/cursor/cursor-exec.ts +745 -0
- package/src/providers/integrations/cursor/cursor-http2.ts +2 -0
- package/src/providers/integrations/cursor/cursor-request.ts +35 -20
- package/src/providers/integrations/cursor/cursor-shared.ts +26 -5
- package/src/providers/integrations/cursor/cursor.ts +139 -70
- package/src/providers/integrations/cursor/protocol/messages.ts +22 -29
- package/src/providers/integrations/cursor/protocol/protobuf-schema.ts +37 -0
- package/src/providers/integrations/cursor/protocol/tool-canonical.ts +26 -100
- package/src/providers/integrations/devin/devin-auth.ts +353 -0
- package/src/providers/integrations/devin/devin-catalog.ts +207 -0
- package/src/providers/integrations/devin/devin-http.ts +91 -0
- package/src/providers/integrations/devin/devin-quota.ts +214 -0
- package/src/providers/integrations/devin/devin-shared.ts +111 -0
- package/src/providers/integrations/devin/devin.ts +539 -0
- package/src/providers/integrations/devin/protocol/devin-proto.ts +558 -0
- package/src/providers/integrations/devin/protocol/framing.ts +124 -0
- package/src/providers/integrations/factory/factory-oauth.ts +17 -44
- package/src/providers/integrations/factory/factory.ts +2 -2
- package/src/providers/integrations/freebuff/assets/official-system-manager.txt +88 -0
- package/src/providers/integrations/freebuff/assets/official-system-worker.txt +57 -0
- package/src/providers/integrations/freebuff/assets/official-tools.json +1070 -0
- package/src/providers/integrations/freebuff/freebuff-catalog.ts +120 -0
- package/src/providers/integrations/freebuff/freebuff-constants.ts +1233 -0
- package/src/providers/integrations/freebuff/freebuff-discovery.ts +108 -0
- package/src/providers/integrations/freebuff/freebuff-oauth.ts +240 -0
- package/src/providers/integrations/freebuff/freebuff-quota.ts +260 -0
- package/src/providers/integrations/freebuff/freebuff-virtual.ts +89 -0
- package/src/providers/integrations/freebuff/freebuff.ts +682 -0
- package/src/providers/integrations/freepi/freepi-catalog.ts +60 -0
- package/src/providers/integrations/freepi/freepi-discovery.ts +82 -0
- package/src/providers/integrations/freepi/freepi-headers.ts +21 -0
- package/src/providers/integrations/freepi/freepi-oauth.ts +142 -0
- package/src/providers/integrations/freepi/freepi-protocol.ts +242 -0
- package/src/providers/integrations/freepi/freepi-quota.ts +126 -0
- package/src/providers/integrations/freepi/freepi-realm.ts +35 -0
- package/src/providers/integrations/freepi/freepi.ts +292 -0
- package/src/providers/integrations/gemini.ts +2 -2
- package/src/providers/integrations/grok/grok-oauth.ts +48 -45
- package/src/providers/integrations/grok/grok-ratelimit-store.ts +10 -17
- package/src/providers/integrations/grok/grok-turn-index.ts +11 -16
- package/src/providers/integrations/hermes/hermes-oauth.ts +94 -0
- package/src/providers/integrations/{hermes.ts → hermes/hermes.ts} +13 -21
- package/src/providers/integrations/hermes/index.ts +2 -0
- package/src/providers/integrations/kiro/kiro-catalog.ts +17 -1
- package/src/providers/integrations/kiro/kiro-discovery.ts +10 -12
- package/src/providers/integrations/kiro/kiro-quota.ts +8 -4
- package/src/providers/integrations/kiro/kiro.ts +15 -19
- package/src/providers/integrations/ollamacloud/index.ts +2 -0
- package/src/providers/integrations/{ollamacloud-models.ts → ollamacloud/models.ts} +2 -2
- package/src/providers/integrations/{ollamacloud-quota.ts → ollamacloud/quota.ts} +3 -3
- package/src/providers/integrations/opencode/fingerprint.ts +1 -1
- package/src/providers/integrations/opencode/opencode-go-quota.ts +2 -8
- package/src/providers/integrations/perch/perch-catalog.ts +105 -0
- package/src/providers/integrations/perch/perch-credential.ts +25 -0
- package/src/providers/integrations/perch/perch-oauth.ts +170 -0
- package/src/providers/integrations/perch/perch-protocol.ts +208 -0
- package/src/providers/integrations/perch/perch-quota.ts +123 -0
- package/src/providers/integrations/perch/perch-realm.ts +82 -0
- package/src/providers/integrations/perch/perch-system-prompt.ts +13 -0
- package/src/providers/integrations/perch/perch.ts +305 -0
- package/src/providers/integrations/perch/saffron-system-prompt.txt +8 -0
- package/src/providers/integrations/qoder/chat-proto.ts +328 -328
- package/src/providers/integrations/qoder/claim-qoder-credits.ts +19 -60
- package/src/providers/integrations/qoder/qoder-adapter.ts +90 -40
- package/src/providers/integrations/qoder/qoder-auth.ts +101 -29
- package/src/providers/integrations/qoder/qoder-cache.ts +32 -0
- package/src/providers/integrations/qoder/qoder-catalog.ts +26 -6
- package/src/providers/integrations/qoder/qoder-chat.ts +7 -21
- package/src/providers/integrations/qoder/qoder-checkin.ts +6 -6
- package/src/providers/integrations/qoder/qoder-grpc.ts +210 -7
- package/src/providers/integrations/qoder/qoder-ide.ts +5 -108
- package/src/providers/integrations/qoder/qoder-identity-cache.ts +184 -0
- package/src/providers/integrations/qoder/qoder-native-identity-store.ts +83 -18
- package/src/providers/integrations/qoder/qoder-realm.ts +9 -6
- package/src/providers/integrations/qoder/qoder-umid.ts +109 -342
- package/src/providers/integrations/qoder/qoder-virtual.ts +33 -1
- package/src/providers/integrations/qoder/qoder.ts +3 -7
- package/src/providers/integrations/trae/trae-catalog.ts +9 -14
- package/src/providers/integrations/trae/trae-headers.ts +36 -11
- package/src/providers/integrations/trae/trae-oauth.ts +14 -17
- package/src/providers/integrations/trae/trae-protocol.ts +12 -11
- package/src/providers/integrations/trae/trae-quota.ts +96 -36
- package/src/providers/integrations/trae/trae-realm.ts +74 -15
- package/src/providers/integrations/trae/trae.ts +99 -26
- package/src/providers/integrations/zcode-start-plan/provider.ts +226 -23
- package/src/providers/integrations/zcode-start-plan/quota.ts +19 -2
- package/src/providers/integrations/zro/zro.ts +204 -0
- package/src/providers/operations/account-error-classification.ts +235 -278
- package/src/providers/operations/account-health-service.ts +273 -26
- package/src/providers/operations/account-refresh-status.ts +269 -0
- package/src/providers/operations/account-tier.ts +66 -0
- package/src/providers/operations/client-version-resolver.ts +1 -1
- package/src/providers/operations/client-versions.ts +42 -150
- package/src/providers/operations/cooldown-rules.ts +56 -0
- package/src/providers/operations/{model-catalog-cache.ts → provider-caches.ts} +68 -16
- package/src/providers/operations/provider-catalog-seeder.ts +4 -0
- package/src/providers/operations/provider-catalog-service.ts +1 -1
- package/src/providers/operations/provider-credential-service.ts +102 -32
- package/src/providers/operations/provider-rules/github.ts +48 -0
- package/src/providers/operations/provider-rules/grok.ts +33 -0
- package/src/providers/operations/provider-rules/inception.ts +49 -0
- package/src/providers/operations/provider-rules/index.ts +32 -0
- package/src/providers/operations/provider-rules/perch.ts +37 -0
- package/src/providers/operations/provider-rules/qoder.ts +70 -0
- package/src/providers/operations/provider-rules/trae.ts +81 -0
- package/src/providers/provider-contracts.ts +33 -2
- package/src/providers/{provider-credential-resolver.ts → provider-credentials.ts} +40 -3
- package/src/providers/provider-metadata.ts +99 -14
- package/src/providers/provider-model-definition.ts +36 -0
- package/src/providers/provider-probe.ts +15 -1
- package/src/providers/provider-registry.ts +42 -33
- package/src/providers/quota/quota-limit-windows.ts +16 -5
- package/src/providers/quota/quota-support.ts +1 -1
- package/src/providers/quota/quota-window-parser.ts +90 -18
- package/src/providers/quota/refresh.ts +96 -18
- package/src/security/abuse.ts +71 -0
- package/src/security/api-key-auth-cache.ts +1 -1
- package/src/security/api-key-auth.ts +6 -2
- package/src/transport/dispatch/attempt-finalize.ts +14 -13
- package/src/transport/dispatch/attempt-loop.ts +71 -17
- package/src/transport/dispatch/generation.ts +2 -8
- package/src/transport/dispatch/native-shared.ts +25 -0
- package/src/transport/dispatch/proxy-request.ts +9 -46
- package/src/transport/dispatch/responses-compact.ts +2 -8
- package/src/transport/dispatch/streaming/finalize.ts +9 -6
- package/src/transport/dispatch/streaming/stream.ts +9 -36
- package/src/transport/dispatch/systemone.ts +2 -8
- package/src/transport/dispatch/token-count.ts +2 -6
- package/src/transport/dispatch/upstream.ts +67 -1
- package/src/transport/drain-endpoint.ts +31 -4
- package/src/transport/failure-policy.ts +129 -37
- package/src/transport/guards/body-policy.ts +7 -1
- package/src/transport/guards/error-lifecycle.ts +13 -1
- package/src/transport/guards/request-context.ts +22 -14
- package/src/transport/request/preparer.ts +81 -71
- package/src/transport/routing/bounded-lru.ts +14 -0
- package/src/transport/routing/drain-reserve.ts +129 -0
- package/src/transport/routing/plan-gate.ts +7 -1
- package/src/transport/routing/round-robin.ts +60 -0
- package/src/transport/routing/route-catalog.ts +123 -111
- package/src/transport/routing/route-model.ts +51 -9
- package/src/transport/routing/router.ts +418 -33
- package/src/transport/surface/adapters.ts +11 -2
- package/src/transport/surface/chat/adapter.ts +2 -2
- package/src/transport/surface/chat/encode.ts +47 -4
- package/src/transport/surface/chat/parse.ts +2 -12
- package/src/transport/surface/completion.ts +44 -33
- package/src/transport/surface/content-parts.ts +21 -0
- package/src/transport/surface/messages/adapter.ts +9 -2
- package/src/transport/surface/messages/encode.ts +11 -3
- package/src/transport/surface/messages/parse.ts +2 -1
- package/src/transport/surface/messages/stream.ts +33 -17
- package/src/transport/surface/responses/encode.ts +34 -6
- package/src/transport/surface/responses/parse.ts +3 -11
- package/src/workers/daily-checkin.ts +1 -1
- package/src/workers/quota-refresh-sweep.ts +1 -1
- package/src/workers/scheduler.ts +22 -4
- package/dist/dashboard/assets/BannedPage-DeIbIiBU.js +0 -1
- package/dist/dashboard/assets/BannedPage-DeIbIiBU.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-BRWKPQPf.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-BP-Y5iHN.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-B7JHozVa.js +0 -1
- package/dist/dashboard/assets/CombosPage-B7JHozVa.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-GX7iGGcu.js +0 -1
- package/dist/dashboard/assets/ConsoleLogPage-GX7iGGcu.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-HVonbGEa.js +0 -1
- package/dist/dashboard/assets/CustomizationPage-HVonbGEa.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-xaut4OJ9.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-xaut4OJ9.js.gz +0 -0
- package/dist/dashboard/assets/InjectSystemPromptPanel-Cv-2Ol_K.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-bKfz_b1K.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-B-WQxx_H.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-Z9pJZFo8.js +0 -1
- package/dist/dashboard/assets/OverviewPage-Z9pJZFo8.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CMeDBxR1.js +0 -7
- package/dist/dashboard/assets/ProviderDetailPage-CMeDBxR1.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js +0 -1
- package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-BXPdOpG4.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-BXPdOpG4.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-emlTuxz6.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-CMJKNYWb.js +0 -1
- package/dist/dashboard/assets/QuotaPage-CMJKNYWb.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-BSy7EwGo.js +0 -1
- package/dist/dashboard/assets/SettingsPage-BSy7EwGo.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-CUZrKs9r.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-jWmUdDpy.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-5F9tGVGG.js +0 -56
- package/dist/dashboard/assets/UsagePage-5F9tGVGG.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-KvX83mRe.js +0 -1
- package/dist/dashboard/assets/api-keys-KvX83mRe.js.gz +0 -0
- package/dist/dashboard/assets/app-BEsU8hKs.css +0 -2
- package/dist/dashboard/assets/app-BEsU8hKs.css.gz +0 -0
- package/dist/dashboard/assets/assert-CidQVoNo.js.gz +0 -0
- package/dist/dashboard/assets/badge-DyI_mJfi.js.gz +0 -0
- package/dist/dashboard/assets/banner-B-yXtCqb.js.gz +0 -0
- package/dist/dashboard/assets/brand/bansos_page.png +0 -0
- package/dist/dashboard/assets/brand/denia_bansos.jpg +0 -0
- package/dist/dashboard/assets/brand/favicon.jpg +0 -0
- package/dist/dashboard/assets/brand/login_register.jpg +0 -0
- package/dist/dashboard/assets/brand/login_register_initial.jpg +0 -0
- package/dist/dashboard/assets/brand/og_bansos.jpg +0 -0
- package/dist/dashboard/assets/brand/og_image.jpg +0 -0
- package/dist/dashboard/assets/brand/sidebar_ico.jpg +0 -0
- package/dist/dashboard/assets/card-_HwAEqFF.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js +0 -1
- package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-ByjCk0o9.js.gz +0 -0
- package/dist/dashboard/assets/dialog-BZhOlfz-.js.gz +0 -0
- package/dist/dashboard/assets/drawer-BADqIRYP.js.gz +0 -0
- package/dist/dashboard/assets/entry-COaC-irN.js.gz +0 -0
- package/dist/dashboard/assets/entry-Ca7LSCet.js +0 -4
- package/dist/dashboard/assets/entry-Ca7LSCet.js.gz +0 -0
- package/dist/dashboard/assets/entry-PDd27uCd.js +0 -1
- package/dist/dashboard/assets/entry-PDd27uCd.js.gz +0 -0
- package/dist/dashboard/assets/formatters-DcHJyRiu.js.gz +0 -0
- package/dist/dashboard/assets/index-CgKmFngv.js.gz +0 -0
- package/dist/dashboard/assets/inline-B2BirKry.js.gz +0 -0
- package/dist/dashboard/assets/input-D-qV9dKY.js.gz +0 -0
- package/dist/dashboard/assets/layout-B7tVWhtV.js.gz +0 -0
- package/dist/dashboard/assets/list-row-CtkGE3bF.js.gz +0 -0
- package/dist/dashboard/assets/live-DGizF06-.js.gz +0 -0
- package/dist/dashboard/assets/minimize-2-BTmMTK4o.js +0 -1
- package/dist/dashboard/assets/minimize-2-BTmMTK4o.js.gz +0 -0
- package/dist/dashboard/assets/mutation-GU7XdEjz.js.gz +0 -0
- package/dist/dashboard/assets/network-C9UE-bOr.js.gz +0 -0
- package/dist/dashboard/assets/numbers-BnAd22a_.js.gz +0 -0
- package/dist/dashboard/assets/pagination-BAywyMzS.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-YSoQOUh6.js.gz +0 -0
- package/dist/dashboard/assets/quota-DJ1QlAOq.js +0 -1
- package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
- package/dist/dashboard/assets/routing-fnCsxgNO.js +0 -1
- package/dist/dashboard/assets/routing-fnCsxgNO.js.gz +0 -0
- package/dist/dashboard/assets/select-BCOcv8WK.js.gz +0 -0
- package/dist/dashboard/assets/settings-BxslsErn.js +0 -1
- package/dist/dashboard/assets/settings-BxslsErn.js.gz +0 -0
- package/dist/dashboard/assets/stack-ByKt3Q1g.js.gz +0 -0
- package/dist/dashboard/assets/state-DXbGTx7N.js.gz +0 -0
- package/dist/dashboard/assets/switch-ZeTsW623.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-CbCP4w1o.js.gz +0 -0
- package/dist/dashboard/console-backgrounds.jpg +0 -0
- package/dist/dashboard/console-backgrounds_initial.jpg +0 -0
- package/dist/dashboard/donation/qris-risun-marke.png +0 -0
- package/src/providers/bundled-discovery-providers.ts +0 -98
- package/src/providers/configured-openai-providers.ts +0 -120
- package/src/providers/integrations/buddy/turing-device-token.ts +0 -262
- package/src/providers/integrations/buddy/workbuddy/cn-oauth.ts +0 -30
- package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +0 -58
- package/src/providers/integrations/buddy/workbuddy/oauth.ts +0 -31
- package/src/providers/integrations/buddy/workbuddy/provider.ts +0 -209
- package/src/providers/integrations/buddy/workbuddy/quota.ts +0 -161
- package/src/providers/integrations/buddy/workbuddy/shared.ts +0 -285
- package/src/providers/operations/model-discovery-cache.ts +0 -34
- package/src/providers/operations/probe-fetch.ts +0 -15
- package/src/providers/operations/provider-version-cache.ts +0 -30
- package/src/providers/provider-capabilities.ts +0 -33
- package/src/providers/provider-credential-envelope.ts +0 -43
- /package/dist/dashboard/assets/{app-CTxoqyYB.js → app-DIXLjojg.js} +0 -0
- /package/dist/dashboard/assets/{app-CTxoqyYB.js.gz → app-DIXLjojg.js.gz} +0 -0
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { isRecord } from "../../protocol/codecs";
|
|
11
11
|
import { GatewayError } from "../../protocol/gateway-error";
|
|
12
12
|
import { BUDDY_PROVIDER_IDS } from "../provider-metadata";
|
|
13
|
+
import type { AccountTier } from "./account-tier";
|
|
13
14
|
import { parseProviderResetDuration, parseUpstreamBackoff } from "../../protocol/upstream-parsing";
|
|
14
15
|
import {
|
|
15
16
|
resolveAccountAuthRejectedCooldownMs,
|
|
@@ -22,17 +23,17 @@ import {
|
|
|
22
23
|
resolveAccountTransientCooldownMs,
|
|
23
24
|
resolveAccountUnclassifiedCooldownMs,
|
|
24
25
|
} from "../../config/cooldowns";
|
|
26
|
+
import {
|
|
27
|
+
githubRules,
|
|
28
|
+
qoderPlanGatedRule,
|
|
29
|
+
qoderModelQuotaRule,
|
|
30
|
+
traeRules,
|
|
31
|
+
inceptionRules,
|
|
32
|
+
perchRules,
|
|
33
|
+
isGrokFreeTierExhaustion,
|
|
34
|
+
GROK_QUOTA_COOLDOWN_MS,
|
|
35
|
+
} from "./provider-rules";
|
|
25
36
|
const DEFAULT_RATE_LIMIT_COOLDOWN_MS = resolveAccountRateLimitCooldownMs;
|
|
26
|
-
/**
|
|
27
|
-
* Inception's per-minute limit window (1 minute).
|
|
28
|
-
*
|
|
29
|
-
* Inception's Free tier throttles per minute (1 000 requests / 1M input
|
|
30
|
-
* tokens / 100K output tokens) and the window resets every minute. The generic
|
|
31
|
-
* 15-minute rate-limit fallback would park a healthy key long after the window
|
|
32
|
-
* refilled; 1 minute is the provider's own cadence, hardcoded because it is a
|
|
33
|
-
* fact about the upstream, not a deployment knob.
|
|
34
|
-
*/
|
|
35
|
-
const INCEPTION_RATE_LIMIT_COOLDOWN_MS = 60_000;
|
|
36
37
|
// Fallback when the error states no usable duration: deliberately short (1h,
|
|
37
38
|
// not 24h) — quota state is re-probed on the next failure, so an unknown
|
|
38
39
|
// reset cadence self-corrects instead of parking the account for a day.
|
|
@@ -51,39 +52,13 @@ export const COOLDOWN_RECOVERY_BATCH = 500;
|
|
|
51
52
|
* account returns to rotation quickly.
|
|
52
53
|
*/
|
|
53
54
|
const UNCLASSIFIED_COOLDOWN_MS = resolveAccountUnclassifiedCooldownMs;
|
|
54
|
-
/**
|
|
55
|
-
* xAI Grok Build's free tier resets on a rolling 24-hour window, so its
|
|
56
|
-
* exhaustion is a full-day quota cooldown — never the generic 1h fallback,
|
|
57
|
-
* which let the account re-enter rotation inside the window.
|
|
58
|
-
*/
|
|
59
|
-
const GROK_QUOTA_COOLDOWN_MS = 24 * 60 * 60 * 1000;
|
|
60
|
-
/**
|
|
61
|
-
* Trae answers `4013 "AI service is not available in the current region now."`
|
|
62
|
-
* when the upstream does not serve the account's region. A day because the
|
|
63
|
-
* verdict is durable: it reproduces on every request until the region opens,
|
|
64
|
-
* and the account must not be re-dialled every thirty seconds for the whole
|
|
65
|
-
* window.
|
|
66
|
-
*/
|
|
67
|
-
const TRAE_REGION_COOLDOWN_MS = 24 * 60 * 60 * 1000;
|
|
68
|
-
/**
|
|
69
|
-
* Trae's free-tier quota (`4008` / "exceeded the quota") resets daily, so its
|
|
70
|
-
* exhaustion is a full-day model backoff — never the generic 1h fallback,
|
|
71
|
-
* which let the account re-enter rotation inside the window.
|
|
72
|
-
*/
|
|
73
|
-
const TRAE_QUOTA_COOLDOWN_MS = 24 * 60 * 60 * 1000;
|
|
74
|
-
/**
|
|
75
|
-
* Qoder's quota verdicts (`110` billing-daily-count, the upstream-499 close)
|
|
76
|
-
* are per-model: one model's spend cap does not drain another. A day matches
|
|
77
|
-
* the daily billing window the provider names.
|
|
78
|
-
*/
|
|
79
|
-
const QODER_QUOTA_COOLDOWN_MS = 24 * 60 * 60 * 1000;
|
|
80
55
|
/**
|
|
81
56
|
* The buddy family's policy/channel block (`11140`, `11128`) is a *base* backoff,
|
|
82
57
|
* not a flat penalty — see {@link resolveAccountPolicyBlockCooldownMs}. It used
|
|
83
58
|
* to be two constants, 24h for the content block and 6h for the channel one,
|
|
84
59
|
* both applied on the first observation. That was wrong in the direction that
|
|
85
60
|
* costs most: `11140` is also what a wrong desktop `User-Agent` brand answers
|
|
86
|
-
* (`
|
|
61
|
+
* (`codebuddy/shared.ts`), so after we fixed a header regression every buddy
|
|
87
62
|
* account stayed dead for up to a further day, and one prompt that failed a
|
|
88
63
|
* safety review cost a whole account a day. As a base it escalates through the
|
|
89
64
|
* consecutive-failure ladder, which tells the two causes apart by what they do
|
|
@@ -179,12 +154,6 @@ export type AccountErrorCategory =
|
|
|
179
154
|
| "model_capacity"
|
|
180
155
|
| "model_unreachable"
|
|
181
156
|
| "auth_invalidated"
|
|
182
|
-
/**
|
|
183
|
-
* Deprecated: an upstream ban. Kept so existing health rows keep rendering
|
|
184
|
-
* instead of showing a blank cause. No longer produced — see
|
|
185
|
-
* `policy_blocked_banned` and `policy_blocked_invalid_request`.
|
|
186
|
-
*/
|
|
187
|
-
| "policy_blocked"
|
|
188
157
|
/**
|
|
189
158
|
* The upstream banned this credential: a verdict on the account, e.g. buddy
|
|
190
159
|
* `11140` "request illegal" with "Your account has been restricted due to
|
|
@@ -259,6 +228,12 @@ export interface AccountFailureEvidence {
|
|
|
259
228
|
* limit the provider said resets in 23 days.
|
|
260
229
|
*/
|
|
261
230
|
readonly upstreamBody?: unknown;
|
|
231
|
+
/**
|
|
232
|
+
* The account's tier, derived from its plan (`mapPlanToTier`). Read only by
|
|
233
|
+
* the writer when it resolves tier-aware cooldown rules; the classifier
|
|
234
|
+
* itself does not branch on it.
|
|
235
|
+
*/
|
|
236
|
+
readonly accountTier?: AccountTier;
|
|
262
237
|
}
|
|
263
238
|
|
|
264
239
|
export interface AccountErrorClassification {
|
|
@@ -290,6 +265,15 @@ export interface AccountErrorClassification {
|
|
|
290
265
|
* reads it: a provider-stated window is the truth and is never multiplied.
|
|
291
266
|
*/
|
|
292
267
|
readonly providerStated?: boolean;
|
|
268
|
+
/**
|
|
269
|
+
* True when the classification is a verdict on the *credential* (not a
|
|
270
|
+
* transient backoff): the request should rotate to a sibling account now
|
|
271
|
+
* instead of waiting out the cooldown on this one. Usage-limit exhaustion,
|
|
272
|
+
* spending caps, and banned/revoked credentials rotate; per-minute
|
|
273
|
+
* throttles (TPM/RPM), model-capacity overload, and concurrency caps back
|
|
274
|
+
* off on the same account instead.
|
|
275
|
+
*/
|
|
276
|
+
readonly rotateAccount?: boolean;
|
|
293
277
|
/**
|
|
294
278
|
* True when the upstream reported the model as one it does not serve (the
|
|
295
279
|
* buddy family's `11102`), rather than one this account merely cannot reach.
|
|
@@ -299,6 +283,20 @@ export interface AccountErrorClassification {
|
|
|
299
283
|
* apart — it needs to know whether to count the hit and escalate the window.
|
|
300
284
|
*/
|
|
301
285
|
readonly modelUnsupported?: boolean;
|
|
286
|
+
/**
|
|
287
|
+
* This quota verdict closes only the requested model on an otherwise usable
|
|
288
|
+
* account. Kept separate from `category`: quota exhaustion remains the
|
|
289
|
+
* operator-facing cause, while the writer routes it through modelCooldowns.
|
|
290
|
+
*/
|
|
291
|
+
readonly modelScoped?: boolean;
|
|
292
|
+
/**
|
|
293
|
+
* Where the cooldown duration came from (D18). A duration resolved from a
|
|
294
|
+
* tier/provider rule (`provider-rule`) is a policy decision and must NOT go
|
|
295
|
+
* through `escalateFallbackCooldown` — the 1h escalation cap would cut the
|
|
296
|
+
* 24h Trae rule down to an hour on the first failure. Only a pure fallback
|
|
297
|
+
* (`fallback`) escalates.
|
|
298
|
+
*/
|
|
299
|
+
readonly ruleSource?: "provider-rule" | "fallback";
|
|
302
300
|
}
|
|
303
301
|
|
|
304
302
|
/**
|
|
@@ -344,6 +342,26 @@ export function escalateFallbackCooldown(
|
|
|
344
342
|
return Math.min(cooldownMs * 2 ** level, resolveAccountCooldownEscalationMaxMs());
|
|
345
343
|
}
|
|
346
344
|
|
|
345
|
+
/**
|
|
346
|
+
* Returns a cooldown duration with the D18 escalation split applied.
|
|
347
|
+
*
|
|
348
|
+
* A duration that came from a tier/provider rule (`ruleSource === "provider-rule"`)
|
|
349
|
+
* is a policy decision and must never pass through
|
|
350
|
+
* {@link escalateFallbackCooldown}: the 1h cap would truncate the 24h Trae
|
|
351
|
+
* free-tier rule to one hour on the first failure. Only a pure fallback
|
|
352
|
+
* (`ruleSource === "fallback"`, or unset for callers that predate the split)
|
|
353
|
+
* escalates.
|
|
354
|
+
*/
|
|
355
|
+
export function applyRuleEscalation(
|
|
356
|
+
cooldownMs: number,
|
|
357
|
+
providerStated: boolean,
|
|
358
|
+
consecutiveFailures: number,
|
|
359
|
+
ruleSource: "provider-rule" | "fallback" | undefined,
|
|
360
|
+
): number {
|
|
361
|
+
if (ruleSource === "provider-rule") return cooldownMs;
|
|
362
|
+
return escalateFallbackCooldown(cooldownMs, providerStated, consecutiveFailures);
|
|
363
|
+
}
|
|
364
|
+
|
|
347
365
|
/**
|
|
348
366
|
* The later of a candidate deadline and the one already recorded.
|
|
349
367
|
*
|
|
@@ -458,26 +476,6 @@ function banWording(lower: string): boolean {
|
|
|
458
476
|
);
|
|
459
477
|
}
|
|
460
478
|
|
|
461
|
-
/**
|
|
462
|
-
* Whether GitHub Copilot stated the account lacks a license — a ban-shaped
|
|
463
|
-
* verdict on the account, not a refreshable auth rejection.
|
|
464
|
-
*
|
|
465
|
-
* Observed live (2026-11-03): the health log showed
|
|
466
|
-
* `unauthorized: not licensed to use Copilot` and `forbidden` on
|
|
467
|
-
* `gpt-6-luna`. Copilot answers 401 for a bad credential and 403 for a
|
|
468
|
-
* license/entitlement refusal, so `forbidden`/`unauthorized` on this surface
|
|
469
|
-
* is the same verdict. No amount of waiting or refreshing changes it — the
|
|
470
|
-
* subscription cannot serve this credential.
|
|
471
|
-
*/
|
|
472
|
-
function copilotNotLicensed(lower: string): boolean {
|
|
473
|
-
return (
|
|
474
|
-
lower.includes("not licensed") ||
|
|
475
|
-
lower.includes("forbidden") ||
|
|
476
|
-
lower.includes("unauthorized") ||
|
|
477
|
-
lower.includes("license")
|
|
478
|
-
);
|
|
479
|
-
}
|
|
480
|
-
|
|
481
479
|
/**
|
|
482
480
|
* Bounded to a day, matching `MAX_COOLDOWN_MS` in the failure policy: a stated
|
|
483
481
|
* window longer than that is a monthly or plan-level limit, and parking an
|
|
@@ -496,6 +494,19 @@ function tryParseUpstreamBody(text: string): unknown {
|
|
|
496
494
|
}
|
|
497
495
|
}
|
|
498
496
|
|
|
497
|
+
/** Reads `error.message` from a parsed upstream body object. */
|
|
498
|
+
function nestedErrorMessage(body: unknown): string {
|
|
499
|
+
if (typeof body !== "object" || body === null) return "";
|
|
500
|
+
const record = body as Record<string, unknown>;
|
|
501
|
+
if (typeof record["message"] === "string") return record["message"];
|
|
502
|
+
const error = record["error"];
|
|
503
|
+
if (typeof error === "object" && error !== null) {
|
|
504
|
+
const message = (error as Record<string, unknown>)["message"];
|
|
505
|
+
if (typeof message === "string") return message;
|
|
506
|
+
}
|
|
507
|
+
return "";
|
|
508
|
+
}
|
|
509
|
+
|
|
499
510
|
/**
|
|
500
511
|
* Reads a reset window the upstream stated in structured fields.
|
|
501
512
|
*
|
|
@@ -509,13 +520,26 @@ function tryParseUpstreamBody(text: string): unknown {
|
|
|
509
520
|
* (`resets_at`, seconds or milliseconds) and a relative delay
|
|
510
521
|
* (`resets_in_seconds`). The later of the two wins, since they are the same
|
|
511
522
|
* claim computed at slightly different instants.
|
|
523
|
+
*
|
|
524
|
+
* Structured Google rpc error details are read too — Vertex/Antigravity returns
|
|
525
|
+
* the reset inside `details[]` as `ErrorInfo.metadata.quotaResetDelay` /
|
|
526
|
+
* `quotaResetTimeStamp` and `RetryInfo.retryDelay` (a Go duration like
|
|
527
|
+
* `596805.017558959s`). Without this a "Resets in 165h46m45s" refusal cooled
|
|
528
|
+
* for the 15-minute fallback and re-entered rotation hours before it could
|
|
529
|
+
* serve. The prose `message` is parsed through `parseProviderResetDuration` as
|
|
530
|
+
* a last resort, because several providers state the window only there.
|
|
512
531
|
*/
|
|
513
532
|
function parseUpstreamBodyReset(body: unknown): number | null {
|
|
514
533
|
if (typeof body !== "object" || body === null) return null;
|
|
515
534
|
const record = body as Record<string, unknown>;
|
|
516
535
|
// Some providers nest under `error`, some return it at the top level.
|
|
517
|
-
const
|
|
536
|
+
const errorRecord = (record["error"] as Record<string, unknown> | undefined) ?? undefined;
|
|
537
|
+
const candidates = [record, errorRecord];
|
|
518
538
|
let best: number | null = null;
|
|
539
|
+
const consider = (delay: number | null | undefined): void => {
|
|
540
|
+
if (typeof delay !== "number" || !Number.isFinite(delay) || delay <= 0) return;
|
|
541
|
+
best = best === null ? delay : Math.max(best, delay);
|
|
542
|
+
};
|
|
519
543
|
for (const candidate of candidates) {
|
|
520
544
|
if (typeof candidate !== "object" || candidate === null) continue;
|
|
521
545
|
const now = Date.now();
|
|
@@ -526,8 +550,7 @@ function parseUpstreamBodyReset(body: unknown): number | null {
|
|
|
526
550
|
// milliseconds ~13, and the two are four orders of magnitude apart — so
|
|
527
551
|
// the smaller is scaled up rather than guessed from a threshold date.
|
|
528
552
|
const epochMs = absolute < 1e11 ? absolute * 1000 : absolute;
|
|
529
|
-
|
|
530
|
-
if (delay > 0) best = best === null ? delay : Math.max(best, delay);
|
|
553
|
+
consider(epochMs - now);
|
|
531
554
|
}
|
|
532
555
|
|
|
533
556
|
const relative =
|
|
@@ -535,19 +558,69 @@ function parseUpstreamBodyReset(body: unknown): number | null {
|
|
|
535
558
|
candidate["reset_in_seconds"] ??
|
|
536
559
|
candidate["retry_after_seconds"];
|
|
537
560
|
if (typeof relative === "number" && Number.isFinite(relative) && relative > 0) {
|
|
538
|
-
|
|
539
|
-
best = best === null ? delay : Math.max(best, delay);
|
|
561
|
+
consider(relative * 1000);
|
|
540
562
|
}
|
|
541
563
|
|
|
542
564
|
const minutes = candidate["limit_window_minutes"] ?? candidate["reset_in_minutes"];
|
|
543
565
|
if (typeof minutes === "number" && Number.isFinite(minutes) && minutes > 0) {
|
|
544
|
-
|
|
545
|
-
|
|
566
|
+
consider(minutes * 60_000);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// The body's own prose (both top-level and nested under `error`): providers
|
|
570
|
+
// that state the window only in the message ("Resets in 165h46m45s") are
|
|
571
|
+
// honored rather than cooled for the 15-minute fallback.
|
|
572
|
+
const prose = candidate["message"];
|
|
573
|
+
if (typeof prose === "string" && prose.length > 0) {
|
|
574
|
+
consider(parseProviderResetDuration(prose));
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Google rpc `details[]` (Vertex/Antigravity error frames): `ErrorInfo`
|
|
579
|
+
// metadata carries the reset delay/timestamp, `RetryInfo` carries the retry
|
|
580
|
+
// delay directly.
|
|
581
|
+
const details = errorRecord?.["details"];
|
|
582
|
+
if (Array.isArray(details)) {
|
|
583
|
+
for (const detail of details) {
|
|
584
|
+
if (typeof detail !== "object" || detail === null) continue;
|
|
585
|
+
const entry = detail as Record<string, unknown>;
|
|
586
|
+
const type = typeof entry["@type"] === "string" ? entry["@type"] : "";
|
|
587
|
+
if (type.includes("google.rpc.ErrorInfo")) {
|
|
588
|
+
const metadata = entry["metadata"] as Record<string, unknown> | undefined;
|
|
589
|
+
const delay = metadata?.["quotaResetDelay"] ?? metadata?.["quotaResetDelaySeconds"];
|
|
590
|
+
consider(parseGoDuration(delay));
|
|
591
|
+
const stamp =
|
|
592
|
+
metadata?.["quotaResetTimeStamp"] ?? metadata?.["quotaResetTimestamp"];
|
|
593
|
+
if (typeof stamp === "string" && stamp.length > 0) {
|
|
594
|
+
const t = new Date(stamp).getTime();
|
|
595
|
+
if (Number.isFinite(t)) consider(t - Date.now());
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (type.includes("google.rpc.RetryInfo")) {
|
|
599
|
+
consider(parseGoDuration(entry["retryDelay"]));
|
|
600
|
+
}
|
|
601
|
+
// Some providers put the retry delay on the detail without a type.
|
|
602
|
+
consider(parseGoDuration(entry["retryDelay"]));
|
|
546
603
|
}
|
|
547
604
|
}
|
|
548
605
|
return best === null ? null : Math.min(MAX_UPSTREAM_STATED_COOLDOWN_MS, Math.round(best));
|
|
549
606
|
}
|
|
550
607
|
|
|
608
|
+
/** Parses a Go duration string (`596805.017558959s`, `1h30m0s`) to ms. */
|
|
609
|
+
function parseGoDuration(value: unknown): number | null {
|
|
610
|
+
if (typeof value !== "string") return null;
|
|
611
|
+
const text = value.trim().replace(/\s+/g, "");
|
|
612
|
+
if (!/^(?:(\d+(?:\.\d+)?)h)?(?:(\d+(?:\.\d+)?)m)?(?:(\d+(?:\.\d+)?)s)?$/.test(text)) {
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
const match = /^(?:(\d+(?:\.\d+)?)h)?(?:(\d+(?:\.\d+)?)m)?(?:(\d+(?:\.\d+)?)s)?$/.exec(text);
|
|
616
|
+
if (!match) return null;
|
|
617
|
+
let ms = 0;
|
|
618
|
+
if (match[1] !== undefined) ms += Number(match[1]) * 3_600_000;
|
|
619
|
+
if (match[2] !== undefined) ms += Number(match[2]) * 60_000;
|
|
620
|
+
if (match[3] !== undefined) ms += Number(match[3]) * 1_000;
|
|
621
|
+
return ms > 0 ? ms : null;
|
|
622
|
+
}
|
|
623
|
+
|
|
551
624
|
|
|
552
625
|
interface AccountErrorContext {
|
|
553
626
|
readonly message: string;
|
|
@@ -580,6 +653,7 @@ interface AccountErrorContext {
|
|
|
580
653
|
explanation: string,
|
|
581
654
|
mutatesAccount?: boolean,
|
|
582
655
|
modelUnsupported?: boolean,
|
|
656
|
+
modelScoped?: boolean,
|
|
583
657
|
) => AccountErrorClassification;
|
|
584
658
|
}
|
|
585
659
|
|
|
@@ -598,14 +672,14 @@ interface AccountErrorContext {
|
|
|
598
672
|
* classifier's locals, no allocation per classification): the table is built
|
|
599
673
|
* once per call from values already computed for every arm.
|
|
600
674
|
*/
|
|
601
|
-
interface AccountErrorRule {
|
|
675
|
+
export interface AccountErrorRule {
|
|
602
676
|
readonly name: string;
|
|
603
677
|
readonly match: (signals: AccountErrorSignals) => boolean;
|
|
604
678
|
readonly decide: (signals: AccountErrorSignals) => AccountErrorClassification;
|
|
605
679
|
}
|
|
606
680
|
|
|
607
681
|
/** The shared signals every arm reads, computed once per classification. */
|
|
608
|
-
interface AccountErrorSignals {
|
|
682
|
+
export interface AccountErrorSignals {
|
|
609
683
|
readonly message: string;
|
|
610
684
|
readonly lower: string;
|
|
611
685
|
readonly folded: string;
|
|
@@ -635,10 +709,36 @@ interface AccountErrorSignals {
|
|
|
635
709
|
readonly queuedUpstream: boolean;
|
|
636
710
|
readonly validationRequired: boolean;
|
|
637
711
|
readonly validationUrl: string | undefined;
|
|
712
|
+
/** True when this failure should switch to a sibling account rather than wait. */
|
|
713
|
+
readonly rotateAccount: boolean;
|
|
638
714
|
}
|
|
639
715
|
|
|
640
716
|
const VALIDATION_URL_RE = /https?:\/\/[^\s"'\\]+/;
|
|
641
717
|
|
|
718
|
+
/**
|
|
719
|
+
* True when the wording names a *transient* rolling throttle — per-minute
|
|
720
|
+
* token/request windows (TPM/RPM), "too many requests", "per minute" — that
|
|
721
|
+
* self-heals within the window, so it must back off on the same account rather
|
|
722
|
+
* than rotate to a sibling. Mirrors oh-my-pi's `RATE_LIMIT_EXCEEDED` lane.
|
|
723
|
+
*/
|
|
724
|
+
function transientRateLimitWording(lower: string): boolean {
|
|
725
|
+
return (
|
|
726
|
+
lower.includes("rate limit") ||
|
|
727
|
+
lower.includes("rate_limit") ||
|
|
728
|
+
lower.includes("too many requests") ||
|
|
729
|
+
lower.includes("per minute") ||
|
|
730
|
+
lower.includes("tokens per minute") ||
|
|
731
|
+
lower.includes("requests per minute") ||
|
|
732
|
+
lower.includes("tpm limit") ||
|
|
733
|
+
lower.includes("rpm limit") ||
|
|
734
|
+
lower.includes("tpm exhausted") ||
|
|
735
|
+
lower.includes("rpm exhausted") ||
|
|
736
|
+
lower.includes("ratelimitexceeded.endpointtpm") ||
|
|
737
|
+
lower.includes("ratelimitexceeded.endpointrpm") ||
|
|
738
|
+
lower.includes("presque")
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
|
|
642
742
|
/**
|
|
643
743
|
* Extracts the Google `validation_url` from an Antigravity rejection.
|
|
644
744
|
*
|
|
@@ -719,13 +819,27 @@ function accountErrorContext(error: unknown, options?: AccountFailureEvidence):
|
|
|
719
819
|
const headerCooldown =
|
|
720
820
|
options?.retryAfterMs ??
|
|
721
821
|
parseUpstreamBackoff({ get: getHeader });
|
|
722
|
-
const messageCooldown = parseProviderResetDuration(message);
|
|
723
822
|
const rawUpstreamResponse =
|
|
724
823
|
origin === "upstream" &&
|
|
725
824
|
error instanceof GatewayError &&
|
|
726
825
|
typeof error.details.raw === "string"
|
|
727
826
|
? error.details.raw
|
|
728
827
|
: undefined;
|
|
828
|
+
// The surface `message` may be a generic placeholder ("Provider returned HTTP
|
|
829
|
+
// 429.") even when the upstream body states a reset window. Fall through to
|
|
830
|
+
// the raw body's prose when the surface message names none, so a
|
|
831
|
+
// "Resets in 165h46m45s" refusal is honored instead of cooling for the
|
|
832
|
+
// 15-minute fallback.
|
|
833
|
+
const messageCooldown =
|
|
834
|
+
parseProviderResetDuration(message) ??
|
|
835
|
+
(rawUpstreamResponse === undefined
|
|
836
|
+
? null
|
|
837
|
+
: parseProviderResetDuration(rawUpstreamResponse)) ??
|
|
838
|
+
(options?.upstreamBody === undefined
|
|
839
|
+
? null
|
|
840
|
+
: typeof options.upstreamBody === "string"
|
|
841
|
+
? parseProviderResetDuration(options.upstreamBody)
|
|
842
|
+
: parseProviderResetDuration(nestedErrorMessage(options.upstreamBody)));
|
|
729
843
|
// The body's own fields, when they can be reached. Checked alongside the
|
|
730
844
|
// header and the prose rather than instead of them: a provider may state the
|
|
731
845
|
// window in any of the three, and the longest honest statement wins.
|
|
@@ -762,6 +876,7 @@ function accountErrorContext(error: unknown, options?: AccountFailureEvidence):
|
|
|
762
876
|
explanation: string,
|
|
763
877
|
mutatesAccount = canMutate,
|
|
764
878
|
modelUnsupported = false,
|
|
879
|
+
modelScoped = false,
|
|
765
880
|
): AccountErrorClassification => ({
|
|
766
881
|
category,
|
|
767
882
|
status,
|
|
@@ -775,7 +890,18 @@ function accountErrorContext(error: unknown, options?: AccountFailureEvidence):
|
|
|
775
890
|
// invisible). Defaulting true keeps existing callers conservative.
|
|
776
891
|
routesVisible: true,
|
|
777
892
|
providerStated,
|
|
893
|
+
// Usage-limit exhaustion, spending caps, and credential verdicts rotate to a
|
|
894
|
+
// sibling account now; transient throttles (per-minute, capacity) do not.
|
|
895
|
+
// The transient guard also applies to a category the classifier resolved to
|
|
896
|
+
// `quota_exhausted` — a TPM/RPM throttle phrased with quota wording
|
|
897
|
+
// ("tpm exhausted (type=quota_exceeded_error)") still self-heals within the
|
|
898
|
+
// minute and must not burn a sibling credential.
|
|
899
|
+
rotateAccount:
|
|
900
|
+
(category === "quota_exhausted" || category === "policy_blocked_banned") &&
|
|
901
|
+
!(statusCode === 429 && transientRateLimitWording(lower)) &&
|
|
902
|
+
!(statusCode === 503 || statusCode === 529),
|
|
778
903
|
modelUnsupported,
|
|
904
|
+
modelScoped,
|
|
779
905
|
});
|
|
780
906
|
|
|
781
907
|
return {
|
|
@@ -952,9 +1078,12 @@ export function classifyAccountError(
|
|
|
952
1078
|
lower.includes("have exceeded the quota") ||
|
|
953
1079
|
lower.includes("balance exhausted") ||
|
|
954
1080
|
lower.includes("insufficient balance") ||
|
|
1081
|
+
lower.includes("insufficient credits") ||
|
|
955
1082
|
lower.includes("out of credits") ||
|
|
956
1083
|
lower.includes("credits exhausted") ||
|
|
957
1084
|
lower.includes("spending limit") ||
|
|
1085
|
+
lower.includes("spending cap") ||
|
|
1086
|
+
lower.includes("monthly spend limit") ||
|
|
958
1087
|
lower.includes("billing daily count exceeded") ||
|
|
959
1088
|
// Same Qoder family reached by body text when the code is not parsed out.
|
|
960
1089
|
(lower.includes("pricingurl") && lower.includes("qoder.com/pricing")));
|
|
@@ -1185,6 +1314,16 @@ export function classifyAccountError(
|
|
|
1185
1314
|
queuedUpstream,
|
|
1186
1315
|
validationRequired,
|
|
1187
1316
|
validationUrl,
|
|
1317
|
+
// A quota/spend/cap signal rotates to a sibling account; a per-minute
|
|
1318
|
+
// throttle, model capacity, or concurrency cap backs off on the same one.
|
|
1319
|
+
// `quotaSignal` is the broad price-refusal match — exclude explicit
|
|
1320
|
+
// throttles it would otherwise sweep (see the rate-limit and Inception
|
|
1321
|
+
// arms, which match the same signals and must keep backoff-only behavior).
|
|
1322
|
+
rotateAccount:
|
|
1323
|
+
quotaSignal &&
|
|
1324
|
+
!modelFrequencyWindow &&
|
|
1325
|
+
!(statusCode === 429 && transientRateLimitWording(lower)) &&
|
|
1326
|
+
!(statusCode === 503 || statusCode === 529),
|
|
1188
1327
|
};
|
|
1189
1328
|
|
|
1190
1329
|
for (const rule of ACCOUNT_ERROR_RULES) {
|
|
@@ -1283,33 +1422,7 @@ const ACCOUNT_ERROR_RULES: readonly AccountErrorRule[] = [
|
|
|
1283
1422
|
true,
|
|
1284
1423
|
),
|
|
1285
1424
|
},
|
|
1286
|
-
|
|
1287
|
-
// GitHub Copilot answers a license/entitlement refusal with 403 and
|
|
1288
|
-
// "not licensed to use Copilot" / "forbidden" / "unauthorized". That is a
|
|
1289
|
-
// verdict on the account: the subscription cannot serve this credential,
|
|
1290
|
-
// and no amount of waiting or refreshing changes it. Cooldown cycling
|
|
1291
|
-
// (auth_invalidated + 5m backoff) kept a dead account in rotation where a
|
|
1292
|
-
// client's retry budget burned sibling accounts — the same failure the
|
|
1293
|
-
// buddy ban exists to prevent. Disable immediately. Must run before
|
|
1294
|
-
// `credential-rejected`, which would claim the bare 403 as a refreshable
|
|
1295
|
-
// auth rejection.
|
|
1296
|
-
name: "github-copilot-not-licensed",
|
|
1297
|
-
match: (s) =>
|
|
1298
|
-
s.origin === "upstream" &&
|
|
1299
|
-
s.providerId === "github" &&
|
|
1300
|
-
copilotNotLicensed(s.lower),
|
|
1301
|
-
decide: (s) =>
|
|
1302
|
-
s.result(
|
|
1303
|
-
"policy_blocked_banned",
|
|
1304
|
-
"disabled",
|
|
1305
|
-
0,
|
|
1306
|
-
null,
|
|
1307
|
-
s.reason("GitHub Copilot account not licensed"),
|
|
1308
|
-
// A license refusal is evidence about the account even at provider
|
|
1309
|
-
// scope, for the same reason a named revocation or a buddy ban is.
|
|
1310
|
-
true,
|
|
1311
|
-
),
|
|
1312
|
-
},
|
|
1425
|
+
...githubRules,
|
|
1313
1426
|
{
|
|
1314
1427
|
name: "credential-rejected",
|
|
1315
1428
|
match: (s) =>
|
|
@@ -1401,30 +1514,7 @@ const ACCOUNT_ERROR_RULES: readonly AccountErrorRule[] = [
|
|
|
1401
1514
|
decide: (s) =>
|
|
1402
1515
|
s.result("quota_exhausted", "cooldown", 0, null, s.reason("Request price refused by the provider"), false),
|
|
1403
1516
|
},
|
|
1404
|
-
|
|
1405
|
-
// Qoder's code-112 pricing refusal (`{"code":"112","message":"{\"pricingUrl\":...}"}`)
|
|
1406
|
-
// is a "this model is not included in this account's plan, buy more" verdict
|
|
1407
|
-
// for one pair — the account keeps serving its other models. The probe path
|
|
1408
|
-
// does not always carry the code in its evidence (only the message does), so
|
|
1409
|
-
// the match keys on the provider id plus the pricing-page wording as well.
|
|
1410
|
-
// It must run before `quota-exhausted`, which would park the whole account.
|
|
1411
|
-
name: "qoder-plan-gated",
|
|
1412
|
-
match: (s) =>
|
|
1413
|
-
(s.providerId === "qoder" || s.providerId === "qodercn") &&
|
|
1414
|
-
(s.providerCode === "112" || s.lower.includes("pricingurl")),
|
|
1415
|
-
decide: (s) => {
|
|
1416
|
-
const cooldownMs = unsupportedModelCooldownMs(s.modelBlockHits);
|
|
1417
|
-
return s.result(
|
|
1418
|
-
"model_unreachable",
|
|
1419
|
-
"cooldown",
|
|
1420
|
-
cooldownMs,
|
|
1421
|
-
new Date(Date.now() + cooldownMs),
|
|
1422
|
-
s.reason("Model not included in this account's plan"),
|
|
1423
|
-
s.origin === "upstream" && s.modelId !== undefined,
|
|
1424
|
-
true,
|
|
1425
|
-
);
|
|
1426
|
-
},
|
|
1427
|
-
},
|
|
1517
|
+
qoderPlanGatedRule,
|
|
1428
1518
|
{
|
|
1429
1519
|
name: "model-plan-gated",
|
|
1430
1520
|
match: (s) => s.quotaSignal && s.providerCode === "112",
|
|
@@ -1441,106 +1531,29 @@ const ACCOUNT_ERROR_RULES: readonly AccountErrorRule[] = [
|
|
|
1441
1531
|
);
|
|
1442
1532
|
},
|
|
1443
1533
|
},
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
// now."` when the upstream does not serve the account's region. That is a
|
|
1447
|
-
// durable verdict on the account, not a transient transport fault: it
|
|
1448
|
-
// reproduces on every request until the region opens, so it must cool hard
|
|
1449
|
-
// for a day rather than be re-dialled every thirty seconds. Must run
|
|
1450
|
-
// before `transport-fault`, which would read the 502 as a short
|
|
1451
|
-
// `server_error`.
|
|
1452
|
-
name: "trae-region-unavailable",
|
|
1453
|
-
match: (s) =>
|
|
1454
|
-
(s.providerId === "trae" || s.providerId === "traecn") &&
|
|
1455
|
-
(s.providerCode === "4013" || s.lower.includes("not available in the current region")),
|
|
1456
|
-
decide: (s) => {
|
|
1457
|
-
const cooldownMs = TRAE_REGION_COOLDOWN_MS;
|
|
1458
|
-
return s.result(
|
|
1459
|
-
"quota_exhausted",
|
|
1460
|
-
"cooldown",
|
|
1461
|
-
cooldownMs,
|
|
1462
|
-
new Date(Date.now() + cooldownMs),
|
|
1463
|
-
s.reason("Trae service unavailable in the current region"),
|
|
1464
|
-
// Account-scoped provider evidence still has to reach the writer: a
|
|
1465
|
-
// stream frame carries no credential detail, so `canMutate` is false
|
|
1466
|
-
// and without this the account stays in rotation to reproduce the
|
|
1467
|
-
// refusal for the whole window the region is closed for.
|
|
1468
|
-
true,
|
|
1469
|
-
);
|
|
1470
|
-
},
|
|
1471
|
-
},
|
|
1472
|
-
{
|
|
1473
|
-
// Trae's free-tier quota (`4008` / "exceeded the quota") resets daily.
|
|
1474
|
-
// Treating it like the generic quota cooldown (1h account-wide fallback)
|
|
1475
|
-
// let the account re-enter rotation inside the window and reproduce the
|
|
1476
|
-
// refusal every few minutes — the "cooldown not holding" report. The
|
|
1477
|
-
// verdict is a full-day backoff on *any* model (the account's quota is
|
|
1478
|
-
// spent, so every model on it shares the wall). Scoped to the model so
|
|
1479
|
-
// the account is not disabled wholesale; the 1-day window means a model
|
|
1480
|
-
// stays parked for the day instead of flapping. Must run before
|
|
1481
|
-
// `quota-exhausted`, which would apply the generic shorter fallback.
|
|
1482
|
-
name: "trae-quota-exhausted",
|
|
1483
|
-
match: (s) =>
|
|
1484
|
-
(s.providerId === "trae" || s.providerId === "traecn") &&
|
|
1485
|
-
(s.providerCode === "4008" || s.lower.includes("have exceeded the quota")),
|
|
1486
|
-
decide: (s) => {
|
|
1487
|
-
const cooldownMs = s.statedCooldownMs ?? TRAE_QUOTA_COOLDOWN_MS;
|
|
1488
|
-
return s.result(
|
|
1489
|
-
"model_unreachable",
|
|
1490
|
-
"cooldown",
|
|
1491
|
-
cooldownMs,
|
|
1492
|
-
new Date(Date.now() + cooldownMs),
|
|
1493
|
-
s.reason("Trae quota exhausted for this model"),
|
|
1494
|
-
s.origin === "upstream" && s.modelId !== undefined,
|
|
1495
|
-
);
|
|
1496
|
-
},
|
|
1497
|
-
},
|
|
1498
|
-
{
|
|
1499
|
-
// Qoder's quota verdicts — `110` "Billing daily count exceeded" and the
|
|
1500
|
-
// upstream-499 exhausted-quota close — are per-model: one model's spend
|
|
1501
|
-
// cap does not drain another. Scoping them to the model keeps the account
|
|
1502
|
-
// serving its other models. Must run before `quota-exhausted`, which
|
|
1503
|
-
// parks the whole account.
|
|
1504
|
-
name: "qoder-model-quota",
|
|
1505
|
-
match: (s) =>
|
|
1506
|
-
(s.providerId === "qoder" || s.providerId === "qodercn") && s.quotaSignal,
|
|
1507
|
-
decide: (s) => {
|
|
1508
|
-
const cooldownMs = s.statedCooldownMs ?? QODER_QUOTA_COOLDOWN_MS;
|
|
1509
|
-
return s.result(
|
|
1510
|
-
"model_unreachable",
|
|
1511
|
-
"cooldown",
|
|
1512
|
-
cooldownMs,
|
|
1513
|
-
new Date(Date.now() + cooldownMs),
|
|
1514
|
-
s.reason("Qoder quota exhausted for this model"),
|
|
1515
|
-
s.origin === "upstream" && s.modelId !== undefined,
|
|
1516
|
-
);
|
|
1517
|
-
},
|
|
1518
|
-
},
|
|
1534
|
+
...traeRules,
|
|
1535
|
+
qoderModelQuotaRule,
|
|
1519
1536
|
{
|
|
1520
1537
|
name: "quota-exhausted",
|
|
1521
|
-
match: (s) =>
|
|
1538
|
+
match: (s) =>
|
|
1539
|
+
s.origin === "upstream" &&
|
|
1540
|
+
s.quotaSignal &&
|
|
1541
|
+
// A transient rolling throttle phrased with quota wording (TPM/RPM
|
|
1542
|
+
// "tpm exhausted (type=quota_exceeded_error)") self-heals within the
|
|
1543
|
+
// minute; it belongs to the rate-limit arm below, never here.
|
|
1544
|
+
!transientRateLimitWording(s.lower),
|
|
1522
1545
|
decide: (s) => {
|
|
1523
1546
|
// xAI Grok Build's free tier resets on a rolling window. Use a duration
|
|
1524
1547
|
// stated by the provider when present; otherwise park for the conservative
|
|
1525
1548
|
// 24-hour fallback.
|
|
1526
|
-
const grokFreeTier =
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
s.providerCode.includes("credits") ||
|
|
1535
|
-
s.lower.includes("free-usage") ||
|
|
1536
|
-
s.lower.includes("free usage") ||
|
|
1537
|
-
s.lower.includes("included free usage") ||
|
|
1538
|
-
s.lower.includes("rolling 24-hour") ||
|
|
1539
|
-
s.lower.includes("rolling 24 hour") ||
|
|
1540
|
-
s.lower.includes("spending limit") ||
|
|
1541
|
-
s.lower.includes("out of credits") ||
|
|
1542
|
-
s.lower.includes("run out of credits") ||
|
|
1543
|
-
s.lower.includes("need a grok subscription");
|
|
1549
|
+
const grokFreeTier = isGrokFreeTierExhaustion(s);
|
|
1550
|
+
// Only the explicit free-usage allowance is model-scoped. Subscription
|
|
1551
|
+
// spending caps are account-level and still park the whole credential.
|
|
1552
|
+
const grokFreeUsageModelScope =
|
|
1553
|
+
s.providerId === "grok" &&
|
|
1554
|
+
(s.providerCode === "subscription:free-usage-exhausted" ||
|
|
1555
|
+
s.lower.includes("subscription:free-usage-exhausted") ||
|
|
1556
|
+
s.lower.includes("included free usage"));
|
|
1544
1557
|
const buddyCreditsExhausted =
|
|
1545
1558
|
s.providerCode === "14018" || s.lower.includes("credits exhausted");
|
|
1546
1559
|
const cooldownMs = grokFreeTier
|
|
@@ -1559,41 +1572,13 @@ const ACCOUNT_ERROR_RULES: readonly AccountErrorRule[] = [
|
|
|
1559
1572
|
new Date(Date.now() + cooldownMs),
|
|
1560
1573
|
s.reason("Provider quota exhausted"),
|
|
1561
1574
|
s.origin === "upstream",
|
|
1575
|
+
false,
|
|
1576
|
+
grokFreeUsageModelScope && s.modelId !== undefined,
|
|
1562
1577
|
);
|
|
1563
1578
|
},
|
|
1564
1579
|
},
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
// tokens / 100K output tokens) and the window resets every minute. A
|
|
1568
|
-
// generic 429 would cool the account for the 15-minute default, leaving a
|
|
1569
|
-
// healthy key parked long after the window refilled — the source of
|
|
1570
|
-
// "limitasiya lumayan keras". This arm must run before `rate-limit`: it
|
|
1571
|
-
// matches the same signals and shortens the window to 1 minute so routing
|
|
1572
|
-
// moves to the next account and the flagged one returns almost at once.
|
|
1573
|
-
name: "inception-per-minute-limit",
|
|
1574
|
-
match: (s) =>
|
|
1575
|
-
s.origin === "upstream" &&
|
|
1576
|
-
s.providerId === "inception" &&
|
|
1577
|
-
(s.statusCode === 429 ||
|
|
1578
|
-
s.providerCode === "rate_limit_reached" ||
|
|
1579
|
-
s.providerCode === "rate_limit_exceeded" ||
|
|
1580
|
-
s.lower.includes("rate_limit") ||
|
|
1581
|
-
s.lower.includes("rate limit") ||
|
|
1582
|
-
s.lower.includes("too many requests") ||
|
|
1583
|
-
s.lower.includes("tokens per minute") ||
|
|
1584
|
-
s.lower.includes("input token")),
|
|
1585
|
-
decide: (s) => {
|
|
1586
|
-
const cooldownMs = s.statedCooldownMs ?? INCEPTION_RATE_LIMIT_COOLDOWN_MS;
|
|
1587
|
-
return s.result(
|
|
1588
|
-
"rate_limit_transient",
|
|
1589
|
-
"cooldown",
|
|
1590
|
-
cooldownMs,
|
|
1591
|
-
new Date(Date.now() + cooldownMs),
|
|
1592
|
-
s.reason("Inception per-minute input-token limit"),
|
|
1593
|
-
s.origin === "upstream",
|
|
1594
|
-
);
|
|
1595
|
-
},
|
|
1596
|
-
},
|
|
1580
|
+
...inceptionRules,
|
|
1581
|
+
...perchRules,
|
|
1597
1582
|
{
|
|
1598
1583
|
name: "rate-limit",
|
|
1599
1584
|
match: (s) =>
|
|
@@ -1685,34 +1670,6 @@ const ACCOUNT_ERROR_RULES: readonly AccountErrorRule[] = [
|
|
|
1685
1670
|
);
|
|
1686
1671
|
},
|
|
1687
1672
|
},
|
|
1688
|
-
{
|
|
1689
|
-
// Qoder closes an exhausted-quota stream with upstream 499 (nginx
|
|
1690
|
-
// client-close style). The gateway's own aborts are local `transport_closed`
|
|
1691
|
-
// and never carry a qoder providerId, so a 499 scoped to qoder is a quota
|
|
1692
|
-
// verdict, not a transport fault. It must run before `transport-fault`,
|
|
1693
|
-
// which otherwise reads the status as a 5xx-class outage and cools the
|
|
1694
|
-
// account for the shorter transient window — or leaves it unmutated.
|
|
1695
|
-
//
|
|
1696
|
-
// The verdict is per-model, like Qoder's other quota signs: one model's
|
|
1697
|
-
// spend cap does not drain another, so it cools the pair, never the whole
|
|
1698
|
-
// account.
|
|
1699
|
-
name: "qoder-499-quota",
|
|
1700
|
-
match: (s) =>
|
|
1701
|
-
(s.providerId === "qoder" || s.providerId === "qodercn") &&
|
|
1702
|
-
s.statusCode === 499 &&
|
|
1703
|
-
s.origin === "upstream",
|
|
1704
|
-
decide: (s) => {
|
|
1705
|
-
const cooldownMs = s.statedCooldownMs ?? QODER_QUOTA_COOLDOWN_MS;
|
|
1706
|
-
return s.result(
|
|
1707
|
-
"model_unreachable",
|
|
1708
|
-
"cooldown",
|
|
1709
|
-
cooldownMs,
|
|
1710
|
-
new Date(Date.now() + cooldownMs),
|
|
1711
|
-
s.reason("Qoder quota exhausted for this model (upstream 499)"),
|
|
1712
|
-
s.origin === "upstream" && s.modelId !== undefined,
|
|
1713
|
-
);
|
|
1714
|
-
},
|
|
1715
|
-
},
|
|
1716
1673
|
{
|
|
1717
1674
|
name: "transport-fault",
|
|
1718
1675
|
match: (s) =>
|