cartethyia 2.7.9 → 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 -2
- package/dist/dashboard/assets/{BannedPage-BxoG1MrL.js → BannedPage-Cc0vKpMG.js} +1 -1
- package/dist/dashboard/assets/BannedPage-Cc0vKpMG.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-ixwgKk53.js → CliToolDetailPage-Bqj2aa1T.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-Bqj2aa1T.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-BmttNV3U.js → CliToolsPage-CXsaOAO2.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-CXsaOAO2.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-BE3qwwfC.js → CombosPage-BeLpJ4ma.js} +1 -1
- package/dist/dashboard/assets/CombosPage-BeLpJ4ma.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-CRZdmMIX.js → ConsoleLogPage-DnV0uZ3m.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-DnV0uZ3m.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-Dv_FJ6Er.js → CustomizationPage-CBxAjo63.js} +1 -1
- 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-BOoO8rpz.js → InjectSystemPromptPanel-BWFLECmz.js} +2 -2
- package/dist/dashboard/assets/InjectSystemPromptPanel-BWFLECmz.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-ChJuNNVN.js → LoginPage-CgoPc5B9.js} +1 -1
- package/dist/dashboard/assets/LoginPage-CgoPc5B9.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-BZAJtPg1.js → ModelPicker-D9bFQern.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-D9bFQern.js.gz +0 -0
- package/dist/dashboard/assets/{OverviewPage-BE40s3u5.js → OverviewPage-B_p4tL8k.js} +1 -1
- 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-CQ6bPUf_.js → ProviderIcon-D_0PiZtg.js} +1 -1
- 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-BJ87Pp0U.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-C5HPWqEv.js → SettingsPage-CctTga2C.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-CctTga2C.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-CFNcATJJ.js → SetupPage-febLU4r1.js} +1 -1
- package/dist/dashboard/assets/SetupPage-febLU4r1.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-YoD4tY5l.js → StudioPage-M6fvR5Wz.js} +1 -1
- package/dist/dashboard/assets/StudioPage-M6fvR5Wz.js.gz +0 -0
- package/dist/dashboard/assets/{UsagePage-DxjaAhb_.js → UsagePage-nyeuRqz9.js} +1 -1
- package/dist/dashboard/assets/UsagePage-nyeuRqz9.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-BTlkq_Uk.js → api-keys-BFOlvrJC.js} +1 -1
- 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-CqTLw5e7.js +1 -0
- package/dist/dashboard/assets/assert-CqTLw5e7.js.gz +0 -0
- package/dist/dashboard/assets/{badge-DdbT_7Am.js → badge-C83EqLB0.js} +1 -1
- package/dist/dashboard/assets/badge-C83EqLB0.js.gz +0 -0
- package/dist/dashboard/assets/{banner-CsGN-nbm.js → banner-D1xn7pIe.js} +1 -1
- package/dist/dashboard/assets/banner-D1xn7pIe.js.gz +0 -0
- package/dist/dashboard/assets/{card-mgZ_TLLZ.js → card-DU3Lq5Lm.js} +1 -1
- package/dist/dashboard/assets/card-DU3Lq5Lm.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-A1-v8-03.js → cli-tools-Dw1U_drK.js} +1 -1
- package/dist/dashboard/assets/cli-tools-Dw1U_drK.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-CJ4Zty89.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-B5L2jPQ_.js → dialog-BjFR5UHy.js} +1 -1
- package/dist/dashboard/assets/dialog-BjFR5UHy.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-DkjUouAj.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-B-LVLV6o.js → entry-CECVELyy.js} +1 -1
- package/dist/dashboard/assets/entry-CECVELyy.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CAhdYl3B.js → entry-DYYofQ7W.js} +1 -1
- package/dist/dashboard/assets/entry-DYYofQ7W.js.gz +0 -0
- package/dist/dashboard/assets/{formatters-CrsdLVsn.js → formatters-DLW4i8zK.js} +1 -1
- package/dist/dashboard/assets/formatters-DLW4i8zK.js.gz +0 -0
- package/dist/dashboard/assets/{index-BU64Lqvp.js → index-1-fCqe64.js} +2 -2
- package/dist/dashboard/assets/index-1-fCqe64.js.gz +0 -0
- package/dist/dashboard/assets/{inline-B2aDLyyg.js → inline-DGvU2oLK.js} +1 -1
- package/dist/dashboard/assets/inline-DGvU2oLK.js.gz +0 -0
- package/dist/dashboard/assets/{input-BCVkdR9d.js → input-iNnO1X2K.js} +1 -1
- package/dist/dashboard/assets/input-iNnO1X2K.js.gz +0 -0
- package/dist/dashboard/assets/{layout-DNcAdMK-.js → layout-0q8UGypb.js} +1 -1
- package/dist/dashboard/assets/layout-0q8UGypb.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-DgrIH0hw.js → list-row-C0w2bQjJ.js} +1 -1
- package/dist/dashboard/assets/list-row-C0w2bQjJ.js.gz +0 -0
- package/dist/dashboard/assets/{live-Cae01iu8.js → live-M5aJ_xCa.js} +1 -1
- package/dist/dashboard/assets/live-M5aJ_xCa.js.gz +0 -0
- package/dist/dashboard/assets/{network-CRib7H_2.js → network-BKeQGT3K.js} +1 -1
- package/dist/dashboard/assets/network-BKeQGT3K.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-BKQE8Xpz.js → numbers-CivAk3da.js} +1 -1
- package/dist/dashboard/assets/numbers-CivAk3da.js.gz +0 -0
- package/dist/dashboard/assets/{pagination-ecPvG_8g.js → pagination-CpzvI5ep.js} +1 -1
- package/dist/dashboard/assets/pagination-CpzvI5ep.js.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/quota-DiPD6UKH.js +1 -0
- package/dist/dashboard/assets/quota-DiPD6UKH.js.gz +0 -0
- package/dist/dashboard/assets/{routing-Ckm1UXuC.js → routing-BFQX2VBc.js} +1 -1
- package/dist/dashboard/assets/routing-BFQX2VBc.js.gz +0 -0
- package/dist/dashboard/assets/{select-ChoFWyKz.js → select-CXbck0jG.js} +2 -2
- package/dist/dashboard/assets/select-CXbck0jG.js.gz +0 -0
- package/dist/dashboard/assets/{settings-CKjFPlKW.js → settings-C_XvMHh9.js} +1 -1
- package/dist/dashboard/assets/settings-C_XvMHh9.js.gz +0 -0
- package/dist/dashboard/assets/{stack-DWCwU3fh.js → stack-DggRiCxE.js} +1 -1
- package/dist/dashboard/assets/stack-DggRiCxE.js.gz +0 -0
- package/dist/dashboard/assets/{state-Cg3VL5J_.js → state-ClFzfbYp.js} +1 -1
- package/dist/dashboard/assets/state-ClFzfbYp.js.gz +0 -0
- package/dist/dashboard/assets/{switch-Db70BMix.js → switch-CTIFxKqS.js} +1 -1
- package/dist/dashboard/assets/switch-CTIFxKqS.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-BqdE0LuM.js → useMutation-BClG3yWj.js} +1 -1
- package/dist/dashboard/assets/useMutation-BClG3yWj.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/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 +1 -1
- package/src/boot/flush.ts +20 -0
- package/src/boot/lifecycle.ts +7 -5
- package/src/config/routing.ts +26 -0
- package/src/config/spec-metadata.ts +72 -0
- package/src/config/spec.ts +20 -0
- 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/cli-tools/service.ts +6 -12
- package/src/console/console-router-guards.ts +23 -16
- package/src/console/domain-registration.ts +0 -2
- 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/store.ts +0 -53
- package/src/console/providers/catalog/contracts.ts +2 -2
- package/src/console/providers/catalog/discover-constants.ts +6 -0
- package/src/console/providers/catalog/routes.ts +1 -0
- package/src/console/providers/catalog/store.ts +21 -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 +6 -18
- package/src/console/quota/quota-view.ts +19 -3
- package/src/console/routing/pools/operations.ts +31 -46
- package/src/console/shared/errors.ts +33 -6
- package/src/main.ts +2 -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/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 +18 -0
- package/src/persistence/migrate.ts +6 -3
- package/src/persistence/schema.ts +8 -0
- package/src/persistence/ttl-cache.ts +20 -2
- package/src/protocol/codecs.ts +1 -1
- 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 +3 -4
- package/src/protocol/response/chat.ts +12 -20
- 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/providers/adapters/compatible-adapter.ts +71 -12
- 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 +30 -49
- package/src/providers/configured-provider.ts +148 -0
- package/src/providers/default-registry.ts +61 -36
- package/src/providers/discovery/probe-phases.ts +14 -4
- package/src/providers/discovery/probing-service.ts +15 -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 +5 -1
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +173 -17
- package/src/providers/integrations/buddy/buddy-identity.ts +19 -6
- package/src/providers/integrations/buddy/capabilities.ts +2 -2
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +3 -0
- package/src/providers/integrations/buddy/codebuddy/provider.ts +4 -0
- package/src/providers/integrations/buddy/codebuddy/shared.ts +37 -22
- 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-exec.ts +9 -2
- package/src/providers/integrations/cursor/cursor-request.ts +26 -12
- package/src/providers/integrations/cursor/cursor-shared.ts +7 -4
- package/src/providers/integrations/cursor/cursor.ts +89 -40
- 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/devin/protocol/devin-proto.ts +1 -27
- package/src/providers/integrations/factory/factory-oauth.ts +17 -44
- 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/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.ts → hermes/hermes.ts} +9 -9
- 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/opencode-go-quota.ts +2 -8
- package/src/providers/integrations/perch/perch-credential.ts +25 -0
- package/src/providers/integrations/perch/perch-protocol.ts +0 -3
- package/src/providers/integrations/perch/perch-quota.ts +2 -16
- package/src/providers/integrations/perch/perch.ts +3 -13
- package/src/providers/integrations/qoder/claim-qoder-credits.ts +18 -59
- package/src/providers/integrations/qoder/qoder-adapter.ts +1 -12
- package/src/providers/integrations/qoder/qoder-auth.ts +101 -29
- package/src/providers/integrations/qoder/qoder-catalog.ts +20 -5
- package/src/providers/integrations/qoder/qoder-chat.ts +7 -21
- package/src/providers/integrations/qoder/qoder-checkin.ts +4 -4
- package/src/providers/integrations/qoder/qoder-grpc.ts +0 -1
- package/src/providers/integrations/qoder/qoder-ide.ts +0 -107
- package/src/providers/integrations/qoder/qoder.ts +0 -1
- package/src/providers/integrations/trae/trae-protocol.ts +9 -5
- package/src/providers/integrations/trae/trae-quota.ts +6 -28
- package/src/providers/integrations/trae/trae-realm.ts +38 -0
- package/src/providers/integrations/trae/trae.ts +7 -24
- 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 +199 -307
- package/src/providers/operations/account-health-service.ts +82 -1
- package/src/providers/operations/account-refresh-status.ts +22 -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 +5 -51
- package/src/providers/operations/cooldown-rules.ts +11 -16
- 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 +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 +4 -2
- package/src/providers/{provider-credential-resolver.ts → provider-credentials.ts} +40 -3
- package/src/providers/provider-metadata.ts +40 -10
- 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 +75 -18
- 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 +8 -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 +8 -36
- 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/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/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.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/providers/bundled-discovery-providers.ts +0 -98
- package/src/providers/configured-openai-providers.ts +0 -120
- 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-DIXLjojg.js} +0 -0
- /package/dist/dashboard/assets/{app-B7kIWFm5.js.gz → app-DIXLjojg.js.gz} +0 -0
|
@@ -23,23 +23,17 @@ import {
|
|
|
23
23
|
resolveAccountTransientCooldownMs,
|
|
24
24
|
resolveAccountUnclassifiedCooldownMs,
|
|
25
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";
|
|
26
36
|
const DEFAULT_RATE_LIMIT_COOLDOWN_MS = resolveAccountRateLimitCooldownMs;
|
|
27
|
-
/**
|
|
28
|
-
* Perch's `turn_rate_limited` window. The upstream does not state a reset and
|
|
29
|
-
* answers the same on every ticket mint while the window is exhausted, so a
|
|
30
|
-
* day matches the practical cadence and stops the redial loop.
|
|
31
|
-
*/
|
|
32
|
-
const PERCH_TURN_RATE_LIMIT_COOLDOWN_MS = 24 * 60 * 60 * 1000;
|
|
33
|
-
/**
|
|
34
|
-
* Inception's per-minute limit window (1 minute).
|
|
35
|
-
*
|
|
36
|
-
* Inception's Free tier throttles per minute (1 000 requests / 1M input
|
|
37
|
-
* tokens / 100K output tokens) and the window resets every minute. The generic
|
|
38
|
-
* 15-minute rate-limit fallback would park a healthy key long after the window
|
|
39
|
-
* refilled; 1 minute is the provider's own cadence, hardcoded because it is a
|
|
40
|
-
* fact about the upstream, not a deployment knob.
|
|
41
|
-
*/
|
|
42
|
-
const INCEPTION_RATE_LIMIT_COOLDOWN_MS = 60_000;
|
|
43
37
|
// Fallback when the error states no usable duration: deliberately short (1h,
|
|
44
38
|
// not 24h) — quota state is re-probed on the next failure, so an unknown
|
|
45
39
|
// reset cadence self-corrects instead of parking the account for a day.
|
|
@@ -58,32 +52,6 @@ export const COOLDOWN_RECOVERY_BATCH = 500;
|
|
|
58
52
|
* account returns to rotation quickly.
|
|
59
53
|
*/
|
|
60
54
|
const UNCLASSIFIED_COOLDOWN_MS = resolveAccountUnclassifiedCooldownMs;
|
|
61
|
-
/**
|
|
62
|
-
* xAI Grok Build's free tier resets on a rolling 24-hour window, so its
|
|
63
|
-
* exhaustion is a full-day quota cooldown — never the generic 1h fallback,
|
|
64
|
-
* which let the account re-enter rotation inside the window.
|
|
65
|
-
*/
|
|
66
|
-
const GROK_QUOTA_COOLDOWN_MS = 24 * 60 * 60 * 1000;
|
|
67
|
-
/**
|
|
68
|
-
* Trae answers `4013 "AI service is not available in the current region now."`
|
|
69
|
-
* when the upstream does not serve the account's region. A day because the
|
|
70
|
-
* verdict is durable: it reproduces on every request until the region opens,
|
|
71
|
-
* and the account must not be re-dialled every thirty seconds for the whole
|
|
72
|
-
* window.
|
|
73
|
-
*/
|
|
74
|
-
const TRAE_REGION_COOLDOWN_MS = 24 * 60 * 60 * 1000;
|
|
75
|
-
/**
|
|
76
|
-
* Trae's free-tier quota (`4008` / "exceeded the quota") resets daily, so its
|
|
77
|
-
* exhaustion is a full-day model backoff — never the generic 1h fallback,
|
|
78
|
-
* which let the account re-enter rotation inside the window.
|
|
79
|
-
*/
|
|
80
|
-
const TRAE_QUOTA_COOLDOWN_MS = 24 * 60 * 60 * 1000;
|
|
81
|
-
/**
|
|
82
|
-
* Qoder's quota verdicts (`110` billing-daily-count, the upstream-499 close)
|
|
83
|
-
* are per-model: one model's spend cap does not drain another. A day matches
|
|
84
|
-
* the daily billing window the provider names.
|
|
85
|
-
*/
|
|
86
|
-
const QODER_QUOTA_COOLDOWN_MS = 24 * 60 * 60 * 1000;
|
|
87
55
|
/**
|
|
88
56
|
* The buddy family's policy/channel block (`11140`, `11128`) is a *base* backoff,
|
|
89
57
|
* not a flat penalty — see {@link resolveAccountPolicyBlockCooldownMs}. It used
|
|
@@ -186,12 +154,6 @@ export type AccountErrorCategory =
|
|
|
186
154
|
| "model_capacity"
|
|
187
155
|
| "model_unreachable"
|
|
188
156
|
| "auth_invalidated"
|
|
189
|
-
/**
|
|
190
|
-
* Deprecated: an upstream ban. Kept so existing health rows keep rendering
|
|
191
|
-
* instead of showing a blank cause. No longer produced — see
|
|
192
|
-
* `policy_blocked_banned` and `policy_blocked_invalid_request`.
|
|
193
|
-
*/
|
|
194
|
-
| "policy_blocked"
|
|
195
157
|
/**
|
|
196
158
|
* The upstream banned this credential: a verdict on the account, e.g. buddy
|
|
197
159
|
* `11140` "request illegal" with "Your account has been restricted due to
|
|
@@ -303,6 +265,15 @@ export interface AccountErrorClassification {
|
|
|
303
265
|
* reads it: a provider-stated window is the truth and is never multiplied.
|
|
304
266
|
*/
|
|
305
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;
|
|
306
277
|
/**
|
|
307
278
|
* True when the upstream reported the model as one it does not serve (the
|
|
308
279
|
* buddy family's `11102`), rather than one this account merely cannot reach.
|
|
@@ -312,6 +283,12 @@ export interface AccountErrorClassification {
|
|
|
312
283
|
* apart — it needs to know whether to count the hit and escalate the window.
|
|
313
284
|
*/
|
|
314
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;
|
|
315
292
|
/**
|
|
316
293
|
* Where the cooldown duration came from (D18). A duration resolved from a
|
|
317
294
|
* tier/provider rule (`provider-rule`) is a policy decision and must NOT go
|
|
@@ -499,26 +476,6 @@ function banWording(lower: string): boolean {
|
|
|
499
476
|
);
|
|
500
477
|
}
|
|
501
478
|
|
|
502
|
-
/**
|
|
503
|
-
* Whether GitHub Copilot stated the account lacks a license — a ban-shaped
|
|
504
|
-
* verdict on the account, not a refreshable auth rejection.
|
|
505
|
-
*
|
|
506
|
-
* Observed live (2026-11-03): the health log showed
|
|
507
|
-
* `unauthorized: not licensed to use Copilot` and `forbidden` on
|
|
508
|
-
* `gpt-6-luna`. Copilot answers 401 for a bad credential and 403 for a
|
|
509
|
-
* license/entitlement refusal, so `forbidden`/`unauthorized` on this surface
|
|
510
|
-
* is the same verdict. No amount of waiting or refreshing changes it — the
|
|
511
|
-
* subscription cannot serve this credential.
|
|
512
|
-
*/
|
|
513
|
-
function copilotNotLicensed(lower: string): boolean {
|
|
514
|
-
return (
|
|
515
|
-
lower.includes("not licensed") ||
|
|
516
|
-
lower.includes("forbidden") ||
|
|
517
|
-
lower.includes("unauthorized") ||
|
|
518
|
-
lower.includes("license")
|
|
519
|
-
);
|
|
520
|
-
}
|
|
521
|
-
|
|
522
479
|
/**
|
|
523
480
|
* Bounded to a day, matching `MAX_COOLDOWN_MS` in the failure policy: a stated
|
|
524
481
|
* window longer than that is a monthly or plan-level limit, and parking an
|
|
@@ -537,6 +494,19 @@ function tryParseUpstreamBody(text: string): unknown {
|
|
|
537
494
|
}
|
|
538
495
|
}
|
|
539
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
|
+
|
|
540
510
|
/**
|
|
541
511
|
* Reads a reset window the upstream stated in structured fields.
|
|
542
512
|
*
|
|
@@ -550,13 +520,26 @@ function tryParseUpstreamBody(text: string): unknown {
|
|
|
550
520
|
* (`resets_at`, seconds or milliseconds) and a relative delay
|
|
551
521
|
* (`resets_in_seconds`). The later of the two wins, since they are the same
|
|
552
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.
|
|
553
531
|
*/
|
|
554
532
|
function parseUpstreamBodyReset(body: unknown): number | null {
|
|
555
533
|
if (typeof body !== "object" || body === null) return null;
|
|
556
534
|
const record = body as Record<string, unknown>;
|
|
557
535
|
// Some providers nest under `error`, some return it at the top level.
|
|
558
|
-
const
|
|
536
|
+
const errorRecord = (record["error"] as Record<string, unknown> | undefined) ?? undefined;
|
|
537
|
+
const candidates = [record, errorRecord];
|
|
559
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
|
+
};
|
|
560
543
|
for (const candidate of candidates) {
|
|
561
544
|
if (typeof candidate !== "object" || candidate === null) continue;
|
|
562
545
|
const now = Date.now();
|
|
@@ -567,8 +550,7 @@ function parseUpstreamBodyReset(body: unknown): number | null {
|
|
|
567
550
|
// milliseconds ~13, and the two are four orders of magnitude apart — so
|
|
568
551
|
// the smaller is scaled up rather than guessed from a threshold date.
|
|
569
552
|
const epochMs = absolute < 1e11 ? absolute * 1000 : absolute;
|
|
570
|
-
|
|
571
|
-
if (delay > 0) best = best === null ? delay : Math.max(best, delay);
|
|
553
|
+
consider(epochMs - now);
|
|
572
554
|
}
|
|
573
555
|
|
|
574
556
|
const relative =
|
|
@@ -576,19 +558,69 @@ function parseUpstreamBodyReset(body: unknown): number | null {
|
|
|
576
558
|
candidate["reset_in_seconds"] ??
|
|
577
559
|
candidate["retry_after_seconds"];
|
|
578
560
|
if (typeof relative === "number" && Number.isFinite(relative) && relative > 0) {
|
|
579
|
-
|
|
580
|
-
best = best === null ? delay : Math.max(best, delay);
|
|
561
|
+
consider(relative * 1000);
|
|
581
562
|
}
|
|
582
563
|
|
|
583
564
|
const minutes = candidate["limit_window_minutes"] ?? candidate["reset_in_minutes"];
|
|
584
565
|
if (typeof minutes === "number" && Number.isFinite(minutes) && minutes > 0) {
|
|
585
|
-
|
|
586
|
-
|
|
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"]));
|
|
587
603
|
}
|
|
588
604
|
}
|
|
589
605
|
return best === null ? null : Math.min(MAX_UPSTREAM_STATED_COOLDOWN_MS, Math.round(best));
|
|
590
606
|
}
|
|
591
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
|
+
|
|
592
624
|
|
|
593
625
|
interface AccountErrorContext {
|
|
594
626
|
readonly message: string;
|
|
@@ -621,6 +653,7 @@ interface AccountErrorContext {
|
|
|
621
653
|
explanation: string,
|
|
622
654
|
mutatesAccount?: boolean,
|
|
623
655
|
modelUnsupported?: boolean,
|
|
656
|
+
modelScoped?: boolean,
|
|
624
657
|
) => AccountErrorClassification;
|
|
625
658
|
}
|
|
626
659
|
|
|
@@ -639,14 +672,14 @@ interface AccountErrorContext {
|
|
|
639
672
|
* classifier's locals, no allocation per classification): the table is built
|
|
640
673
|
* once per call from values already computed for every arm.
|
|
641
674
|
*/
|
|
642
|
-
interface AccountErrorRule {
|
|
675
|
+
export interface AccountErrorRule {
|
|
643
676
|
readonly name: string;
|
|
644
677
|
readonly match: (signals: AccountErrorSignals) => boolean;
|
|
645
678
|
readonly decide: (signals: AccountErrorSignals) => AccountErrorClassification;
|
|
646
679
|
}
|
|
647
680
|
|
|
648
681
|
/** The shared signals every arm reads, computed once per classification. */
|
|
649
|
-
interface AccountErrorSignals {
|
|
682
|
+
export interface AccountErrorSignals {
|
|
650
683
|
readonly message: string;
|
|
651
684
|
readonly lower: string;
|
|
652
685
|
readonly folded: string;
|
|
@@ -676,10 +709,36 @@ interface AccountErrorSignals {
|
|
|
676
709
|
readonly queuedUpstream: boolean;
|
|
677
710
|
readonly validationRequired: boolean;
|
|
678
711
|
readonly validationUrl: string | undefined;
|
|
712
|
+
/** True when this failure should switch to a sibling account rather than wait. */
|
|
713
|
+
readonly rotateAccount: boolean;
|
|
679
714
|
}
|
|
680
715
|
|
|
681
716
|
const VALIDATION_URL_RE = /https?:\/\/[^\s"'\\]+/;
|
|
682
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
|
+
|
|
683
742
|
/**
|
|
684
743
|
* Extracts the Google `validation_url` from an Antigravity rejection.
|
|
685
744
|
*
|
|
@@ -760,13 +819,27 @@ function accountErrorContext(error: unknown, options?: AccountFailureEvidence):
|
|
|
760
819
|
const headerCooldown =
|
|
761
820
|
options?.retryAfterMs ??
|
|
762
821
|
parseUpstreamBackoff({ get: getHeader });
|
|
763
|
-
const messageCooldown = parseProviderResetDuration(message);
|
|
764
822
|
const rawUpstreamResponse =
|
|
765
823
|
origin === "upstream" &&
|
|
766
824
|
error instanceof GatewayError &&
|
|
767
825
|
typeof error.details.raw === "string"
|
|
768
826
|
? error.details.raw
|
|
769
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)));
|
|
770
843
|
// The body's own fields, when they can be reached. Checked alongside the
|
|
771
844
|
// header and the prose rather than instead of them: a provider may state the
|
|
772
845
|
// window in any of the three, and the longest honest statement wins.
|
|
@@ -803,6 +876,7 @@ function accountErrorContext(error: unknown, options?: AccountFailureEvidence):
|
|
|
803
876
|
explanation: string,
|
|
804
877
|
mutatesAccount = canMutate,
|
|
805
878
|
modelUnsupported = false,
|
|
879
|
+
modelScoped = false,
|
|
806
880
|
): AccountErrorClassification => ({
|
|
807
881
|
category,
|
|
808
882
|
status,
|
|
@@ -816,7 +890,18 @@ function accountErrorContext(error: unknown, options?: AccountFailureEvidence):
|
|
|
816
890
|
// invisible). Defaulting true keeps existing callers conservative.
|
|
817
891
|
routesVisible: true,
|
|
818
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),
|
|
819
903
|
modelUnsupported,
|
|
904
|
+
modelScoped,
|
|
820
905
|
});
|
|
821
906
|
|
|
822
907
|
return {
|
|
@@ -993,9 +1078,12 @@ export function classifyAccountError(
|
|
|
993
1078
|
lower.includes("have exceeded the quota") ||
|
|
994
1079
|
lower.includes("balance exhausted") ||
|
|
995
1080
|
lower.includes("insufficient balance") ||
|
|
1081
|
+
lower.includes("insufficient credits") ||
|
|
996
1082
|
lower.includes("out of credits") ||
|
|
997
1083
|
lower.includes("credits exhausted") ||
|
|
998
1084
|
lower.includes("spending limit") ||
|
|
1085
|
+
lower.includes("spending cap") ||
|
|
1086
|
+
lower.includes("monthly spend limit") ||
|
|
999
1087
|
lower.includes("billing daily count exceeded") ||
|
|
1000
1088
|
// Same Qoder family reached by body text when the code is not parsed out.
|
|
1001
1089
|
(lower.includes("pricingurl") && lower.includes("qoder.com/pricing")));
|
|
@@ -1226,6 +1314,16 @@ export function classifyAccountError(
|
|
|
1226
1314
|
queuedUpstream,
|
|
1227
1315
|
validationRequired,
|
|
1228
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),
|
|
1229
1327
|
};
|
|
1230
1328
|
|
|
1231
1329
|
for (const rule of ACCOUNT_ERROR_RULES) {
|
|
@@ -1324,33 +1422,7 @@ const ACCOUNT_ERROR_RULES: readonly AccountErrorRule[] = [
|
|
|
1324
1422
|
true,
|
|
1325
1423
|
),
|
|
1326
1424
|
},
|
|
1327
|
-
|
|
1328
|
-
// GitHub Copilot answers a license/entitlement refusal with 403 and
|
|
1329
|
-
// "not licensed to use Copilot" / "forbidden" / "unauthorized". That is a
|
|
1330
|
-
// verdict on the account: the subscription cannot serve this credential,
|
|
1331
|
-
// and no amount of waiting or refreshing changes it. Cooldown cycling
|
|
1332
|
-
// (auth_invalidated + 5m backoff) kept a dead account in rotation where a
|
|
1333
|
-
// client's retry budget burned sibling accounts — the same failure the
|
|
1334
|
-
// buddy ban exists to prevent. Disable immediately. Must run before
|
|
1335
|
-
// `credential-rejected`, which would claim the bare 403 as a refreshable
|
|
1336
|
-
// auth rejection.
|
|
1337
|
-
name: "github-copilot-not-licensed",
|
|
1338
|
-
match: (s) =>
|
|
1339
|
-
s.origin === "upstream" &&
|
|
1340
|
-
s.providerId === "github" &&
|
|
1341
|
-
copilotNotLicensed(s.lower),
|
|
1342
|
-
decide: (s) =>
|
|
1343
|
-
s.result(
|
|
1344
|
-
"policy_blocked_banned",
|
|
1345
|
-
"disabled",
|
|
1346
|
-
0,
|
|
1347
|
-
null,
|
|
1348
|
-
s.reason("GitHub Copilot account not licensed"),
|
|
1349
|
-
// A license refusal is evidence about the account even at provider
|
|
1350
|
-
// scope, for the same reason a named revocation or a buddy ban is.
|
|
1351
|
-
true,
|
|
1352
|
-
),
|
|
1353
|
-
},
|
|
1425
|
+
...githubRules,
|
|
1354
1426
|
{
|
|
1355
1427
|
name: "credential-rejected",
|
|
1356
1428
|
match: (s) =>
|
|
@@ -1442,30 +1514,7 @@ const ACCOUNT_ERROR_RULES: readonly AccountErrorRule[] = [
|
|
|
1442
1514
|
decide: (s) =>
|
|
1443
1515
|
s.result("quota_exhausted", "cooldown", 0, null, s.reason("Request price refused by the provider"), false),
|
|
1444
1516
|
},
|
|
1445
|
-
|
|
1446
|
-
// Qoder's code-112 pricing refusal (`{"code":"112","message":"{\"pricingUrl\":...}"}`)
|
|
1447
|
-
// is a "this model is not included in this account's plan, buy more" verdict
|
|
1448
|
-
// for one pair — the account keeps serving its other models. The probe path
|
|
1449
|
-
// does not always carry the code in its evidence (only the message does), so
|
|
1450
|
-
// the match keys on the provider id plus the pricing-page wording as well.
|
|
1451
|
-
// It must run before `quota-exhausted`, which would park the whole account.
|
|
1452
|
-
name: "qoder-plan-gated",
|
|
1453
|
-
match: (s) =>
|
|
1454
|
-
(s.providerId === "qoder" || s.providerId === "qodercn") &&
|
|
1455
|
-
(s.providerCode === "112" || s.lower.includes("pricingurl")),
|
|
1456
|
-
decide: (s) => {
|
|
1457
|
-
const cooldownMs = unsupportedModelCooldownMs(s.modelBlockHits);
|
|
1458
|
-
return s.result(
|
|
1459
|
-
"model_unreachable",
|
|
1460
|
-
"cooldown",
|
|
1461
|
-
cooldownMs,
|
|
1462
|
-
new Date(Date.now() + cooldownMs),
|
|
1463
|
-
s.reason("Model not included in this account's plan"),
|
|
1464
|
-
s.origin === "upstream" && s.modelId !== undefined,
|
|
1465
|
-
true,
|
|
1466
|
-
);
|
|
1467
|
-
},
|
|
1468
|
-
},
|
|
1517
|
+
qoderPlanGatedRule,
|
|
1469
1518
|
{
|
|
1470
1519
|
name: "model-plan-gated",
|
|
1471
1520
|
match: (s) => s.quotaSignal && s.providerCode === "112",
|
|
@@ -1482,106 +1531,29 @@ const ACCOUNT_ERROR_RULES: readonly AccountErrorRule[] = [
|
|
|
1482
1531
|
);
|
|
1483
1532
|
},
|
|
1484
1533
|
},
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
// now."` when the upstream does not serve the account's region. That is a
|
|
1488
|
-
// durable verdict on the account, not a transient transport fault: it
|
|
1489
|
-
// reproduces on every request until the region opens, so it must cool hard
|
|
1490
|
-
// for a day rather than be re-dialled every thirty seconds. Must run
|
|
1491
|
-
// before `transport-fault`, which would read the 502 as a short
|
|
1492
|
-
// `server_error`.
|
|
1493
|
-
name: "trae-region-unavailable",
|
|
1494
|
-
match: (s) =>
|
|
1495
|
-
(s.providerId === "trae" || s.providerId === "traecn") &&
|
|
1496
|
-
(s.providerCode === "4013" || s.lower.includes("not available in the current region")),
|
|
1497
|
-
decide: (s) => {
|
|
1498
|
-
const cooldownMs = TRAE_REGION_COOLDOWN_MS;
|
|
1499
|
-
return s.result(
|
|
1500
|
-
"quota_exhausted",
|
|
1501
|
-
"cooldown",
|
|
1502
|
-
cooldownMs,
|
|
1503
|
-
new Date(Date.now() + cooldownMs),
|
|
1504
|
-
s.reason("Trae service unavailable in the current region"),
|
|
1505
|
-
// Account-scoped provider evidence still has to reach the writer: a
|
|
1506
|
-
// stream frame carries no credential detail, so `canMutate` is false
|
|
1507
|
-
// and without this the account stays in rotation to reproduce the
|
|
1508
|
-
// refusal for the whole window the region is closed for.
|
|
1509
|
-
true,
|
|
1510
|
-
);
|
|
1511
|
-
},
|
|
1512
|
-
},
|
|
1513
|
-
{
|
|
1514
|
-
// Trae's free-tier quota (`4008` / "exceeded the quota") resets daily.
|
|
1515
|
-
// Treating it like the generic quota cooldown (1h account-wide fallback)
|
|
1516
|
-
// let the account re-enter rotation inside the window and reproduce the
|
|
1517
|
-
// refusal every few minutes — the "cooldown not holding" report. The
|
|
1518
|
-
// verdict is a full-day backoff on *any* model (the account's quota is
|
|
1519
|
-
// spent, so every model on it shares the wall). Scoped to the model so
|
|
1520
|
-
// the account is not disabled wholesale; the 1-day window means a model
|
|
1521
|
-
// stays parked for the day instead of flapping. Must run before
|
|
1522
|
-
// `quota-exhausted`, which would apply the generic shorter fallback.
|
|
1523
|
-
name: "trae-quota-exhausted",
|
|
1524
|
-
match: (s) =>
|
|
1525
|
-
(s.providerId === "trae" || s.providerId === "traecn") &&
|
|
1526
|
-
(s.providerCode === "4008" || s.lower.includes("have exceeded the quota")),
|
|
1527
|
-
decide: (s) => {
|
|
1528
|
-
const cooldownMs = s.statedCooldownMs ?? TRAE_QUOTA_COOLDOWN_MS;
|
|
1529
|
-
return s.result(
|
|
1530
|
-
"model_unreachable",
|
|
1531
|
-
"cooldown",
|
|
1532
|
-
cooldownMs,
|
|
1533
|
-
new Date(Date.now() + cooldownMs),
|
|
1534
|
-
s.reason("Trae quota exhausted for this model"),
|
|
1535
|
-
s.origin === "upstream" && s.modelId !== undefined,
|
|
1536
|
-
);
|
|
1537
|
-
},
|
|
1538
|
-
},
|
|
1539
|
-
{
|
|
1540
|
-
// Qoder's quota verdicts — `110` "Billing daily count exceeded" and the
|
|
1541
|
-
// upstream-499 exhausted-quota close — are per-model: one model's spend
|
|
1542
|
-
// cap does not drain another. Scoping them to the model keeps the account
|
|
1543
|
-
// serving its other models. Must run before `quota-exhausted`, which
|
|
1544
|
-
// parks the whole account.
|
|
1545
|
-
name: "qoder-model-quota",
|
|
1546
|
-
match: (s) =>
|
|
1547
|
-
(s.providerId === "qoder" || s.providerId === "qodercn") && s.quotaSignal,
|
|
1548
|
-
decide: (s) => {
|
|
1549
|
-
const cooldownMs = s.statedCooldownMs ?? QODER_QUOTA_COOLDOWN_MS;
|
|
1550
|
-
return s.result(
|
|
1551
|
-
"model_unreachable",
|
|
1552
|
-
"cooldown",
|
|
1553
|
-
cooldownMs,
|
|
1554
|
-
new Date(Date.now() + cooldownMs),
|
|
1555
|
-
s.reason("Qoder quota exhausted for this model"),
|
|
1556
|
-
s.origin === "upstream" && s.modelId !== undefined,
|
|
1557
|
-
);
|
|
1558
|
-
},
|
|
1559
|
-
},
|
|
1534
|
+
...traeRules,
|
|
1535
|
+
qoderModelQuotaRule,
|
|
1560
1536
|
{
|
|
1561
1537
|
name: "quota-exhausted",
|
|
1562
|
-
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),
|
|
1563
1545
|
decide: (s) => {
|
|
1564
1546
|
// xAI Grok Build's free tier resets on a rolling window. Use a duration
|
|
1565
1547
|
// stated by the provider when present; otherwise park for the conservative
|
|
1566
1548
|
// 24-hour fallback.
|
|
1567
|
-
const grokFreeTier =
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
s.providerCode.includes("credits") ||
|
|
1576
|
-
s.lower.includes("free-usage") ||
|
|
1577
|
-
s.lower.includes("free usage") ||
|
|
1578
|
-
s.lower.includes("included free usage") ||
|
|
1579
|
-
s.lower.includes("rolling 24-hour") ||
|
|
1580
|
-
s.lower.includes("rolling 24 hour") ||
|
|
1581
|
-
s.lower.includes("spending limit") ||
|
|
1582
|
-
s.lower.includes("out of credits") ||
|
|
1583
|
-
s.lower.includes("run out of credits") ||
|
|
1584
|
-
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"));
|
|
1585
1557
|
const buddyCreditsExhausted =
|
|
1586
1558
|
s.providerCode === "14018" || s.lower.includes("credits exhausted");
|
|
1587
1559
|
const cooldownMs = grokFreeTier
|
|
@@ -1600,65 +1572,13 @@ const ACCOUNT_ERROR_RULES: readonly AccountErrorRule[] = [
|
|
|
1600
1572
|
new Date(Date.now() + cooldownMs),
|
|
1601
1573
|
s.reason("Provider quota exhausted"),
|
|
1602
1574
|
s.origin === "upstream",
|
|
1575
|
+
false,
|
|
1576
|
+
grokFreeUsageModelScope && s.modelId !== undefined,
|
|
1603
1577
|
);
|
|
1604
1578
|
},
|
|
1605
1579
|
},
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
// tokens / 100K output tokens) and the window resets every minute. A
|
|
1609
|
-
// generic 429 would cool the account for the 15-minute default, leaving a
|
|
1610
|
-
// healthy key parked long after the window refilled — the source of
|
|
1611
|
-
// "limitasiya lumayan keras". This arm must run before `rate-limit`: it
|
|
1612
|
-
// matches the same signals and shortens the window to 1 minute so routing
|
|
1613
|
-
// moves to the next account and the flagged one returns almost at once.
|
|
1614
|
-
name: "inception-per-minute-limit",
|
|
1615
|
-
match: (s) =>
|
|
1616
|
-
s.origin === "upstream" &&
|
|
1617
|
-
s.providerId === "inception" &&
|
|
1618
|
-
(s.statusCode === 429 ||
|
|
1619
|
-
s.providerCode === "rate_limit_reached" ||
|
|
1620
|
-
s.providerCode === "rate_limit_exceeded" ||
|
|
1621
|
-
s.lower.includes("rate_limit") ||
|
|
1622
|
-
s.lower.includes("rate limit") ||
|
|
1623
|
-
s.lower.includes("too many requests") ||
|
|
1624
|
-
s.lower.includes("tokens per minute") ||
|
|
1625
|
-
s.lower.includes("input token")),
|
|
1626
|
-
decide: (s) => {
|
|
1627
|
-
const cooldownMs = s.statedCooldownMs ?? INCEPTION_RATE_LIMIT_COOLDOWN_MS;
|
|
1628
|
-
return s.result(
|
|
1629
|
-
"rate_limit_transient",
|
|
1630
|
-
"cooldown",
|
|
1631
|
-
cooldownMs,
|
|
1632
|
-
new Date(Date.now() + cooldownMs),
|
|
1633
|
-
s.reason("Inception per-minute input-token limit"),
|
|
1634
|
-
s.origin === "upstream",
|
|
1635
|
-
);
|
|
1636
|
-
},
|
|
1637
|
-
},
|
|
1638
|
-
{
|
|
1639
|
-
// Perch's `turn_rate_limited` is a per-turn API window, not a per-request
|
|
1640
|
-
// 429: the account keeps answering but every call mints another ticket
|
|
1641
|
-
// into the same exhausted window. The stated remedy ("contact us for
|
|
1642
|
-
// direct API access") implies the window is long — park the account for a
|
|
1643
|
-
// day so routing picks a sibling instead of hammering the same key.
|
|
1644
|
-
name: "perch-turn-rate-limit",
|
|
1645
|
-
match: (s) =>
|
|
1646
|
-
s.origin === "upstream" &&
|
|
1647
|
-
s.providerId === "perch" &&
|
|
1648
|
-
(s.providerCode === "turn_rate_limited" ||
|
|
1649
|
-
(s.statusCode === 429 && s.lower.includes("turn rate"))),
|
|
1650
|
-
decide: (s) => {
|
|
1651
|
-
const cooldownMs = s.statedCooldownMs ?? PERCH_TURN_RATE_LIMIT_COOLDOWN_MS;
|
|
1652
|
-
return s.result(
|
|
1653
|
-
"rate_limit_transient",
|
|
1654
|
-
"cooldown",
|
|
1655
|
-
cooldownMs,
|
|
1656
|
-
new Date(Date.now() + cooldownMs),
|
|
1657
|
-
s.reason("Perch turn rate limit"),
|
|
1658
|
-
s.origin === "upstream",
|
|
1659
|
-
);
|
|
1660
|
-
},
|
|
1661
|
-
},
|
|
1580
|
+
...inceptionRules,
|
|
1581
|
+
...perchRules,
|
|
1662
1582
|
{
|
|
1663
1583
|
name: "rate-limit",
|
|
1664
1584
|
match: (s) =>
|
|
@@ -1750,34 +1670,6 @@ const ACCOUNT_ERROR_RULES: readonly AccountErrorRule[] = [
|
|
|
1750
1670
|
);
|
|
1751
1671
|
},
|
|
1752
1672
|
},
|
|
1753
|
-
{
|
|
1754
|
-
// Qoder closes an exhausted-quota stream with upstream 499 (nginx
|
|
1755
|
-
// client-close style). The gateway's own aborts are local `transport_closed`
|
|
1756
|
-
// and never carry a qoder providerId, so a 499 scoped to qoder is a quota
|
|
1757
|
-
// verdict, not a transport fault. It must run before `transport-fault`,
|
|
1758
|
-
// which otherwise reads the status as a 5xx-class outage and cools the
|
|
1759
|
-
// account for the shorter transient window — or leaves it unmutated.
|
|
1760
|
-
//
|
|
1761
|
-
// The verdict is per-model, like Qoder's other quota signs: one model's
|
|
1762
|
-
// spend cap does not drain another, so it cools the pair, never the whole
|
|
1763
|
-
// account.
|
|
1764
|
-
name: "qoder-499-quota",
|
|
1765
|
-
match: (s) =>
|
|
1766
|
-
(s.providerId === "qoder" || s.providerId === "qodercn") &&
|
|
1767
|
-
s.statusCode === 499 &&
|
|
1768
|
-
s.origin === "upstream",
|
|
1769
|
-
decide: (s) => {
|
|
1770
|
-
const cooldownMs = s.statedCooldownMs ?? QODER_QUOTA_COOLDOWN_MS;
|
|
1771
|
-
return s.result(
|
|
1772
|
-
"model_unreachable",
|
|
1773
|
-
"cooldown",
|
|
1774
|
-
cooldownMs,
|
|
1775
|
-
new Date(Date.now() + cooldownMs),
|
|
1776
|
-
s.reason("Qoder quota exhausted for this model (upstream 499)"),
|
|
1777
|
-
s.origin === "upstream" && s.modelId !== undefined,
|
|
1778
|
-
);
|
|
1779
|
-
},
|
|
1780
|
-
},
|
|
1781
1673
|
{
|
|
1782
1674
|
name: "transport-fault",
|
|
1783
1675
|
match: (s) =>
|