cartethyia 2.7.3 → 2.7.4
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/dist/dashboard/assets/{BannedPage-2TJD37Hb.js → BannedPage-ChG5svFb.js} +1 -1
- package/dist/dashboard/assets/BannedPage-ChG5svFb.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-xefJNRmJ.js → CliToolDetailPage-BIcWQun7.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-Rj9iHQ3F.js → CliToolsPage-Bf2VelDe.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-JEbYJFxw.js → CombosPage-DCgm5g6g.js} +1 -1
- package/dist/dashboard/assets/CombosPage-DCgm5g6g.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js +1 -0
- package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js +1 -0
- package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-C4w7vea-.js → LoginPage-U6AKaJcF.js} +1 -1
- package/dist/dashboard/assets/LoginPage-U6AKaJcF.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-BtPcAfot.js → ModelPicker-BMS2t8BA.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-BMS2t8BA.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js +1 -0
- package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js +5 -0
- package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js +9 -0
- package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-L0skzML8.js +1 -0
- package/dist/dashboard/assets/QuotaPage-L0skzML8.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js +1 -0
- package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-BQS0Rx_u.js → SetupPage-f0DE1h0-.js} +1 -1
- package/dist/dashboard/assets/SetupPage-f0DE1h0-.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-BAL5npBC.js → SortableList-ClzpwyET.js} +1 -1
- package/dist/dashboard/assets/SortableList-ClzpwyET.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-TP7voAQ0.js → StudioPage-Cq5tW7hW.js} +3 -3
- package/dist/dashboard/assets/StudioPage-Cq5tW7hW.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js +56 -0
- package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-C38iXoO4.js +1 -0
- package/dist/dashboard/assets/api-keys-C38iXoO4.js.gz +0 -0
- package/dist/dashboard/assets/app-a159k0ub.css +2 -0
- package/dist/dashboard/assets/app-a159k0ub.css.gz +0 -0
- package/dist/dashboard/assets/assert-DbRA8EG2.js +15 -0
- package/dist/dashboard/assets/assert-DbRA8EG2.js.gz +0 -0
- package/dist/dashboard/assets/{badge-B_DMpMJN.js → badge-D9CS6dZk.js} +1 -1
- package/dist/dashboard/assets/badge-D9CS6dZk.js.gz +0 -0
- package/dist/dashboard/assets/{banner-DMrMPmth.js → banner-DympBNq4.js} +1 -1
- package/dist/dashboard/assets/banner-DympBNq4.js.gz +0 -0
- package/dist/dashboard/assets/{card-Be5Uzamx.js → card-ibRWU_uT.js} +1 -1
- package/dist/dashboard/assets/card-ibRWU_uT.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-63qBGaRw.js +1 -0
- package/dist/dashboard/assets/cli-tools-63qBGaRw.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-DOfrVmsi.js → clipboard-button-DJm-9pz7.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-DJm-9pz7.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-C9omdjQt.js → dialog-8fiE6tec.js} +1 -1
- package/dist/dashboard/assets/dialog-8fiE6tec.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-Bm8WKaGY.js → drawer-CqGYeOrd.js} +1 -1
- package/dist/dashboard/assets/drawer-CqGYeOrd.js.gz +0 -0
- package/dist/dashboard/assets/{entry-q5HwZ8nN.js → entry-BSAsYsmV.js} +1 -1
- package/dist/dashboard/assets/entry-BSAsYsmV.js.gz +0 -0
- package/dist/dashboard/assets/entry-CiIQULZD.js +4 -0
- package/dist/dashboard/assets/entry-CiIQULZD.js.gz +0 -0
- package/dist/dashboard/assets/entry-Daz0lJ-W.js +1 -0
- package/dist/dashboard/assets/entry-Daz0lJ-W.js.gz +0 -0
- package/dist/dashboard/assets/{formatters-BTScrQpe.js → formatters-D-MTJxMr.js} +1 -1
- package/dist/dashboard/assets/formatters-D-MTJxMr.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-Orjuzemr.js → github-badge-CPK47xQx.js} +1 -1
- package/dist/dashboard/assets/github-badge-CPK47xQx.js.gz +0 -0
- package/dist/dashboard/assets/{index-cPrX5vn6.js → index-DBNPOfhV.js} +2 -2
- package/dist/dashboard/assets/index-DBNPOfhV.js.gz +0 -0
- package/dist/dashboard/assets/{inline-Dvux8w-0.js → inline-BT0zntEr.js} +1 -1
- package/dist/dashboard/assets/inline-BT0zntEr.js.gz +0 -0
- package/dist/dashboard/assets/{input-CU1Rgf3p.js → input-Da2ZViWg.js} +1 -1
- package/dist/dashboard/assets/input-Da2ZViWg.js.gz +0 -0
- package/dist/dashboard/assets/{layout-C_7A6H55.js → layout-BXNeLw6M.js} +1 -1
- package/dist/dashboard/assets/layout-BXNeLw6M.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-CzFvogbj.js → list-row-BycOrrIV.js} +1 -1
- package/dist/dashboard/assets/list-row-BycOrrIV.js.gz +0 -0
- package/dist/dashboard/assets/live-DCQNZpF7.js +1 -0
- package/dist/dashboard/assets/live-DCQNZpF7.js.gz +0 -0
- package/dist/dashboard/assets/minimize-2-BTmMTK4o.js +1 -0
- package/dist/dashboard/assets/minimize-2-BTmMTK4o.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-JJGQm8TD.js → mutation-Rr0TIvcj.js} +1 -1
- package/dist/dashboard/assets/mutation-Rr0TIvcj.js.gz +0 -0
- package/dist/dashboard/assets/network-9Yj2ZyLo.js +1 -0
- package/dist/dashboard/assets/network-9Yj2ZyLo.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-BNDZnsUE.js → numbers-CEagj6ZZ.js} +2 -2
- package/dist/dashboard/assets/numbers-CEagj6ZZ.js.gz +0 -0
- package/dist/dashboard/assets/{query-keys-CLaNz8Xz.js → query-keys-BloKtaXJ.js} +1 -1
- package/dist/dashboard/assets/query-keys-BloKtaXJ.js.gz +0 -0
- package/dist/dashboard/assets/quota-Caw7wqci.js +1 -0
- package/dist/dashboard/assets/quota-Caw7wqci.js.gz +0 -0
- package/dist/dashboard/assets/routing-CO9zCUus.js +1 -0
- package/dist/dashboard/assets/routing-CO9zCUus.js.gz +0 -0
- package/dist/dashboard/assets/{select-B3iTetHN.js → select-BIYIBkMU.js} +2 -2
- package/dist/dashboard/assets/select-BIYIBkMU.js.gz +0 -0
- package/dist/dashboard/assets/settings-jS3-PNSA.js +1 -0
- package/dist/dashboard/assets/settings-jS3-PNSA.js.gz +0 -0
- package/dist/dashboard/assets/{stack-KrOrm7JZ.js → stack-DfgnFDWY.js} +1 -1
- package/dist/dashboard/assets/stack-DfgnFDWY.js.gz +0 -0
- package/dist/dashboard/assets/{state-DLS7IDMy.js → state-CkfdYWel.js} +1 -1
- package/dist/dashboard/assets/state-CkfdYWel.js.gz +0 -0
- package/dist/dashboard/assets/{switch-42ItvbAs.js → switch-SYm237HG.js} +1 -1
- package/dist/dashboard/assets/switch-SYm237HG.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-Dmnfh1Ue.js → useMutation-BQqCRNIi.js} +1 -1
- package/dist/dashboard/assets/useMutation-BQqCRNIi.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 +55 -54
- package/migrations/full-postgres/0027_qoder_identity_genuine_seed.sql +9 -10
- package/migrations/full-postgres/0032_add_provider_pool_accounts.sql +53 -0
- package/migrations/full-postgres/0033_add_api_key_ban.sql +9 -0
- package/migrations/full-postgres/0034_add_api_key_ban_until.sql +7 -0
- package/migrations/full-postgres/0035_align_pool_account_status_enum.sql +21 -0
- package/migrations/lite-sqlite3/0027_qoder_identity_genuine_seed.sql +9 -10
- package/migrations/lite-sqlite3/0032_add_provider_pool_accounts.sql +44 -0
- package/migrations/lite-sqlite3/0033_add_api_key_ban.sql +8 -0
- package/migrations/lite-sqlite3/0034_add_api_key_ban_until.sql +5 -0
- package/migrations/lite-sqlite3/0035_align_pool_account_status_enum.sql +6 -0
- package/package.json +2 -1
- package/src/boot/app.ts +9 -8
- package/src/config/network.ts +7 -3
- package/src/console/api-keys/contracts.ts +52 -0
- package/src/console/api-keys/routes.ts +93 -4
- package/src/console/audit/routes.ts +3 -3
- package/src/console/auth/account-reset.ts +4 -4
- package/src/console/auth/session.ts +3 -3
- package/src/console/backup/contracts.ts +2 -0
- package/src/console/backup/export-endpoint.ts +6 -6
- package/src/console/backup/routes.ts +3 -3
- package/src/console/cli-tools/routes.ts +3 -3
- package/src/console/console-router-guards.ts +2 -2
- package/src/console/console-router.ts +4 -4
- package/src/console/data-management/routes.ts +3 -3
- package/src/console/domain-registration.ts +2 -2
- package/src/console/observability/contracts.ts +4 -0
- package/src/console/observability/live.ts +3 -3
- package/src/console/observability/logs.ts +3 -3
- package/src/console/observability/performance.ts +3 -3
- package/src/console/observability/routes.ts +3 -3
- package/src/console/observability/store.ts +16 -16
- package/src/console/providers/catalog/contracts.ts +43 -22
- package/src/console/providers/catalog/provider-operations.ts +17 -2
- package/src/console/providers/catalog/routes.ts +3 -3
- package/src/console/providers/catalog/store-helpers.ts +349 -326
- package/src/console/providers/catalog/store.ts +1523 -1292
- package/src/console/providers/detail/routes.ts +3 -3
- package/src/console/providers/oauth/routes.ts +3 -3
- package/src/console/quota/account-quota-tenant.ts +26 -1
- package/src/console/quota/account-test-egress.ts +4 -4
- package/src/console/routing/model/routes.ts +3 -3
- package/src/console/routing/pools/contracts.ts +11 -8
- package/src/console/routing/pools/operations.ts +14 -7
- package/src/console/routing/pools/probe-verdict.ts +8 -3
- package/src/console/routing/pools/relay-deploy.ts +9 -9
- package/src/console/routing/pools/routes.ts +3 -3
- package/src/console/routing/pools/store.ts +21 -1
- package/src/console/settings/routes.ts +3 -3
- package/src/console/share/share-router.ts +12 -12
- package/src/console/share/share-stats.ts +105 -35
- package/src/console/shared/read-through-cache.ts +38 -2
- package/src/console/updates/routes.ts +7 -4
- package/src/network/outbound-fetch.ts +75 -4
- package/src/network/pool/resolver.ts +39 -39
- package/src/network/pool-cooldown-store.ts +71 -16
- package/src/network/pool-health-machine.ts +21 -1
- package/src/network/rate-limit-verdict.ts +68 -0
- package/src/network/ssrf.ts +4 -4
- package/src/observability/event-query.ts +320 -16
- package/src/observability/telemetry-status.ts +1 -1
- package/src/persistence/db-sqlite.ts +7 -2
- package/src/persistence/readiness.ts +18 -6
- package/src/persistence/schema.ts +66 -7
- package/src/persistence/stores/api-key-store.ts +99 -3
- package/src/persistence/stores/telemetry-store.ts +1 -1
- package/src/protocol/codecs.ts +6 -6
- package/src/protocol/gateway-error.ts +6 -3
- package/src/protocol/image-generation.ts +2 -2
- package/src/protocol/response/chat.ts +2 -2
- package/src/protocol/response/codex.ts +1 -1
- package/src/protocol/response/gemini.ts +1 -1
- package/src/protocol/response/messages.ts +5 -5
- package/src/protocol/response/responses.ts +2 -2
- package/src/protocol/sse-decoder.ts +14 -3
- package/src/protocol/translation/capabilities.ts +2 -2
- package/src/protocol/upstream/send-claude-messages.ts +3 -3
- package/src/providers/adapters/base-adapter.ts +1 -1
- package/src/providers/adapters/compatible-adapter.ts +2 -2
- package/src/providers/authentication/oauth-refresh-service.ts +2 -2
- package/src/providers/configured-provider.ts +1 -1
- package/src/providers/discovery/probe-phases.ts +2 -2
- package/src/providers/discovery/probing-service.ts +12 -12
- package/src/providers/integrations/agentrouter/agentrouter.ts +2 -3
- package/src/providers/integrations/alibabacp/alibabacp.ts +2 -2
- package/src/providers/integrations/anthropic.ts +4 -11
- package/src/providers/integrations/antigravity/antigravity.ts +13 -24
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -1
- package/src/providers/integrations/buddy/buddy-discovery-shared.ts +37 -6
- package/src/providers/integrations/buddy/buddy-identity.ts +22 -1
- package/src/providers/integrations/buddy/buddy-media.ts +16 -16
- package/src/providers/integrations/buddy/codebuddy/provider.ts +1 -1
- package/src/providers/integrations/buddy/workbuddy/provider.ts +1 -1
- package/src/providers/integrations/claude/claude-betas.ts +1 -1
- package/src/providers/integrations/claude/claude-credentials.ts +1 -1
- package/src/providers/integrations/claude/claude-slow-lane-retry.ts +1 -1
- package/src/providers/integrations/claude/claude.ts +3 -10
- package/src/providers/integrations/cline/cline.ts +1 -1
- package/src/providers/integrations/cloudflareai/cloudflareai.ts +4 -7
- package/src/providers/integrations/codex/codex-errors.ts +1 -1
- package/src/providers/integrations/codex/codex.ts +14 -15
- package/src/providers/integrations/commandcode/commandcode.ts +4 -4
- package/src/providers/integrations/commandcode/quota.ts +5 -1
- package/src/providers/integrations/cursor/cursor-http2.ts +11 -11
- package/src/providers/integrations/cursor/cursor.ts +15 -15
- package/src/providers/integrations/factory/factory.ts +4 -4
- package/src/providers/integrations/gemini.ts +11 -11
- package/src/providers/integrations/github/github.ts +6 -6
- package/src/providers/integrations/grok/grok.ts +2 -6
- package/src/providers/integrations/inferhub.ts +4 -11
- package/src/providers/integrations/kimi/kimi.ts +4 -11
- package/src/providers/integrations/kiro/kiro.ts +10 -10
- package/src/providers/integrations/muse/muse.ts +6 -9
- package/src/providers/integrations/perplexity.ts +2 -2
- package/src/providers/integrations/qoder/qoder-catalog.ts +47 -9
- package/src/providers/integrations/qoder/qoder-checkin.ts +26 -8
- package/src/providers/integrations/qoder/qoder-native-identity-store.ts +9 -8
- package/src/providers/integrations/qoder/qoder-realm.ts +15 -0
- package/src/providers/integrations/qoder/qoder-umid.ts +12 -13
- package/src/providers/integrations/qoder/qoder-virtual.ts +2 -2
- package/src/providers/integrations/qoder/qoder.ts +188 -22
- package/src/providers/integrations/tokenharbor.ts +2 -2
- package/src/providers/integrations/trae/trae.ts +6 -9
- package/src/providers/integrations/zcode/oauth.ts +1 -1
- package/src/providers/operations/provider-catalog-service.ts +4 -4
- package/src/providers/operations/provider-credential-service.ts +16 -4
- package/src/providers/operations/upstream-deadline.ts +3 -3
- package/src/providers/provider-contracts.ts +47 -3
- package/src/providers/provider-credential-resolver.ts +1 -1
- package/src/providers/provider-registry.ts +4 -4
- package/src/providers/quota/quota-contracts.ts +24 -10
- package/src/providers/quota/refresh.ts +2 -2
- package/src/providers/search/search-provider.ts +5 -5
- package/src/security/abuse.ts +4 -4
- package/src/security/admission/reasons.ts +17 -17
- package/src/security/api-key-auth.ts +4 -4
- package/src/shared/coerce.ts +19 -0
- package/src/transport/dispatch/attempt-finalize.ts +4 -3
- package/src/transport/dispatch/attempt-loop.ts +10 -7
- package/src/transport/dispatch/empty-stream.ts +6 -6
- package/src/transport/dispatch/generation.ts +9 -9
- package/src/transport/dispatch/leases.ts +8 -8
- package/src/transport/dispatch/proxy-request.ts +7 -7
- package/src/transport/dispatch/responses-compact.ts +11 -11
- package/src/transport/dispatch/retry-policy.ts +4 -67
- package/src/transport/dispatch/stream-bridge.ts +1 -1
- package/src/transport/dispatch/streaming/finalize.ts +9 -8
- package/src/transport/dispatch/systemone.ts +8 -8
- package/src/transport/dispatch/token-count.ts +9 -9
- package/src/transport/dispatch/websearch.ts +6 -6
- package/src/transport/drain-endpoint.ts +2 -2
- package/src/transport/failure-policy.ts +13 -6
- package/src/transport/guards/auth-guard.ts +6 -5
- package/src/transport/guards/body-policy.ts +4 -4
- package/src/transport/guards/error-lifecycle.ts +18 -3
- package/src/transport/guards/guard-errors.ts +7 -3
- package/src/transport/guards/headroom-guard.ts +1 -1
- package/src/transport/guards/pipeline.ts +6 -6
- package/src/transport/guards/readiness-guard.ts +16 -2
- package/src/transport/guards/request-context.ts +3 -3
- package/src/transport/request/preparer.ts +20 -20
- package/src/transport/request/state.ts +4 -7
- package/src/transport/routing/route-catalog.ts +97 -2
- package/src/transport/routing/route-model.ts +7 -7
- package/src/transport/routing/router.ts +2 -2
- package/src/transport/shutdown-notice.ts +19 -7
- package/src/transport/surface/completion.ts +1 -1
- package/src/transport/surface/messages/encode.ts +1 -1
- package/dist/dashboard/assets/BannedPage-2TJD37Hb.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-xefJNRmJ.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-Rj9iHQ3F.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-JEbYJFxw.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-C3xx_inM.js +0 -1
- package/dist/dashboard/assets/ConsoleLogPage-C3xx_inM.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-Dw306gJU.js +0 -1
- package/dist/dashboard/assets/CustomizationPage-Dw306gJU.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-jaBo1TWV.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-jaBo1TWV.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-C4w7vea-.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-BtPcAfot.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-CGC5gNs1.js +0 -1
- package/dist/dashboard/assets/OverviewPage-CGC5gNs1.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-DQcthNGt.js +0 -4
- package/dist/dashboard/assets/ProviderDetailPage-DQcthNGt.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-0OsXRGXe.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-0OsXRGXe.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-DdPc8slb.js +0 -9
- package/dist/dashboard/assets/ProxyPage-DdPc8slb.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-Ci5O4BnH.js +0 -1
- package/dist/dashboard/assets/QuotaPage-Ci5O4BnH.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CDZjTt87.js +0 -1
- package/dist/dashboard/assets/SettingsPage-CDZjTt87.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-BQS0Rx_u.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-BAL5npBC.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-TP7voAQ0.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-Cj_fEzhg.js +0 -56
- package/dist/dashboard/assets/UsagePage-Cj_fEzhg.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-MbI7e7rt.js +0 -1
- package/dist/dashboard/assets/api-keys-MbI7e7rt.js.gz +0 -0
- package/dist/dashboard/assets/app-B2vyPoII.css +0 -2
- package/dist/dashboard/assets/app-B2vyPoII.css.gz +0 -0
- package/dist/dashboard/assets/assert-C0YYuZJ4.js +0 -1
- package/dist/dashboard/assets/assert-C0YYuZJ4.js.gz +0 -0
- package/dist/dashboard/assets/badge-B_DMpMJN.js.gz +0 -0
- package/dist/dashboard/assets/banner-DMrMPmth.js.gz +0 -0
- package/dist/dashboard/assets/card-Be5Uzamx.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-muFRphDu.js +0 -1
- package/dist/dashboard/assets/cli-tools-muFRphDu.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-DOfrVmsi.js.gz +0 -0
- package/dist/dashboard/assets/dialog-C9omdjQt.js.gz +0 -0
- package/dist/dashboard/assets/drawer-Bm8WKaGY.js.gz +0 -0
- package/dist/dashboard/assets/entry-DS90gUks.js +0 -1
- package/dist/dashboard/assets/entry-DS90gUks.js.gz +0 -0
- package/dist/dashboard/assets/entry-j40LJotO.js +0 -4
- package/dist/dashboard/assets/entry-j40LJotO.js.gz +0 -0
- package/dist/dashboard/assets/entry-q5HwZ8nN.js.gz +0 -0
- package/dist/dashboard/assets/formatters-BTScrQpe.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-Orjuzemr.js.gz +0 -0
- package/dist/dashboard/assets/index-cPrX5vn6.js.gz +0 -0
- package/dist/dashboard/assets/inline-Dvux8w-0.js.gz +0 -0
- package/dist/dashboard/assets/input-CU1Rgf3p.js.gz +0 -0
- package/dist/dashboard/assets/layout-C_7A6H55.js.gz +0 -0
- package/dist/dashboard/assets/list-row-CzFvogbj.js.gz +0 -0
- package/dist/dashboard/assets/live-KaG15HLQ.js +0 -1
- package/dist/dashboard/assets/live-KaG15HLQ.js.gz +0 -0
- package/dist/dashboard/assets/minimize-2-D-MN8W8a.js +0 -1
- package/dist/dashboard/assets/minimize-2-D-MN8W8a.js.gz +0 -0
- package/dist/dashboard/assets/mutation-JJGQm8TD.js.gz +0 -0
- package/dist/dashboard/assets/numbers-BNDZnsUE.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-CLaNz8Xz.js.gz +0 -0
- package/dist/dashboard/assets/quota-DZGyp0N5.js +0 -1
- package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
- package/dist/dashboard/assets/routing-BEH32QOC.js +0 -1
- package/dist/dashboard/assets/routing-BEH32QOC.js.gz +0 -0
- package/dist/dashboard/assets/select-B3iTetHN.js.gz +0 -0
- package/dist/dashboard/assets/settings-Dt3Eqbnq.js +0 -1
- package/dist/dashboard/assets/settings-Dt3Eqbnq.js.gz +0 -0
- package/dist/dashboard/assets/stack-KrOrm7JZ.js.gz +0 -0
- package/dist/dashboard/assets/state-DLS7IDMy.js.gz +0 -0
- package/dist/dashboard/assets/switch-42ItvbAs.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-Dmnfh1Ue.js.gz +0 -0
- /package/dist/dashboard/assets/{app-wvwCi4Eh.js → app-BayTTy01.js} +0 -0
- /package/dist/dashboard/assets/{app-wvwCi4Eh.js.gz → app-BayTTy01.js.gz} +0 -0
|
@@ -89,7 +89,7 @@ function assertAdapterIdentity(expected: ProviderId, adapter: ProviderAdapter):
|
|
|
89
89
|
// error: `invalid_request` would blame the caller and invite a byte-identical
|
|
90
90
|
// retry. `internal_error` (500) says what is true — the gateway is
|
|
91
91
|
// misconfigured and the request never had a chance.
|
|
92
|
-
throw new GatewayError("internal_error", 500, "Loaded adapter ID does not match registration", {
|
|
92
|
+
throw new GatewayError("internal_error", 500, "Loaded adapter ID does not match registration (request {requestId}). Even we have bad days — check the registry wiring 🐻", {
|
|
93
93
|
provider_id: expected,
|
|
94
94
|
adapter_id: adapter.provider_id,
|
|
95
95
|
});
|
|
@@ -122,12 +122,12 @@ export class ProviderRegistry {
|
|
|
122
122
|
register(registration: ProviderRegistration): void {
|
|
123
123
|
const providerId = parseProviderId(registration.provider_id);
|
|
124
124
|
if (this.#registrations.has(providerId)) {
|
|
125
|
-
throw new GatewayError("invalid_request", 409, "Provider is already registered", {
|
|
125
|
+
throw new GatewayError("invalid_request", 409, "Provider is already registered (request {requestId}). This name's taken — pick another alias ✨", {
|
|
126
126
|
provider_id: providerId,
|
|
127
127
|
});
|
|
128
128
|
}
|
|
129
129
|
if (providerId !== registration.provider_id) {
|
|
130
|
-
throw new GatewayError("internal_error", 500, "Provider registration ID is not normalized", {
|
|
130
|
+
throw new GatewayError("internal_error", 500, "Provider registration ID is not normalized (request {requestId}). Even we have bad days — check the registry wiring 🐻", {
|
|
131
131
|
provider_id: registration.provider_id,
|
|
132
132
|
});
|
|
133
133
|
}
|
|
@@ -158,7 +158,7 @@ export class ProviderRegistry {
|
|
|
158
158
|
upsert(registration: ProviderRegistration): boolean {
|
|
159
159
|
const providerId = parseProviderId(registration.provider_id);
|
|
160
160
|
if (providerId !== registration.provider_id) {
|
|
161
|
-
throw new GatewayError("internal_error", 500, "Provider registration ID is not normalized", {
|
|
161
|
+
throw new GatewayError("internal_error", 500, "Provider registration ID is not normalized (request {requestId}). Even we have bad days — check the registry wiring 🐻", {
|
|
162
162
|
provider_id: registration.provider_id,
|
|
163
163
|
});
|
|
164
164
|
}
|
|
@@ -58,20 +58,27 @@ export function totalRemainingCredit(
|
|
|
58
58
|
* independent quota windows, or `null` when neither is reported.
|
|
59
59
|
*
|
|
60
60
|
* Pooled credits are summed before converting to percent, so one exhausted
|
|
61
|
-
* bonus pack cannot hide balance in another.
|
|
62
|
-
*
|
|
61
|
+
* bonus pack cannot hide balance in another. A pooled window without a ceiling
|
|
62
|
+
* (Command Code credits) contributes its absolute remaining to the credit sum
|
|
63
|
+
* but cannot produce a percent, so it is not counted in the percent ratio.
|
|
64
|
+
* Independent windows keep their own remaining percent and the lowest one
|
|
65
|
+
* gates the account.
|
|
63
66
|
*/
|
|
64
67
|
export function totalRemainingPercent(
|
|
65
68
|
windows: readonly ProviderQuotaWindow[],
|
|
66
69
|
): number | null {
|
|
67
70
|
let creditLimit = 0;
|
|
68
71
|
let creditRemaining = 0;
|
|
72
|
+
let hasPooledLimit = false;
|
|
69
73
|
let independentPercent: number | null = null;
|
|
70
74
|
for (const window of windows) {
|
|
71
75
|
if (window.accounting === "pooled") {
|
|
72
76
|
const balance = creditWindowBalance(window);
|
|
73
77
|
if (balance !== null) {
|
|
74
|
-
|
|
78
|
+
if (balance.limit !== null) {
|
|
79
|
+
creditLimit += balance.limit;
|
|
80
|
+
hasPooledLimit = true;
|
|
81
|
+
}
|
|
75
82
|
creditRemaining += balance.remaining;
|
|
76
83
|
}
|
|
77
84
|
continue;
|
|
@@ -85,8 +92,13 @@ export function totalRemainingPercent(
|
|
|
85
92
|
independentPercent =
|
|
86
93
|
independentPercent === null ? clamped : Math.min(independentPercent, clamped);
|
|
87
94
|
}
|
|
88
|
-
|
|
89
|
-
|
|
95
|
+
// A pooled window with a reported ceiling (including a 0 ceiling — a spent
|
|
96
|
+
// balance) always defines a percent: 0 ceiling means 0% remaining.
|
|
97
|
+
const creditPercent = hasPooledLimit
|
|
98
|
+
? creditLimit > 0
|
|
99
|
+
? Math.min(100, Math.max(0, (creditRemaining / creditLimit) * 100))
|
|
100
|
+
: 0
|
|
101
|
+
: null;
|
|
90
102
|
if (creditPercent === null) return independentPercent;
|
|
91
103
|
if (independentPercent === null) return creditPercent;
|
|
92
104
|
return Math.min(creditPercent, independentPercent);
|
|
@@ -94,20 +106,22 @@ export function totalRemainingPercent(
|
|
|
94
106
|
|
|
95
107
|
function creditWindowBalance(
|
|
96
108
|
window: ProviderQuotaWindow,
|
|
97
|
-
): { readonly limit: number; readonly remaining: number } | null {
|
|
109
|
+
): { readonly limit: number | null; readonly remaining: number } | null {
|
|
110
|
+
// A ceiling of 0 is a spent balance, not an absence of data; only a window
|
|
111
|
+
// with no remaining figure at all is skipped. `limit` may be null (Command
|
|
112
|
+
// Code reports credits without a ceiling) — the remaining total still counts.
|
|
98
113
|
const limit =
|
|
99
|
-
typeof window.limit === "number" && Number.isFinite(window.limit) && window.limit
|
|
114
|
+
typeof window.limit === "number" && Number.isFinite(window.limit) && window.limit >= 0
|
|
100
115
|
? window.limit
|
|
101
116
|
: null;
|
|
102
|
-
if (limit === null) return null;
|
|
103
117
|
let remaining: number | null = null;
|
|
104
118
|
if (typeof window.remaining === "number" && Number.isFinite(window.remaining)) {
|
|
105
119
|
remaining = window.remaining;
|
|
106
|
-
} else if (typeof window.used === "number" && Number.isFinite(window.used)) {
|
|
120
|
+
} else if (typeof window.used === "number" && Number.isFinite(window.used) && limit !== null) {
|
|
107
121
|
remaining = limit - window.used;
|
|
108
122
|
}
|
|
109
123
|
if (remaining === null) return null;
|
|
110
|
-
return { limit, remaining: Math.
|
|
124
|
+
return { limit, remaining: Math.max(0, remaining) };
|
|
111
125
|
}
|
|
112
126
|
|
|
113
127
|
const TIMEOUT_MS = 15_000;
|
|
@@ -16,7 +16,7 @@ import { type FetchLike, type ProviderQuotaResult } from "./quota-contracts";
|
|
|
16
16
|
import type { ProviderId } from "../provider-contracts";
|
|
17
17
|
import type { ProviderRegistry } from "../provider-registry";
|
|
18
18
|
import { GLOBAL_QUOTA_LENS, setCachedQuota } from "./cache";
|
|
19
|
-
import {
|
|
19
|
+
import { asJsonbRecord } from "../../shared/coerce";
|
|
20
20
|
|
|
21
21
|
/** Why an account quota refresh failed; `null` means the fetch succeeded. */
|
|
22
22
|
export type QuotaRefreshFailureCode = "missing_credential" | "quota_fetch_failed";
|
|
@@ -355,7 +355,7 @@ async function runQuotaRefresh(
|
|
|
355
355
|
? (deps.markApiKeyCredential?.(target.providerId, credential) ?? credential)
|
|
356
356
|
: credential;
|
|
357
357
|
try {
|
|
358
|
-
const authState =
|
|
358
|
+
const authState = asJsonbRecord(target.authState);
|
|
359
359
|
quota = await fetchProviderQuota(deps.providerRegistry, target.providerId, marked, fetcher, {
|
|
360
360
|
// Carried so a collector can reuse a reading captured from real
|
|
361
361
|
// traffic rather than spending a request to rediscover it.
|
|
@@ -53,7 +53,7 @@ function stringField(record: Record<string, unknown> | undefined, key: string):
|
|
|
53
53
|
export function searchParamsFromBody(body: Record<string, unknown>): SearchParams {
|
|
54
54
|
const query = typeof body.query === "string" ? body.query.trim() : "";
|
|
55
55
|
if (query.length === 0)
|
|
56
|
-
throw new GatewayError("invalid_request", 400, "search query is required");
|
|
56
|
+
throw new GatewayError("invalid_request", 400, "search query is required (request {requestId}). Check the shape of what you sent — then resend 🪞");
|
|
57
57
|
const rawMax = body.max_results;
|
|
58
58
|
const maxResults =
|
|
59
59
|
typeof rawMax === "number" && Number.isFinite(rawMax)
|
|
@@ -90,7 +90,7 @@ export function createSearchAdapter(spec: SearchProviderSpec): ProviderAdapter {
|
|
|
90
90
|
throw new GatewayError(
|
|
91
91
|
"capability_unsupported",
|
|
92
92
|
400,
|
|
93
|
-
`${spec.provider_id} serves web search, not a chat wire
|
|
93
|
+
`${spec.provider_id} serves web search, not a chat wire (request {requestId}). Know your tools — this one just isn't in the box 🧰`,
|
|
94
94
|
);
|
|
95
95
|
},
|
|
96
96
|
async websearch(
|
|
@@ -113,7 +113,7 @@ export function createSearchAdapter(spec: SearchProviderSpec): ProviderAdapter {
|
|
|
113
113
|
throw new GatewayError(
|
|
114
114
|
"platform_unavailable",
|
|
115
115
|
502,
|
|
116
|
-
`${spec.provider_id} search unreachable: ${error instanceof Error ? error.message : String(error)}
|
|
116
|
+
`${spec.provider_id} search unreachable (request {requestId}): ${error instanceof Error ? error.message : String(error)}. The connection ghosted us — come back in a moment 👻`,
|
|
117
117
|
);
|
|
118
118
|
}
|
|
119
119
|
if (!res.ok) {
|
|
@@ -121,7 +121,7 @@ export function createSearchAdapter(spec: SearchProviderSpec): ProviderAdapter {
|
|
|
121
121
|
throw new GatewayError(
|
|
122
122
|
res.status >= 500 ? "platform_unavailable" : "invalid_request",
|
|
123
123
|
res.status,
|
|
124
|
-
`${spec.provider_id} search error: ${detail.slice(0, 240)}
|
|
124
|
+
`${spec.provider_id} search error (request {requestId}): ${detail.slice(0, 240)}. The upstream came back empty-handed — retry 🌙`,
|
|
125
125
|
);
|
|
126
126
|
}
|
|
127
127
|
const raw = await res.text();
|
|
@@ -135,7 +135,7 @@ export function createSearchAdapter(spec: SearchProviderSpec): ProviderAdapter {
|
|
|
135
135
|
throw new GatewayError(
|
|
136
136
|
"platform_unavailable",
|
|
137
137
|
502,
|
|
138
|
-
`${spec.provider_id} search returned an unparseable body
|
|
138
|
+
`${spec.provider_id} search returned an unparseable body (request {requestId}). The upstream came back empty-handed — retry 🌙`,
|
|
139
139
|
);
|
|
140
140
|
}
|
|
141
141
|
const results = spec.normalize(data);
|
package/src/security/abuse.ts
CHANGED
|
@@ -399,7 +399,7 @@ export class IpAbuseProtectionService {
|
|
|
399
399
|
readonly signal?: AbortSignal;
|
|
400
400
|
}): Promise<void> {
|
|
401
401
|
if (input.signal?.aborted) {
|
|
402
|
-
throw new GatewayError("admission_unavailable", 503, "The abuse check never ran because the request was already cancelled.", {
|
|
402
|
+
throw new GatewayError("admission_unavailable", 503, "The abuse check never ran because the request was already cancelled (request {requestId}). Send it again whenever ready 🙃", {
|
|
403
403
|
reason: "admission-unavailable",
|
|
404
404
|
});
|
|
405
405
|
}
|
|
@@ -436,7 +436,7 @@ export class IpAbuseProtectionService {
|
|
|
436
436
|
|
|
437
437
|
if (outcome.banned) {
|
|
438
438
|
metrics.cartethyia_ip_abuse_refusals_total.inc(1, { reason: "ip-banned" });
|
|
439
|
-
throw new GatewayError("
|
|
439
|
+
throw new GatewayError("rate_limited", 429, "This address is temporarily banned for too many failed requests (request {requestId}). Wait for the ban to lift, then slow down. You've maxed this window — pace yourself ⏳", {
|
|
440
440
|
// Distinguishes this from the rate-limited 429 below and from an API
|
|
441
441
|
// key's own budget. It used to be "admission-unavailable", which is
|
|
442
442
|
// the label for an *unreachable* store — so every refusal from this
|
|
@@ -454,7 +454,7 @@ export class IpAbuseProtectionService {
|
|
|
454
454
|
|
|
455
455
|
if (outcome.count > this.maxRequestsPerWindow) {
|
|
456
456
|
metrics.cartethyia_ip_abuse_refusals_total.inc(1, { reason: "ip-rate-limited" });
|
|
457
|
-
throw new GatewayError("
|
|
457
|
+
throw new GatewayError("rate_limited", 429, "This address is sending requests faster than the abuse limit allows (request {requestId}). Back off and retry after the window rolls over. You've maxed this window — pace yourself ⏳", {
|
|
458
458
|
reason: "ip-rate-limited",
|
|
459
459
|
ip,
|
|
460
460
|
route: input.route,
|
|
@@ -464,7 +464,7 @@ export class IpAbuseProtectionService {
|
|
|
464
464
|
}
|
|
465
465
|
} catch (err) {
|
|
466
466
|
if (err instanceof GatewayError) throw err;
|
|
467
|
-
throw new GatewayError("admission_unavailable", 503, "The abuse-tracking store is unreachable, so requests from this address cannot be checked. Retry shortly.", {
|
|
467
|
+
throw new GatewayError("admission_unavailable", 503, "The abuse-tracking store is unreachable, so requests from this address cannot be checked (request {requestId}). Retry shortly. The bouncer stepped out — come back shortly 🚪", {
|
|
468
468
|
reason: "admission-unavailable",
|
|
469
469
|
cause: (err as Error).message,
|
|
470
470
|
});
|
|
@@ -19,26 +19,26 @@ const REASON_META: Record<
|
|
|
19
19
|
}
|
|
20
20
|
> = {
|
|
21
21
|
"rpm-exhausted": {
|
|
22
|
-
message: "This key has used its requests-per-minute allowance. Wait for the window to roll over, or raise the key's rpm limit.",
|
|
23
|
-
code: "
|
|
22
|
+
message: "This key has used its requests-per-minute allowance (request {requestId}). Wait for the window to roll over, or raise the key's rpm limit. You've maxed this window — pace yourself ⏳",
|
|
23
|
+
code: "rate_limited",
|
|
24
24
|
status: 429,
|
|
25
25
|
metricLabel: "rpm_exhausted",
|
|
26
26
|
},
|
|
27
27
|
"daily-token-limit": {
|
|
28
|
-
message: "This key has used today's token allowance. It resets at the start of the next day, or raise the key's daily limit.",
|
|
29
|
-
code: "
|
|
28
|
+
message: "This key has used today's token allowance (request {requestId}). It resets at the start of the next day, or raise the key's daily limit. Pace yourself — it resets tomorrow ⏳",
|
|
29
|
+
code: "rate_limited",
|
|
30
30
|
status: 429,
|
|
31
31
|
metricLabel: "daily",
|
|
32
32
|
},
|
|
33
33
|
"monthly-token-limit": {
|
|
34
|
-
message: "This key has used this month's token allowance. It resets at the start of the next month, or raise the key's monthly limit.",
|
|
35
|
-
code: "
|
|
34
|
+
message: "This key has used this month's token allowance (request {requestId}). It resets at the start of the next month, or raise the key's monthly limit. Saving up for next month? It resets then ⏳",
|
|
35
|
+
code: "rate_limited",
|
|
36
36
|
status: 429,
|
|
37
37
|
metricLabel: "monthly",
|
|
38
38
|
},
|
|
39
39
|
"lifetime-token-budget": {
|
|
40
|
-
message: "This key has spent its lifetime token budget. Raise the budget, or issue a new key.",
|
|
41
|
-
code: "
|
|
40
|
+
message: "This key has spent its lifetime token budget (request {requestId}). Raise the budget, or issue a new key. The piggy bank is empty — top it up or mint a new key 🐷",
|
|
41
|
+
code: "rate_limited",
|
|
42
42
|
status: 429,
|
|
43
43
|
metricLabel: "lifetime",
|
|
44
44
|
},
|
|
@@ -47,27 +47,27 @@ const REASON_META: Record<
|
|
|
47
47
|
// max-inflight. The two used to be indistinguishable and both named the
|
|
48
48
|
// provider, sending operators to the wrong setting after a rejection. The
|
|
49
49
|
// `details` carry the limit so a client can tell which one fired.
|
|
50
|
-
message: "This API key is already serving its maximum number of concurrent requests. Retry shortly, or raise the key's concurrency limit.",
|
|
50
|
+
message: "This API key is already serving its maximum number of concurrent requests (request {requestId}). Retry shortly, or raise the key's concurrency limit. It'll free up in a moment 🌊",
|
|
51
51
|
code: "capacity_exhausted",
|
|
52
52
|
status: 429,
|
|
53
53
|
metricLabel: "concurrency",
|
|
54
54
|
},
|
|
55
55
|
"tenant-capacity-exhausted": {
|
|
56
|
-
message: "This tenant is already serving its maximum number of concurrent requests. Retry shortly, or raise the tenant's concurrency limit.",
|
|
56
|
+
message: "This tenant is already serving its maximum number of concurrent requests (request {requestId}). Retry shortly, or raise the tenant's concurrency limit. The tenant is maxed out right now — chill 🏠",
|
|
57
57
|
code: "tenant_capacity_exhausted",
|
|
58
58
|
status: 429,
|
|
59
59
|
metricLabel: "tenant_capacity",
|
|
60
60
|
},
|
|
61
61
|
"model-not-allowed": {
|
|
62
|
-
message: "This key is not allowed to use this model under its allowlist. Add the model to the key, or use one it already allows.",
|
|
63
|
-
code: "
|
|
64
|
-
status:
|
|
62
|
+
message: "This key is not allowed to use this model under its allowlist (request {requestId}). Add the model to the key, or use one it already allows. The door's locked for this one — check the allowlist 🔒",
|
|
63
|
+
code: "model_not_allowed",
|
|
64
|
+
status: 403,
|
|
65
65
|
metricLabel: "model_not_allowed",
|
|
66
66
|
},
|
|
67
67
|
"model-denied": {
|
|
68
|
-
message: "This key's denylist blocks this model. Remove the model from the denylist, or use a different model.",
|
|
69
|
-
code: "
|
|
70
|
-
status:
|
|
68
|
+
message: "This key's denylist blocks this model (request {requestId}). Remove the model from the denylist, or use a different model. This one's on the banned list — pick another 🚫",
|
|
69
|
+
code: "model_not_allowed",
|
|
70
|
+
status: 403,
|
|
71
71
|
metricLabel: "model_not_allowed",
|
|
72
72
|
},
|
|
73
73
|
"admission-unavailable": {
|
|
@@ -76,7 +76,7 @@ const REASON_META: Record<
|
|
|
76
76
|
// `invalid_counter`, `unknown_reservation`, `invalid_actual_usage`). The
|
|
77
77
|
// second is not an outage, so the text must not claim one — it names the
|
|
78
78
|
// effect the client can act on, and the diagnostic stays in `details.reason`.
|
|
79
|
-
message: "The gateway could not admit this request. Retry shortly; if it persists, check the instance's Redis or coordination backend.",
|
|
79
|
+
message: "The gateway could not admit this request (request {requestId}). Retry shortly; if it persists, check the instance's Redis or coordination backend. The bouncer stepped out — come back shortly 🚪",
|
|
80
80
|
code: "admission_unavailable",
|
|
81
81
|
status: 503,
|
|
82
82
|
metricLabel: "admission_unavailable",
|
|
@@ -239,7 +239,7 @@ export interface ResolvedApiKey {
|
|
|
239
239
|
* indistinguishable — distinguishing them would let a caller confirm that a
|
|
240
240
|
* guessed token once existed — so there is no variant carrying a reason.
|
|
241
241
|
*/
|
|
242
|
-
const INVALID_KEY_MESSAGE = "Invalid API key. Check the key you are sending, or create a new one in API Keys and send it as: Authorization: Bearer <key>.";
|
|
242
|
+
const INVALID_KEY_MESSAGE = "Invalid API key (request {requestId}). Check the key you are sending, or create a new one in API Keys and send it as: Authorization: Bearer <key>. Wrong key, right door — double-check and come back 🔑";
|
|
243
243
|
|
|
244
244
|
export function unknownApiKeyError(): GatewayError {
|
|
245
245
|
return new GatewayError("authentication_failed", 401, INVALID_KEY_MESSAGE);
|
|
@@ -258,7 +258,7 @@ export function modelsListApiKeyError(): GatewayError {
|
|
|
258
258
|
return new GatewayError(
|
|
259
259
|
"authentication_failed",
|
|
260
260
|
401,
|
|
261
|
-
"Invalid API key. Rotate the default Cartethyia API key in API Keys and send the new key as: Authorization: Bearer <key>.",
|
|
261
|
+
"Invalid API key (request {requestId}). Rotate the default Cartethyia API key in API Keys and send the new key as: Authorization: Bearer <key>. Wrong key, right door — rotate and try again 🔑",
|
|
262
262
|
);
|
|
263
263
|
}
|
|
264
264
|
|
|
@@ -279,12 +279,12 @@ export function requestToken(headers: Headers | Record<string, string>): string
|
|
|
279
279
|
// *different* credentials stay a conflict — accepting whichever came first
|
|
280
280
|
// would silently decide the request's identity.
|
|
281
281
|
if (bearerMatch?.[1] === apiKey) return apiKey;
|
|
282
|
-
throw new GatewayError("invalid_request", 400, "The request sends two different credentials. Send exactly one key, either as Authorization: Bearer <key> or as the api-key header.");
|
|
282
|
+
throw new GatewayError("invalid_request", 400, "The request sends two different credentials (request {requestId}). Send exactly one key, either as Authorization: Bearer <key> or as the api-key header. Pick one key and stick with it 🤝");
|
|
283
283
|
}
|
|
284
284
|
if (hasApiKey) return apiKey;
|
|
285
285
|
const token = bearerMatch?.[1];
|
|
286
286
|
if (token === undefined)
|
|
287
|
-
throw new GatewayError("
|
|
287
|
+
throw new GatewayError("authentication_failed", 401, "The request carries no usable credential (request {requestId}). Send the key as: Authorization: Bearer <key>. We need a key to let you in 🔑");
|
|
288
288
|
return token;
|
|
289
289
|
}
|
|
290
290
|
|
package/src/shared/coerce.ts
CHANGED
|
@@ -13,6 +13,25 @@ export function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
|
13
13
|
: undefined;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Narrow a stored `auth_state`/JSONB value to a plain object. Postgres'
|
|
18
|
+
* drizzle `jsonb` arrives as a POJO; SQLite's text-backed column arrives as
|
|
19
|
+
* a JSON STRING. Parse the string form so both backends hand downstream
|
|
20
|
+
* readers the same shape — otherwise auth-state-dependent providers (Qoder's
|
|
21
|
+
* userId/machineId, Trae's region, …) silently lose their identity on the
|
|
22
|
+
* SQLite backend.
|
|
23
|
+
*/
|
|
24
|
+
export function asJsonbRecord(value: unknown): Record<string, unknown> | undefined {
|
|
25
|
+
if (typeof value === "string") {
|
|
26
|
+
try {
|
|
27
|
+
return asRecord(JSON.parse(value));
|
|
28
|
+
} catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return asRecord(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
16
35
|
/** Narrow to a finite number, else `undefined`. `NaN`/`Infinity` are not numbers here. */
|
|
17
36
|
export function asNumber(value: unknown): number | undefined {
|
|
18
37
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
@@ -425,7 +425,7 @@ export function terminalFailure(
|
|
|
425
425
|
return new GatewayError(
|
|
426
426
|
"transport_unavailable",
|
|
427
427
|
502,
|
|
428
|
-
"The upstream closed the response without reporting a result. Retry; if it keeps happening, the account or pool is unstable.",
|
|
428
|
+
"The upstream closed the response without reporting a result (request {requestId}). Retry; if it keeps happening, the account or pool is unstable. The connection ghosted us — come back in a moment 👻",
|
|
429
429
|
{},
|
|
430
430
|
"upstream",
|
|
431
431
|
);
|
|
@@ -437,7 +437,7 @@ export function terminalFailure(
|
|
|
437
437
|
const details: Record<string, unknown> = {};
|
|
438
438
|
if (terminal.provider_stop_reason) details["provider_code"] = terminal.provider_stop_reason;
|
|
439
439
|
if (terminal.stop_details) Object.assign(details, terminal.stop_details);
|
|
440
|
-
const message = typeof details["message"] === "string" ? details["message"] : "upstream request failed";
|
|
440
|
+
const message = typeof details["message"] === "string" ? details["message"] : "upstream request failed (request {requestId}). The upstream came back empty-handed — retry 🌙";
|
|
441
441
|
// Preserve the upstream's error code when the provider embedded it in
|
|
442
442
|
// `stop_details` (Cursor writes `quota_exceeded` and `authentication_failed`
|
|
443
443
|
// there). Wrapping every terminal failure as `transport_unavailable` discarded
|
|
@@ -460,7 +460,7 @@ export function terminalFailure(
|
|
|
460
460
|
// reports it as truncation instead.
|
|
461
461
|
return options.truncated === true
|
|
462
462
|
? undefined
|
|
463
|
-
: new GatewayError("transport_closed", 499, "request was cancelled");
|
|
463
|
+
: new GatewayError("transport_closed", 499, "The request was cancelled (request {requestId}). Send it again whenever ready 🙃");
|
|
464
464
|
}
|
|
465
465
|
return undefined;
|
|
466
466
|
}
|
|
@@ -823,6 +823,7 @@ export async function completeAttempt(
|
|
|
823
823
|
(completion.error instanceof ProxyConnectRefusalError ? "network" : undefined);
|
|
824
824
|
await recordPoolDispatchOutcome(completion.db, completion.networkPoolId, {
|
|
825
825
|
succeeded: completion.status === "completed",
|
|
826
|
+
...(completion.providerId === undefined ? {} : { providerId: completion.providerId }),
|
|
826
827
|
...(completion.error === undefined ? {} : { error: completion.error }),
|
|
827
828
|
// A CONNECT refusal is the proxy's own answer, given before any
|
|
828
829
|
// upstream was dialed, so it is a network fault even though the
|
|
@@ -41,7 +41,8 @@ import {
|
|
|
41
41
|
type ProviderExchangeCapture,
|
|
42
42
|
} from "./attempt-finalize";
|
|
43
43
|
import { repriceUsage } from "../../providers/provider-usage";
|
|
44
|
-
import {
|
|
44
|
+
import { classifyEgressFailure } from "../../network/rate-limit-verdict";
|
|
45
|
+
import { isOAuthCredentialInvalidated } from "./retry-policy";
|
|
45
46
|
import { drainAbortReason } from "../shutdown-notice";
|
|
46
47
|
|
|
47
48
|
/**
|
|
@@ -316,7 +317,7 @@ export async function runAttemptLoop<TResult, TAdapter>(
|
|
|
316
317
|
};
|
|
317
318
|
try {
|
|
318
319
|
if (state.abortController.signal.aborted)
|
|
319
|
-
throw new GatewayError("transport_closed", 499, "request was cancelled");
|
|
320
|
+
throw new GatewayError("transport_closed", 499, "The request was cancelled (request {requestId}). Send it again whenever ready 🙃");
|
|
320
321
|
const { credential, adapter } = await input.prepare(candidate);
|
|
321
322
|
const host = input.resolveHost?.(candidate);
|
|
322
323
|
// Single attempt-lease manager owns acquisition order (admission →
|
|
@@ -353,9 +354,10 @@ export async function runAttemptLoop<TResult, TAdapter>(
|
|
|
353
354
|
});
|
|
354
355
|
} catch (error) {
|
|
355
356
|
// A drain aborts the controller, so a mid-flight attempt unwinds here as
|
|
356
|
-
// an abort. Surface the typed shutdown error (503 shutting_down
|
|
357
|
-
// restart_for_update) instead of an opaque
|
|
358
|
-
// client's JSON error — not just the SSE path —
|
|
357
|
+
// an abort. Surface the typed shutdown error (503 shutting_down, reason
|
|
358
|
+
// restart_for_update for an update drain) instead of an opaque
|
|
359
|
+
// `transport_closed`, so the client's JSON error — not just the SSE path —
|
|
360
|
+
// names the real cause.
|
|
359
361
|
const drain = drainAbortReason(state.abortController.signal.reason);
|
|
360
362
|
if (drain !== undefined) error = drain;
|
|
361
363
|
lastError = error;
|
|
@@ -374,11 +376,12 @@ export async function runAttemptLoop<TResult, TAdapter>(
|
|
|
374
376
|
// single candidate carrying every pool). Judging it there is what made a
|
|
375
377
|
// refused egress end the request instead of failing over to a healthy
|
|
376
378
|
// one.
|
|
377
|
-
const
|
|
379
|
+
const egressVerdict = !cancelled ? classifyEgressFailure(error, candidate.provider_id) : { kind: "none" } as const;
|
|
378
380
|
// A free-tier refusal is only ever this provider's: the guard is gated on
|
|
379
381
|
// `ipScopedRateLimit` in the provider metadata, so a 403 from any other
|
|
380
382
|
// upstream still means what it always did and never retires an egress.
|
|
381
|
-
const
|
|
383
|
+
const rateLimitedPool = egressVerdict.kind === "cooldown";
|
|
384
|
+
const blockedPool = egressVerdict.kind === "blocked";
|
|
382
385
|
const poolScopedFailure =
|
|
383
386
|
(rateLimitedPool || blockedPool) &&
|
|
384
387
|
networkPoolId !== undefined &&
|
|
@@ -107,7 +107,7 @@ export async function fetchWithEmptyStreamRecovery(
|
|
|
107
107
|
const sleep = deps.policy?.sleep ?? abortableSleep;
|
|
108
108
|
|
|
109
109
|
if (endpoints.length === 0) {
|
|
110
|
-
throw new GatewayError("platform_unavailable", 502, "No endpoints available", {}, "upstream");
|
|
110
|
+
throw new GatewayError("platform_unavailable", 502, "No endpoints available (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
for (let endpointIndex = 0; endpointIndex < endpoints.length; endpointIndex++) {
|
|
@@ -118,7 +118,7 @@ export async function fetchWithEmptyStreamRecovery(
|
|
|
118
118
|
for (let replay = 0; replay <= replaysAllowed; replay++) {
|
|
119
119
|
// Check abort before each fetch
|
|
120
120
|
if (deps.signal.aborted) {
|
|
121
|
-
throw new GatewayError("transport_closed", 499, "
|
|
121
|
+
throw new GatewayError("transport_closed", 499, "The request was aborted (request {requestId}). Send it again whenever ready 🙃");
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
// Check deadline floor
|
|
@@ -128,7 +128,7 @@ export async function fetchWithEmptyStreamRecovery(
|
|
|
128
128
|
throw new GatewayError(
|
|
129
129
|
"platform_unavailable",
|
|
130
130
|
502,
|
|
131
|
-
"Deadline exhausted before empty-stream replay",
|
|
131
|
+
"Deadline exhausted before empty-stream replay (request {requestId}). Some things just take longer 🐢",
|
|
132
132
|
{},
|
|
133
133
|
"upstream",
|
|
134
134
|
);
|
|
@@ -181,7 +181,7 @@ export async function fetchWithEmptyStreamRecovery(
|
|
|
181
181
|
throw new GatewayError(
|
|
182
182
|
"platform_unavailable",
|
|
183
183
|
502,
|
|
184
|
-
"All endpoints returned empty streams",
|
|
184
|
+
"All endpoints returned empty streams (request {requestId}). The upstream came back empty-handed — retry 🌙",
|
|
185
185
|
{},
|
|
186
186
|
"upstream",
|
|
187
187
|
);
|
|
@@ -204,11 +204,11 @@ async function fetchWithTimeout(
|
|
|
204
204
|
// It must surface as a deadline failure, never as a `transport_closed`
|
|
205
205
|
// 499 — that code is reserved for genuine client cancellations and would
|
|
206
206
|
// otherwise hide a real upstream wait behind "cancelled by client".
|
|
207
|
-
reject(new GatewayError("deadline_exceeded", 504, "First event timeout", {}, "upstream"));
|
|
207
|
+
reject(new GatewayError("deadline_exceeded", 504, "First event timeout (request {requestId}). Some things just take longer 🐢", {}, "upstream"));
|
|
208
208
|
}, timeoutMs);
|
|
209
209
|
signal.addEventListener("abort", () => {
|
|
210
210
|
clearTimeout(timer);
|
|
211
|
-
reject(new GatewayError("transport_closed", 499, "
|
|
211
|
+
reject(new GatewayError("transport_closed", 499, "The request was aborted (request {requestId}). Send it again whenever ready 🙃"));
|
|
212
212
|
}, { once: true });
|
|
213
213
|
});
|
|
214
214
|
|
|
@@ -163,18 +163,18 @@ export function createNativeSurfaceHandler(
|
|
|
163
163
|
throw new GatewayError(
|
|
164
164
|
"invalid_request",
|
|
165
165
|
400,
|
|
166
|
-
`The ${noun} body must be a JSON object. Send one with at least a model and a prompt
|
|
166
|
+
`The ${noun} body must be a JSON object (request {requestId}). Send one with at least a model and a prompt. Check the shape of what you sent — then resend 🪞`,
|
|
167
167
|
);
|
|
168
168
|
}
|
|
169
169
|
const generation = body as Record<string, unknown>;
|
|
170
170
|
if (typeof generation.model !== "string" || generation.model.trim().length === 0) {
|
|
171
|
-
throw new GatewayError("invalid_request", 400, `A model is required: it names the ${noun} provider to use
|
|
171
|
+
throw new GatewayError("invalid_request", 400, `A model is required (request {requestId}): it names the ${noun} provider to use. Check the shape of what you sent — then resend 🪞`);
|
|
172
172
|
}
|
|
173
173
|
if (!authorization.snapshot.scopes?.includes("routing:invoke" as never)) {
|
|
174
174
|
throw new GatewayError(
|
|
175
|
-
"
|
|
175
|
+
"policy_rejected",
|
|
176
176
|
403,
|
|
177
|
-
`${noun} generation not allowed for this key — enable routing:invoke on the API key
|
|
177
|
+
`${noun} generation not allowed for this key (request {requestId}) — enable routing:invoke on the API key. Not allowed by policy — no hard feelings 🫂`,
|
|
178
178
|
);
|
|
179
179
|
}
|
|
180
180
|
// Trust boundary only. Keep accepting `messages` for chat-backed providers
|
|
@@ -183,7 +183,7 @@ export function createNativeSurfaceHandler(
|
|
|
183
183
|
const hasPrompt = typeof generation.prompt === "string" && generation.prompt.trim().length > 0;
|
|
184
184
|
const hasMessages = Array.isArray(generation.messages) && generation.messages.length > 0;
|
|
185
185
|
if (!hasPrompt && !hasMessages) {
|
|
186
|
-
throw new GatewayError("invalid_request", 400, "prompt is required");
|
|
186
|
+
throw new GatewayError("invalid_request", 400, "prompt is required (request {requestId}). Check the shape of what you sent — then resend 🪞");
|
|
187
187
|
}
|
|
188
188
|
if (generation.n !== undefined) {
|
|
189
189
|
const n = generation.n;
|
|
@@ -191,7 +191,7 @@ export function createNativeSurfaceHandler(
|
|
|
191
191
|
throw new GatewayError(
|
|
192
192
|
"invalid_request",
|
|
193
193
|
400,
|
|
194
|
-
`n must be an integer between 1 and ${MAX_ITEMS_PER_REQUEST}
|
|
194
|
+
`n must be an integer between 1 and ${MAX_ITEMS_PER_REQUEST} (request {requestId}). Check the shape of what you sent — then resend 🪞`,
|
|
195
195
|
);
|
|
196
196
|
}
|
|
197
197
|
}
|
|
@@ -219,7 +219,7 @@ export function createNativeSurfaceHandler(
|
|
|
219
219
|
candidates: prepared.candidates,
|
|
220
220
|
tenantId: prepared.authorization.tenantId,
|
|
221
221
|
strictPoolSelection: false,
|
|
222
|
-
exhaustedError: new GatewayError("
|
|
222
|
+
exhaustedError: new GatewayError("route_unavailable", 503, `no eligible ${noun} generation route (request {requestId}). The bouncer stepped out — come back shortly 🚪`),
|
|
223
223
|
prepare: async (candidate) => {
|
|
224
224
|
const credential = candidate.provider_account_id
|
|
225
225
|
? await resolveCredentialForAccount(
|
|
@@ -239,7 +239,7 @@ export function createNativeSurfaceHandler(
|
|
|
239
239
|
throw new GatewayError(
|
|
240
240
|
"capability_unsupported",
|
|
241
241
|
400,
|
|
242
|
-
`${noun} generation is unavailable for this provider
|
|
242
|
+
`${noun} generation is unavailable for this provider (request {requestId}). Know your tools — this one just isn't in the box 🧰`,
|
|
243
243
|
);
|
|
244
244
|
}
|
|
245
245
|
return { credential, adapter };
|
|
@@ -344,7 +344,7 @@ export function createNativeSurfaceHandler(
|
|
|
344
344
|
new GatewayError(
|
|
345
345
|
"capability_unsupported",
|
|
346
346
|
400,
|
|
347
|
-
`${noun} generation is unavailable for this provider
|
|
347
|
+
`${noun} generation is unavailable for this provider (request {requestId}). Know your tools — this one just isn't in the box 🧰`,
|
|
348
348
|
),
|
|
349
349
|
)
|
|
350
350
|
: adapter
|
|
@@ -141,7 +141,7 @@ async function acquirePoolSlot(
|
|
|
141
141
|
throw new GatewayError(
|
|
142
142
|
"proxy_pool_unhealthy",
|
|
143
143
|
503,
|
|
144
|
-
"Every proxy pool this route requires is unhealthy or disabled. Repair a pool, or relax the route's pool requirement.",
|
|
144
|
+
"Every proxy pool this route requires is unhealthy or disabled (request {requestId}). Repair a pool, or relax the route's pool requirement. This bridge looks shaky — repair or reroute 🔧",
|
|
145
145
|
{ reason: "no_healthy_pool" },
|
|
146
146
|
"network",
|
|
147
147
|
);
|
|
@@ -172,7 +172,7 @@ async function acquirePoolSlot(
|
|
|
172
172
|
throw new GatewayError(
|
|
173
173
|
"proxy_pool_unavailable",
|
|
174
174
|
503,
|
|
175
|
-
"No active proxy pool is available because coordination is unavailable.",
|
|
175
|
+
"No active proxy pool is available because coordination is unavailable (request {requestId}). The bouncer stepped out — come back shortly 🚪",
|
|
176
176
|
{ reason: "coordination_unavailable" },
|
|
177
177
|
"network",
|
|
178
178
|
);
|
|
@@ -219,14 +219,14 @@ async function acquirePoolSlot(
|
|
|
219
219
|
failure.reason === "at_capacity" || failure.reason === "provider_at_capacity" ? 429 : 503;
|
|
220
220
|
const message =
|
|
221
221
|
failure.reason === "at_capacity"
|
|
222
|
-
? "Every proxy pool for this provider is at its concurrency limit. Retry shortly, or add a pool or raise its capacity."
|
|
222
|
+
? "Every proxy pool for this provider is at its concurrency limit (request {requestId}). Retry shortly, or add a pool or raise its capacity. The pools are full right now — chill 🌊"
|
|
223
223
|
: failure.reason === "provider_at_capacity"
|
|
224
|
-
? "Every proxy pool has reached this provider's per-proxy concurrency limit. Retry shortly, raise the limit in the provider's routing settings, or add a pool."
|
|
224
|
+
? "Every proxy pool has reached this provider's per-proxy concurrency limit (request {requestId}). Retry shortly, raise the limit in the provider's routing settings, or add a pool. The proxies are working overtime — hang tight ⚡️"
|
|
225
225
|
: failure.reason === "blocked"
|
|
226
|
-
? "All proxy pools are saturated for this provider: the upstream refuses every egress configured for it. Replace a blocked proxy, or add one the upstream has not flagged."
|
|
226
|
+
? "All proxy pools are saturated for this provider (request {requestId}): the upstream refuses every egress configured for it. Replace a blocked proxy, or add one the upstream has not flagged. All roads blocked — time to find a new route 🗺️"
|
|
227
227
|
: failure.reason === "cooldown"
|
|
228
|
-
? "Every proxy pool for this provider is cooling down after upstream failures. Retry once the cooldown resets."
|
|
229
|
-
: "No active proxy pool is configured for this tenant and provider. Add one, or route this provider through direct egress.";
|
|
228
|
+
? "Every proxy pool for this provider is cooling down after upstream failures (request {requestId}). Retry once the cooldown resets. The proxies are catching their breath — zzz 💤"
|
|
229
|
+
: "No active proxy pool is configured for this tenant and provider (request {requestId}). Add one, or route this provider through direct egress. No bridge to cross — configure a pool first 🏗️";
|
|
230
230
|
throw new GatewayError(code, status, message, {
|
|
231
231
|
pools: failure.pools,
|
|
232
232
|
// Only measured evidence: a cooldown's real reset instant. The
|
|
@@ -279,7 +279,7 @@ export async function acquireAttemptLeases(
|
|
|
279
279
|
: await acquirePoolSlot(candidate, source, input.poolSelector, input.strictPoolSelection);
|
|
280
280
|
if (slot) partial.proxySlot = slot;
|
|
281
281
|
if (input.strictPoolSelection && input.signal.aborted)
|
|
282
|
-
throw new GatewayError("transport_closed", 499, "request was cancelled");
|
|
282
|
+
throw new GatewayError("transport_closed", 499, "The request was cancelled (request {requestId}). Send it again whenever ready 🙃");
|
|
283
283
|
partial.reservation = await source.routingEngine.reserve({
|
|
284
284
|
...source.plan,
|
|
285
285
|
candidates: [candidate],
|