cartethyia 2.5.3 → 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-hfCzi-Ux.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-DEDyKBlC.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-bsLuZmYH.js → ConsoleLogPage-BVdpfiz1.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-BVdpfiz1.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-_UWhwziJ.js → CustomizationPage-BR5HHvbi.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-BR5HHvbi.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-mUfcknqm.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-mUfcknqm.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-DDVIfZGW.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-uPiGVTTK.js +1 -0
- 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-D6pmQ86L.js → SetupPage-UB690SE2.js} +1 -1
- package/dist/dashboard/assets/SetupPage-UB690SE2.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-DWvKiuSl.js → SortableList-BLujdX7N.js} +1 -1
- package/dist/dashboard/assets/SortableList-BLujdX7N.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-CmkUrgdj.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-NHwYq95t.js → assert-C9FD_JwJ.js} +1 -1
- package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
- package/dist/dashboard/assets/{badge-6uzDDvqs.js → badge-CR72dasT.js} +1 -1
- package/dist/dashboard/assets/badge-CR72dasT.js.gz +0 -0
- package/dist/dashboard/assets/{banner-4_WNg2WZ.js → banner-CI9_eSfO.js} +1 -1
- package/dist/dashboard/assets/banner-CI9_eSfO.js.gz +0 -0
- package/dist/dashboard/assets/{card--AtFedAq.js → card-DS7CQEsO.js} +1 -1
- package/dist/dashboard/assets/card-DS7CQEsO.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-B3e7dx3N.js → cli-tools-CNqzrfdb.js} +1 -1
- package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-NreUx6c8.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/database-Dz9N3zGd.js +1 -0
- package/dist/dashboard/assets/database-Dz9N3zGd.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-B9Xvtnfj.js +1 -0
- 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-D_eM3IgG.js → github-badge-sltk1qUi.js} +1 -1
- package/dist/dashboard/assets/github-badge-sltk1qUi.js.gz +0 -0
- package/dist/dashboard/assets/{index-BydyKaDz.js → index-CmxZr14F.js} +2 -2
- package/dist/dashboard/assets/index-CmxZr14F.js.gz +0 -0
- package/dist/dashboard/assets/{inline-BcNGF8Gr.js → inline-BhY2sSnO.js} +1 -1
- package/dist/dashboard/assets/inline-BhY2sSnO.js.gz +0 -0
- package/dist/dashboard/assets/{input-BG41et9G.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-Gh_W4OR_.js → list-row-1zzZYM0B.js} +1 -1
- package/dist/dashboard/assets/list-row-1zzZYM0B.js.gz +0 -0
- package/dist/dashboard/assets/{live-BJGYRBnR.js → live-BivGfuUs.js} +1 -1
- package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-CmffjLuG.js +1 -0
- package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-BkoLM5Ba.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-D7wX2vM5.js → query-keys-BFhGpwR1.js} +1 -1
- package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
- package/dist/dashboard/assets/{routing-CcusqXA7.js → routing-CAb4F37R.js} +1 -1
- package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
- package/dist/dashboard/assets/{select-BO6h8yNv.js → select-BYR_5K2c.js} +2 -2
- package/dist/dashboard/assets/select-BYR_5K2c.js.gz +0 -0
- package/dist/dashboard/assets/{settings-mpmXTgUa.js → settings-DNX9sydi.js} +1 -1
- package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
- package/dist/dashboard/assets/{stack-DwLmjUFK.js → stack-BHTfXdz_.js} +1 -1
- package/dist/dashboard/assets/stack-BHTfXdz_.js.gz +0 -0
- package/dist/dashboard/assets/{state-DYdQmdAl.js → state-mPDRlQkl.js} +1 -1
- package/dist/dashboard/assets/state-mPDRlQkl.js.gz +0 -0
- package/dist/dashboard/assets/switch-DOmJxg7H.js +1 -0
- package/dist/dashboard/assets/switch-DOmJxg7H.js.gz +0 -0
- package/dist/dashboard/assets/terminal-0_IjPWTk.js +1 -0
- package/dist/dashboard/assets/terminal-0_IjPWTk.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-w4u4CVue.js → useMutation-Dem0lXQj.js} +1 -1
- package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
- package/dist/dashboard/assets/zap-C4EeVW4q.js +1 -0
- package/dist/dashboard/assets/zap-C4EeVW4q.js.gz +0 -0
- package/dist/dashboard/donation/qris-risun-marke.png +0 -0
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +57 -57
- 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/autostart.ts +237 -0
- package/scripts/cli/{ui/background.ts → background.ts} +1 -1
- package/scripts/cli/{command/console-menu.ts → console-menu.ts} +275 -248
- 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 +88 -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/update.ts +40 -0
- package/scripts/cli/version.ts +67 -0
- package/scripts/cli/{ui/wordmark.ts → wordmark.ts} +58 -58
- package/src/boot/app.ts +11 -7
- package/src/boot/dependencies.ts +50 -19
- 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 +52 -7
- package/src/config/telemetry.ts +26 -0
- 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/bucket-spend.ts +15 -4
- 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/contracts.ts +15 -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/console-router.ts +8 -0
- package/src/console/data-management/contracts.ts +100 -0
- package/src/console/data-management/operations.ts +234 -0
- package/src/console/data-management/routes.ts +103 -0
- package/src/console/domain-registration.ts +45 -1
- package/src/console/observability/contracts.ts +524 -485
- package/src/console/observability/live.ts +22 -0
- package/src/console/observability/store.ts +1138 -1018
- package/src/console/observability/usage-dimensions.ts +4 -4
- package/src/console/providers/catalog/catalog-projections.ts +6 -7
- package/src/console/providers/catalog/contracts.ts +6 -0
- package/src/console/providers/catalog/model-operations.ts +5 -2
- package/src/console/providers/catalog/provider-operations.ts +24 -4
- package/src/console/providers/catalog/public-model-store.ts +8 -11
- package/src/console/providers/catalog/routes.ts +7 -0
- package/src/console/providers/catalog/store.ts +1553 -1532
- package/src/console/providers/oauth/routes.ts +8 -12
- package/src/console/quota/account-quota-shared.ts +52 -39
- package/src/console/quota/account-quota-tenant.ts +6 -6
- package/src/console/quota/quota-view.ts +1 -1
- package/src/console/quota/refresh.ts +441 -429
- 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/import-parse.ts +206 -0
- package/src/console/routing/pools/operations.ts +543 -501
- 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/routes.ts +181 -164
- package/src/console/routing/pools/store.ts +8 -8
- package/src/console/settings/contracts.ts +95 -2
- package/src/console/settings/store.ts +81 -3
- package/src/console/shared/cursor.ts +8 -8
- package/src/console/shared/errors.ts +1 -3
- package/src/console/updates/routes.ts +16 -0
- package/src/main.ts +10 -0
- 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 +317 -0
- package/src/observability/event-day-index.ts +440 -0
- package/src/observability/event-log.ts +155 -6
- package/src/observability/logger.ts +6 -6
- package/src/observability/payload-capture.ts +158 -33
- package/src/observability/payload-store.ts +82 -61
- 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/health-event-flush.ts +36 -0
- package/src/persistence/readiness.ts +1 -1
- package/src/persistence/schema.ts +75 -29
- 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 +33 -11
- package/src/protocol/canonical-model.ts +11 -3
- package/src/protocol/codecs.ts +139 -6
- package/src/protocol/gateway-error.ts +6 -4
- package/src/protocol/image-clamp.ts +19 -1
- package/src/protocol/registry.ts +8 -0
- package/src/protocol/request/chat.ts +21 -1
- package/src/protocol/request/codex.ts +10 -1
- package/src/protocol/request/gemini.ts +9 -2
- package/src/protocol/request/messages.ts +18 -7
- 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/adapters/compatible-adapter.ts +3 -0
- package/src/providers/authentication/jwt-validator.ts +9 -10
- package/src/providers/authentication/oauth-refresh-service.ts +9 -11
- package/src/providers/bundled-discovery-providers.ts +122 -119
- package/src/providers/configured-openai-providers.ts +4 -4
- package/src/providers/default-registry.ts +5 -4
- 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/openai-model-discovery.ts +5 -0
- package/src/providers/discovery/openrouter-free-roster.ts +15 -17
- 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 +27 -20
- package/src/providers/integrations/cline/cline.ts +537 -539
- package/src/providers/integrations/codex/codex-headers.ts +25 -39
- package/src/providers/integrations/codex/codex-oauth.ts +1 -1
- package/src/providers/integrations/codex/codex.ts +2 -29
- 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 -0
- package/src/providers/integrations/cursor/cursor-request.ts +616 -471
- package/src/providers/integrations/cursor/cursor-session-manager.ts +319 -281
- package/src/providers/integrations/cursor/cursor.ts +1333 -960
- package/src/providers/integrations/cursor/protocol/messages.ts +25 -0
- package/src/providers/integrations/factory/factory-models.ts +3 -5
- package/src/providers/integrations/factory/factory.ts +8 -1
- package/src/providers/integrations/gemini.ts +6 -1
- package/src/providers/integrations/grok/grok-install-id.ts +2 -3
- package/src/providers/integrations/grok/grok-quota.ts +279 -177
- package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -0
- package/src/providers/integrations/grok/grok-ratelimit.ts +218 -0
- 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 +19 -20
- 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-quota.ts +192 -0
- 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-quota.ts +384 -0
- 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 +75 -27
- 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-contracts.ts +7 -0
- package/src/providers/provider-metadata.ts +10 -8
- package/src/providers/provider-registry.ts +6 -7
- package/src/providers/provider-usage.ts +1 -1
- package/src/providers/quota/quota-support.ts +8 -0
- 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 +27 -17
- 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/generation.ts +6 -0
- package/src/transport/dispatch/proxy-request.ts +24 -1
- package/src/transport/dispatch/ratelimit-capture.ts +37 -0
- 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 +19 -6
- package/src/transport/dispatch/upstream.ts +9 -0
- 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 +9 -8
- package/src/transport/request/state.ts +21 -8
- package/src/transport/request/tenant-preferences.ts +60 -0
- package/src/transport/routing/admission.ts +4 -0
- package/src/transport/routing/route-catalog.ts +65 -10
- 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-hfCzi-Ux.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-DLGqYEwI.js +0 -1
- package/dist/dashboard/assets/CliToolDetailPage-DLGqYEwI.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-DEDyKBlC.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-CfuYiQ8Z.js +0 -1
- package/dist/dashboard/assets/CombosPage-CfuYiQ8Z.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-bsLuZmYH.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-_UWhwziJ.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-Cjfc5WFS.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-Cjfc5WFS.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-DDVIfZGW.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js +0 -1
- package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-B53YYMng.js +0 -1
- package/dist/dashboard/assets/OverviewPage-B53YYMng.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-B0KaAQf0.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage-B0KaAQf0.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-Dx31DgIO.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-Dx31DgIO.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-BpnR4kXT.js +0 -7
- package/dist/dashboard/assets/ProxyPage-BpnR4kXT.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BpOQCnun.js +0 -1
- package/dist/dashboard/assets/QuotaPage-BpOQCnun.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-DV1eo2J_.js +0 -1
- package/dist/dashboard/assets/SettingsPage-DV1eo2J_.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-D6pmQ86L.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-DWvKiuSl.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-CmkUrgdj.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-B47uOydr.js +0 -56
- package/dist/dashboard/assets/UsagePage-B47uOydr.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-uNywYlfv.js +0 -1
- package/dist/dashboard/assets/api-keys-uNywYlfv.js.gz +0 -0
- package/dist/dashboard/assets/app-DxivpfYG.css +0 -2
- package/dist/dashboard/assets/app-DxivpfYG.css.gz +0 -0
- package/dist/dashboard/assets/assert-NHwYq95t.js.gz +0 -0
- package/dist/dashboard/assets/badge-6uzDDvqs.js.gz +0 -0
- package/dist/dashboard/assets/banner-4_WNg2WZ.js.gz +0 -0
- package/dist/dashboard/assets/card--AtFedAq.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-B3e7dx3N.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-NreUx6c8.js.gz +0 -0
- package/dist/dashboard/assets/dialog-BtpsJU98.js +0 -1
- package/dist/dashboard/assets/dialog-BtpsJU98.js.gz +0 -0
- package/dist/dashboard/assets/download-A0SW9OCW.js +0 -1
- package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
- package/dist/dashboard/assets/drawer-WwwtzihF.js +0 -1
- package/dist/dashboard/assets/drawer-WwwtzihF.js.gz +0 -0
- package/dist/dashboard/assets/entry-CkryrIea.js +0 -4
- package/dist/dashboard/assets/entry-CkryrIea.js.gz +0 -0
- package/dist/dashboard/assets/entry-TJIye_jT.js +0 -1
- package/dist/dashboard/assets/entry-TJIye_jT.js.gz +0 -0
- package/dist/dashboard/assets/entry-jy9_4-Ke.js +0 -1
- package/dist/dashboard/assets/entry-jy9_4-Ke.js.gz +0 -0
- package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-D_eM3IgG.js.gz +0 -0
- package/dist/dashboard/assets/index-BydyKaDz.js.gz +0 -0
- package/dist/dashboard/assets/inline-BcNGF8Gr.js.gz +0 -0
- package/dist/dashboard/assets/input-BG41et9G.js.gz +0 -0
- package/dist/dashboard/assets/layout-C8mer_Ak.js +0 -1
- package/dist/dashboard/assets/layout-C8mer_Ak.js.gz +0 -0
- package/dist/dashboard/assets/list-row-Gh_W4OR_.js.gz +0 -0
- package/dist/dashboard/assets/live-BJGYRBnR.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-DeVLDA7I.js +0 -1
- package/dist/dashboard/assets/model-bans-DeVLDA7I.js.gz +0 -0
- package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
- package/dist/dashboard/assets/numbers-Bs4LOOuf.js +0 -2
- package/dist/dashboard/assets/numbers-Bs4LOOuf.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-D7wX2vM5.js.gz +0 -0
- package/dist/dashboard/assets/routing-CcusqXA7.js.gz +0 -0
- package/dist/dashboard/assets/select-BO6h8yNv.js.gz +0 -0
- package/dist/dashboard/assets/settings-mpmXTgUa.js.gz +0 -0
- package/dist/dashboard/assets/sparkles-BRaRUxAU.js +0 -1
- package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
- package/dist/dashboard/assets/stack-DwLmjUFK.js.gz +0 -0
- package/dist/dashboard/assets/state-DYdQmdAl.js.gz +0 -0
- package/dist/dashboard/assets/switch-CidSfbG7.js +0 -1
- package/dist/dashboard/assets/switch-CidSfbG7.js.gz +0 -0
- package/dist/dashboard/assets/terminal-Bd4ee8TA.js +0 -1
- package/dist/dashboard/assets/terminal-Bd4ee8TA.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-w4u4CVue.js.gz +0 -0
- package/scripts/cli/command/admin.ts +0 -65
- package/scripts/cli/command/doctor.ts +0 -257
- package/scripts/cli/command/update.ts +0 -23
- 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-CafWtVXA.js → app-BIt446yE.js} +0 -0
- /package/dist/dashboard/assets/{app-CafWtVXA.js.gz → app-BIt446yE.js.gz} +0 -0
- /package/scripts/cli/{ui/prompt.ts → prompt.ts} +0 -0
|
@@ -1,210 +1,211 @@
|
|
|
1
|
-
import type { RedisClient } from "../../persistence/redis";
|
|
2
|
-
import { redisEvalNumber } from "../../persistence/redis";
|
|
3
|
-
import type { AdmissionCounterStore, AdmissionReserveRequest } from "./contracts";
|
|
4
|
-
import { assertResult, RECONCILE_SCRIPT, RELEASE_SCRIPT, RESERVE_SCRIPT } from "./lua";
|
|
5
|
-
import {
|
|
6
|
-
DAILY_COUNTER_TTL_SECONDS,
|
|
7
|
-
LEASE_KEY_TTL_SECONDS,
|
|
8
|
-
LEASE_TTL_MS,
|
|
9
|
-
MONTHLY_COUNTER_TTL_SECONDS,
|
|
10
|
-
dailyBucket,
|
|
11
|
-
monthlyBucket,
|
|
12
|
-
} from "./ttl";
|
|
13
|
-
|
|
14
|
-
export class RedisAdmissionCounterStore implements AdmissionCounterStore {
|
|
15
|
-
constructor(private readonly redis: RedisClient) {}
|
|
16
|
-
async reserve(request: AdmissionReserveRequest): Promise<void> {
|
|
17
|
-
const id = request.reservationId ?? `${request.apiKeyId}:${request.now}:${crypto.randomUUID()}`;
|
|
18
|
-
const lifetimeKey = `admission:lifetime:${request.apiKeyId}`;
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
`admission:
|
|
23
|
-
`admission:
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
`admission:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
//
|
|
30
|
-
// it
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
String(request.
|
|
51
|
-
|
|
52
|
-
request.
|
|
53
|
-
request.
|
|
54
|
-
request.
|
|
55
|
-
request.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
request.
|
|
60
|
-
request.
|
|
61
|
-
|
|
62
|
-
String(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
* NX
|
|
102
|
-
*
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
readonly
|
|
107
|
-
readonly
|
|
108
|
-
readonly
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
"
|
|
153
|
-
"
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
1
|
+
import type { RedisClient } from "../../persistence/redis";
|
|
2
|
+
import { redisEvalNumber } from "../../persistence/redis";
|
|
3
|
+
import type { AdmissionCounterStore, AdmissionReserveRequest } from "./contracts";
|
|
4
|
+
import { assertResult, RECONCILE_SCRIPT, RELEASE_SCRIPT, RESERVE_SCRIPT } from "./lua";
|
|
5
|
+
import {
|
|
6
|
+
DAILY_COUNTER_TTL_SECONDS,
|
|
7
|
+
LEASE_KEY_TTL_SECONDS,
|
|
8
|
+
LEASE_TTL_MS,
|
|
9
|
+
MONTHLY_COUNTER_TTL_SECONDS,
|
|
10
|
+
dailyBucket,
|
|
11
|
+
monthlyBucket,
|
|
12
|
+
} from "./ttl";
|
|
13
|
+
|
|
14
|
+
export class RedisAdmissionCounterStore implements AdmissionCounterStore {
|
|
15
|
+
constructor(private readonly redis: RedisClient) {}
|
|
16
|
+
async reserve(request: AdmissionReserveRequest): Promise<void> {
|
|
17
|
+
const id = request.reservationId ?? `${request.apiKeyId}:${request.now}:${crypto.randomUUID()}`;
|
|
18
|
+
const lifetimeKey = `admission:lifetime:${request.apiKeyId}`;
|
|
19
|
+
const rpmConcId = request.rpmConcurrencyKeyId ?? request.apiKeyId;
|
|
20
|
+
const keys = [
|
|
21
|
+
`admission:rpm:${rpmConcId}`,
|
|
22
|
+
`admission:daily:${request.apiKeyId}:${dailyBucket(request.now)}`,
|
|
23
|
+
`admission:monthly:${request.apiKeyId}:${monthlyBucket(request.now)}`,
|
|
24
|
+
`admission:concurrent:${rpmConcId}`,
|
|
25
|
+
lifetimeKey,
|
|
26
|
+
`admission:lease:${id}`,
|
|
27
|
+
`admission:tenant_concurrent:${request.tenantId}`,
|
|
28
|
+
];
|
|
29
|
+
// The lifetime seed is the one write the Lua script cannot re-issue: once
|
|
30
|
+
// the counter exists it never re-reads Postgres. If we are about to create
|
|
31
|
+
// it, prefer a fresh persisted read over the ≤3s-stale snapshot value,
|
|
32
|
+
// otherwise a snapshot that missed a recent commit freezes a low baseline
|
|
33
|
+
// for the counter's whole 35-day TTL. `EXISTS` is a single extra command
|
|
34
|
+
// and runs only for keys that actually have a lifetime budget.
|
|
35
|
+
let lifetimeSeed = request.lifetimeConsumed;
|
|
36
|
+
if (request.lifetimeBudget != null && request.freshLifetimeConsumed) {
|
|
37
|
+
const exists = await this.redis.exists(lifetimeKey).catch(() => 1);
|
|
38
|
+
if (exists === 0) {
|
|
39
|
+
const fresh = await request.freshLifetimeConsumed().catch(() => undefined);
|
|
40
|
+
if (typeof fresh === "number" && Number.isFinite(fresh) && fresh >= 0) {
|
|
41
|
+
lifetimeSeed = Math.floor(fresh);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const result = await redisEvalNumber(
|
|
46
|
+
this.redis,
|
|
47
|
+
RESERVE_SCRIPT,
|
|
48
|
+
keys.length,
|
|
49
|
+
...keys,
|
|
50
|
+
String(request.now),
|
|
51
|
+
String(request.estimatedTokens),
|
|
52
|
+
request.rpmLimit == null ? "" : String(request.rpmLimit),
|
|
53
|
+
request.dailyLimit == null ? "" : String(request.dailyLimit),
|
|
54
|
+
request.monthlyLimit == null ? "" : String(request.monthlyLimit),
|
|
55
|
+
request.lifetimeBudget == null ? "" : String(request.lifetimeBudget),
|
|
56
|
+
request.concurrencyLimit == null ? "" : String(request.concurrencyLimit),
|
|
57
|
+
id,
|
|
58
|
+
String(lifetimeSeed),
|
|
59
|
+
request.tenantConcurrencyLimit == null ? "" : String(request.tenantConcurrencyLimit),
|
|
60
|
+
request.tenantId,
|
|
61
|
+
request.apiKeyId,
|
|
62
|
+
String(LEASE_TTL_MS),
|
|
63
|
+
String(LEASE_KEY_TTL_SECONDS),
|
|
64
|
+
);
|
|
65
|
+
assertResult(result, "reserve");
|
|
66
|
+
}
|
|
67
|
+
async reconcile(
|
|
68
|
+
apiKeyId: string,
|
|
69
|
+
_reserved: number,
|
|
70
|
+
actual: number,
|
|
71
|
+
reservationId?: string,
|
|
72
|
+
): Promise<void> {
|
|
73
|
+
if (!reservationId) throw new Error("reservation id required for atomic reconcile");
|
|
74
|
+
const result = await redisEvalNumber(
|
|
75
|
+
this.redis,
|
|
76
|
+
RECONCILE_SCRIPT,
|
|
77
|
+
1,
|
|
78
|
+
`admission:lease:${reservationId}`,
|
|
79
|
+
String(actual),
|
|
80
|
+
apiKeyId,
|
|
81
|
+
);
|
|
82
|
+
assertResult(result, "reconcile");
|
|
83
|
+
}
|
|
84
|
+
async release(
|
|
85
|
+
apiKeyId: string,
|
|
86
|
+
_reserved: number,
|
|
87
|
+
reservationId?: string,
|
|
88
|
+
): Promise<void> {
|
|
89
|
+
if (!reservationId) throw new Error("reservation id required for atomic release");
|
|
90
|
+
const result = await redisEvalNumber(
|
|
91
|
+
this.redis,
|
|
92
|
+
RELEASE_SCRIPT,
|
|
93
|
+
1,
|
|
94
|
+
`admission:lease:${reservationId}`,
|
|
95
|
+
apiKeyId,
|
|
96
|
+
);
|
|
97
|
+
assertResult(result, "release");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* `SET NX`-seeds the current bucket's counters from already-recorded usage.
|
|
102
|
+
* NX is what makes this safe: an existing counter is the live running total,
|
|
103
|
+
* and overwriting it would erase spend. Best-effort by contract.
|
|
104
|
+
*/
|
|
105
|
+
async seedBuckets(request: {
|
|
106
|
+
readonly apiKeyId: string;
|
|
107
|
+
readonly now: number;
|
|
108
|
+
readonly daily?: number;
|
|
109
|
+
readonly monthly?: number;
|
|
110
|
+
}): Promise<void> {
|
|
111
|
+
try {
|
|
112
|
+
const operations: Promise<unknown>[] = [];
|
|
113
|
+
if (request.daily !== undefined && request.daily > 0) {
|
|
114
|
+
operations.push(
|
|
115
|
+
this.redis.set(
|
|
116
|
+
`admission:daily:${request.apiKeyId}:${dailyBucket(request.now)}`,
|
|
117
|
+
Math.floor(request.daily),
|
|
118
|
+
"EX",
|
|
119
|
+
DAILY_COUNTER_TTL_SECONDS,
|
|
120
|
+
"NX",
|
|
121
|
+
),
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
if (request.monthly !== undefined && request.monthly > 0) {
|
|
125
|
+
operations.push(
|
|
126
|
+
this.redis.set(
|
|
127
|
+
`admission:monthly:${request.apiKeyId}:${monthlyBucket(request.now)}`,
|
|
128
|
+
Math.floor(request.monthly),
|
|
129
|
+
"EX",
|
|
130
|
+
MONTHLY_COUNTER_TTL_SECONDS,
|
|
131
|
+
"NX",
|
|
132
|
+
),
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
await Promise.all(operations);
|
|
136
|
+
} catch {
|
|
137
|
+
// Seeding is an optimisation, not a correctness gate.
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Drops every counter for one key (revocation). Active in-flight leases
|
|
143
|
+
* settle through the release script first so their concurrency slots do
|
|
144
|
+
* not leak; remaining lease rows expire via TTL. Best-effort by contract.
|
|
145
|
+
*/
|
|
146
|
+
async purge(apiKeyId: string): Promise<void> {
|
|
147
|
+
try {
|
|
148
|
+
let cursor = "0";
|
|
149
|
+
do {
|
|
150
|
+
const [nextCursor, keys] = await this.redis.scan(
|
|
151
|
+
cursor,
|
|
152
|
+
"MATCH",
|
|
153
|
+
"admission:lease:*",
|
|
154
|
+
"COUNT",
|
|
155
|
+
100,
|
|
156
|
+
);
|
|
157
|
+
cursor = String(nextCursor);
|
|
158
|
+
const leaseKeys = keys as string[];
|
|
159
|
+
if (leaseKeys.length === 0) continue;
|
|
160
|
+
// One pipeline for the whole scan page instead of an HGET per lease:
|
|
161
|
+
// the owner is a hash field, not part of the key, so the page has to be
|
|
162
|
+
// read to find this key's leases — but it can be read in one round trip.
|
|
163
|
+
// The key format stays `admission:lease:<reservationId>` (the id is not
|
|
164
|
+
// always derived from the key id) so leases written before a deploy are
|
|
165
|
+
// still found.
|
|
166
|
+
const pipeline = this.redis.pipeline();
|
|
167
|
+
for (const leaseKey of leaseKeys) pipeline.hget(leaseKey, "api_key_id");
|
|
168
|
+
const owners = await pipeline.exec().catch(() => null);
|
|
169
|
+
if (owners === null) continue;
|
|
170
|
+
const owned = leaseKeys.filter((_, index) => {
|
|
171
|
+
const entry = owners[index];
|
|
172
|
+
return Array.isArray(entry) && entry[1] === apiKeyId;
|
|
173
|
+
});
|
|
174
|
+
for (const leaseKey of owned) {
|
|
175
|
+
await redisEvalNumber(this.redis, RELEASE_SCRIPT, 1, leaseKey, apiKeyId).catch(
|
|
176
|
+
() => -1,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
} while (cursor !== "0");
|
|
180
|
+
let counterCursor = "0";
|
|
181
|
+
const patterns = [`admission:*:${apiKeyId}`, `admission:*:${apiKeyId}:*`];
|
|
182
|
+
for (const pattern of patterns) {
|
|
183
|
+
do {
|
|
184
|
+
const [nextCursor, keys] = await this.redis.scan(
|
|
185
|
+
counterCursor,
|
|
186
|
+
"MATCH",
|
|
187
|
+
pattern,
|
|
188
|
+
"COUNT",
|
|
189
|
+
100,
|
|
190
|
+
);
|
|
191
|
+
counterCursor = String(nextCursor);
|
|
192
|
+
if ((keys as string[]).length > 0) await this.redis.del(...(keys as string[]));
|
|
193
|
+
} while (counterCursor !== "0");
|
|
194
|
+
}
|
|
195
|
+
} catch {
|
|
196
|
+
// Best-effort: revocation already succeeded; stale counters expire via TTL.
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Releases a lease identified only by its Redis key (used by the sweeper).
|
|
203
|
+
* Reads the owning api key id from the lease hash and runs RELEASE_SCRIPT,
|
|
204
|
+
* which also releases the per-tenant concurrency slot when present.
|
|
205
|
+
*/
|
|
206
|
+
export async function releaseAdmissionLease(redis: RedisClient, leaseKey: string): Promise<void> {
|
|
207
|
+
const apiKeyId = await redis.hget(leaseKey, "api_key_id");
|
|
208
|
+
if (!apiKeyId) throw new Error("admission lease missing api_key_id");
|
|
209
|
+
const result = await redisEvalNumber(redis, RELEASE_SCRIPT, 1, leaseKey, apiKeyId);
|
|
210
|
+
assertResult(result, "release");
|
|
211
|
+
}
|
|
@@ -101,6 +101,10 @@ export class ApiKeyAdmissionService {
|
|
|
101
101
|
await this.store.reserve({
|
|
102
102
|
reservationId,
|
|
103
103
|
apiKeyId,
|
|
104
|
+
// rpm and concurrency are tracked per child key so each share recipient
|
|
105
|
+
// has its own independent window and slot. Token budgets (daily/monthly/
|
|
106
|
+
// lifetime) remain on `apiKeyId` (the parent) — the shared pool.
|
|
107
|
+
...(snapshot.api_key_id !== apiKeyId ? { rpmConcurrencyKeyId: snapshot.api_key_id } : {}),
|
|
104
108
|
now,
|
|
105
109
|
estimatedTokens,
|
|
106
110
|
rpmLimit: snapshot.rpm ?? null,
|
|
@@ -69,7 +69,7 @@ export class ApiKeyStoreUnavailableError extends Error {
|
|
|
69
69
|
* Injected rather than imported: this module is on the auth path and must not
|
|
70
70
|
* depend on the persistence/write layer it would otherwise pull in. Set once at
|
|
71
71
|
* boot (`boot/dependencies.ts`); until then it reads as zero, which is the
|
|
72
|
-
* value the code
|
|
72
|
+
* value the code uses when no source is installed.
|
|
73
73
|
*/
|
|
74
74
|
let pendingUsage: (apiKeyId: string) => number = () => 0;
|
|
75
75
|
|
|
@@ -85,11 +85,11 @@ export function setPendingUsageSource(source: (apiKeyId: string) => number): voi
|
|
|
85
85
|
pendingUsage = source;
|
|
86
86
|
}
|
|
87
87
|
/**
|
|
88
|
-
* Negative-result fast path.
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
88
|
+
* Negative-result fast path. A repeat miss for an unknown/revoked token within
|
|
89
|
+
* 1.5 s costs one HMAC + one map lookup and no query. Keyed by the full HMAC: a
|
|
90
|
+
* cheap pre-hash would let a crafted collision reject someone else's valid key,
|
|
91
|
+
* while the HMAC key can only match its own token. Bound below beside the
|
|
92
|
+
* positive cache.
|
|
93
93
|
*/
|
|
94
94
|
|
|
95
95
|
/**
|
|
@@ -136,8 +136,8 @@ function evictAuthNegativeOldest(): void {
|
|
|
136
136
|
* mutation that changes a key's scopes, limits, or revoked state.
|
|
137
137
|
*/
|
|
138
138
|
export function invalidateApiKeyCache(keyId?: string): void {
|
|
139
|
-
// A mutation may mint the
|
|
140
|
-
// must go with the positive ones.
|
|
139
|
+
// A mutation may mint a token the negative cache has already rejected, so
|
|
140
|
+
// negative entries must go with the positive ones.
|
|
141
141
|
authNegativeCache.clear();
|
|
142
142
|
if (keyId === undefined) {
|
|
143
143
|
authCache.clear();
|
|
@@ -314,13 +314,23 @@ export interface ResolvedApiKey {
|
|
|
314
314
|
* surface it hit. Unknown, revoked and expired stay deliberately
|
|
315
315
|
* indistinguishable — distinguishing them would let a caller confirm that a
|
|
316
316
|
* guessed token once existed — so there is no variant carrying a reason.
|
|
317
|
-
*
|
|
318
|
-
* The rotate instruction is unconditional for the same reason. The key that
|
|
319
|
-
* fails is usually the one seeded at first boot, which cannot be deleted and
|
|
320
|
-
* only rotates, and an operator hitting this on a personal key loses nothing by
|
|
321
|
-
* being told the same thing.
|
|
322
317
|
*/
|
|
318
|
+
const INVALID_KEY_MESSAGE = "Invalid API key. Check the key you are sending, or create a new one in API Keys and send it as: Authorization: Bearer <key>.";
|
|
319
|
+
|
|
323
320
|
export function unknownApiKeyError(): GatewayError {
|
|
321
|
+
return new GatewayError("authentication_failed", 401, INVALID_KEY_MESSAGE);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* The 401 for the public model surface specifically.
|
|
326
|
+
*
|
|
327
|
+
* Model Studio is where the seeded default key is normally used, and that key
|
|
328
|
+
* cannot be deleted — it only rotates. Naming rotation there is actionable. The
|
|
329
|
+
* same sentence on a proxy request was misleading instead: a revoked or mistyped
|
|
330
|
+
* personal key was answered with an instruction that could not apply to it,
|
|
331
|
+
* sending the operator to rotate a key they were never using.
|
|
332
|
+
*/
|
|
333
|
+
export function modelsListApiKeyError(): GatewayError {
|
|
324
334
|
return new GatewayError(
|
|
325
335
|
"authentication_failed",
|
|
326
336
|
401,
|
|
@@ -430,9 +440,9 @@ export async function resolveApiKeyAuthorization(
|
|
|
430
440
|
row.parentKeyId !== null ? await store.findActiveById(row.parentKeyId) : undefined;
|
|
431
441
|
// Share children authenticate as themselves but inherit the template's live
|
|
432
442
|
if (row.parentKeyId !== null && parent === undefined) return undefined;
|
|
433
|
-
// policy. Copy-on-issue alone
|
|
434
|
-
// limit or
|
|
435
|
-
// the
|
|
443
|
+
// policy. Copy-on-issue alone goes stale the moment an operator tightens a
|
|
444
|
+
// limit or blocks a client router on the parent — existing recipients would
|
|
445
|
+
// keep the issued row values forever. Reading the parent here makes denylist /
|
|
436
446
|
// one-time / recurring edits apply on the next cache miss.
|
|
437
447
|
const policy = parent ?? row;
|
|
438
448
|
// Only a `share` parent can have children, and children are the only thing
|
|
@@ -488,7 +498,7 @@ export async function resolveApiKeyAuthorization(
|
|
|
488
498
|
// That figure moves constantly, and the module already documents the snapshot
|
|
489
499
|
// as advisory on that axis (spending is enforced by the admission counter),
|
|
490
500
|
// so a peer serving it stale for up to the TTL is the same tradeoff the
|
|
491
|
-
// in-process cache
|
|
501
|
+
// in-process cache makes.
|
|
492
502
|
void writeShared("auth", hash, JSON.stringify(value), AUTH_CACHE_TTL_MS);
|
|
493
503
|
return value;
|
|
494
504
|
}
|