cartethyia 2.5.4 → 2.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +13 -4
- package/README.md +2 -2
- package/dist/dashboard/assets/{BannedPage-C2orBsI2.js → BannedPage-CADDq4IU.js} +1 -1
- package/dist/dashboard/assets/BannedPage-CADDq4IU.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-_z1hID7E.js +1 -0
- package/dist/dashboard/assets/CliToolDetailPage-_z1hID7E.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-BV6pxVQo.js → CliToolsPage-DuO1Z7dq.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-DuO1Z7dq.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-DNEPi3xU.js +1 -0
- package/dist/dashboard/assets/CombosPage-DNEPi3xU.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-CW-wPmJ6.js → ConsoleLogPage-BVdpfiz1.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-BVdpfiz1.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-0p8uX7vB.js → CustomizationPage-BR5HHvbi.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-BR5HHvbi.js.gz +0 -0
- package/dist/dashboard/assets/{HealthEventsModal-BjqSMfko.js → HealthEventsModal-mUfcknqm.js} +1 -1
- package/dist/dashboard/assets/HealthEventsModal-mUfcknqm.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-BvzCx8Kr.js → LoginPage-V5faV9V2.js} +1 -1
- package/dist/dashboard/assets/LoginPage-V5faV9V2.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-C7pDb41R.js +1 -0
- package/dist/dashboard/assets/ModelPicker-C7pDb41R.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-BDu20Jdx.js +1 -0
- package/dist/dashboard/assets/OverviewPage-BDu20Jdx.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-Dllr_aFO.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage-Dllr_aFO.js.gz +0 -0
- package/dist/dashboard/assets/{ProvidersPage-Js9jUPSb.js → ProvidersPage-uPiGVTTK.js} +1 -1
- package/dist/dashboard/assets/ProvidersPage-uPiGVTTK.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-BaUkO-4O.js +7 -0
- package/dist/dashboard/assets/ProxyPage-BaUkO-4O.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-o0IFAsFb.js +1 -0
- package/dist/dashboard/assets/QuotaPage-o0IFAsFb.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CYWqbXjp.js +1 -0
- package/dist/dashboard/assets/SettingsPage-CYWqbXjp.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-DDbMQsoo.js → SetupPage-UB690SE2.js} +1 -1
- package/dist/dashboard/assets/SetupPage-UB690SE2.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-CB3Z7VTf.js → SortableList-BLujdX7N.js} +1 -1
- package/dist/dashboard/assets/SortableList-BLujdX7N.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-D95UCPHI.js → StudioPage-D5C2raAc.js} +3 -3
- package/dist/dashboard/assets/StudioPage-D5C2raAc.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-8uIcxa1W.js +56 -0
- package/dist/dashboard/assets/UsagePage-8uIcxa1W.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-DQYLQxpW.js +1 -0
- package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
- package/dist/dashboard/assets/app-CX_Z8cMs.css +2 -0
- package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
- package/dist/dashboard/assets/{assert-Chi7pCnt.js → assert-C9FD_JwJ.js} +1 -1
- package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
- package/dist/dashboard/assets/{badge-D4pQapKj.js → badge-CR72dasT.js} +1 -1
- package/dist/dashboard/assets/badge-CR72dasT.js.gz +0 -0
- package/dist/dashboard/assets/{banner-DH6cg-_B.js → banner-CI9_eSfO.js} +1 -1
- package/dist/dashboard/assets/banner-CI9_eSfO.js.gz +0 -0
- package/dist/dashboard/assets/{card-BKqlUOe4.js → card-DS7CQEsO.js} +1 -1
- package/dist/dashboard/assets/card-DS7CQEsO.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-fTgljTGA.js → cli-tools-CNqzrfdb.js} +1 -1
- package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-D5Ys_sM0.js → clipboard-button-611pSsaX.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-611pSsaX.js.gz +0 -0
- package/dist/dashboard/assets/{fingerprint-pattern-BWuHtbW4.js → concurrency-BDF0ok7y.js} +1 -1
- package/dist/dashboard/assets/concurrency-BDF0ok7y.js.gz +0 -0
- package/dist/dashboard/assets/dialog-k37exjNt.js +1 -0
- package/dist/dashboard/assets/dialog-k37exjNt.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CX7N8ucR.js +1 -0
- package/dist/dashboard/assets/drawer-CX7N8ucR.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CQBr6U9c.js → entry-B9Xvtnfj.js} +1 -1
- package/dist/dashboard/assets/entry-B9Xvtnfj.js.gz +0 -0
- package/dist/dashboard/assets/entry-CTP8AyiS.js +1 -0
- package/dist/dashboard/assets/entry-CTP8AyiS.js.gz +0 -0
- package/dist/dashboard/assets/entry-DxBHnauy.js +4 -0
- package/dist/dashboard/assets/entry-DxBHnauy.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-BlptL8Uf.js → github-badge-sltk1qUi.js} +1 -1
- package/dist/dashboard/assets/github-badge-sltk1qUi.js.gz +0 -0
- package/dist/dashboard/assets/{index-js6TJvdr.js → index-CmxZr14F.js} +2 -2
- package/dist/dashboard/assets/index-CmxZr14F.js.gz +0 -0
- package/dist/dashboard/assets/{inline-DUWDSmDV.js → inline-BhY2sSnO.js} +1 -1
- package/dist/dashboard/assets/inline-BhY2sSnO.js.gz +0 -0
- package/dist/dashboard/assets/{input-LVjgZq4f.js → input-V1umFiBk.js} +1 -1
- package/dist/dashboard/assets/input-V1umFiBk.js.gz +0 -0
- package/dist/dashboard/assets/layout-BBOYycyS.js +1 -0
- package/dist/dashboard/assets/layout-BBOYycyS.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-DupvLfDx.js → list-row-1zzZYM0B.js} +1 -1
- package/dist/dashboard/assets/list-row-1zzZYM0B.js.gz +0 -0
- package/dist/dashboard/assets/{live-C4pEJUqI.js → live-BivGfuUs.js} +1 -1
- package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
- package/dist/dashboard/assets/{model-bans-BRiuufkB.js → model-bans-CmffjLuG.js} +1 -1
- package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-B4ROwk8L.js → mutation-46P0oWMe.js} +1 -1
- package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
- package/dist/dashboard/assets/numbers-DkfMR5Qs.js +2 -0
- package/dist/dashboard/assets/numbers-DkfMR5Qs.js.gz +0 -0
- package/dist/dashboard/assets/{query-keys-gmjzC3RS.js → query-keys-BFhGpwR1.js} +1 -1
- package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
- package/dist/dashboard/assets/{routing-C7tBT5o-.js → routing-CAb4F37R.js} +1 -1
- package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
- package/dist/dashboard/assets/{select-WUCXorXw.js → select-BYR_5K2c.js} +1 -1
- package/dist/dashboard/assets/select-BYR_5K2c.js.gz +0 -0
- package/dist/dashboard/assets/{settings-C_zKl1GS.js → settings-DNX9sydi.js} +1 -1
- package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
- package/dist/dashboard/assets/{stack-_3O0F8Xn.js → stack-BHTfXdz_.js} +1 -1
- package/dist/dashboard/assets/stack-BHTfXdz_.js.gz +0 -0
- package/dist/dashboard/assets/{state-BgTxHcJS.js → state-mPDRlQkl.js} +1 -1
- package/dist/dashboard/assets/state-mPDRlQkl.js.gz +0 -0
- package/dist/dashboard/assets/{switch-DEjTtOWf.js → switch-DOmJxg7H.js} +1 -1
- package/dist/dashboard/assets/switch-DOmJxg7H.js.gz +0 -0
- package/dist/dashboard/assets/tokens-COzario9.js +1 -0
- package/dist/dashboard/assets/tokens-COzario9.js.gz +0 -0
- package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js +1 -0
- package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-BWSC0UAS.js → useMutation-Dem0lXQj.js} +1 -1
- package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +54 -54
- package/migrations/README.md +52 -52
- package/migrations/lite-sqlite3/0000_baseline.sql +2 -2
- package/package.json +96 -95
- package/scripts/cli/admin.ts +125 -0
- package/scripts/cli/{ui/background.ts → background.ts} +1 -1
- package/scripts/cli/{command/console-menu.ts → console-menu.ts} +275 -244
- package/scripts/cli/doctor.ts +37 -0
- package/scripts/cli/{support/env.ts → env.ts} +99 -99
- package/scripts/cli/{gateway/spawn.ts → gateway.ts} +112 -24
- package/scripts/cli/instance.ts +224 -0
- package/scripts/cli/main.ts +61 -18
- package/scripts/cli/{support/platform.ts → platform.ts} +64 -64
- package/scripts/cli/port.ts +100 -0
- package/scripts/cli/{support/services.ts → services.ts} +88 -88
- package/scripts/cli/{command/setup.ts → setup.ts} +35 -34
- package/scripts/cli/{command/start.ts → start.ts} +10 -5
- package/scripts/cli/{support/startup-signal.ts → startup-signal.ts} +23 -23
- package/scripts/cli/tray/index.ts +62 -12
- package/scripts/cli/{command/tray.ts → tray.ts} +208 -208
- package/scripts/cli/{command/update.ts → update.ts} +39 -39
- package/scripts/cli/version.ts +67 -0
- package/scripts/cli/{ui/wordmark.ts → wordmark.ts} +58 -58
- package/src/boot/app.ts +697 -693
- package/src/boot/dependencies.ts +647 -646
- package/src/boot/timeout.ts +2 -13
- package/src/config/cooldowns.ts +186 -138
- package/src/config/network.ts +8 -14
- package/src/config/providers.ts +7 -10
- package/src/config/runtime.ts +76 -76
- package/src/config/security.ts +15 -0
- package/src/config/spec.ts +20 -30
- package/src/config/telemetry.ts +0 -20
- package/src/config/timeouts.ts +0 -6
- package/src/console/admin/model-abuse/contracts.ts +28 -6
- package/src/console/admin/model-abuse/routes.ts +56 -3
- package/src/console/api-keys/contracts.ts +410 -414
- package/src/console/api-keys/routes.ts +881 -880
- package/src/console/auth/credentials.ts +3 -3
- package/src/console/auth/setup-service.ts +2 -2
- package/src/console/backup/export-endpoint.ts +6 -2
- package/src/console/backup/nine-router.ts +2 -5
- package/src/console/backup/service.ts +4 -0
- package/src/console/backup/store.ts +192 -29
- package/src/console/backup/validate.ts +3 -45
- package/src/console/cli-tools/fs-ops.ts +487 -487
- package/src/console/console-router-guards.ts +1 -1
- package/src/console/data-management/contracts.ts +13 -6
- package/src/console/data-management/operations.ts +23 -18
- package/src/console/data-management/routes.ts +0 -1
- package/src/console/domain-registration.ts +10 -5
- package/src/console/observability/live.ts +22 -0
- package/src/console/observability/store.ts +39 -16
- package/src/console/observability/usage-dimensions.ts +4 -4
- package/src/console/providers/catalog/catalog-projections.ts +6 -7
- package/src/console/providers/catalog/model-operations.ts +5 -2
- package/src/console/providers/catalog/provider-operations.ts +4 -4
- package/src/console/providers/catalog/public-model-store.ts +8 -11
- package/src/console/providers/catalog/store.ts +1553 -1553
- package/src/console/providers/oauth/routes.ts +8 -12
- package/src/console/quota/account-quota-tenant.ts +6 -6
- package/src/console/quota/quota-view.ts +1 -1
- package/src/console/remote-support/contracts.ts +61 -0
- package/src/console/remote-support/routes.ts +65 -0
- package/src/console/remote-support/service.ts +207 -0
- package/src/console/response-compression.ts +0 -3
- package/src/console/routing/pools/probe-result.ts +3 -2
- package/src/console/routing/pools/probe-verdict.ts +132 -132
- package/src/console/routing/pools/store.ts +8 -8
- package/src/console/settings/contracts.ts +58 -2
- package/src/console/settings/store.ts +30 -3
- package/src/console/shared/cursor.ts +8 -8
- package/src/console/shared/errors.ts +1 -3
- package/src/network/http2-bidi.ts +2 -4
- package/src/network/pool/agent.ts +478 -478
- package/src/network/pool/selector.ts +5 -4
- package/src/network/retry.ts +1 -2
- package/src/network/ssrf.ts +5 -4
- package/src/observability/event-compaction.ts +80 -78
- package/src/observability/event-day-index.ts +13 -15
- package/src/observability/event-log.ts +4 -4
- package/src/observability/logger.ts +6 -6
- package/src/observability/payload-capture.ts +151 -33
- package/src/observability/payload-store.ts +397 -440
- package/src/observability/redaction.ts +5 -6
- package/src/observability/request-notify.ts +43 -0
- package/src/observability/telemetry-status.ts +8 -9
- package/src/observability/token-speed.ts +5 -4
- package/src/persistence/db-mode.ts +7 -7
- package/src/persistence/readiness.ts +1 -1
- package/src/persistence/schema.ts +1406 -1402
- package/src/persistence/shared-cache.ts +1 -2
- package/src/persistence/stores/api-key-store.ts +18 -6
- package/src/persistence/stores/share-store.ts +4 -3
- package/src/persistence/stores/telemetry-store.ts +236 -237
- package/src/protocol/canonical-model.ts +11 -3
- package/src/protocol/codecs.ts +138 -4
- package/src/protocol/gateway-error.ts +6 -4
- package/src/protocol/request/chat.ts +21 -1
- package/src/protocol/request/codex.ts +10 -1
- package/src/protocol/request/messages.ts +4 -5
- package/src/protocol/request/responses.ts +9 -1
- package/src/protocol/response/chat.ts +34 -4
- package/src/protocol/response/codex.ts +7 -4
- package/src/protocol/response/dsml.ts +140 -28
- package/src/protocol/response/responses.ts +11 -5
- package/src/protocol/stream-error-frames.ts +4 -5
- package/src/providers/authentication/jwt-validator.ts +9 -10
- package/src/providers/authentication/oauth-refresh-service.ts +9 -11
- package/src/providers/configured-openai-providers.ts +134 -136
- package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
- package/src/providers/discovery/catalog/catalog-format.ts +3 -6
- package/src/providers/discovery/models-dev-catalog.ts +354 -354
- package/src/providers/discovery/probe-phases.ts +4 -4
- package/src/providers/discovery/probe-wire.ts +6 -13
- package/src/providers/discovery/probing-service.ts +1117 -1045
- package/src/providers/integrations/alibabacp/alibabacp.ts +1 -1
- package/src/providers/integrations/antigravity/antigravity-quota.ts +5 -7
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +23 -2
- package/src/providers/integrations/buddy/buddy-checkin.ts +1 -4
- package/src/providers/integrations/buddy/buddy-cli-headers.ts +23 -6
- package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -1
- package/src/providers/integrations/buddy/buddy-identity.ts +52 -0
- package/src/providers/integrations/buddy/buddy-media.ts +2 -2
- package/src/providers/integrations/buddy/buddy-usage-store.ts +3 -4
- package/src/providers/integrations/buddy/codebuddy/quota.ts +2 -13
- package/src/providers/integrations/buddy/codebuddy/shared.ts +118 -115
- package/src/providers/integrations/buddy/workbuddy/quota.ts +0 -11
- package/src/providers/integrations/buddy/workbuddy/shared.ts +5 -2
- package/src/providers/integrations/claude/claude.ts +608 -608
- package/src/providers/integrations/cline/cline-quota.ts +132 -132
- package/src/providers/integrations/cline/cline.ts +537 -539
- package/src/providers/integrations/codex/codex-oauth.ts +1 -1
- package/src/providers/integrations/commandcode/commandcode.ts +674 -675
- package/src/providers/integrations/commandcode/discovery.ts +117 -116
- package/src/providers/integrations/cursor/cursor-auth.ts +418 -418
- package/src/providers/integrations/cursor/cursor-quota.ts +309 -310
- package/src/providers/integrations/cursor/cursor.ts +11 -163
- package/src/providers/integrations/factory/factory-models.ts +3 -5
- package/src/providers/integrations/grok/grok-install-id.ts +2 -3
- package/src/providers/integrations/grok/grok.ts +6 -7
- package/src/providers/integrations/hermes.ts +14 -18
- package/src/providers/integrations/kiro/kiro-discovery.ts +1 -1
- package/src/providers/integrations/kiro/kiro.ts +1 -1
- package/src/providers/integrations/mistral.ts +6 -6
- package/src/providers/integrations/opencode/fingerprint.ts +3 -3
- package/src/providers/integrations/opencode/provider.ts +8 -8
- package/src/providers/integrations/qoder/qoder-catalog.ts +450 -450
- package/src/providers/integrations/qoder/qoder-oauth.ts +388 -310
- package/src/providers/integrations/qoder/qoder.ts +884 -865
- package/src/providers/integrations/shared/connect.ts +6 -6
- package/src/providers/integrations/trae/trae-catalog.ts +1 -2
- package/src/providers/integrations/trae/trae.ts +2 -2
- package/src/providers/integrations/zcode/oauth.ts +6 -1
- package/src/providers/operations/account-health-service.ts +1528 -1523
- package/src/providers/operations/account-reset-service.ts +1 -1
- package/src/providers/operations/client-versions.ts +6 -6
- package/src/providers/operations/custom-cli-headers.ts +3 -4
- package/src/providers/operations/provider-catalog-seeder.ts +95 -95
- package/src/providers/operations/provider-catalog-service.ts +3 -4
- package/src/providers/provider-metadata.ts +4 -8
- package/src/providers/provider-registry.ts +6 -7
- package/src/providers/provider-usage.ts +1 -1
- package/src/security/abuse.ts +15 -10
- package/src/security/access-control.ts +144 -146
- package/src/security/admission/contracts.ts +11 -0
- package/src/security/admission/in-memory-store.ts +4 -3
- package/src/security/admission/lease-sweep.ts +6 -6
- package/src/security/admission/reasons.ts +5 -1
- package/src/security/admission/redis-store.ts +211 -210
- package/src/security/admission/service.ts +4 -0
- package/src/security/api-key-auth.ts +504 -504
- package/src/security/ip-boundary.ts +155 -15
- package/src/security/model-abuse.ts +366 -94
- package/src/security/outbound-headers.ts +3 -4
- package/src/shared/sleep.ts +4 -0
- package/src/transport/dispatch/attempt-finalize.ts +47 -5
- package/src/transport/dispatch/attempt-loop.ts +531 -530
- package/src/transport/dispatch/stream-bridge.ts +1 -3
- package/src/transport/dispatch/streaming/finalize.ts +1 -1
- package/src/transport/dispatch/streaming/stream.ts +5 -6
- package/src/transport/failure-policy.ts +162 -8
- package/src/transport/guards/auth-guard.ts +7 -4
- package/src/transport/guards/concurrency-guard.ts +0 -5
- package/src/transport/guards/error-lifecycle.ts +13 -6
- package/src/transport/guards/pipeline.ts +15 -3
- package/src/transport/guards/request-context.ts +8 -2
- package/src/transport/request/inflight.ts +5 -6
- package/src/transport/request/preparer.ts +2 -2
- package/src/transport/request/state.ts +12 -8
- package/src/transport/routing/admission.ts +4 -0
- package/src/transport/routing/route-catalog.ts +8 -1
- package/src/transport/routing/route-model.ts +46 -14
- package/src/transport/routing/router.ts +21 -14
- package/src/transport/surface/completion.ts +2 -3
- package/src/transport/surface/dialects.ts +5 -6
- package/src/transport/surface/responses/encode.ts +38 -0
- package/src/transport/surface/responses/parse.ts +4 -0
- package/src/transport/translation/capabilities.ts +2 -3
- package/src/transport/translation/quirks.ts +3 -3
- package/src/workers/buddy-growth-tasks.ts +5 -10
- package/src/workers/daily-checkin.ts +17 -23
- package/src/workers/quota-refresh-sweep.ts +26 -26
- package/dist/dashboard/assets/BannedPage-C2orBsI2.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js +0 -1
- package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-BV6pxVQo.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-Bsan4679.js +0 -1
- package/dist/dashboard/assets/CombosPage-Bsan4679.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-CW-wPmJ6.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-0p8uX7vB.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-BjqSMfko.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-BvzCx8Kr.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-BY1aKEkP.js +0 -1
- package/dist/dashboard/assets/ModelPicker-BY1aKEkP.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js +0 -1
- package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-Js9jUPSb.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js +0 -7
- package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js +0 -1
- package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CS86ho5X.js +0 -1
- package/dist/dashboard/assets/SettingsPage-CS86ho5X.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-DDbMQsoo.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-CB3Z7VTf.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-D95UCPHI.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-Cnae9SWA.js +0 -56
- package/dist/dashboard/assets/UsagePage-Cnae9SWA.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-VY68Mhd1.js +0 -1
- package/dist/dashboard/assets/api-keys-VY68Mhd1.js.gz +0 -0
- package/dist/dashboard/assets/app-COlK95oi.css +0 -2
- package/dist/dashboard/assets/app-COlK95oi.css.gz +0 -0
- package/dist/dashboard/assets/assert-Chi7pCnt.js.gz +0 -0
- package/dist/dashboard/assets/badge-D4pQapKj.js.gz +0 -0
- package/dist/dashboard/assets/banner-DH6cg-_B.js.gz +0 -0
- package/dist/dashboard/assets/card-BKqlUOe4.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-fTgljTGA.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-D5Ys_sM0.js.gz +0 -0
- package/dist/dashboard/assets/dialog-BYj-8WuK.js +0 -1
- package/dist/dashboard/assets/dialog-BYj-8WuK.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CmXIZ6H1.js +0 -1
- package/dist/dashboard/assets/drawer-CmXIZ6H1.js.gz +0 -0
- package/dist/dashboard/assets/entry-CBg5zeu8.js +0 -1
- package/dist/dashboard/assets/entry-CBg5zeu8.js.gz +0 -0
- package/dist/dashboard/assets/entry-CQBr6U9c.js.gz +0 -0
- package/dist/dashboard/assets/entry-CgdSAzEV.js +0 -4
- package/dist/dashboard/assets/entry-CgdSAzEV.js.gz +0 -0
- package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-BlptL8Uf.js.gz +0 -0
- package/dist/dashboard/assets/index-js6TJvdr.js.gz +0 -0
- package/dist/dashboard/assets/inline-DUWDSmDV.js.gz +0 -0
- package/dist/dashboard/assets/input-LVjgZq4f.js.gz +0 -0
- package/dist/dashboard/assets/layout-CPfxpsfy.js +0 -1
- package/dist/dashboard/assets/layout-CPfxpsfy.js.gz +0 -0
- package/dist/dashboard/assets/list-row-DupvLfDx.js.gz +0 -0
- package/dist/dashboard/assets/live-C4pEJUqI.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-BRiuufkB.js.gz +0 -0
- package/dist/dashboard/assets/mutation-B4ROwk8L.js.gz +0 -0
- package/dist/dashboard/assets/numbers-YNcHvRY7.js +0 -2
- package/dist/dashboard/assets/numbers-YNcHvRY7.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-gmjzC3RS.js.gz +0 -0
- package/dist/dashboard/assets/routing-C7tBT5o-.js.gz +0 -0
- package/dist/dashboard/assets/select-WUCXorXw.js.gz +0 -0
- package/dist/dashboard/assets/settings-C_zKl1GS.js.gz +0 -0
- package/dist/dashboard/assets/stack-_3O0F8Xn.js.gz +0 -0
- package/dist/dashboard/assets/state-BgTxHcJS.js.gz +0 -0
- package/dist/dashboard/assets/switch-DEjTtOWf.js.gz +0 -0
- package/dist/dashboard/assets/tokens-DDxMbSRL.js +0 -1
- package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
- package/dist/dashboard/assets/use-presence-BSZEBx7U.js +0 -1
- package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-BWSC0UAS.js.gz +0 -0
- package/scripts/cli/command/admin.ts +0 -65
- package/scripts/cli/command/doctor.ts +0 -257
- package/scripts/cli/gateway/drain-token.ts +0 -78
- package/scripts/cli/gateway/instance-lock.ts +0 -105
- package/scripts/cli/gateway/stop.ts +0 -51
- package/scripts/cli/support/bun.ts +0 -27
- package/scripts/cli/support/version.ts +0 -37
- /package/dist/dashboard/assets/{app-DCQZRmHY.js → app-BIt446yE.js} +0 -0
- /package/dist/dashboard/assets/{app-DCQZRmHY.js.gz → app-BIt446yE.js.gz} +0 -0
- /package/scripts/cli/{ui/prompt.ts → prompt.ts} +0 -0
|
@@ -44,9 +44,7 @@ export function createDispatchStreamEncoder(
|
|
|
44
44
|
return {
|
|
45
45
|
push: (event) => encoder.push(event).map((wire) => formatSseEvent(wire.type, wire)),
|
|
46
46
|
finish: () => encoder.finish().map((wire) => formatSseEvent(wire.type, wire)),
|
|
47
|
-
encodeError: (
|
|
48
|
-
formatSseEvent("response.error", { type: "response.error", origin, code, message, details }),
|
|
49
|
-
],
|
|
47
|
+
encodeError: (input) => encoder.encodeError(input).map((wire) => formatSseEvent(wire.type, wire)),
|
|
50
48
|
};
|
|
51
49
|
}
|
|
52
50
|
if (surface === "messages") {
|
|
@@ -154,7 +154,7 @@ export class StreamFinalizer {
|
|
|
154
154
|
*
|
|
155
155
|
* Once the upstream has produced anything — even surface-invisible
|
|
156
156
|
* reasoning deltas — it is alive: the stall bound governs and the tighter
|
|
157
|
-
* first-chunk bound
|
|
157
|
+
* first-chunk bound does not apply.
|
|
158
158
|
*
|
|
159
159
|
* The request deadline is a fixed ceiling measured from ingress, and a
|
|
160
160
|
* stream that keeps sending past it is not stalled. A live WorkBuddy stream
|
|
@@ -232,10 +232,9 @@ export async function dispatchStreamingAttempt(input: StreamingDispatchInput): P
|
|
|
232
232
|
});
|
|
233
233
|
// `ProviderAdapter.dispatch` is declared to return a non-nullable
|
|
234
234
|
// `AsyncIterable`, so an adapter that returned `undefined` would be a
|
|
235
|
-
// type violation rather than a state this code could handle
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
// have. Enforcing it belongs at the adapter boundary, not here.
|
|
235
|
+
// type violation rather than a state this code could handle — no null check
|
|
236
|
+
// is needed here, and adding one would advertise a contract the interface
|
|
237
|
+
// does not have. Enforcing it belongs at the adapter boundary.
|
|
239
238
|
const createUpstreamIterable = () =>
|
|
240
239
|
adapter.dispatch(
|
|
241
240
|
dispatchRequest,
|
|
@@ -375,8 +374,8 @@ export async function dispatchStreamingAttempt(input: StreamingDispatchInput): P
|
|
|
375
374
|
// reasoning item when the client is Chat Completions). Keep
|
|
376
375
|
// pulling until at least one wire chunk is enqueued; returning
|
|
377
376
|
// from pull with an empty queue can leave Web Streams waiting
|
|
378
|
-
// forever
|
|
379
|
-
//
|
|
377
|
+
// forever — a client then sees one assistant header and then a
|
|
378
|
+
// 60s socket reset.
|
|
380
379
|
for (;;) {
|
|
381
380
|
// Re-arm on every upstream read: the watchdog fires only
|
|
382
381
|
// when the provider stays silent longer than the bound.
|
|
@@ -229,6 +229,35 @@ export function extractUpstreamMessage(body: unknown): string {
|
|
|
229
229
|
* Reading only a nested string dropped the code for the second family
|
|
230
230
|
* entirely, so the operator saw a bare message with no provider code.
|
|
231
231
|
*/
|
|
232
|
+
/**
|
|
233
|
+
* The provider code hidden inside one string value, or `undefined`.
|
|
234
|
+
*
|
|
235
|
+
* Recognizes both a nested JSON envelope written as an escaped string
|
|
236
|
+
* (`"{\"code\":\"11-128\"}"`) and the bare escaped form (`"\"code\\\":\\\"11-128\""`).
|
|
237
|
+
* Cheap by construction: the scan is a substring test, and only a string that
|
|
238
|
+
* looks like it carries a code is handed to `JSON.parse`, so a large message
|
|
239
|
+
* body costs one `includes` rather than a parse.
|
|
240
|
+
*/
|
|
241
|
+
function parseEmbeddedCode(value: string): string | undefined {
|
|
242
|
+
if (!value.includes("code")) return undefined;
|
|
243
|
+
// An escaped envelope: try it as JSON first, then with its escapes collapsed.
|
|
244
|
+
for (const candidate of [value, value.replace(/\\+"/g, '"')]) {
|
|
245
|
+
const trimmed = candidate.trim();
|
|
246
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith('"')) continue;
|
|
247
|
+
let parsed: unknown;
|
|
248
|
+
try {
|
|
249
|
+
parsed = JSON.parse(trimmed);
|
|
250
|
+
} catch {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const code = upstreamProviderCode(parsed);
|
|
254
|
+
if (code !== undefined) return code;
|
|
255
|
+
}
|
|
256
|
+
// A bare `"code": "11-128"` fragment inside a longer message.
|
|
257
|
+
const match = value.match(/\\*["']?code\\*["']?\s*:\s*\\*["']?([A-Za-z0-9_.:-]{3,32})/i);
|
|
258
|
+
return match?.[1];
|
|
259
|
+
}
|
|
260
|
+
|
|
232
261
|
export function upstreamProviderCode(body: unknown): string | undefined {
|
|
233
262
|
if (!isRecord(body)) return undefined;
|
|
234
263
|
const nested = isRecord(body.error) ? body.error : undefined;
|
|
@@ -236,6 +265,20 @@ export function upstreamProviderCode(body: unknown): string | undefined {
|
|
|
236
265
|
for (const candidate of [ext?.code, nested?.code, body.code]) {
|
|
237
266
|
if (typeof candidate === "string" && candidate.length > 0) return candidate;
|
|
238
267
|
}
|
|
268
|
+
// A provider may embed its error envelope as an *escaped string* rather than a
|
|
269
|
+
// nested object — `"error": "{\"code\":\"11-128\",...}"` — so none of the
|
|
270
|
+
// candidates above is a record and the code is invisible. That is the shape
|
|
271
|
+
// the reference gateways detect, and it matters because the channel code is
|
|
272
|
+
// what decides whether an account is cooled: missing it, a blocked account
|
|
273
|
+
// stays in rotation failing every request.
|
|
274
|
+
for (const candidate of [ext, nested, body]) {
|
|
275
|
+
if (!isRecord(candidate)) continue;
|
|
276
|
+
for (const value of Object.values(candidate)) {
|
|
277
|
+
if (typeof value !== "string") continue;
|
|
278
|
+
const embedded = parseEmbeddedCode(value);
|
|
279
|
+
if (embedded !== undefined) return embedded;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
239
282
|
// A numeric code is still the provider's identifier; keep it as text so an
|
|
240
283
|
// operator can search for it. The nested string above wins when both exist.
|
|
241
284
|
const numeric = ext?.code ?? nested?.code ?? body.code;
|
|
@@ -364,6 +407,14 @@ export function classifyUpstreamFailure(error: unknown): UpstreamFailurePolicy {
|
|
|
364
407
|
error.code === "model_not_found" ||
|
|
365
408
|
error.code === "admission_unavailable" ||
|
|
366
409
|
error.code === "capacity_exhausted" ||
|
|
410
|
+
// A provider max-inflight rejection: the plan's next sibling account may
|
|
411
|
+
// have a free slot, exactly as with an API-key concurrency rejection.
|
|
412
|
+
error.code === "provider_capacity_exhausted" ||
|
|
413
|
+
// The upstream accepted the request and is holding it in a queue. Another
|
|
414
|
+
// account of the same provider is very likely not queued, so failover is
|
|
415
|
+
// the faster answer — and if every account is queued, the caller still has
|
|
416
|
+
// the stated interval to wait for.
|
|
417
|
+
error.code === "upstream_queued" ||
|
|
367
418
|
error.code === "accounts_unavailable" ||
|
|
368
419
|
error.code === "accounts_rate_limited" ||
|
|
369
420
|
error.code === "quota_exceeded" ||
|
|
@@ -499,8 +550,8 @@ export interface TerminalOutcome {
|
|
|
499
550
|
* Derives a failed attempt's status, error category, and error origin **from
|
|
500
551
|
* one decision**, so the three can never contradict each other.
|
|
501
552
|
*
|
|
502
|
-
*
|
|
503
|
-
*
|
|
553
|
+
* Computing them independently at a dispatch call site lets them
|
|
554
|
+
* drift: a request that failed on an upstream condition could be recorded
|
|
504
555
|
* `cancelled` (499) while its category said `transport_unavailable` and its
|
|
505
556
|
* http status said 502 — a row no operator could trust. Bundling them here
|
|
506
557
|
* makes the inconsistency unrepresentable: a cancel always carries
|
|
@@ -591,6 +642,88 @@ export function isContextLengthFailure(body: unknown): boolean {
|
|
|
591
642
|
}
|
|
592
643
|
return false;
|
|
593
644
|
}
|
|
645
|
+
/**
|
|
646
|
+
* How long an upstream asked the caller to wait before retrying, in
|
|
647
|
+
* milliseconds, or `undefined` when the body states no duration.
|
|
648
|
+
*
|
|
649
|
+
* `retryAfterSeconds` is the field the queue-shaped refusals carry. It is read
|
|
650
|
+
* from a few plausible spellings because the upstreams that queue a request do
|
|
651
|
+
* not agree on one, and a queue with no duration still has to be reported as a
|
|
652
|
+
* queue — the caller's wait is then bounded by the caller, not by us.
|
|
653
|
+
*/
|
|
654
|
+
export function queueRetryAfterMs(body: unknown): number | undefined {
|
|
655
|
+
if (!isRecord(body)) return undefined;
|
|
656
|
+
const envelope = isRecord(body.error) ? body.error : body;
|
|
657
|
+
for (const source of [envelope, body]) {
|
|
658
|
+
for (const key of ["retryAfterSeconds", "retry_after_seconds", "retryAfterMs", "retry_after"]) {
|
|
659
|
+
const raw = source[key];
|
|
660
|
+
if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) {
|
|
661
|
+
return key.toLowerCase().endsWith("ms") ? Math.round(raw) : Math.round(raw * 1000);
|
|
662
|
+
}
|
|
663
|
+
if (typeof raw === "string") {
|
|
664
|
+
const value = Number(raw.trim());
|
|
665
|
+
if (Number.isFinite(value) && value > 0) {
|
|
666
|
+
return key.toLowerCase().endsWith("ms") ? Math.round(value) : Math.round(value * 1000);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return undefined;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* True when an upstream error body says the request is **queued**, not refused.
|
|
676
|
+
*
|
|
677
|
+
* Qoder answers `403` with `{"code":"10605", "isQueued":true,
|
|
678
|
+
* "retryAfterSeconds":N}`. That is not a rejection — the request is accepted and
|
|
679
|
+
* will be served once capacity frees up — but it arrives on a status the
|
|
680
|
+
* gateway treats as a failure, so without this it surfaced as a 403 the client
|
|
681
|
+
* read as "you may not do this" and the turn ended. Reporting it as a queue
|
|
682
|
+
* lets the caller wait for the stated interval instead.
|
|
683
|
+
*
|
|
684
|
+
* Recognized conservatively: an explicit `isQueued` flag, the `10605` code, or
|
|
685
|
+
* a message naming a queue. A bare word like "queue" appearing anywhere in a
|
|
686
|
+
* long body is not enough on its own, which is why the message test requires
|
|
687
|
+
* the queue to be what the message is *about* rather than a word it contains.
|
|
688
|
+
*/
|
|
689
|
+
export function isUpstreamQueued(body: unknown): boolean {
|
|
690
|
+
if (!isRecord(body)) return false;
|
|
691
|
+
const envelope = isRecord(body.error) ? body.error : body;
|
|
692
|
+
if (envelope.isQueued === true || body.isQueued === true) return true;
|
|
693
|
+
if (envelope.queued === true || body.queued === true) return true;
|
|
694
|
+
for (const candidate of [envelope.code, body.code]) {
|
|
695
|
+
if (typeof candidate === "string" && candidate.trim() === "10605") return true;
|
|
696
|
+
}
|
|
697
|
+
for (const candidate of [envelope.message, envelope.msg, body.message, body.msg]) {
|
|
698
|
+
if (typeof candidate !== "string") continue;
|
|
699
|
+
const lower = candidate.toLowerCase();
|
|
700
|
+
// "is queued" / "queued, retry in Ns" — the queue as the message's subject.
|
|
701
|
+
if (/\bqueued\b|\bis queue\b|resource_limit/.test(lower)) return true;
|
|
702
|
+
}
|
|
703
|
+
return false;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/** The error a queued upstream request becomes: retryable, with its wait. */
|
|
707
|
+
export function upstreamQueuedError(
|
|
708
|
+
providerId: string,
|
|
709
|
+
retryAfterMs: number | undefined,
|
|
710
|
+
detail: string,
|
|
711
|
+
): GatewayError {
|
|
712
|
+
return new GatewayError(
|
|
713
|
+
"upstream_queued",
|
|
714
|
+
503,
|
|
715
|
+
detail.length > 0
|
|
716
|
+
? detail
|
|
717
|
+
: "The upstream has queued this request. Retry after the stated interval.",
|
|
718
|
+
{
|
|
719
|
+
providerId,
|
|
720
|
+
reason: "upstream-queued",
|
|
721
|
+
...(retryAfterMs === undefined ? {} : { retryAfterMs, cooldownMs: retryAfterMs }),
|
|
722
|
+
},
|
|
723
|
+
"upstream",
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
|
|
594
727
|
const QUOTA_ERROR_IDENTIFIERS: ReadonlySet<string> = new Set([
|
|
595
728
|
"rate_limit_exceeded",
|
|
596
729
|
"rate_limit_error",
|
|
@@ -815,12 +948,31 @@ export async function mapUpstreamHttpError(response: Response, providerId: strin
|
|
|
815
948
|
parsedBody = undefined;
|
|
816
949
|
}
|
|
817
950
|
const identifier = upstreamErrorIdentifier(parsedBody) ?? providerCode;
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
951
|
+
// A queued request is checked *before* the status table: the upstream accepted
|
|
952
|
+
// the request and will serve it, but says so on a failure status (Qoder's 403
|
|
953
|
+
// + 10605). Letting the table classify it first turned "wait, then retry"
|
|
954
|
+
// into a refusal the client could not distinguish from a permissions error.
|
|
955
|
+
const upstreamQueued = isUpstreamQueued(parsedBody);
|
|
956
|
+
// The body's stated interval wins; the header is only a fallback, because an
|
|
957
|
+
// upstream that queues a request usually states the wait in the body and
|
|
958
|
+
// leaves `Retry-After` off entirely.
|
|
959
|
+
const queuedWaitMs = upstreamQueued
|
|
960
|
+
? (queueRetryAfterMs(parsedBody) ??
|
|
961
|
+
(parseUpstreamBackoff(response.headers) ?? undefined))
|
|
962
|
+
: undefined;
|
|
963
|
+
const classification = upstreamQueued
|
|
964
|
+
? { code: "upstream_queued" as const, status: 503, origin: "upstream" as const }
|
|
965
|
+
: isContextLengthFailure(parsedBody)
|
|
966
|
+
? { code: "context_length_exceeded" as const, status: 413, origin: "upstream" as const }
|
|
967
|
+
: classifyUpstreamError(response.status, identifier);
|
|
821
968
|
const extracted = extractUpstreamMessage(parsedBody ?? text);
|
|
822
969
|
const message = (extracted.length > 0 ? extracted : text.trim() || `Provider returned HTTP ${response.status}.`).slice(0, MAX_UPSTREAM_ERROR_BYTES).replace(/[\r\n]+/g, " ");
|
|
823
|
-
const
|
|
970
|
+
const headerWaitMs = parseUpstreamBackoff(response.headers);
|
|
971
|
+
// A queue carries its own interval, which is the one the caller must honour:
|
|
972
|
+
// it is what the upstream promised, while `Retry-After` may be absent or may
|
|
973
|
+
// describe a different window. `cooldownMs` rides along so the account-health
|
|
974
|
+
// layer can also read it.
|
|
975
|
+
const retryAfterMs = queuedWaitMs ?? headerWaitMs;
|
|
824
976
|
const requestId = upstreamRequestId(response.headers);
|
|
825
977
|
const reportedProviderCode = providerCode ?? identifier;
|
|
826
978
|
throw new GatewayError(classification.code, classification.status, message, {
|
|
@@ -830,6 +982,9 @@ export async function mapUpstreamHttpError(response: Response, providerId: strin
|
|
|
830
982
|
...(requestId ? { upstreamRequestId: requestId } : {}),
|
|
831
983
|
raw: text.slice(0, MAX_UPSTREAM_ERROR_BYTES),
|
|
832
984
|
...(retryAfterMs === null || retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
985
|
+
...(queuedWaitMs !== undefined
|
|
986
|
+
? { reason: "upstream-queued", cooldownMs: queuedWaitMs }
|
|
987
|
+
: {}),
|
|
833
988
|
...(classification.rateLimitScope
|
|
834
989
|
? { rateLimitScope: classification.rateLimitScope }
|
|
835
990
|
: {}),
|
|
@@ -895,8 +1050,7 @@ export function isRetryableFailure(error: unknown): boolean {
|
|
|
895
1050
|
* timeout.
|
|
896
1051
|
*
|
|
897
1052
|
* Owned here so the attempt loop, the empty-stream replay, and the Codex
|
|
898
|
-
* retry budget cannot drift apart
|
|
899
|
-
* the constant.
|
|
1053
|
+
* retry budget cannot drift apart.
|
|
900
1054
|
*/
|
|
901
1055
|
export const MIN_RETRY_BUDGET_MS = 15_000;
|
|
902
1056
|
|
|
@@ -90,14 +90,17 @@ export function createApiKeyAuthenticationMiddleware(deps: {
|
|
|
90
90
|
);
|
|
91
91
|
// Abuse ban: refuse before parse/prepare and before authorization is
|
|
92
92
|
// recorded, so a banned caller cannot keep producing failed rows. The ban
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
// is keyed on the client address *and* the API key: one key can be shared
|
|
94
|
+
// by every recipient of a share link, so refusing the address alone would
|
|
95
|
+
// punish callers that did nothing, while refusing the key alone would let
|
|
96
|
+
// the caller that probed mint a fresh one.
|
|
96
97
|
if (deps.modelStrikes) {
|
|
97
98
|
const state = deps.stateStore.get(request);
|
|
98
99
|
const ip = state?.clientIdentity?.address;
|
|
99
100
|
if (ip !== undefined) {
|
|
100
|
-
const banned = await deps.modelStrikes
|
|
101
|
+
const banned = await deps.modelStrikes
|
|
102
|
+
.check({ ip, apiKeyId: authorization.id })
|
|
103
|
+
.catch(() => false);
|
|
101
104
|
if (banned) throw modelAbuseBannedError();
|
|
102
105
|
}
|
|
103
106
|
}
|
|
@@ -22,11 +22,6 @@ const PROCESS_CONCURRENCY_RETRY_MS = 3_000;
|
|
|
22
22
|
|
|
23
23
|
let inFlight = 0;
|
|
24
24
|
|
|
25
|
-
/** The current in-flight count. Exposed for tests; not a control surface. */
|
|
26
|
-
export function processInFlight(): number {
|
|
27
|
-
return inFlight;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
25
|
export function createProcessConcurrencyMiddleware(): Elysia {
|
|
31
26
|
return new Elysia()
|
|
32
27
|
.beforeHandle(({ request }) => {
|
|
@@ -12,6 +12,7 @@ import { computeTokensPerSec } from "../../observability/token-speed";
|
|
|
12
12
|
import { pushStructuredConsoleLog } from "../../observability/log-ring";
|
|
13
13
|
import type { ConsoleLogLevel, ConsoleLogMetadata } from "../../observability/log-ring";
|
|
14
14
|
import { isProxyDispatchRoute } from "./body-policy";
|
|
15
|
+
import { notifyRequestCompleted } from "../../observability/request-notify";
|
|
15
16
|
|
|
16
17
|
interface ElysiaBuiltinError {
|
|
17
18
|
readonly status?: number;
|
|
@@ -217,12 +218,12 @@ export function finalizeRequestTelemetry(
|
|
|
217
218
|
return;
|
|
218
219
|
}
|
|
219
220
|
|
|
220
|
-
// An early rejection never reaches `completeAttempt`,
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
//
|
|
221
|
+
// An early rejection never reaches `completeAttempt`, the caller that writes
|
|
222
|
+
// a payload frame — so a 400 from ingress, auth, parse, or the preparer would
|
|
223
|
+
// produce a request row whose four capture panels were all empty, and the one
|
|
224
|
+
// case an operator most wants to read (what did the client actually send?)
|
|
225
|
+
// would have nothing to read. Capture the ingress body and the error the
|
|
226
|
+
// client saw, keyed to the same request id as the row.
|
|
226
227
|
if (requestData === undefined && state.ingressBody !== undefined) {
|
|
227
228
|
void captureEarlyRejectionPayload(state, authorization);
|
|
228
229
|
}
|
|
@@ -235,6 +236,12 @@ export function finalizeRequestTelemetry(
|
|
|
235
236
|
tokensPerSec,
|
|
236
237
|
}),
|
|
237
238
|
);
|
|
239
|
+
notifyRequestCompleted({
|
|
240
|
+
tenantId: authorization.tenantId,
|
|
241
|
+
requestId: state.requestId,
|
|
242
|
+
status,
|
|
243
|
+
httpStatus: requestStatus,
|
|
244
|
+
});
|
|
238
245
|
}
|
|
239
246
|
|
|
240
247
|
/** Console-log severity for a terminal request status. */
|
|
@@ -28,7 +28,12 @@ import type { CartethyiaDatabase } from "../../persistence/db-handle";
|
|
|
28
28
|
import type { RedisClient } from "../../persistence/redis";
|
|
29
29
|
import type { TelemetryBatchBuffer } from "../../observability/telemetry-buffer";
|
|
30
30
|
import type { ModelStrikeService } from "../../security/model-abuse";
|
|
31
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
TelemetryPayloadCapture,
|
|
33
|
+
payloadCaptureKeepsBodies,
|
|
34
|
+
payloadRetentionMs,
|
|
35
|
+
resolvePayloadCapture,
|
|
36
|
+
} from "../../observability/payload-capture";
|
|
32
37
|
|
|
33
38
|
/** Shared state/context carried by every ordered transport stage. */
|
|
34
39
|
export interface TransportPipelineContext {
|
|
@@ -167,8 +172,15 @@ export function createTransportPipeline(context: TransportPipelineContext): Tran
|
|
|
167
172
|
stateStore: context.stateStore,
|
|
168
173
|
telemetryBuffer: context.telemetry,
|
|
169
174
|
// One capture for the process, shared with every request state so an
|
|
170
|
-
// early rejection can record its payload from the finalize hook.
|
|
171
|
-
|
|
175
|
+
// early rejection can record its payload from the finalize hook. It
|
|
176
|
+
// resolves the tenant's retention itself because an early rejection has
|
|
177
|
+
// no preference memo on its state — nothing resolved one before the
|
|
178
|
+
// request was refused.
|
|
179
|
+
payloadCapture: new TelemetryPayloadCapture(context.db, (tenantId) =>
|
|
180
|
+
resolvePayloadCapture(context.db, tenantId).then(
|
|
181
|
+
(resolved) => (resolved.ok ? resolved.retentionMs : payloadRetentionMs()),
|
|
182
|
+
),
|
|
183
|
+
),
|
|
172
184
|
});
|
|
173
185
|
} else {
|
|
174
186
|
registerRequestCleanup(rootApp, { stateStore: context.stateStore });
|
|
@@ -269,7 +269,13 @@ export function createProxyRoutePreparationMiddleware(deps: {
|
|
|
269
269
|
throw new GatewayError("admission_unavailable", 503, "The request was not authenticated and parsed, so this route cannot be served. Resend the request.");
|
|
270
270
|
const strikes = deps.modelStrikes;
|
|
271
271
|
const ip = state.clientIdentity?.address;
|
|
272
|
-
|
|
272
|
+
// The key rides along so a strike is charged to the caller that made it.
|
|
273
|
+
// Without it, one recipient of a shared link could escalate a ban onto
|
|
274
|
+
// every other caller behind the same address.
|
|
275
|
+
const strikeIdentity =
|
|
276
|
+
strikes && ip !== undefined
|
|
277
|
+
? { strikes, ip, apiKeyId: state.authorization?.id ?? null }
|
|
278
|
+
: undefined;
|
|
273
279
|
try {
|
|
274
280
|
state.preparedRequest = await deps.preparer.prepare({
|
|
275
281
|
canonicalRequest: state.canonicalRequest,
|
|
@@ -286,7 +292,7 @@ export function createProxyRoutePreparationMiddleware(deps: {
|
|
|
286
292
|
// request, or any other failure is not the caller probing for models.
|
|
287
293
|
if (strikeIdentity && isModelRejection(error)) {
|
|
288
294
|
const outcome = await strikeIdentity.strikes
|
|
289
|
-
.noteInvalid({ ip: strikeIdentity.ip })
|
|
295
|
+
.noteInvalid({ ip: strikeIdentity.ip, apiKeyId: strikeIdentity.apiKeyId })
|
|
290
296
|
.catch(() => null);
|
|
291
297
|
if (outcome?.banned === true) throw modelAbuseBannedError();
|
|
292
298
|
if (outcome) {
|
|
@@ -4,12 +4,11 @@
|
|
|
4
4
|
* provider-dispatch leases, keyed by `requestId`, carrying the client IP
|
|
5
5
|
* captured at ingress and the hard deadline the flight must not outlive.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* the
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* one place that evicts a request, so a flight can no longer outlive its state.
|
|
7
|
+
* The registry is *owned* by the `ProxyRequestStateStore` (`state.ts`), which
|
|
8
|
+
* is also the one place that evicts a request, so a flight cannot outlive its
|
|
9
|
+
* state. A separate process-global counter alongside the store's
|
|
10
|
+
* `liveControllers` map would be a second registry able to disagree with it: a
|
|
11
|
+
* flight the store had already torn down would stay counted there forever.
|
|
13
12
|
*
|
|
14
13
|
* The store hands each entry an `expiresAtMs` (the request's hard deadline). A
|
|
15
14
|
* periodic backstop sweep reads `overdue()` and force-releases any flight whose
|
|
@@ -241,8 +241,8 @@ export class ProxyRequestPreparer {
|
|
|
241
241
|
// than dispatched: a provider rejects the whole request for one corrupt
|
|
242
242
|
// attachment, and the buddy family reports it with no field named, so the
|
|
243
243
|
// caller gets a 400 it cannot act on. Running this before capability
|
|
244
|
-
// derivation also means a request whose only image was defective
|
|
245
|
-
//
|
|
244
|
+
// derivation also means a request whose only image was defective does not
|
|
245
|
+
// require the `image` capability — it plans like the text request it has
|
|
246
246
|
// become, instead of being routed to a vision route for nothing.
|
|
247
247
|
const requestWithAttachments = dropCorruptAttachments(request).request;
|
|
248
248
|
// Continuation affinity is resolved before anything else: a request
|
|
@@ -100,22 +100,26 @@ export interface ProxyRequestState {
|
|
|
100
100
|
* Remote-diagnosis field: the drawer reads empty for reasons invisible from
|
|
101
101
|
* the client — the tenant's capture mode, a missing tenant, a non-terminal
|
|
102
102
|
* attempt — and without the reason an operator cannot tell "not configured"
|
|
103
|
-
* from "broken". The four panels are the only surface that could
|
|
104
|
-
*
|
|
103
|
+
* from "broken". The four panels are the only surface that could hint at it,
|
|
104
|
+
* and they look identical in both cases.
|
|
105
105
|
*/
|
|
106
106
|
payloadCaptureState?: PayloadCaptureState;
|
|
107
107
|
/**
|
|
108
108
|
* This request's resolved payload-capture preference, memoized on the request.
|
|
109
109
|
*
|
|
110
110
|
* The preference is a tenant setting, so it cannot change between attempts of
|
|
111
|
-
* one request. It
|
|
112
|
-
*
|
|
113
|
-
* store — so
|
|
114
|
-
*
|
|
115
|
-
* whether capture is on.
|
|
111
|
+
* one request. It is read twice — once per attempt to decide whether to tee
|
|
112
|
+
* the provider body, and again on the terminal attempt to decide what to
|
|
113
|
+
* store — so memoizing it here makes the second read free and keeps the two
|
|
114
|
+
* from disagreeing about whether capture is on.
|
|
116
115
|
*/
|
|
117
116
|
payloadCapturePreference?:
|
|
118
|
-
| {
|
|
117
|
+
| {
|
|
118
|
+
readonly ok: true;
|
|
119
|
+
readonly mode: TelemetryPayloadMode;
|
|
120
|
+
readonly depth: TelemetryPayloadDepth;
|
|
121
|
+
readonly retentionMs: number;
|
|
122
|
+
}
|
|
119
123
|
| { readonly ok: false };
|
|
120
124
|
/**
|
|
121
125
|
* This request's resolved inline-attachment ceiling, memoized for the same
|
|
@@ -60,6 +60,10 @@ function admissionDirective(candidate: RouteCandidate): AdmissionDirective {
|
|
|
60
60
|
/** The one shape of an admission rejection: the metric, then the decision. */
|
|
61
61
|
function capacityRejected(): AdmissionDecision {
|
|
62
62
|
metrics.proxy_admission_total.inc(1, { reason: "provider_capacity" });
|
|
63
|
+
// `reason` is the routing-internal label, not the public code. It stays the
|
|
64
|
+
// string the router and its tests match on; the envelope code is
|
|
65
|
+
// `provider_capacity_exhausted` so this layer cannot be confused with an
|
|
66
|
+
// API-key `max_concurrent` rejection (which owns `capacity_exhausted`).
|
|
63
67
|
return { admitted: false, reason: "capacity_exhausted" };
|
|
64
68
|
}
|
|
65
69
|
|
|
@@ -404,7 +404,7 @@ class RouteCatalogRepository {
|
|
|
404
404
|
// The cooldown override is a console_settings preference, read inside
|
|
405
405
|
// the same fan-out rather than per request: one more row set on a
|
|
406
406
|
// query that already runs, instead of a Postgres round trip on every
|
|
407
|
-
// admitted request
|
|
407
|
+
// admitted request.
|
|
408
408
|
this.db
|
|
409
409
|
.select({ tenantId: consoleSettings.tenantId, preferences: consoleSettings.preferences })
|
|
410
410
|
.from(consoleSettings),
|
|
@@ -747,6 +747,13 @@ class RouteCatalogRepository {
|
|
|
747
747
|
// The account is still cooling — this only stops it from being
|
|
748
748
|
// excluded. `plan()` reads `cooldown_until` to answer the
|
|
749
749
|
// all-cooling case, so it stays populated either way.
|
|
750
|
+
//
|
|
751
|
+
// Neither `health_status` nor `cooldown_kind` is set: the
|
|
752
|
+
// evaluator excludes a hard cooldown on `health_status ===
|
|
753
|
+
// "cooldown"` alone, so stamping it here would keep excluding
|
|
754
|
+
// the very accounts the override exists to reach — a spent quota
|
|
755
|
+
// or a policy block, which are the ones an operator actually
|
|
756
|
+
// wants to override.
|
|
750
757
|
candidate.cooldown_until = account.cooldownUntil.getTime();
|
|
751
758
|
} else {
|
|
752
759
|
candidate.health_status = "cooldown";
|
|
@@ -255,8 +255,8 @@ export interface AdmissionController {
|
|
|
255
255
|
* Translates the evaluator's internal reason codes into operator English.
|
|
256
256
|
*
|
|
257
257
|
* These names are the vocabulary the routing layers use with each other —
|
|
258
|
-
* `model_cooldown`, `cooldown_hard`, `locked` —
|
|
259
|
-
*
|
|
258
|
+
* `model_cooldown`, `cooldown_hard`, `locked` — not something to print into the
|
|
259
|
+
* public message, where the client would read an enum instead of an
|
|
260
260
|
* explanation. The codes stay in the envelope's `reasons` field for anyone who
|
|
261
261
|
* wants them; the sentence carries what they mean.
|
|
262
262
|
*
|
|
@@ -324,11 +324,43 @@ export function capabilityUnsupportedError(capability: string): GatewayError {
|
|
|
324
324
|
);
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
-
|
|
327
|
+
/**
|
|
328
|
+
* Every candidate in the plan refused admission: each account is already
|
|
329
|
+
* serving its configured max-inflight.
|
|
330
|
+
*
|
|
331
|
+
* Deliberately NOT `capacity_exhausted`. That code also belongs to the API-key
|
|
332
|
+
* admission layer (`security/admission`, reason `concurrency-limit`), which
|
|
333
|
+
* enforces a key's own `max_concurrent`. Sharing one code made the two
|
|
334
|
+
* indistinguishable on the wire: an operator who raised the provider's
|
|
335
|
+
* max-inflight saw no change because the rejection was the *key's* ceiling, and
|
|
336
|
+
* vice versa. Distinct codes let each be read off the envelope alone.
|
|
337
|
+
*
|
|
338
|
+
* The counts are carried in `details` because the interesting question is never
|
|
339
|
+
* "was it rejected" but "was every account really full, or did the plan only
|
|
340
|
+
* carry one candidate" — the latter is a routing/eligibility problem that
|
|
341
|
+
* raising the ceiling will not fix.
|
|
342
|
+
*/
|
|
343
|
+
export function capacityExhaustedError(context?: {
|
|
344
|
+
readonly providerId?: string;
|
|
345
|
+
readonly model?: string;
|
|
346
|
+
readonly candidateCount?: number;
|
|
347
|
+
readonly maxInflight?: number;
|
|
348
|
+
}): GatewayError {
|
|
349
|
+
const onlyOne = context?.candidateCount === 1;
|
|
328
350
|
return new GatewayError(
|
|
329
|
-
"
|
|
351
|
+
"provider_capacity_exhausted",
|
|
330
352
|
429,
|
|
331
|
-
|
|
353
|
+
onlyOne
|
|
354
|
+
? "The only account routing this model is at its concurrency limit. Retry shortly; if it persists, raise the provider's max-inflight or add another account."
|
|
355
|
+
: "Every account is at its concurrency limit. Retry shortly; if it persists, raise the provider's max-inflight or add an account.",
|
|
356
|
+
{
|
|
357
|
+
...(context?.providerId !== undefined ? { providerId: context.providerId } : {}),
|
|
358
|
+
...(context?.model !== undefined ? { model: context.model } : {}),
|
|
359
|
+
...(context?.candidateCount !== undefined
|
|
360
|
+
? { candidate_count: context.candidateCount }
|
|
361
|
+
: {}),
|
|
362
|
+
...(context?.maxInflight !== undefined ? { maxInflight: context.maxInflight } : {}),
|
|
363
|
+
},
|
|
332
364
|
);
|
|
333
365
|
}
|
|
334
366
|
|
|
@@ -423,10 +455,10 @@ export class InMemoryRouteSnapshotService implements RouteSnapshotService {
|
|
|
423
455
|
*
|
|
424
456
|
* The tag is what keeps a mutation from being lost. A build reads the catalog
|
|
425
457
|
* over ten Postgres round-trips, so a dashboard write can land while one is
|
|
426
|
-
* in flight. Without the tag two things
|
|
427
|
-
* *after* the mutation
|
|
428
|
-
* that build then
|
|
429
|
-
* data plane
|
|
458
|
+
* in flight. Without the tag two things go wrong: a reader that arrives
|
|
459
|
+
* *after* the mutation is handed the still-running pre-mutation build, and
|
|
460
|
+
* that build then overwrites the cache the mutation has just cleared — so the
|
|
461
|
+
* data plane serves stale routing until some later write invalidates it
|
|
430
462
|
* again. A build may only be reused by readers at its own revision, and may
|
|
431
463
|
* only populate the cache if no mutation happened while it ran.
|
|
432
464
|
*/
|
|
@@ -491,9 +523,9 @@ export class InMemoryRouteSnapshotService implements RouteSnapshotService {
|
|
|
491
523
|
]),
|
|
492
524
|
)
|
|
493
525
|
: undefined;
|
|
494
|
-
// One literal for both routing modes
|
|
495
|
-
//
|
|
496
|
-
//
|
|
526
|
+
// One literal for both routing modes: two verbatim copies differing only by
|
|
527
|
+
// the `providerRouting` key would mean a snapshot field added to one branch
|
|
528
|
+
// silently vanishes on the other.
|
|
497
529
|
const snapshot = deepFreeze({
|
|
498
530
|
revision,
|
|
499
531
|
candidates: Object.freeze([...built.candidates]) as readonly RouteCandidate[],
|
|
@@ -508,8 +540,8 @@ export class InMemoryRouteSnapshotService implements RouteSnapshotService {
|
|
|
508
540
|
});
|
|
509
541
|
// A mutation during the build means this snapshot describes a catalog that
|
|
510
542
|
// has already been superseded. Returning it is fine — the caller asked
|
|
511
|
-
// before the mutation — but caching it would strand the data plane on
|
|
512
|
-
//
|
|
543
|
+
// before the mutation — but caching it would strand the data plane on stale
|
|
544
|
+
// routing until the next write.
|
|
513
545
|
if (revision === this.revision) this.snapshot = snapshot;
|
|
514
546
|
return snapshot;
|
|
515
547
|
}
|