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
|
@@ -100,23 +100,36 @@ 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 };
|
|
124
|
+
/**
|
|
125
|
+
* This request's resolved inline-attachment ceiling, memoized for the same
|
|
126
|
+
* reason the capture preference is: it is a tenant setting, so it cannot
|
|
127
|
+
* change between attempts, and reading it per attempt would pay for it
|
|
128
|
+
* several times on a request that fails over.
|
|
129
|
+
*
|
|
130
|
+
* `undefined` means the deployment default.
|
|
131
|
+
*/
|
|
132
|
+
imageClampMaxBytes?: number;
|
|
120
133
|
/**
|
|
121
134
|
* Set by the dispatch handler synchronously before returning a streaming
|
|
122
135
|
* response. `afterResponse` lifecycle hooks read it to skip finalization
|
|
@@ -13,6 +13,66 @@ import type { PreparedProxyRequest } from "./preparer";
|
|
|
13
13
|
import { normalizeThinkingConfig } from "../translation/thinking";
|
|
14
14
|
import { compressRequest } from "./rtk/compress-request";
|
|
15
15
|
import { preferencesReaderFor } from "../../persistence/tenant-preferences";
|
|
16
|
+
import { resolveUpstreamTimeoutMs } from "../../config/timeouts";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Re-arms the request deadline from the tenant's timeout override.
|
|
20
|
+
*
|
|
21
|
+
* The initial deadline is armed before authentication, when the tenant is not
|
|
22
|
+
* yet known, so a per-tenant timeout can only be applied here — once the
|
|
23
|
+
* snapshot says who is asking. `extendDeadline` keeps the timer, the state
|
|
24
|
+
* field, and the in-flight record in step, so the backstop sweep cannot
|
|
25
|
+
* force-release a request whose budget legitimately grew.
|
|
26
|
+
*
|
|
27
|
+
* Non-fatal: a settings read outage leaves the deployment default in place
|
|
28
|
+
* rather than failing a dispatchable request.
|
|
29
|
+
*/
|
|
30
|
+
export async function applyTenantDeadlineOverride(
|
|
31
|
+
tenantId: string,
|
|
32
|
+
db: CartethyiaDatabase,
|
|
33
|
+
extend: (ms: number) => void,
|
|
34
|
+
): Promise<void> {
|
|
35
|
+
try {
|
|
36
|
+
const prefs = await preferencesReaderFor(db).readPreferences(tenantId);
|
|
37
|
+
const seconds = prefs?.upstreamTimeoutSeconds;
|
|
38
|
+
if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds <= 0) return;
|
|
39
|
+
const wanted = Math.floor(seconds * 1000);
|
|
40
|
+
// Only ever shorten or leave alone a deadline that has already begun: an
|
|
41
|
+
// override longer than the default cannot give back time the request has
|
|
42
|
+
// already spent, and arming a longer one would silently extend a budget
|
|
43
|
+
// the caller's own client has likely abandoned.
|
|
44
|
+
const remaining = Math.min(wanted, resolveUpstreamTimeoutMs());
|
|
45
|
+
extend(remaining);
|
|
46
|
+
} catch {
|
|
47
|
+
// Non-fatal
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The tenant's inline-attachment byte ceiling, or `undefined` for the default.
|
|
53
|
+
*
|
|
54
|
+
* Read through the shared revision-keyed preference reader, so the cost is a
|
|
55
|
+
* memory hit rather than a Postgres round trip — this is consulted while
|
|
56
|
+
* building a request body, on the dispatch path.
|
|
57
|
+
*
|
|
58
|
+
* Non-fatal: a settings read outage falls back to the deployment default,
|
|
59
|
+
* which is the safe direction (the default refuses less, but it never admits
|
|
60
|
+
* something no upstream will take).
|
|
61
|
+
*/
|
|
62
|
+
export async function resolveTenantImageClampMaxBytes(
|
|
63
|
+
db: CartethyiaDatabase,
|
|
64
|
+
tenantId: string | undefined,
|
|
65
|
+
): Promise<number | undefined> {
|
|
66
|
+
if (tenantId === undefined) return undefined;
|
|
67
|
+
try {
|
|
68
|
+
const prefs = await preferencesReaderFor(db).readPreferences(tenantId);
|
|
69
|
+
const bytes = prefs?.imageClampMaxBytes;
|
|
70
|
+
if (typeof bytes !== "number" || !Number.isFinite(bytes) || bytes <= 0) return undefined;
|
|
71
|
+
return Math.floor(bytes);
|
|
72
|
+
} catch {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
16
76
|
|
|
17
77
|
/**
|
|
18
78
|
* Applies request shaping owned by tenant preferences and deployment config:
|
|
@@ -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
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { asc, eq, ne } from "drizzle-orm";
|
|
4
4
|
import type { CartethyiaDatabase } from "../../persistence/db-handle";
|
|
5
5
|
import {
|
|
6
|
+
consoleSettings,
|
|
6
7
|
models,
|
|
7
8
|
modelAliases,
|
|
8
9
|
modelCombos,
|
|
@@ -383,7 +384,7 @@ class RouteCatalogRepository {
|
|
|
383
384
|
constructor(private readonly db: CartethyiaDatabase) {}
|
|
384
385
|
|
|
385
386
|
async loadRouteCatalogSnapshot(tenantId?: string): Promise<RouteCatalogSnapshotResult> {
|
|
386
|
-
const [providerRows, modelRows, accountRows, aliasRows, comboRows, routingRows, poolRows, disabledModelRows, cliMappingRows, cliMappingSettingRows, poolSettingRows] =
|
|
387
|
+
const [providerRows, modelRows, accountRows, aliasRows, comboRows, routingRows, poolRows, disabledModelRows, cliMappingRows, cliMappingSettingRows, poolSettingRows, settingsRows] =
|
|
387
388
|
await Promise.all([
|
|
388
389
|
this.db.select(PROVIDER_COLUMNS).from(providers).where(eq(providers.enabled, true)),
|
|
389
390
|
this.db.select(MODEL_COLUMNS).from(models),
|
|
@@ -400,8 +401,38 @@ class RouteCatalogRepository {
|
|
|
400
401
|
this.db.select(CLI_MAPPING_COLUMNS).from(cliToolMappings),
|
|
401
402
|
this.db.select(CLI_MAPPING_SETTING_COLUMNS).from(cliToolSettings),
|
|
402
403
|
this.db.select(POOL_SETTING_COLUMNS).from(poolRoutingSettings),
|
|
404
|
+
// The cooldown override is a console_settings preference, read inside
|
|
405
|
+
// the same fan-out rather than per request: one more row set on a
|
|
406
|
+
// query that already runs, instead of a Postgres round trip on every
|
|
407
|
+
// admitted request.
|
|
408
|
+
this.db
|
|
409
|
+
.select({ tenantId: consoleSettings.tenantId, preferences: consoleSettings.preferences })
|
|
410
|
+
.from(consoleSettings),
|
|
403
411
|
]);
|
|
404
412
|
const mergedModelRows = mergeModelCatalog(modelRows);
|
|
413
|
+
/**
|
|
414
|
+
* Tenants that asked to ignore an account-wide cooldown.
|
|
415
|
+
*
|
|
416
|
+
* Read-only override: the cooldown is still on the row, still shown in
|
|
417
|
+
* Health, and still recovered by the sweep. This only stops it from
|
|
418
|
+
* excluding the candidate, so turning the setting off needs no cleanup.
|
|
419
|
+
*
|
|
420
|
+
* Per-(account, model) cooldowns are deliberately NOT overridden — see the
|
|
421
|
+
* preference's own doc.
|
|
422
|
+
*/
|
|
423
|
+
const cooldownOverrideTenants = new Set<string>();
|
|
424
|
+
for (const row of settingsRows ?? []) {
|
|
425
|
+
if (row.preferences?.forceDisableCooldown === true && row.tenantId) {
|
|
426
|
+
cooldownOverrideTenants.add(row.tenantId);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* A shared account has no tenant of its own (`tenant_id IS NULL`), so a
|
|
431
|
+
* tenant-keyed override would never match it — yet a shared account is
|
|
432
|
+
* exactly the one every tenant dials, and parking it stops all of them.
|
|
433
|
+
* When any tenant asks, the override covers shared accounts too.
|
|
434
|
+
*/
|
|
435
|
+
const anyTenantOverrides = cooldownOverrideTenants.size > 0;
|
|
405
436
|
|
|
406
437
|
// Compile tenant-scoped disables into a lookup keyed by the same
|
|
407
438
|
// composite identity the `models` table uses. A disabled key suppresses
|
|
@@ -701,16 +732,40 @@ class RouteCatalogRepository {
|
|
|
701
732
|
// Expired cooldowns read as healthy here without a fire-and-forget
|
|
702
733
|
// write: AccountHealthSweeper materializes the recovery every 30s,
|
|
703
734
|
// so the builder stays a pure read (no UPDATE racing the request).
|
|
735
|
+
//
|
|
736
|
+
// `forceDisableCooldown` skips the exclusion entirely. The cooldown
|
|
737
|
+
// is still real and still recorded — it simply stops deciding
|
|
738
|
+
// routing. That is what makes the setting reversible: nothing was
|
|
739
|
+
// written, so turning it off needs no cleanup, and Health still
|
|
740
|
+
// reports the truth about the account.
|
|
704
741
|
if (account.cooldownUntil && account.cooldownUntil.getTime() > Date.now()) {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
742
|
+
const overrideActive =
|
|
743
|
+
rowTenantId === null
|
|
744
|
+
? anyTenantOverrides
|
|
745
|
+
: cooldownOverrideTenants.has(rowTenantId);
|
|
746
|
+
if (overrideActive) {
|
|
747
|
+
// The account is still cooling — this only stops it from being
|
|
748
|
+
// excluded. `plan()` reads `cooldown_until` to answer the
|
|
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.
|
|
757
|
+
candidate.cooldown_until = account.cooldownUntil.getTime();
|
|
758
|
+
} else {
|
|
759
|
+
candidate.health_status = "cooldown";
|
|
760
|
+
candidate.cooldown_kind = hardCooldownCategory(account.lastErrorCategory)
|
|
761
|
+
? "hard"
|
|
762
|
+
: "soft";
|
|
763
|
+
// The deadline rides with the exclusion it explains. `plan()`
|
|
764
|
+
// reads it to answer the all-cooling case with the real reset
|
|
765
|
+
// instead of a bare 429, so a client backs off for the window
|
|
766
|
+
// that is actually open rather than inventing one.
|
|
767
|
+
candidate.cooldown_until = account.cooldownUntil.getTime();
|
|
768
|
+
}
|
|
714
769
|
}
|
|
715
770
|
}
|
|
716
771
|
|
|
@@ -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
|
}
|
|
@@ -226,10 +226,10 @@ function spreadDeadline(cooldownUntil: number | undefined): { cooldown_until?: n
|
|
|
226
226
|
* The soonest moment a quota window the caller is blocked on refills.
|
|
227
227
|
*
|
|
228
228
|
* A usage-floor exclusion is time-bounded by construction — the floor compares
|
|
229
|
-
* the *remaining* allowance, and the window states when that allowance resets
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
229
|
+
* the *remaining* allowance, and the window states when that allowance resets.
|
|
230
|
+
* The exclusion carries a deadline so `plan()` can tell a client how long to
|
|
231
|
+
* wait and `earliestResetMs` reports a value. The minimum across windows, for
|
|
232
|
+
* the same reason `earliestResetMs` takes it: the account
|
|
233
233
|
* can serve again as soon as its first window refills, so quoting the slowest
|
|
234
234
|
* would hold the client off longer than necessary.
|
|
235
235
|
*
|
|
@@ -317,9 +317,8 @@ export class EligibilityEvaluator {
|
|
|
317
317
|
return { eligible: false, reason: "model_cooldown", candidate, ...spreadDeadline(state.cooldown_until) };
|
|
318
318
|
if (state.health_status === "cooldown" && state.cooldown_kind === "hard")
|
|
319
319
|
return { eligible: false, reason: "cooldown_hard", candidate, ...spreadDeadline(state.cooldown_until) };
|
|
320
|
-
// The floor is checked here, not after selection:
|
|
321
|
-
//
|
|
322
|
-
// its floor still won the route. An account with no reading is left
|
|
320
|
+
// The floor is checked here, not after selection: an account below its
|
|
321
|
+
// floor must not win the route. An account with no reading is left
|
|
323
322
|
// eligible — a missing quota fetch is not proof the allowance is gone.
|
|
324
323
|
if (belowUsageFloor(state))
|
|
325
324
|
return {
|
|
@@ -487,8 +486,8 @@ export class RoutingEngine {
|
|
|
487
486
|
* Stamps each account with its provider's usage floor and the quota reading
|
|
488
487
|
* the console last fetched, so eligibility can reject an account that has
|
|
489
488
|
* spent down to the floor. The reading is the live cache, not the route
|
|
490
|
-
* snapshot: a snapshot rebuilt every few minutes
|
|
491
|
-
*
|
|
489
|
+
* snapshot: a snapshot rebuilt every few minutes routes on a stale balance
|
|
490
|
+
* and lets a spent account through.
|
|
492
491
|
*/
|
|
493
492
|
private async applyUsageFloor(
|
|
494
493
|
candidates: readonly RouteCandidate[],
|
|
@@ -598,10 +597,10 @@ export class RoutingEngine {
|
|
|
598
597
|
d.reason === "model_cooldown" ||
|
|
599
598
|
// A usage-floor exclusion whose window states a reset is the same
|
|
600
599
|
// shape as the two above: the account is resting until a named
|
|
601
|
-
// instant, not broken.
|
|
602
|
-
//
|
|
603
|
-
//
|
|
604
|
-
//
|
|
600
|
+
// instant, not broken. Falling through to 503 tells the client the
|
|
601
|
+
// model is down while every account serving it is merely waiting for
|
|
602
|
+
// its window to refill — the "503 DOWN while the model is allowed"
|
|
603
|
+
// report again. Only a floor with *no* stated
|
|
605
604
|
// reset stays unavailable: there is nothing to retry against yet.
|
|
606
605
|
(d.reason === "quota_exhausted" && typeof d.cooldown_until === "number"),
|
|
607
606
|
);
|
|
@@ -828,7 +827,15 @@ export class RoutingEngine {
|
|
|
828
827
|
const admitted = await this.admission.admit(candidate);
|
|
829
828
|
if (admitted.admitted) return this.reservations.acquire(candidate, plan.revision);
|
|
830
829
|
}
|
|
831
|
-
|
|
830
|
+
// Last candidate's identity names the provider the plan was actually for;
|
|
831
|
+
// every candidate shares it, so any one of them is representative.
|
|
832
|
+
const last = plan.candidates[plan.candidates.length - 1];
|
|
833
|
+
throw capacityExhaustedError({
|
|
834
|
+
...(last?.provider_id !== undefined ? { providerId: last.provider_id } : {}),
|
|
835
|
+
...(plan.resolved_model !== undefined ? { model: plan.resolved_model } : {}),
|
|
836
|
+
candidateCount: plan.candidates.length,
|
|
837
|
+
...(last?.max_inflight !== undefined ? { maxInflight: last.max_inflight } : {}),
|
|
838
|
+
});
|
|
832
839
|
}
|
|
833
840
|
async release(reservation: Reservation): Promise<void> {
|
|
834
841
|
await this.admission.release(reservation);
|
|
@@ -45,9 +45,8 @@ function stopReason(events: readonly CanonicalEvent[]): string | null {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
|
-
* Completion wire usage. Delegates to the chat builder (same wire family)
|
|
49
|
-
* completion responses
|
|
50
|
-
* them (intended fix; previously only the three totals were forwarded).
|
|
48
|
+
* Completion wire usage. Delegates to the chat builder (same wire family), so
|
|
49
|
+
* completion responses carry cache/reasoning details, not just the three totals.
|
|
51
50
|
*/
|
|
52
51
|
function usage(events: readonly CanonicalEvent[]): OpenAiUsage | undefined {
|
|
53
52
|
const terminal = [...events].reverse().find((event) => event.type === "terminal");
|
|
@@ -79,12 +79,11 @@ function parseToolObject(item: Record<string, unknown>, dialect: ToolDialect): T
|
|
|
79
79
|
}
|
|
80
80
|
} else if (dialect === "responses") {
|
|
81
81
|
// A Responses caller may still send the Chat-shaped envelope — the two
|
|
82
|
-
// spellings are interchangeable in the wild
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
// Chat dialect does before falling back to the flat Responses spelling.
|
|
82
|
+
// spellings are interchangeable in the wild. Unwrap it the way the Chat
|
|
83
|
+
// dialect does before falling back to the flat Responses spelling; taking
|
|
84
|
+
// `schema = item` instead ships the whole tool object upstream as the
|
|
85
|
+
// parameter schema and comes back as `invalid_function_parameters`
|
|
86
|
+
// (CodeBuddy 11129).
|
|
88
87
|
const wrapped = isRecord(item["function"]) ? item["function"] : undefined;
|
|
89
88
|
schema =
|
|
90
89
|
item["input_schema"] ??
|
|
@@ -704,6 +704,44 @@ export class ResponsesEventEncoder extends SurfaceStreamEncoder<CanonicalEvent,
|
|
|
704
704
|
throw new ResponsesLifecycleError("Responses stream ended without a terminal event");
|
|
705
705
|
return [];
|
|
706
706
|
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Encodes a fatal stream error as `response.error` followed immediately by
|
|
710
|
+
* `response.failed`.
|
|
711
|
+
*
|
|
712
|
+
* The OpenAI Responses SDK (and any strict client) requires a terminal
|
|
713
|
+
* lifecycle event (`response.completed`, `response.failed`, or
|
|
714
|
+
* `response.incomplete`) to close the stream cleanly. Emitting only
|
|
715
|
+
* `response.error` and then closing the SSE connection causes the SDK to
|
|
716
|
+
* throw "stream closed before a terminal response event was received".
|
|
717
|
+
*
|
|
718
|
+
* `response.error` is kept so the client can surface the error message;
|
|
719
|
+
* `response.failed` immediately follows so the client's state machine
|
|
720
|
+
* reaches a terminal state and stops waiting.
|
|
721
|
+
*/
|
|
722
|
+
encodeError(input: {
|
|
723
|
+
readonly origin: string;
|
|
724
|
+
readonly code: string;
|
|
725
|
+
readonly message: string;
|
|
726
|
+
readonly details: unknown;
|
|
727
|
+
}): ResponsesWireEvent[] {
|
|
728
|
+
if (this.terminal) return [];
|
|
729
|
+
this.terminal = true;
|
|
730
|
+
const errorEvent = this.emit("response.error", {
|
|
731
|
+
origin: input.origin,
|
|
732
|
+
code: input.code,
|
|
733
|
+
message: input.message,
|
|
734
|
+
details: input.details,
|
|
735
|
+
});
|
|
736
|
+
const response = this.responseObject("failed");
|
|
737
|
+
response["error"] = {
|
|
738
|
+
type: "server_error",
|
|
739
|
+
code: input.code,
|
|
740
|
+
message: input.message,
|
|
741
|
+
};
|
|
742
|
+
const failedEvent = this.emit("response.failed", { response });
|
|
743
|
+
return [errorEvent, failedEvent];
|
|
744
|
+
}
|
|
707
745
|
}
|
|
708
746
|
|
|
709
747
|
/** Encode synchronous canonical events into ordered Responses wire events. */
|
|
@@ -456,6 +456,10 @@ function parseInputItem(item: ResponsesInputItem): {
|
|
|
456
456
|
encrypted_content: encrypted,
|
|
457
457
|
};
|
|
458
458
|
if (summary !== undefined) reasoningPart.summary = summary;
|
|
459
|
+
// The upstream binds encrypted reasoning to the item id it was issued
|
|
460
|
+
// for; a replay under a different id is rejected outright.
|
|
461
|
+
const itemId = typeof item["id"] === "string" ? item["id"] : undefined;
|
|
462
|
+
if (itemId !== undefined) reasoningPart.item_id = itemId;
|
|
459
463
|
return {
|
|
460
464
|
message: { role: "assistant", content: [reasoningPart] },
|
|
461
465
|
metadata: itemMetadata(item),
|
|
@@ -334,9 +334,8 @@ export function deriveRequiredCapabilities(
|
|
|
334
334
|
for (const key of Object.keys(request.generation_controls)) {
|
|
335
335
|
// `extension:*` keys are passthrough hints to the wire codec, not route
|
|
336
336
|
// capabilities: the codec reads the names it understands and drops the
|
|
337
|
-
// rest. Gating them here
|
|
338
|
-
//
|
|
339
|
-
// `metadata`, etc. and re-warning per request.
|
|
337
|
+
// rest. Gating them here would strip `include_usage`, `prompt_cache_key`,
|
|
338
|
+
// `user`, `metadata`, etc. and re-warn per request.
|
|
340
339
|
if (key.startsWith("extension:")) continue;
|
|
341
340
|
capabilities.push(`generation_control:${key}`);
|
|
342
341
|
}
|
|
@@ -180,9 +180,9 @@ export function applyParamQuirks(
|
|
|
180
180
|
const quirk = findParamQuirk(providerId, request.model);
|
|
181
181
|
// The thinking-sampling strip is universal, not quirk-gated: extended
|
|
182
182
|
// thinking rejects temperature/top_p/top_k on every Messages-wire upstream.
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
183
|
+
// It runs before the early return below so a provider with no PARAM_QUIRKS
|
|
184
|
+
// row still gets it — otherwise a strict upstream takes a 400 the strip
|
|
185
|
+
// exists to prevent. Declarative rows still apply after.
|
|
186
186
|
|
|
187
187
|
let controls = request.generation_controls;
|
|
188
188
|
let changed = false;
|
|
@@ -22,6 +22,7 @@ import { randomUUID } from "node:crypto";
|
|
|
22
22
|
import type { CartethyiaDatabase } from "../persistence/db-handle";
|
|
23
23
|
import { buddyTaskLedger, providerAccounts } from "../persistence/schema";
|
|
24
24
|
import { log } from "../observability/logger";
|
|
25
|
+
import { sleep } from "../shared/sleep";
|
|
25
26
|
import { buddyAccountUid } from "../providers/integrations/buddy/buddy-oauth-shared";
|
|
26
27
|
import {
|
|
27
28
|
adoptFirstBuddy,
|
|
@@ -207,12 +208,6 @@ async function forAccount(
|
|
|
207
208
|
return run(call);
|
|
208
209
|
}
|
|
209
210
|
|
|
210
|
-
/** Pause between accounts, so one pass does not look like a burst upstream. */
|
|
211
|
-
function sleep(ms: number): Promise<void> {
|
|
212
|
-
const { promise, resolve } = Promise.withResolvers<void>();
|
|
213
|
-
setTimeout(resolve, ms);
|
|
214
|
-
return promise;
|
|
215
|
-
}
|
|
216
211
|
/** Walks the accounts, skipping any already settled today. */
|
|
217
212
|
async function runTask(
|
|
218
213
|
deps: BuddyTaskDeps,
|
|
@@ -353,10 +348,10 @@ export function buddyTrialTask(deps: BuddyTaskDeps) {
|
|
|
353
348
|
* Growth-activity report: the `/v2/report` batch that lights the login streak and
|
|
354
349
|
* scores the daily growth event.
|
|
355
350
|
*
|
|
356
|
-
* It
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
351
|
+
* It is a once-a-day task with its own ledger, gated by the same
|
|
352
|
+
* (day, account, task) ledger as every other growth task: running it on the
|
|
353
|
+
* quota sweep would post two rounds of four events per account per sweep, and
|
|
354
|
+
* the sweep runs every minute.
|
|
360
355
|
*
|
|
361
356
|
* The report is not a credit claim, so its failure never marks an account
|
|
362
357
|
* unhealthy — the result is logged and the ledger is stamped either way.
|
|
@@ -120,9 +120,8 @@ export function ledgerKey(dayKey: string, accountId: string): string {
|
|
|
120
120
|
* With Redis the `SET NX EX` is the cross-process guard (two gateway instances
|
|
121
121
|
* cannot both attempt the same account). Without it the ledger *table* is the
|
|
122
122
|
* guard, and its composite PK makes the insert atomic — so Lite records the
|
|
123
|
-
* attempt it just made instead of silently returning `true`, which
|
|
124
|
-
*
|
|
125
|
-
* forever.
|
|
123
|
+
* attempt it just made instead of silently returning `true`, which leaves the
|
|
124
|
+
* console unable to answer "attempted today" and shows "Check-in due" forever.
|
|
126
125
|
*/
|
|
127
126
|
async function reserveDaySlot(
|
|
128
127
|
db: CartethyiaDatabase | undefined,
|
|
@@ -160,8 +159,8 @@ async function reserveDaySlot(
|
|
|
160
159
|
* Releases today's slot for many accounts in one round-trip.
|
|
161
160
|
*
|
|
162
161
|
* The sweep reserves a slot per account as it walks the wave; releasing them
|
|
163
|
-
* one at a time on the error path
|
|
164
|
-
* failed account. A pass that budgets out with twenty failures
|
|
162
|
+
* one at a time on the error path turns every failed pass into a Redis RTT per
|
|
163
|
+
* failed account. A pass that budgets out with twenty failures pays twenty
|
|
165
164
|
* round-trips for bookkeeping that has no bearing on the claim itself.
|
|
166
165
|
*
|
|
167
166
|
* A pipeline is used when Redis is present (one `MULTI/DEL` for the whole
|
|
@@ -246,8 +245,8 @@ export async function markCheckinAttempted(args: {
|
|
|
246
245
|
* The read belongs next to the writes because there are two places the marker
|
|
247
246
|
* can live — the Redis key when Redis is present, the `daily_checkin_ledger`
|
|
248
247
|
* row when it is not — and only this module knows which one its own writes
|
|
249
|
-
* used. A caller that
|
|
250
|
-
* every Lite deployment, where the sweep
|
|
248
|
+
* used. A caller that reads the Redis key directly answers "not attempted" on
|
|
249
|
+
* every Lite deployment, where the sweep writes the row.
|
|
251
250
|
*
|
|
252
251
|
* Absent or unreadable means unknown, never "not claimed": the sweep may
|
|
253
252
|
* simply not have reached the account yet.
|
|
@@ -355,9 +354,9 @@ export function supportsDailyCheckin(providerId: string): boolean {
|
|
|
355
354
|
* Wraps a proxy-bound fetcher so a 403 falls back to a direct connection once.
|
|
356
355
|
*
|
|
357
356
|
* A proxy that answers 403 (auth, expired pool credential, a provider that
|
|
358
|
-
* rejects the egress)
|
|
359
|
-
*
|
|
360
|
-
*
|
|
357
|
+
* rejects the egress) would fail the whole claim even though the account's own
|
|
358
|
+
* credential was fine — the proxy is the only thing wrong. One direct retry
|
|
359
|
+
* costs nothing when the proxy was healthy (the first attempt already
|
|
361
360
|
* succeeded) and rescues the claim when it was not.
|
|
362
361
|
*
|
|
363
362
|
* The fallback only fires on 403, not on every error: a 401 means the account
|
|
@@ -375,15 +374,10 @@ export function withDirectFallback(fetcher: typeof fetch): typeof fetch {
|
|
|
375
374
|
return wrapped;
|
|
376
375
|
}
|
|
377
376
|
/**
|
|
378
|
-
* The outcome of one daily check-in pass: the claim alone.
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
/**
|
|
383
|
-
* Runs the full daily growth pass for one account, once per calendar day: the
|
|
384
|
-
* check-in claim alone. The growth-activity report moved to its own once-a-day
|
|
385
|
-
* growth task (`buddy-activity-report`) with its own ledger, so it is not
|
|
386
|
-
* re-POSTed on every sweep. See `buddy-growth-tasks.ts`.
|
|
377
|
+
* The outcome of one daily check-in pass: the claim alone. The
|
|
378
|
+
* growth-activity report runs as its own once-a-day growth task
|
|
379
|
+
* (`buddy-activity-report`) with its own ledger, so it is not re-POSTed on
|
|
380
|
+
* every sweep. See `buddy-growth-tasks.ts`.
|
|
387
381
|
*/
|
|
388
382
|
export type DailyGrowthPassResult = {
|
|
389
383
|
readonly checkin: DailyCheckinResult;
|
|
@@ -448,10 +442,10 @@ export async function attemptDailyGrowthPass(args: {
|
|
|
448
442
|
|
|
449
443
|
// Only a transient `error` releases the slot so a later pass today can retry
|
|
450
444
|
// (network, 5xx). `unavailable` is the status probe answering that no
|
|
451
|
-
// activity is running — retrying that every pass
|
|
452
|
-
// slot on a door the provider
|
|
453
|
-
//
|
|
454
|
-
//
|
|
445
|
+
// activity is running — retrying that every pass spends the account's daily
|
|
446
|
+
// slot on a door the provider has closed, and releasing the marker also
|
|
447
|
+
// erases the evidence that anything was attempted: the console then reads
|
|
448
|
+
// "Check-in due" forever while the sweep retries once a minute.
|
|
455
449
|
// `already_claimed`, `not_eligible`, and `event_ended` are terminal for the
|
|
456
450
|
// day and keep the slot.
|
|
457
451
|
return { checkin };
|