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
|
@@ -116,7 +116,7 @@ export async function handleProviderProxyRequest(
|
|
|
116
116
|
const state = deps.stateStore.require(request);
|
|
117
117
|
const prepared = state.preparedRequest;
|
|
118
118
|
if (!prepared)
|
|
119
|
-
throw new GatewayError("
|
|
119
|
+
throw new GatewayError("route_unavailable", 503, "The request was not prepared, so no route can be dialed (request {requestId}). Resend the request. The bouncer stepped out — resend shortly 🚪");
|
|
120
120
|
const canonicalRequest = await applyTenantPreferences(prepared, deps.db);
|
|
121
121
|
// Tenant system-prompt injection, resolved once per request. For providers
|
|
122
122
|
// whose upstream validates the fixed system prompt as a channel identity
|
|
@@ -188,13 +188,13 @@ export async function handleProviderProxyRequest(
|
|
|
188
188
|
tenantId: prepared.authorization.tenantId,
|
|
189
189
|
strictPoolSelection: true,
|
|
190
190
|
resolveHost: (candidate) => deps.byokUpstreamHosts?.get(candidate.provider_id),
|
|
191
|
-
exhaustedError: new GatewayError("
|
|
191
|
+
exhaustedError: new GatewayError("route_unavailable", 503, "No account could be leased for this request (request {requestId}). Retry shortly; if it persists, add an account or check that one is healthy. The bouncer stepped out — come back shortly 🚪"),
|
|
192
192
|
prepare: async (candidate) => {
|
|
193
193
|
if (!candidate.provider_account_id && candidate.requires_account !== false)
|
|
194
194
|
throw new GatewayError(
|
|
195
|
-
"
|
|
195
|
+
"route_unavailable",
|
|
196
196
|
503,
|
|
197
|
-
"This route has no usable credential. Save one on the account, or route to a provider whose account is configured.",
|
|
197
|
+
"This route has no usable credential (request {requestId}). Save one on the account, or route to a provider whose account is configured. Wrong key, right door — check the credential 🔑",
|
|
198
198
|
);
|
|
199
199
|
const credential = candidate.provider_account_id
|
|
200
200
|
? await resolveCredentialForAccount(
|
|
@@ -211,13 +211,13 @@ export async function handleProviderProxyRequest(
|
|
|
211
211
|
};
|
|
212
212
|
const adapter = await deps.resolveProviderAdapter?.(candidate.provider_id);
|
|
213
213
|
if (!adapter)
|
|
214
|
-
throw new GatewayError("
|
|
214
|
+
throw new GatewayError("route_unavailable", 503, "provider adapter not registered (request {requestId}). Even we have bad days — check the registry wiring 🐻");
|
|
215
215
|
if (!isBundledProviderId(candidate.provider_id)) {
|
|
216
216
|
if (!deps.byokUpstreamHosts?.get(candidate.provider_id) || !deps.networkBindingFactory)
|
|
217
217
|
throw new GatewayError(
|
|
218
|
-
"
|
|
218
|
+
"route_unavailable",
|
|
219
219
|
503,
|
|
220
|
-
"validated network binding is required for configurable upstreams",
|
|
220
|
+
"validated network binding is required for configurable upstreams (request {requestId}). No bridge to cross — configure a pool first 🏗️",
|
|
221
221
|
);
|
|
222
222
|
}
|
|
223
223
|
return { credential, adapter };
|
|
@@ -27,10 +27,10 @@ export function createResponsesCompactHandler(deps: ResponsesCompactHandlerDeps)
|
|
|
27
27
|
const authorization = state.authorization;
|
|
28
28
|
const body = state.ingressBody;
|
|
29
29
|
if (!authorization || body === null || typeof body !== "object" || Array.isArray(body))
|
|
30
|
-
throw new GatewayError("invalid_request", 400, "compact request body must be a JSON object");
|
|
30
|
+
throw new GatewayError("invalid_request", 400, "compact request body must be a JSON object (request {requestId}). Check the shape of what you sent — then resend 🪞");
|
|
31
31
|
const compact = body as Record<string, unknown>;
|
|
32
32
|
if (typeof compact.model !== "string" || compact.model.trim().length === 0)
|
|
33
|
-
throw new GatewayError("invalid_request", 400, "model is required");
|
|
33
|
+
throw new GatewayError("invalid_request", 400, "model is required (request {requestId}). Check the shape of what you sent — then resend 🪞");
|
|
34
34
|
// A thinking suffix (`model(high)`) is stripped here for the same reason the
|
|
35
35
|
// canonical preparer strips it: this route matches the model name against
|
|
36
36
|
// the allowlist and the route plan, and `(high)` is not part of any
|
|
@@ -43,18 +43,18 @@ export function createResponsesCompactHandler(deps: ResponsesCompactHandlerDeps)
|
|
|
43
43
|
const compactBody: Record<string, unknown> =
|
|
44
44
|
compactModel === compact.model ? compact : { ...compact, model: compactModel };
|
|
45
45
|
if (!(typeof compact.input === "string" || Array.isArray(compact.input)))
|
|
46
|
-
throw new GatewayError("invalid_request", 400, "input must be a string or array");
|
|
46
|
+
throw new GatewayError("invalid_request", 400, "input must be a string or array (request {requestId}). Check the shape of what you sent — then resend 🪞");
|
|
47
47
|
if ("instructions" in compact && typeof compact.instructions !== "string")
|
|
48
|
-
throw new GatewayError("invalid_request", 400, "instructions must be a string");
|
|
48
|
+
throw new GatewayError("invalid_request", 400, "instructions must be a string (request {requestId}). Check the shape of what you sent — then resend 🪞");
|
|
49
49
|
if (compact.stream === true)
|
|
50
50
|
throw new GatewayError(
|
|
51
51
|
"capability_unsupported",
|
|
52
52
|
400,
|
|
53
|
-
"Responses compact does not support streaming",
|
|
53
|
+
"Responses compact does not support streaming (request {requestId}). Know your tools — this one just isn't in the box 🧰",
|
|
54
54
|
);
|
|
55
55
|
const adapter = (await deps.resolveProviderAdapter?.("codex")) as CodexCompactAdapter | undefined;
|
|
56
56
|
if (!adapter || typeof adapter.compact !== "function")
|
|
57
|
-
throw new GatewayError("admission_unavailable", 503, "Codex compact transport unavailable");
|
|
57
|
+
throw new GatewayError("admission_unavailable", 503, "Codex compact transport unavailable (request {requestId}). The bouncer stepped out — come back shortly 🚪");
|
|
58
58
|
const prepared = await deps.proxyPreparer.prepareNativeCompact({
|
|
59
59
|
model: compactModel,
|
|
60
60
|
authorization,
|
|
@@ -77,16 +77,16 @@ export function createResponsesCompactHandler(deps: ResponsesCompactHandlerDeps)
|
|
|
77
77
|
// Lenient pool semantics: a missing slot tolerates direct egress.
|
|
78
78
|
strictPoolSelection: false,
|
|
79
79
|
exhaustedError: new GatewayError(
|
|
80
|
-
"
|
|
80
|
+
"route_unavailable",
|
|
81
81
|
503,
|
|
82
|
-
"no eligible Codex route",
|
|
82
|
+
"no eligible Codex route (request {requestId}). The bouncer stepped out — come back shortly 🚪",
|
|
83
83
|
),
|
|
84
84
|
prepare: async (candidate) => {
|
|
85
85
|
if (!candidate.provider_account_id)
|
|
86
86
|
throw new GatewayError(
|
|
87
|
-
"
|
|
87
|
+
"route_unavailable",
|
|
88
88
|
503,
|
|
89
|
-
"Codex compact requires an OAuth account",
|
|
89
|
+
"Codex compact requires an OAuth account (request {requestId}). Wrong key, right door — check the credential 🔑",
|
|
90
90
|
);
|
|
91
91
|
const credential = await resolveCredentialForAccount(
|
|
92
92
|
deps.db,
|
|
@@ -100,7 +100,7 @@ export function createResponsesCompactHandler(deps: ResponsesCompactHandlerDeps)
|
|
|
100
100
|
throw new GatewayError(
|
|
101
101
|
"capability_unsupported",
|
|
102
102
|
400,
|
|
103
|
-
"Responses compact requires a Codex ChatGPT OAuth account",
|
|
103
|
+
"Responses compact requires a Codex ChatGPT OAuth account (request {requestId}). Wrong key, right door — check the credential 🔑",
|
|
104
104
|
);
|
|
105
105
|
return { credential, adapter };
|
|
106
106
|
},
|
|
@@ -8,9 +8,12 @@
|
|
|
8
8
|
* than inside it so the loop stays a policy *consumer* — one place decides
|
|
9
9
|
* retryability (`../failure-policy.ts`), one place decides refresh and
|
|
10
10
|
* cooldown eligibility (here).
|
|
11
|
+
*
|
|
12
|
+
* The egress-scoped half of the rate-limit decision lives in
|
|
13
|
+
* `../../network/rate-limit-verdict`; this module keeps only the credential-
|
|
14
|
+
* scoped predicates.
|
|
11
15
|
*/
|
|
12
16
|
import { GatewayError } from "../../protocol/gateway-error";
|
|
13
|
-
import { providerRateLimitIsIpScoped } from "../../providers/provider-metadata";
|
|
14
17
|
|
|
15
18
|
/**
|
|
16
19
|
* Returns true only when an upstream failure is evidence that the OAuth
|
|
@@ -32,69 +35,3 @@ export function isOAuthCredentialInvalidated(error: unknown): error is GatewayEr
|
|
|
32
35
|
if (/web[ _-]?search|web[ _-]?fetch|x[ _-]?search|tool invocation|search backend/i.test(message)) return false;
|
|
33
36
|
return error.details.credentialEvidence === true;
|
|
34
37
|
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* A free-tier IP refusal: the upstream has flagged this egress address, not
|
|
38
|
-
* this request.
|
|
39
|
-
*
|
|
40
|
-
* OpenCode answers a blocked address with `403` and its own
|
|
41
|
-
* `FreeTierError`/`ModelDeprecated`-style body. The request was never read, so
|
|
42
|
-
* retrying it — with the same tools, the same headers, or the same credential —
|
|
43
|
-
* cannot succeed from the same address. Only a different egress can.
|
|
44
|
-
*
|
|
45
|
-
* This is deliberately narrower than "any 403": a content-policy rejection or a
|
|
46
|
-
* licence refusal is also a 403 and must not retire a healthy proxy. The
|
|
47
|
-
* provider must also declare `ipScopedRateLimit`, which is the same fact the
|
|
48
|
-
* 429 path already relies on.
|
|
49
|
-
*/
|
|
50
|
-
export function isFreeTierIpBlock(error: unknown, providerId: string): error is GatewayError {
|
|
51
|
-
if (!(error instanceof GatewayError) || error.origin !== "upstream" || error.status !== 403) {
|
|
52
|
-
return false;
|
|
53
|
-
}
|
|
54
|
-
if (!providerRateLimitIsIpScoped(providerId)) return false;
|
|
55
|
-
const providerCode = typeof error.details.providerCode === "string" ? error.details.providerCode : "";
|
|
56
|
-
const raw = String(error.details.raw ?? "");
|
|
57
|
-
return (
|
|
58
|
-
/freetier|free.?tier/i.test(providerCode) ||
|
|
59
|
-
/FreeTierError|free tier can only be used/i.test(`${error.message} ${raw}`)
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* A provider-scoped 429, on a provider whose limits follow the egress address.
|
|
65
|
-
*
|
|
66
|
-
* Only then is the pool the thing that ran out: the upstream counted requests
|
|
67
|
-
* from this address, so the next attempt must leave by a different one and
|
|
68
|
-
* cooling this pool is the fix. `providerRateLimitIsIpScoped` holds that fact
|
|
69
|
-
* per provider (see the metadata field) rather than inferring it from the
|
|
70
|
-
* response — a 429 states which bucket was exhausted only in the provider's own
|
|
71
|
-
* wording, and every account-keyed provider words it the same way.
|
|
72
|
-
*
|
|
73
|
-
* For an account-keyed provider the 429 belongs to the credential that was
|
|
74
|
-
* dialed. The account health machine already records it and routes the next
|
|
75
|
-
* request to a sibling, so cooling the pool as well only removed healthy
|
|
76
|
-
* accounts from service: every account sharing that egress lost its route for
|
|
77
|
-
* the cooldown window because one of them hit its own limit. A bare 429 with no
|
|
78
|
-
* provider scope stays a request-level limit and never cools the pool.
|
|
79
|
-
*
|
|
80
|
-
* `providerId` is the caller's own candidate, not a field read off the error:
|
|
81
|
-
* the in-stream error frames the protocol modules build carry no `providerId`
|
|
82
|
-
* detail, so reading it from `details` would silently disable the IP-scoped
|
|
83
|
-
* rule on exactly the streaming path it exists for.
|
|
84
|
-
*
|
|
85
|
-
* A type guard on `GatewayError`, so a caller that passes this check can hand
|
|
86
|
-
* the same value to `flagPoolCooldown` without a second narrowing step.
|
|
87
|
-
*/
|
|
88
|
-
export function shouldCooldownPool(error: unknown, providerId: string): error is GatewayError {
|
|
89
|
-
if (
|
|
90
|
-
!(
|
|
91
|
-
error instanceof GatewayError &&
|
|
92
|
-
error.origin === "upstream" &&
|
|
93
|
-
error.status === 429 &&
|
|
94
|
-
error.details.rateLimitScope === "provider"
|
|
95
|
-
)
|
|
96
|
-
) {
|
|
97
|
-
return false;
|
|
98
|
-
}
|
|
99
|
-
return providerRateLimitIsIpScoped(providerId);
|
|
100
|
-
}
|
|
@@ -60,7 +60,7 @@ export function createDispatchStreamEncoder(
|
|
|
60
60
|
throw new GatewayError(
|
|
61
61
|
"capability_unsupported",
|
|
62
62
|
400,
|
|
63
|
-
`Unknown surface: ${surface}
|
|
63
|
+
`Unknown surface: ${surface} (request {requestId}). Know your tools — this one just isn't in the box 🧰`,
|
|
64
64
|
{ surface },
|
|
65
65
|
);
|
|
66
66
|
}
|
|
@@ -24,7 +24,7 @@ import type { Reservation } from "../../routing/route-model";
|
|
|
24
24
|
import { repriceUsage } from "../../../providers/provider-usage";
|
|
25
25
|
import { metrics } from "../../../observability/metrics";
|
|
26
26
|
import { flagPoolBlocked, flagPoolCooldown } from "../../../network/pool-health";
|
|
27
|
-
import {
|
|
27
|
+
import { classifyEgressFailure } from "../../../network/rate-limit-verdict";
|
|
28
28
|
import { releaseAttemptLeases } from "../leases";
|
|
29
29
|
import type { AttemptLeaseOwnership } from "../leases";
|
|
30
30
|
import { finalizeRequestTelemetry } from "../../guards/error-lifecycle";
|
|
@@ -186,7 +186,7 @@ export class StreamFinalizer {
|
|
|
186
186
|
// origin is the upstream's: labelling it `cartethyia` reported a
|
|
187
187
|
// provider that stopped sending as a gateway defect.
|
|
188
188
|
this.input.state.abortController.abort(
|
|
189
|
-
new GatewayError("deadline_exceeded", 504, "The upstream stopped sending data mid-stream. Retry; if it keeps happening, the account or pool is unstable.", {}, "upstream"),
|
|
189
|
+
new GatewayError("deadline_exceeded", 504, "The upstream stopped sending data mid-stream (request {requestId}). Retry; if it keeps happening, the account or pool is unstable. The connection ghosted us — come back in a moment 👻", {}, "upstream"),
|
|
190
190
|
);
|
|
191
191
|
}, timeoutMs);
|
|
192
192
|
}
|
|
@@ -363,7 +363,7 @@ export class StreamFinalizer {
|
|
|
363
363
|
if (drain) return drain;
|
|
364
364
|
if (reason instanceof GatewayError && reason.code === "deadline_exceeded") return reason;
|
|
365
365
|
if (reason instanceof DOMException && reason.name === "TimeoutError")
|
|
366
|
-
return new GatewayError("deadline_exceeded", 504, "request deadline exceeded", {}, "cartethyia");
|
|
366
|
+
return new GatewayError("deadline_exceeded", 504, "request deadline exceeded (request {requestId}). Some things just take longer 🐢", {}, "cartethyia");
|
|
367
367
|
return undefined;
|
|
368
368
|
}
|
|
369
369
|
|
|
@@ -453,7 +453,7 @@ export class StreamFinalizer {
|
|
|
453
453
|
? new GatewayError(
|
|
454
454
|
"transport_unavailable",
|
|
455
455
|
502,
|
|
456
|
-
"The upstream closed the stream before it reported a final result. Retry; if it keeps happening, the account or pool is unstable.",
|
|
456
|
+
"The upstream closed the stream before it reported a final result (request {requestId}). Retry; if it keeps happening, the account or pool is unstable. The connection ghosted us — come back in a moment 👻",
|
|
457
457
|
{},
|
|
458
458
|
"upstream",
|
|
459
459
|
)
|
|
@@ -531,7 +531,7 @@ export class StreamFinalizer {
|
|
|
531
531
|
? new GatewayError(
|
|
532
532
|
"deadline_exceeded",
|
|
533
533
|
504,
|
|
534
|
-
"request deadline exceeded",
|
|
534
|
+
"request deadline exceeded (request {requestId}). Some things just take longer 🐢",
|
|
535
535
|
{},
|
|
536
536
|
"cartethyia",
|
|
537
537
|
)
|
|
@@ -579,10 +579,11 @@ export class StreamFinalizer {
|
|
|
579
579
|
...(pipeline.clientResponseText.length > 0 ? { clientResponseText: pipeline.clientResponseText } : {}),
|
|
580
580
|
});
|
|
581
581
|
this.streamSettled = true;
|
|
582
|
-
if (candidate.networkPoolId && !cancelled && deps.poolSelector) {
|
|
583
|
-
|
|
582
|
+
if (candidate.networkPoolId && !cancelled && deps.poolSelector && err instanceof GatewayError) {
|
|
583
|
+
const egressVerdict = classifyEgressFailure(err, candidate.providerId);
|
|
584
|
+
if (egressVerdict.kind === "cooldown") {
|
|
584
585
|
flagPoolCooldown(deps.poolSelector, deps.db, candidate.networkPoolId, candidate.providerId, err);
|
|
585
|
-
} else if (
|
|
586
|
+
} else if (egressVerdict.kind === "blocked") {
|
|
586
587
|
flagPoolBlocked(deps.poolSelector, deps.db, candidate.networkPoolId, candidate.providerId, err);
|
|
587
588
|
}
|
|
588
589
|
}
|
|
@@ -33,10 +33,10 @@ export function createSystemoneHandler(deps: SystemoneHandlerDeps) {
|
|
|
33
33
|
const authorization = state.authorization;
|
|
34
34
|
const body = state.ingressBody;
|
|
35
35
|
if (!authorization || body === null || typeof body !== "object" || Array.isArray(body))
|
|
36
|
-
throw new GatewayError("invalid_request", 400, "systemone request body must be a JSON object");
|
|
36
|
+
throw new GatewayError("invalid_request", 400, "systemone request body must be a JSON object (request {requestId}). Check the shape of what you sent — then resend 🪞");
|
|
37
37
|
const decision = body as Record<string, unknown>;
|
|
38
38
|
if (typeof decision.model !== "string" || decision.model.trim().length === 0)
|
|
39
|
-
throw new GatewayError("invalid_request", 400, "model is required");
|
|
39
|
+
throw new GatewayError("invalid_request", 400, "model is required (request {requestId}). Check the shape of what you sent — then resend 🪞");
|
|
40
40
|
// A thinking suffix (`model(high)`) is stripped for the same reason the
|
|
41
41
|
// canonical preparer strips it: the route matches the model name against the
|
|
42
42
|
// allowlist and the route plan, and `(high)` is not part of any registered
|
|
@@ -49,14 +49,14 @@ export function createSystemoneHandler(deps: SystemoneHandlerDeps) {
|
|
|
49
49
|
// upstream's to validate. `state` and a `questions` object are what make a
|
|
50
50
|
// System One request a System One request at all.
|
|
51
51
|
if (decision.state === undefined || decision.state === null)
|
|
52
|
-
throw new GatewayError("invalid_request", 400, "state is required");
|
|
52
|
+
throw new GatewayError("invalid_request", 400, "state is required (request {requestId}). Check the shape of what you sent — then resend 🪞");
|
|
53
53
|
if (
|
|
54
54
|
decision.questions === undefined ||
|
|
55
55
|
decision.questions === null ||
|
|
56
56
|
typeof decision.questions !== "object" ||
|
|
57
57
|
Array.isArray(decision.questions)
|
|
58
58
|
)
|
|
59
|
-
throw new GatewayError("invalid_request", 400, "questions must be an object");
|
|
59
|
+
throw new GatewayError("invalid_request", 400, "questions must be an object (request {requestId}). Check the shape of what you sent — then resend 🪞");
|
|
60
60
|
const prepared = await deps.proxyPreparer.prepareNativeService({
|
|
61
61
|
model: bareModel,
|
|
62
62
|
serviceKind: "systemone",
|
|
@@ -80,9 +80,9 @@ export function createSystemoneHandler(deps: SystemoneHandlerDeps) {
|
|
|
80
80
|
// Lenient pool semantics: a missing slot tolerates direct egress.
|
|
81
81
|
strictPoolSelection: false,
|
|
82
82
|
exhaustedError: new GatewayError(
|
|
83
|
-
"
|
|
83
|
+
"route_unavailable",
|
|
84
84
|
503,
|
|
85
|
-
"no eligible System One route",
|
|
85
|
+
"no eligible System One route (request {requestId}). The bouncer stepped out — come back shortly 🚪",
|
|
86
86
|
),
|
|
87
87
|
prepare: async (candidate) => {
|
|
88
88
|
const credential = candidate.provider_account_id
|
|
@@ -103,7 +103,7 @@ export function createSystemoneHandler(deps: SystemoneHandlerDeps) {
|
|
|
103
103
|
throw new GatewayError(
|
|
104
104
|
"capability_unsupported",
|
|
105
105
|
400,
|
|
106
|
-
"System One transport unavailable for this provider",
|
|
106
|
+
"System One transport unavailable for this provider (request {requestId}). Know your tools — this one just isn't in the box 🧰",
|
|
107
107
|
);
|
|
108
108
|
return { credential, adapter };
|
|
109
109
|
},
|
|
@@ -148,7 +148,7 @@ export function createSystemoneHandler(deps: SystemoneHandlerDeps) {
|
|
|
148
148
|
throw new GatewayError(
|
|
149
149
|
response.status >= 500 ? "platform_unavailable" : "invalid_request",
|
|
150
150
|
response.status,
|
|
151
|
-
`System One upstream error: ${detail.slice(0, 240)}`,
|
|
151
|
+
`System One upstream error (request {requestId}): ${detail.slice(0, 240)}`,
|
|
152
152
|
);
|
|
153
153
|
}
|
|
154
154
|
// Prefer the usage the decision endpoint reported; fall back to the
|
|
@@ -57,7 +57,7 @@ export function createTokenCountHandler(deps: TokenCountHandlerDeps) {
|
|
|
57
57
|
throw new GatewayError(
|
|
58
58
|
"invalid_request",
|
|
59
59
|
400,
|
|
60
|
-
"The count_tokens body must be a JSON object, the same shape POST /v1/messages accepts.",
|
|
60
|
+
"The count_tokens body must be a JSON object (request {requestId}), the same shape POST /v1/messages accepts. Check the shape of what you sent — then resend 🪞",
|
|
61
61
|
);
|
|
62
62
|
}
|
|
63
63
|
const countBody = body as Record<string, unknown>;
|
|
@@ -66,16 +66,16 @@ export function createTokenCountHandler(deps: TokenCountHandlerDeps) {
|
|
|
66
66
|
// upstream's own required fields, so the gateway does not restate its own
|
|
67
67
|
// narrower rule and risk rejecting a body the provider would accept.
|
|
68
68
|
if (typeof countBody.model !== "string" || countBody.model.trim().length === 0) {
|
|
69
|
-
throw new GatewayError("invalid_request", 400, "A model is required: it names the provider to count with.");
|
|
69
|
+
throw new GatewayError("invalid_request", 400, "A model is required (request {requestId}): it names the provider to count with. Check the shape of what you sent — then resend 🪞");
|
|
70
70
|
}
|
|
71
71
|
if (!Array.isArray(countBody.messages)) {
|
|
72
|
-
throw new GatewayError("invalid_request", 400, "messages must be an array — there is nothing to count without it.");
|
|
72
|
+
throw new GatewayError("invalid_request", 400, "messages must be an array (request {requestId}) — there is nothing to count without it. Check the shape of what you sent — then resend 🪞");
|
|
73
73
|
}
|
|
74
74
|
if (!authorization.snapshot.scopes?.includes("routing:invoke" as never)) {
|
|
75
75
|
throw new GatewayError(
|
|
76
|
-
"
|
|
76
|
+
"policy_rejected",
|
|
77
77
|
403,
|
|
78
|
-
"token counting not allowed for this key — enable routing:invoke on the API key",
|
|
78
|
+
"token counting not allowed for this key (request {requestId}) — enable routing:invoke on the API key. Not allowed by policy — no hard feelings 🫂",
|
|
79
79
|
);
|
|
80
80
|
}
|
|
81
81
|
|
|
@@ -111,9 +111,9 @@ export function createTokenCountHandler(deps: TokenCountHandlerDeps) {
|
|
|
111
111
|
tenantId: prepared.authorization.tenantId,
|
|
112
112
|
strictPoolSelection: false,
|
|
113
113
|
exhaustedError: new GatewayError(
|
|
114
|
-
"
|
|
114
|
+
"route_unavailable",
|
|
115
115
|
503,
|
|
116
|
-
"no eligible route can count tokens for this model",
|
|
116
|
+
"no eligible route can count tokens for this model (request {requestId}). The bouncer stepped out — come back shortly 🚪",
|
|
117
117
|
),
|
|
118
118
|
prepare: async (candidate) => {
|
|
119
119
|
const credential = candidate.provider_account_id
|
|
@@ -131,7 +131,7 @@ export function createTokenCountHandler(deps: TokenCountHandlerDeps) {
|
|
|
131
131
|
};
|
|
132
132
|
const adapter = await deps.resolveProviderAdapter?.(candidate.provider_id);
|
|
133
133
|
if (!adapter) {
|
|
134
|
-
throw new GatewayError("capability_unsupported", 400, "token counting is unavailable for this provider");
|
|
134
|
+
throw new GatewayError("capability_unsupported", 400, "token counting is unavailable for this provider (request {requestId}). Know your tools — this one just isn't in the box 🧰");
|
|
135
135
|
}
|
|
136
136
|
return { credential, adapter };
|
|
137
137
|
},
|
|
@@ -146,7 +146,7 @@ export function createTokenCountHandler(deps: TokenCountHandlerDeps) {
|
|
|
146
146
|
|
|
147
147
|
const adapter = context.adapter;
|
|
148
148
|
if (typeof adapter.dispatchNativeService !== "function") {
|
|
149
|
-
throw new GatewayError("capability_unsupported", 400, "token counting is unavailable for this provider");
|
|
149
|
+
throw new GatewayError("capability_unsupported", 400, "token counting is unavailable for this provider (request {requestId}). Know your tools — this one just isn't in the box 🧰");
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
const target: ProviderDispatchTarget = {
|
|
@@ -50,10 +50,10 @@ export function createWebsearchHandler(deps: WebsearchHandlerDeps) {
|
|
|
50
50
|
const authorization = state.authorization;
|
|
51
51
|
const body = state.ingressBody;
|
|
52
52
|
if (!authorization || body === null || typeof body !== "object" || Array.isArray(body))
|
|
53
|
-
throw new GatewayError("invalid_request", 400, "The search body must be a JSON object. Send one with at least a model and a query.");
|
|
53
|
+
throw new GatewayError("invalid_request", 400, "The search body must be a JSON object (request {requestId}). Send one with at least a model and a query. Check the shape of what you sent — then resend 🪞");
|
|
54
54
|
const search = body as Record<string, unknown>;
|
|
55
55
|
if (typeof search.model !== "string" || search.model.trim().length === 0)
|
|
56
|
-
throw new GatewayError("invalid_request", 400, "A model is required: it names the search provider to use.");
|
|
56
|
+
throw new GatewayError("invalid_request", 400, "A model is required (request {requestId}): it names the search provider to use. Check the shape of what you sent — then resend 🪞");
|
|
57
57
|
// The search "model" names the provider (or an alias/combo of one). A
|
|
58
58
|
// thinking suffix is stripped for the same reason the canonical preparer
|
|
59
59
|
// strips it: it is not part of any registered id.
|
|
@@ -61,12 +61,12 @@ export function createWebsearchHandler(deps: WebsearchHandlerDeps) {
|
|
|
61
61
|
const searchBody: Record<string, unknown> =
|
|
62
62
|
bareModel === search.model ? search : { ...search, model: bareModel };
|
|
63
63
|
if (!authorization.snapshot.scopes?.includes("search:invoke" as never)) {
|
|
64
|
-
throw new GatewayError("
|
|
64
|
+
throw new GatewayError("policy_rejected", 403, "search not allowed for this key (request {requestId}) — enable search:invoke on the API key. Not allowed by policy — no hard feelings 🫂");
|
|
65
65
|
}
|
|
66
66
|
// Trust boundary only: the query must be a non-empty string. The adapter
|
|
67
67
|
// validates the remaining shape when it maps the body onto its provider.
|
|
68
68
|
if (typeof search.query !== "string" || search.query.trim().length === 0)
|
|
69
|
-
throw new GatewayError("invalid_request", 400, "query is required");
|
|
69
|
+
throw new GatewayError("invalid_request", 400, "query is required (request {requestId}). Check the shape of what you sent — then resend 🪞");
|
|
70
70
|
|
|
71
71
|
const prepared = await deps.proxyPreparer.prepareNativeService({
|
|
72
72
|
model: bareModel,
|
|
@@ -89,7 +89,7 @@ export function createWebsearchHandler(deps: WebsearchHandlerDeps) {
|
|
|
89
89
|
candidates: prepared.candidates,
|
|
90
90
|
tenantId: prepared.authorization.tenantId,
|
|
91
91
|
strictPoolSelection: false,
|
|
92
|
-
exhaustedError: new GatewayError("
|
|
92
|
+
exhaustedError: new GatewayError("route_unavailable", 503, "no eligible search route (request {requestId}). The bouncer stepped out — come back shortly 🚪"),
|
|
93
93
|
prepare: async (candidate) => {
|
|
94
94
|
const credential = candidate.provider_account_id
|
|
95
95
|
? await resolveCredentialForAccount(
|
|
@@ -109,7 +109,7 @@ export function createWebsearchHandler(deps: WebsearchHandlerDeps) {
|
|
|
109
109
|
throw new GatewayError(
|
|
110
110
|
"capability_unsupported",
|
|
111
111
|
400,
|
|
112
|
-
"web search is unavailable for this provider",
|
|
112
|
+
"web search is unavailable for this provider (request {requestId}). Know your tools — this one just isn't in the box 🧰",
|
|
113
113
|
);
|
|
114
114
|
return { credential, adapter };
|
|
115
115
|
},
|
|
@@ -55,9 +55,9 @@ export function createDrainHandler(
|
|
|
55
55
|
): (context: { request: Request }) => Promise<Response> {
|
|
56
56
|
return async ({ request }) => {
|
|
57
57
|
if (!isLoopbackPeer(deps.resolvePeerAddress(request)))
|
|
58
|
-
throw new GatewayError("
|
|
58
|
+
throw new GatewayError("policy_rejected", 403, "drain is only available from loopback (request {requestId}). This operation is only allowed from a trusted loopback connection — no hard feelings 🫂");
|
|
59
59
|
if (!tokenMatches(request.headers.get("x-drain-token"), deps.token))
|
|
60
|
-
throw new GatewayError("
|
|
60
|
+
throw new GatewayError("authentication_failed", 401, "invalid drain token (request {requestId}). Wrong key, right door — check the token 🔑");
|
|
61
61
|
// Acknowledge first, then drain: the response must be on the wire before the
|
|
62
62
|
// shutdown path starts closing sockets, or the caller sees a dropped
|
|
63
63
|
// connection instead of a 202.
|
|
@@ -167,7 +167,7 @@ export function statusToGatewayErrorCode(status: number): GatewayErrorCode {
|
|
|
167
167
|
if (status === 413) return "request_too_large";
|
|
168
168
|
if (status === 415) return "unsupported_media_type";
|
|
169
169
|
if (status === 422) return "upstream_unprocessable";
|
|
170
|
-
if (status === 529) return "
|
|
170
|
+
if (status === 529) return "upstream_overloaded";
|
|
171
171
|
if (status >= 500 && status <= 599) return "platform_unavailable";
|
|
172
172
|
return "invalid_request";
|
|
173
173
|
}
|
|
@@ -274,7 +274,9 @@ export function classifyUpstreamFailure(error: unknown): UpstreamFailurePolicy {
|
|
|
274
274
|
error.code === "capability_unsupported" ||
|
|
275
275
|
error.code === "model_not_found" ||
|
|
276
276
|
error.code === "admission_unavailable" ||
|
|
277
|
+
error.code === "route_unavailable" ||
|
|
277
278
|
error.code === "capacity_exhausted" ||
|
|
279
|
+
error.code === "upstream_overloaded" ||
|
|
278
280
|
// A provider max-inflight rejection: the plan's next sibling account may
|
|
279
281
|
// have a free slot, exactly as with an API-key concurrency rejection.
|
|
280
282
|
error.code === "provider_capacity_exhausted" ||
|
|
@@ -286,6 +288,7 @@ export function classifyUpstreamFailure(error: unknown): UpstreamFailurePolicy {
|
|
|
286
288
|
error.code === "accounts_unavailable" ||
|
|
287
289
|
error.code === "accounts_rate_limited" ||
|
|
288
290
|
error.code === "quota_exceeded" ||
|
|
291
|
+
error.code === "rate_limited" ||
|
|
289
292
|
error.code === "authentication_failed" ||
|
|
290
293
|
error.code === "proxy_auth_required" ||
|
|
291
294
|
error.code === "deadline_exceeded" ||
|
|
@@ -427,10 +430,14 @@ const CATEGORY_HTTP_STATUS: Partial<Record<GatewayErrorCode | "unknown_error", n
|
|
|
427
430
|
// in this process.
|
|
428
431
|
transport_unavailable: 502,
|
|
429
432
|
platform_unavailable: 502,
|
|
433
|
+
upstream_overloaded: 529,
|
|
430
434
|
deadline_exceeded: 504,
|
|
431
435
|
transport_closed: 499,
|
|
432
436
|
shutting_down: 503,
|
|
433
437
|
overloaded: 529,
|
|
438
|
+
route_unavailable: 503,
|
|
439
|
+
model_not_allowed: 403,
|
|
440
|
+
rate_limited: 429,
|
|
434
441
|
unknown_error: 500,
|
|
435
442
|
};
|
|
436
443
|
|
|
@@ -624,7 +631,7 @@ export function upstreamQueuedError(
|
|
|
624
631
|
503,
|
|
625
632
|
detail.length > 0
|
|
626
633
|
? detail
|
|
627
|
-
: "The upstream has queued this request
|
|
634
|
+
: "The upstream has queued this request (request {requestId}); retry after the stated interval. You're in line — no cuts 💛",
|
|
628
635
|
{
|
|
629
636
|
providerId,
|
|
630
637
|
reason: "upstream-queued",
|
|
@@ -816,7 +823,7 @@ export function classifyUpstreamError(
|
|
|
816
823
|
}
|
|
817
824
|
if (normalized !== undefined && CAPACITY_ERROR_IDENTIFIERS.has(normalized))
|
|
818
825
|
return {
|
|
819
|
-
code: "
|
|
826
|
+
code: "upstream_overloaded",
|
|
820
827
|
status: status === 503 || status === 529 ? status : 529,
|
|
821
828
|
origin: "upstream",
|
|
822
829
|
};
|
|
@@ -969,7 +976,7 @@ export async function throwIfHtmlResponse(response: Response): Promise<void> {
|
|
|
969
976
|
throw new GatewayError(
|
|
970
977
|
"transport_unavailable",
|
|
971
978
|
502,
|
|
972
|
-
`Upstream returned an HTML error page: ${summary}
|
|
979
|
+
`Upstream returned an HTML error page: ${summary} (request {requestId}). The provider's edge is having a moment — hang tight ☕️`.slice(0, 320),
|
|
973
980
|
{ upstreamStatus: response.status, contentType, accountScope: true },
|
|
974
981
|
"upstream",
|
|
975
982
|
);
|
|
@@ -1011,11 +1018,11 @@ export function fallbackRetryDelayMs(attemptIndex: number): number {
|
|
|
1011
1018
|
|
|
1012
1019
|
/** Sleeps until the delay expires or the request is aborted. */
|
|
1013
1020
|
export function sleep(ms: number, signal: AbortSignal): Promise<void> {
|
|
1014
|
-
if (signal.aborted) return Promise.reject(new GatewayError("transport_closed", 499, "request was cancelled"));
|
|
1021
|
+
if (signal.aborted) return Promise.reject(new GatewayError("transport_closed", 499, "The request was cancelled (request {requestId}). Send it again whenever ready 🙃"));
|
|
1015
1022
|
if (ms <= 0) return Promise.resolve();
|
|
1016
1023
|
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
|
1017
1024
|
const timer = setTimeout(() => { signal.removeEventListener("abort", onAbort); resolve(); }, ms);
|
|
1018
|
-
const onAbort = () => { clearTimeout(timer); reject(new GatewayError("transport_closed", 499, "request was cancelled")); };
|
|
1025
|
+
const onAbort = () => { clearTimeout(timer); reject(new GatewayError("transport_closed", 499, "The request was cancelled (request {requestId}). Send it again whenever ready 🙃")); };
|
|
1019
1026
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
1020
1027
|
return promise;
|
|
1021
1028
|
}
|
|
@@ -62,9 +62,9 @@ export function createApiKeyAuthenticationMiddleware(deps: {
|
|
|
62
62
|
safe.name = error instanceof Error ? error.name : "Error";
|
|
63
63
|
log.error("[auth] key lookup failed; treating as unavailable", safe);
|
|
64
64
|
throw new GatewayError(
|
|
65
|
-
"
|
|
65
|
+
"admission_unavailable",
|
|
66
66
|
503,
|
|
67
|
-
"The gateway could not verify this API key because its credential store is unavailable. Retry shortly.",
|
|
67
|
+
"The gateway could not verify this API key because its credential store is unavailable (request {requestId}). Retry shortly. The bouncer stepped out — come back shortly 🚪",
|
|
68
68
|
{},
|
|
69
69
|
"cartethyia",
|
|
70
70
|
);
|
|
@@ -76,7 +76,7 @@ export function createApiKeyAuthenticationMiddleware(deps: {
|
|
|
76
76
|
scopes: authorization.scopes,
|
|
77
77
|
});
|
|
78
78
|
if (!decision.scopes.includes("routing:invoke"))
|
|
79
|
-
throw new GatewayError("
|
|
79
|
+
throw new GatewayError("policy_rejected", 403, "This API key is not allowed to invoke routing (request {requestId}). Grant it the routing:invoke scope, or use a key that has it. Not allowed by policy — no hard feelings 🫂");
|
|
80
80
|
// Kill switch: refuse before any upstream is touched. Distinct from the
|
|
81
81
|
// scope check above, which is a property of the key — this is the tenant
|
|
82
82
|
// having stopped all traffic, and it applies to every key the tenant owns.
|
|
@@ -86,7 +86,8 @@ export function createApiKeyAuthenticationMiddleware(deps: {
|
|
|
86
86
|
throw new GatewayError(
|
|
87
87
|
"platform_unavailable",
|
|
88
88
|
503,
|
|
89
|
-
"This gateway has invoke switched off. Nothing upstream is being dialed while the switch is off; turn it back on from the dashboard overview to resume routing.",
|
|
89
|
+
"This gateway has invoke switched off (request {requestId}). Nothing upstream is being dialed while the switch is off; turn it back on from the dashboard overview to resume routing. Flip the switch back on when ready 🔌",
|
|
90
|
+
{ reason: "invoke_switched_off" },
|
|
90
91
|
);
|
|
91
92
|
// A key may refuse a downstream router it has been resold to. This is a
|
|
92
93
|
// policy check on the resolved key, so it belongs with the other
|
|
@@ -100,7 +101,7 @@ export function createApiKeyAuthenticationMiddleware(deps: {
|
|
|
100
101
|
throw new GatewayError(
|
|
101
102
|
"client_router_denied",
|
|
102
103
|
403,
|
|
103
|
-
"No API invocation access for this client.",
|
|
104
|
+
"No API invocation access for this client (request {requestId}). This router isn't on the guest list — denied, but politely 🤗",
|
|
104
105
|
{ reason: "client_router_denied", clientRouter: denied },
|
|
105
106
|
);
|
|
106
107
|
deps.stateStore.require(request).authorization = authorization;
|
|
@@ -77,11 +77,11 @@ export async function readIngressBody(
|
|
|
77
77
|
const isJsonRoute = isJsonProxyRoute(p);
|
|
78
78
|
const contentType = request.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
79
79
|
if (!contentType) {
|
|
80
|
-
if (isJsonRoute) throw new GatewayError("unsupported_media_type", 415, "This endpoint reads JSON, so the request needs a content-type: application/json header.");
|
|
80
|
+
if (isJsonRoute) throw new GatewayError("unsupported_media_type", 415, "This endpoint reads JSON, so the request needs a content-type: application/json header (request {requestId}). We speak JSON here — send JSON 💬");
|
|
81
81
|
return undefined;
|
|
82
82
|
}
|
|
83
83
|
if (contentType !== "application/json") {
|
|
84
|
-
if (isJsonRoute) throw new GatewayError("unsupported_media_type", 415, `This endpoint reads JSON and cannot parse '${contentType}'. Send content-type: application/json
|
|
84
|
+
if (isJsonRoute) throw new GatewayError("unsupported_media_type", 415, `This endpoint reads JSON and cannot parse '${contentType}' (request {requestId}). Send content-type: application/json. We speak JSON here — send JSON 💬`);
|
|
85
85
|
return undefined;
|
|
86
86
|
}
|
|
87
87
|
const maxBytes = options.maxBodyBytes ?? 1_048_576;
|
|
@@ -91,7 +91,7 @@ export async function readIngressBody(
|
|
|
91
91
|
throw bodyTooLargeError(maxBytes);
|
|
92
92
|
|
|
93
93
|
const reader = request.body?.getReader();
|
|
94
|
-
if (!reader) throw new GatewayError("invalid_request", 400, "The request body could not be read. Resend it with a JSON body.");
|
|
94
|
+
if (!reader) throw new GatewayError("invalid_request", 400, "The request body could not be read (request {requestId}). Resend it with a JSON body. Check the shape of what you sent — then resend 🪞");
|
|
95
95
|
// Collect raw chunks then decode once. String `+=` on each chunk reallocates
|
|
96
96
|
// the accumulating UTF-16 string (O(n²) for large Claude Code prompts);
|
|
97
97
|
// Buffer.concat + one TextDecoder pass is linear and allocation-stable.
|
|
@@ -126,7 +126,7 @@ export async function readIngressBody(
|
|
|
126
126
|
return body;
|
|
127
127
|
} catch (error) {
|
|
128
128
|
if (error instanceof GatewayError) throw error;
|
|
129
|
-
throw new GatewayError("invalid_request", 400, "The request body is not valid JSON. Check for a trailing comma or an unclosed brace.");
|
|
129
|
+
throw new GatewayError("invalid_request", 400, "The request body is not valid JSON (request {requestId}). Check for a trailing comma or an unclosed brace. Check the shape of what you sent — then resend 🪞");
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
|