cartethyia 2.7.2 → 2.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +2 -0
- package/dist/dashboard/assets/{BannedPage-XaUmiBrC.js → BannedPage-ChG5svFb.js} +1 -1
- package/dist/dashboard/assets/BannedPage-ChG5svFb.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js +1 -0
- package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js +1 -0
- package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-CXGS_Ct-.js → CombosPage-DCgm5g6g.js} +1 -1
- package/dist/dashboard/assets/CombosPage-DCgm5g6g.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js +1 -0
- package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js +1 -0
- package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js.gz +0 -0
- package/dist/dashboard/assets/{HealthEventsModal-BKtAPYVF.js → HealthEventsModal-DzSWzE_j.js} +1 -1
- package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-BdmRLnq2.js → LoginPage-U6AKaJcF.js} +1 -1
- package/dist/dashboard/assets/LoginPage-U6AKaJcF.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-BH8Tehdi.js → ModelPicker-BMS2t8BA.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-BMS2t8BA.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js +1 -0
- package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js +5 -0
- package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js +9 -0
- package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-L0skzML8.js +1 -0
- package/dist/dashboard/assets/QuotaPage-L0skzML8.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js +1 -0
- package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-f0DE1h0-.js +1 -0
- package/dist/dashboard/assets/SetupPage-f0DE1h0-.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-Dqf4zpS-.js → SortableList-ClzpwyET.js} +1 -1
- package/dist/dashboard/assets/SortableList-ClzpwyET.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-Byj2lAEm.js → StudioPage-Cq5tW7hW.js} +3 -3
- package/dist/dashboard/assets/StudioPage-Cq5tW7hW.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js +56 -0
- package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-C38iXoO4.js +1 -0
- package/dist/dashboard/assets/api-keys-C38iXoO4.js.gz +0 -0
- package/dist/dashboard/assets/app-a159k0ub.css +2 -0
- package/dist/dashboard/assets/app-a159k0ub.css.gz +0 -0
- package/dist/dashboard/assets/arrow-up-59ms1iiT.js +1 -0
- package/dist/dashboard/assets/arrow-up-59ms1iiT.js.gz +0 -0
- package/dist/dashboard/assets/assert-DbRA8EG2.js +15 -0
- package/dist/dashboard/assets/assert-DbRA8EG2.js.gz +0 -0
- package/dist/dashboard/assets/{badge-DC2L43Mj.js → badge-D9CS6dZk.js} +1 -1
- package/dist/dashboard/assets/badge-D9CS6dZk.js.gz +0 -0
- package/dist/dashboard/assets/{banner-BBVcb0J3.js → banner-DympBNq4.js} +1 -1
- package/dist/dashboard/assets/banner-DympBNq4.js.gz +0 -0
- package/dist/dashboard/assets/{card-Cca1XC6G.js → card-ibRWU_uT.js} +1 -1
- package/dist/dashboard/assets/card-ibRWU_uT.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-63qBGaRw.js +1 -0
- package/dist/dashboard/assets/cli-tools-63qBGaRw.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-DUzEbynq.js → clipboard-button-DJm-9pz7.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-DJm-9pz7.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-DFvWhwTa.js → dialog-8fiE6tec.js} +1 -1
- package/dist/dashboard/assets/dialog-8fiE6tec.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CqGYeOrd.js +1 -0
- package/dist/dashboard/assets/drawer-CqGYeOrd.js.gz +0 -0
- package/dist/dashboard/assets/{entry-Bqy8RFvM.js → entry-BSAsYsmV.js} +1 -1
- package/dist/dashboard/assets/entry-BSAsYsmV.js.gz +0 -0
- package/dist/dashboard/assets/entry-CiIQULZD.js +4 -0
- package/dist/dashboard/assets/entry-CiIQULZD.js.gz +0 -0
- package/dist/dashboard/assets/entry-Daz0lJ-W.js +1 -0
- package/dist/dashboard/assets/entry-Daz0lJ-W.js.gz +0 -0
- package/dist/dashboard/assets/{concurrency-BDF0ok7y.js → fingerprint-pattern-BWuHtbW4.js} +1 -1
- package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
- package/dist/dashboard/assets/{formatters-CWWZ-uCZ.js → formatters-D-MTJxMr.js} +1 -1
- package/dist/dashboard/assets/formatters-D-MTJxMr.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-C9e8Ts7e.js → github-badge-CPK47xQx.js} +1 -1
- package/dist/dashboard/assets/github-badge-CPK47xQx.js.gz +0 -0
- package/dist/dashboard/assets/{index-DFVFrnWW.js → index-DBNPOfhV.js} +2 -2
- package/dist/dashboard/assets/index-DBNPOfhV.js.gz +0 -0
- package/dist/dashboard/assets/{inline-BFUcvllj.js → inline-BT0zntEr.js} +1 -1
- package/dist/dashboard/assets/inline-BT0zntEr.js.gz +0 -0
- package/dist/dashboard/assets/{input-C_ILAvBb.js → input-Da2ZViWg.js} +1 -1
- package/dist/dashboard/assets/input-Da2ZViWg.js.gz +0 -0
- package/dist/dashboard/assets/{layout-CBIyT6Rt.js → layout-BXNeLw6M.js} +1 -1
- package/dist/dashboard/assets/layout-BXNeLw6M.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-ebJ_-Nsb.js → list-row-BycOrrIV.js} +1 -1
- package/dist/dashboard/assets/list-row-BycOrrIV.js.gz +0 -0
- package/dist/dashboard/assets/live-DCQNZpF7.js +1 -0
- package/dist/dashboard/assets/live-DCQNZpF7.js.gz +0 -0
- package/dist/dashboard/assets/minimize-2-BTmMTK4o.js +1 -0
- package/dist/dashboard/assets/minimize-2-BTmMTK4o.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-sMD965oO.js → mutation-Rr0TIvcj.js} +1 -1
- package/dist/dashboard/assets/mutation-Rr0TIvcj.js.gz +0 -0
- package/dist/dashboard/assets/network-9Yj2ZyLo.js +1 -0
- package/dist/dashboard/assets/network-9Yj2ZyLo.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-BO9dKaFr.js → numbers-CEagj6ZZ.js} +2 -2
- package/dist/dashboard/assets/numbers-CEagj6ZZ.js.gz +0 -0
- package/dist/dashboard/assets/{query-keys-CfL_Zdoh.js → query-keys-BloKtaXJ.js} +1 -1
- package/dist/dashboard/assets/query-keys-BloKtaXJ.js.gz +0 -0
- package/dist/dashboard/assets/quota-Caw7wqci.js +1 -0
- package/dist/dashboard/assets/quota-Caw7wqci.js.gz +0 -0
- package/dist/dashboard/assets/routing-CO9zCUus.js +1 -0
- package/dist/dashboard/assets/routing-CO9zCUus.js.gz +0 -0
- package/dist/dashboard/assets/{select-DBNVLdUu.js → select-BIYIBkMU.js} +2 -2
- package/dist/dashboard/assets/select-BIYIBkMU.js.gz +0 -0
- package/dist/dashboard/assets/settings-2-Ds9DQp0t.js +1 -0
- package/dist/dashboard/assets/settings-2-Ds9DQp0t.js.gz +0 -0
- package/dist/dashboard/assets/settings-jS3-PNSA.js +1 -0
- package/dist/dashboard/assets/settings-jS3-PNSA.js.gz +0 -0
- package/dist/dashboard/assets/{stack-EMrOMt4I.js → stack-DfgnFDWY.js} +1 -1
- package/dist/dashboard/assets/stack-DfgnFDWY.js.gz +0 -0
- package/dist/dashboard/assets/{state-CWoo7ty5.js → state-CkfdYWel.js} +1 -1
- package/dist/dashboard/assets/state-CkfdYWel.js.gz +0 -0
- package/dist/dashboard/assets/{switch-CCjT6IkF.js → switch-SYm237HG.js} +1 -1
- package/dist/dashboard/assets/switch-SYm237HG.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-DjkLHUcE.js → useMutation-BQqCRNIi.js} +1 -1
- package/dist/dashboard/assets/useMutation-BQqCRNIi.js.gz +0 -0
- package/dist/dashboard/index.html +3 -2
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +118 -115
- package/migrations/full-postgres/0026_add_qoder_native_identity.sql +20 -0
- package/migrations/full-postgres/0027_qoder_identity_genuine_seed.sql +21 -0
- package/migrations/full-postgres/0028_add_family_template_key_telemetry.sql +40 -0
- package/migrations/full-postgres/0029_null_share_child_policy_columns.sql +38 -0
- package/migrations/full-postgres/0030_add_api_key_max_children.sql +10 -0
- package/migrations/full-postgres/0031_tenant_system_prompts.sql +16 -0
- package/migrations/full-postgres/0032_add_provider_pool_accounts.sql +53 -0
- package/migrations/full-postgres/0033_add_api_key_ban.sql +9 -0
- package/migrations/full-postgres/0034_add_api_key_ban_until.sql +7 -0
- package/migrations/full-postgres/0035_align_pool_account_status_enum.sql +21 -0
- package/migrations/lite-sqlite3/0026_add_qoder_native_identity.sql +15 -0
- package/migrations/lite-sqlite3/0027_qoder_identity_genuine_seed.sql +21 -0
- package/migrations/lite-sqlite3/0028_add_family_template_key_telemetry.sql +40 -0
- package/migrations/lite-sqlite3/0029_null_share_child_policy_columns.sql +38 -0
- package/migrations/lite-sqlite3/0030_add_api_key_max_children.sql +10 -0
- package/migrations/lite-sqlite3/0031_tenant_system_prompts.sql +10 -0
- package/migrations/lite-sqlite3/0032_add_provider_pool_accounts.sql +44 -0
- package/migrations/lite-sqlite3/0033_add_api_key_ban.sql +8 -0
- package/migrations/lite-sqlite3/0034_add_api_key_ban_until.sql +5 -0
- package/migrations/lite-sqlite3/0035_align_pool_account_status_enum.sql +6 -0
- package/package.json +2 -1
- package/src/boot/app.ts +9 -8
- package/src/config/network.ts +7 -3
- package/src/console/api-keys/bucket-spend.ts +7 -7
- package/src/console/api-keys/contracts.ts +475 -410
- package/src/console/api-keys/routes.ts +211 -23
- package/src/console/audit/routes.ts +3 -3
- package/src/console/auth/account-reset.ts +4 -4
- package/src/console/auth/session.ts +3 -3
- package/src/console/backup/contracts.ts +2 -0
- package/src/console/backup/export-endpoint.ts +14 -17
- package/src/console/backup/routes.ts +43 -11
- package/src/console/backup/service.ts +11 -6
- package/src/console/backup/store.ts +384 -88
- package/src/console/cli-tools/routes.ts +3 -3
- package/src/console/console-router-guards.ts +2 -2
- package/src/console/console-router.ts +4 -4
- package/src/console/dashboard-assets.ts +16 -1
- package/src/console/data-management/routes.ts +3 -3
- package/src/console/domain-registration.ts +4 -2
- package/src/console/observability/contracts.ts +4 -0
- package/src/console/observability/live.ts +3 -3
- package/src/console/observability/logs.ts +3 -3
- package/src/console/observability/performance.ts +3 -3
- package/src/console/observability/routes.ts +3 -3
- package/src/console/observability/store.ts +16 -16
- package/src/console/providers/catalog/contracts.ts +43 -22
- package/src/console/providers/catalog/provider-operations.ts +17 -2
- package/src/console/providers/catalog/routes.ts +3 -3
- package/src/console/providers/catalog/store-helpers.ts +349 -326
- package/src/console/providers/catalog/store.ts +1523 -1292
- package/src/console/providers/detail/routes.ts +3 -3
- package/src/console/providers/oauth/routes.ts +3 -3
- package/src/console/quota/account-quota-tenant.ts +34 -2
- package/src/console/quota/account-quota.ts +4 -1
- package/src/console/quota/account-test-egress.ts +4 -4
- package/src/console/quota/native-identity.ts +189 -0
- package/src/console/remote-support/contracts.ts +12 -6
- package/src/console/remote-support/service.ts +2 -2
- package/src/console/routing/model/routes.ts +3 -3
- package/src/console/routing/pools/contracts.ts +11 -8
- package/src/console/routing/pools/operations.ts +14 -7
- package/src/console/routing/pools/probe-verdict.ts +8 -3
- package/src/console/routing/pools/relay-deploy.ts +9 -9
- package/src/console/routing/pools/routes.ts +3 -3
- package/src/console/routing/pools/store.ts +21 -1
- package/src/console/settings/routes.ts +3 -3
- package/src/console/share/share-router.ts +51 -18
- package/src/console/share/share-stats.ts +158 -49
- package/src/console/share/share-usage.ts +24 -7
- package/src/console/shared/read-through-cache.ts +38 -2
- package/src/console/system-prompt/contracts.ts +102 -0
- package/src/console/system-prompt/routes.ts +165 -0
- package/src/console/updates/routes.ts +7 -4
- package/src/main.ts +11 -0
- package/src/network/outbound-fetch.ts +75 -4
- package/src/network/pool/resolver.ts +39 -39
- package/src/network/pool-cooldown-store.ts +71 -16
- package/src/network/pool-health-machine.ts +21 -1
- package/src/network/rate-limit-verdict.ts +68 -0
- package/src/network/ssrf.ts +4 -4
- package/src/observability/event-query.ts +437 -22
- package/src/observability/telemetry-status.ts +1 -1
- package/src/persistence/db-pg.ts +42 -0
- package/src/persistence/db-sqlite.ts +7 -2
- package/src/persistence/readiness.ts +18 -6
- package/src/persistence/schema.ts +162 -1
- package/src/persistence/stores/api-key-store.ts +849 -581
- package/src/persistence/stores/share-store.ts +65 -13
- package/src/persistence/stores/telemetry-store.ts +58 -2
- package/src/persistence/system-prompt-store.ts +104 -0
- package/src/protocol/codecs.ts +6 -6
- package/src/protocol/gateway-error.ts +6 -3
- package/src/protocol/image-generation.ts +2 -2
- package/src/protocol/reasoning.ts +65 -1
- package/src/protocol/request/chat.ts +1 -0
- package/src/protocol/response/chat.ts +2 -2
- package/src/protocol/response/codex.ts +1 -1
- package/src/protocol/response/gemini.ts +1 -1
- package/src/protocol/response/messages.ts +5 -5
- package/src/protocol/response/responses.ts +2 -2
- package/src/protocol/sse-decoder.ts +14 -3
- package/src/protocol/translation/capabilities.ts +2 -2
- package/src/protocol/upstream/send-claude-messages.ts +3 -3
- package/src/providers/adapters/base-adapter.ts +1 -1
- package/src/providers/adapters/compatible-adapter.ts +2 -2
- package/src/providers/authentication/oauth-refresh-service.ts +2 -2
- package/src/providers/configured-provider.ts +1 -1
- package/src/providers/discovery/probe-phases.ts +2 -2
- package/src/providers/discovery/probing-service.ts +12 -12
- package/src/providers/integrations/agentrouter/agentrouter.ts +2 -3
- package/src/providers/integrations/alibabacp/alibabacp.ts +2 -2
- package/src/providers/integrations/anthropic.ts +4 -11
- package/src/providers/integrations/antigravity/antigravity.ts +33 -47
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -1
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +30 -2
- package/src/providers/integrations/buddy/buddy-discovery-shared.ts +37 -6
- package/src/providers/integrations/buddy/buddy-identity.ts +22 -1
- package/src/providers/integrations/buddy/buddy-media.ts +21 -17
- package/src/providers/integrations/buddy/codebuddy/provider.ts +6 -2
- package/src/providers/integrations/buddy/workbuddy/provider.ts +6 -2
- package/src/providers/integrations/claude/claude-betas.ts +1 -1
- package/src/providers/integrations/claude/claude-credentials.ts +1 -1
- package/src/providers/integrations/claude/claude-slow-lane-retry.ts +1 -1
- package/src/providers/integrations/claude/claude.ts +3 -10
- package/src/providers/integrations/cline/cline.ts +8 -1
- package/src/providers/integrations/cloudflareai/cloudflareai.ts +4 -7
- package/src/providers/integrations/codex/codex-errors.ts +1 -1
- package/src/providers/integrations/codex/codex.ts +14 -15
- package/src/providers/integrations/commandcode/commandcode.ts +4 -4
- package/src/providers/integrations/commandcode/quota.ts +5 -1
- package/src/providers/integrations/cursor/cursor-http2.ts +11 -11
- package/src/providers/integrations/cursor/cursor.ts +15 -15
- package/src/providers/integrations/factory/factory.ts +4 -4
- package/src/providers/integrations/gemini.ts +11 -11
- package/src/providers/integrations/github/github.ts +26 -7
- package/src/providers/integrations/grok/grok.ts +2 -6
- package/src/providers/integrations/hermes.ts +5 -1
- package/src/providers/integrations/inferhub.ts +4 -11
- package/src/providers/integrations/kilo/kilo.ts +4 -0
- package/src/providers/integrations/kimi/kimi.ts +4 -11
- package/src/providers/integrations/kiro/kiro.ts +10 -10
- package/src/providers/integrations/muse/muse.ts +6 -9
- package/src/providers/integrations/openrouter/provider.ts +4 -0
- package/src/providers/integrations/perplexity.ts +2 -2
- package/src/providers/integrations/qoder/qoder-catalog.ts +47 -9
- package/src/providers/integrations/qoder/qoder-checkin.ts +242 -51
- package/src/providers/integrations/qoder/qoder-native-identity-store.ts +157 -0
- package/src/providers/integrations/qoder/qoder-oauth.ts +12 -3
- package/src/providers/integrations/qoder/qoder-realm.ts +15 -0
- package/src/providers/integrations/qoder/qoder-umid.ts +287 -0
- package/src/providers/integrations/qoder/qoder-virtual.ts +2 -2
- package/src/providers/integrations/qoder/qoder.ts +192 -24
- package/src/providers/integrations/tokenharbor.ts +2 -2
- package/src/providers/integrations/trae/trae.ts +6 -9
- package/src/providers/integrations/zcode/oauth.ts +1 -1
- package/src/providers/operations/account-error-classification.ts +6 -8
- package/src/providers/operations/account-health-service.ts +14 -16
- package/src/providers/operations/client-versions.ts +37 -22
- package/src/providers/operations/provider-catalog-service.ts +4 -4
- package/src/providers/operations/provider-credential-service.ts +16 -4
- package/src/providers/operations/upstream-deadline.ts +3 -3
- package/src/providers/provider-contracts.ts +55 -3
- package/src/providers/provider-credential-resolver.ts +1 -1
- package/src/providers/provider-registry.ts +4 -4
- package/src/providers/quota/quota-contracts.ts +24 -10
- package/src/providers/quota/refresh.ts +2 -2
- package/src/providers/search/search-provider.ts +5 -5
- package/src/security/abuse.ts +4 -4
- package/src/security/access-control.ts +19 -0
- package/src/security/admission/reasons.ts +17 -17
- package/src/security/api-key-auth.ts +18 -5
- package/src/shared/coerce.ts +19 -0
- package/src/transport/dispatch/attempt-finalize.ts +4 -3
- package/src/transport/dispatch/attempt-loop.ts +10 -7
- package/src/transport/dispatch/empty-stream.ts +6 -6
- package/src/transport/dispatch/generation.ts +9 -9
- package/src/transport/dispatch/leases.ts +8 -8
- package/src/transport/dispatch/proxy-request.ts +38 -9
- package/src/transport/dispatch/responses-compact.ts +11 -11
- package/src/transport/dispatch/retry-policy.ts +4 -67
- package/src/transport/dispatch/stream-bridge.ts +1 -1
- package/src/transport/dispatch/streaming/finalize.ts +9 -8
- package/src/transport/dispatch/streaming/stream.ts +16 -1
- package/src/transport/dispatch/systemone.ts +8 -8
- package/src/transport/dispatch/token-count.ts +9 -9
- package/src/transport/dispatch/upstream.ts +10 -0
- package/src/transport/dispatch/websearch.ts +6 -6
- package/src/transport/drain-endpoint.ts +2 -2
- package/src/transport/failure-policy.ts +13 -6
- package/src/transport/guards/auth-guard.ts +6 -5
- package/src/transport/guards/body-policy.ts +4 -4
- package/src/transport/guards/error-lifecycle.ts +18 -3
- package/src/transport/guards/guard-errors.ts +7 -3
- package/src/transport/guards/headroom-guard.ts +1 -1
- package/src/transport/guards/pipeline.ts +6 -6
- package/src/transport/guards/readiness-guard.ts +16 -2
- package/src/transport/guards/request-context.ts +3 -3
- package/src/transport/request/preparer.ts +37 -21
- package/src/transport/request/state.ts +4 -7
- package/src/transport/request/system-prompt-injection.ts +89 -0
- package/src/transport/request/tenant-preferences.ts +43 -0
- package/src/transport/routing/admission.ts +13 -3
- package/src/transport/routing/route-catalog.ts +104 -2
- package/src/transport/routing/route-model.ts +60 -7
- package/src/transport/routing/router.ts +306 -256
- package/src/transport/shutdown-notice.ts +19 -7
- package/src/transport/surface/completion.ts +1 -1
- package/src/transport/surface/messages/encode.ts +1 -1
- package/src/workers/daily-checkin.ts +2 -0
- package/dist/dashboard/assets/BannedPage-XaUmiBrC.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-CqD7QiLM.js +0 -1
- package/dist/dashboard/assets/CliToolDetailPage-CqD7QiLM.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-0XUo4UMn.js +0 -1
- package/dist/dashboard/assets/CliToolsPage-0XUo4UMn.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-CXGS_Ct-.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-DGnGNYnM.js +0 -1
- package/dist/dashboard/assets/ConsoleLogPage-DGnGNYnM.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-DAG5J9EY.js +0 -1
- package/dist/dashboard/assets/CustomizationPage-DAG5J9EY.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-BKtAPYVF.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-BdmRLnq2.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-BH8Tehdi.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-CqMxibJB.js +0 -1
- package/dist/dashboard/assets/OverviewPage-CqMxibJB.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CqMh2NRX.js +0 -4
- package/dist/dashboard/assets/ProviderDetailPage-CqMh2NRX.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-B9uWu4Nz.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-B9uWu4Nz.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-BoWOhpDO.js +0 -9
- package/dist/dashboard/assets/ProxyPage-BoWOhpDO.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-7P2n--Fv.js +0 -1
- package/dist/dashboard/assets/QuotaPage-7P2n--Fv.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CxZ1fTkN.js +0 -1
- package/dist/dashboard/assets/SettingsPage-CxZ1fTkN.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-Co12Zzaw.js +0 -1
- package/dist/dashboard/assets/SetupPage-Co12Zzaw.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-Dqf4zpS-.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-Byj2lAEm.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-CTkLAuRc.js +0 -56
- package/dist/dashboard/assets/UsagePage-CTkLAuRc.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-CUi6UXsR.js +0 -1
- package/dist/dashboard/assets/api-keys-CUi6UXsR.js.gz +0 -0
- package/dist/dashboard/assets/app-KvsUJUMF.css +0 -2
- package/dist/dashboard/assets/app-KvsUJUMF.css.gz +0 -0
- package/dist/dashboard/assets/assert-67xXH5rf.js +0 -1
- package/dist/dashboard/assets/assert-67xXH5rf.js.gz +0 -0
- package/dist/dashboard/assets/badge-DC2L43Mj.js.gz +0 -0
- package/dist/dashboard/assets/banner-BBVcb0J3.js.gz +0 -0
- package/dist/dashboard/assets/card-Cca1XC6G.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-KvsWa1Jl.js +0 -1
- package/dist/dashboard/assets/cli-tools-KvsWa1Jl.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-DUzEbynq.js.gz +0 -0
- package/dist/dashboard/assets/concurrency-BDF0ok7y.js.gz +0 -0
- package/dist/dashboard/assets/dialog-DFvWhwTa.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CFM2sMX1.js +0 -1
- package/dist/dashboard/assets/drawer-CFM2sMX1.js.gz +0 -0
- package/dist/dashboard/assets/entry-Bqy8RFvM.js.gz +0 -0
- package/dist/dashboard/assets/entry-ClCMCWWQ.js +0 -4
- package/dist/dashboard/assets/entry-ClCMCWWQ.js.gz +0 -0
- package/dist/dashboard/assets/entry-_xHo5x6r.js +0 -1
- package/dist/dashboard/assets/entry-_xHo5x6r.js.gz +0 -0
- package/dist/dashboard/assets/formatters-CWWZ-uCZ.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-C9e8Ts7e.js.gz +0 -0
- package/dist/dashboard/assets/index-DFVFrnWW.js.gz +0 -0
- package/dist/dashboard/assets/inline-BFUcvllj.js.gz +0 -0
- package/dist/dashboard/assets/input-C_ILAvBb.js.gz +0 -0
- package/dist/dashboard/assets/layout-CBIyT6Rt.js.gz +0 -0
- package/dist/dashboard/assets/list-row-ebJ_-Nsb.js.gz +0 -0
- package/dist/dashboard/assets/live-D0mm--ME.js +0 -1
- package/dist/dashboard/assets/live-D0mm--ME.js.gz +0 -0
- package/dist/dashboard/assets/minimize-2-D-MN8W8a.js +0 -1
- package/dist/dashboard/assets/minimize-2-D-MN8W8a.js.gz +0 -0
- package/dist/dashboard/assets/mutation-sMD965oO.js.gz +0 -0
- package/dist/dashboard/assets/numbers-BO9dKaFr.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-CfL_Zdoh.js.gz +0 -0
- package/dist/dashboard/assets/quota-DZGyp0N5.js +0 -1
- package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
- package/dist/dashboard/assets/routing-BnIpi1IJ.js +0 -1
- package/dist/dashboard/assets/routing-BnIpi1IJ.js.gz +0 -0
- package/dist/dashboard/assets/select-DBNVLdUu.js.gz +0 -0
- package/dist/dashboard/assets/settings-BAFhDgX8.js +0 -1
- package/dist/dashboard/assets/settings-BAFhDgX8.js.gz +0 -0
- package/dist/dashboard/assets/stack-EMrOMt4I.js.gz +0 -0
- package/dist/dashboard/assets/state-CWoo7ty5.js.gz +0 -0
- package/dist/dashboard/assets/switch-CCjT6IkF.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-DjkLHUcE.js.gz +0 -0
- /package/dist/dashboard/assets/{app-BaWd0puM.js → app-BayTTy01.js} +0 -0
- /package/dist/dashboard/assets/{app-BaWd0puM.js.gz → app-BayTTy01.js.gz} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { and, eq,
|
|
1
|
+
import { and, eq, sql } from "drizzle-orm";
|
|
2
2
|
import type { CartethyiaDatabase } from "../../persistence/db-handle";
|
|
3
3
|
import type { ApiKeyRecord } from "../../persistence/stores/api-key-store";
|
|
4
4
|
import { CachedPreferencesReader, DrizzlePreferencesReader } from "../../persistence/tenant-preferences";
|
|
@@ -9,7 +9,7 @@ import { isGatewayError } from "../../observability/telemetry-status";
|
|
|
9
9
|
import type { RedisClient } from "../../persistence/redis";
|
|
10
10
|
import { cachedRead } from "../shared/read-through-cache";
|
|
11
11
|
import { SHARE_STATS_CACHE_TTL_SECONDS } from "./share-stats";
|
|
12
|
-
import { telemetryUsageTotals } from "../../persistence/schema";
|
|
12
|
+
import { apiKeys, telemetryUsageTotals } from "../../persistence/schema";
|
|
13
13
|
import { shouldMaskClientIp } from "../shared/ip-privacy";
|
|
14
14
|
import { utcDayStart } from "../shared/day";
|
|
15
15
|
|
|
@@ -66,9 +66,16 @@ export interface SharedKeyActivityDetail {
|
|
|
66
66
|
|
|
67
67
|
/** Read-only share-recipient telemetry; payloads and credential material never cross this boundary. */
|
|
68
68
|
export interface ShareActivityPort {
|
|
69
|
+
/**
|
|
70
|
+
* `familyTemplateKeyId` is the share template the children were issued from:
|
|
71
|
+
* the telemetry reads scope to the whole family through it (an `EXISTS`
|
|
72
|
+
* against `api_keys`, constant-wire) rather than through an id list, so the
|
|
73
|
+
* read is the same no matter how many keys the template has minted.
|
|
74
|
+
*/
|
|
69
75
|
getSharedKeySummaries(
|
|
70
76
|
tenantId: string,
|
|
71
77
|
children: readonly ApiKeyRecord[],
|
|
78
|
+
familyTemplateKeyId: string,
|
|
72
79
|
): Promise<readonly SharedKeySummary[]>;
|
|
73
80
|
getSharedKeyDetail(tenantId: string, childKeyId: string): Promise<SharedKeyActivityDetail>;
|
|
74
81
|
}
|
|
@@ -133,15 +140,25 @@ export function createShareUsagePort(deps: ShareUsagePortDeps): ShareActivityPor
|
|
|
133
140
|
const { db, redis } = deps;
|
|
134
141
|
const preferences = new CachedPreferencesReader(new DrizzlePreferencesReader(db));
|
|
135
142
|
return {
|
|
136
|
-
async getSharedKeySummaries(tenantId, children) {
|
|
143
|
+
async getSharedKeySummaries(tenantId, children, familyTemplateKeyId) {
|
|
137
144
|
if (children.length === 0) return [];
|
|
138
|
-
const childIds = children.map((child) => child.id);
|
|
139
145
|
const todayStart = utcDayStart(new Date());
|
|
146
|
+
/**
|
|
147
|
+
* Family membership of the entity the totals/events row is attributed to.
|
|
148
|
+
* Primary predicate is the denormalized template column (so history
|
|
149
|
+
* survives key deletion); the `EXISTS` fallback covers rows written
|
|
150
|
+
* before the column existed.
|
|
151
|
+
*/
|
|
152
|
+
const familyMembership = (entityId: typeof apiKeys.id | typeof telemetryUsageTotals.entityId) =>
|
|
153
|
+
sql`(${telemetryUsageTotals.familyTemplateKeyId} = ${familyTemplateKeyId} or (${telemetryUsageTotals.familyTemplateKeyId} is null and exists (select 1 from ${apiKeys} where ${apiKeys.id} = ${entityId} and (${apiKeys.id} = ${familyTemplateKeyId} or ${apiKeys.parentKeyId} = ${familyTemplateKeyId}))))`;
|
|
140
154
|
const [todayEvents, lifetimeRows, hideClientIp] = await Promise.all([
|
|
141
155
|
// Today's per-key totals as one GROUP BY instead of reading every
|
|
142
156
|
// event of the family. `addEventUsage` needs per-event fields, so the
|
|
143
|
-
// groups are projected into the same shape the loop consumes.
|
|
144
|
-
|
|
157
|
+
// groups are projected into the same shape the loop consumes. The
|
|
158
|
+
// family filter is the template key, not an id list: a family with
|
|
159
|
+
// tens of thousands of minted children must not expand them into bind
|
|
160
|
+
// parameters on every dashboard poll.
|
|
161
|
+
querySqlEvents(db, { tenantId, familyApiKeyId: familyTemplateKeyId, since: todayStart, limit: MAX_SUMMARY_EVENTS }).then(
|
|
145
162
|
(page) => page.events,
|
|
146
163
|
),
|
|
147
164
|
db
|
|
@@ -158,7 +175,7 @@ export function createShareUsagePort(deps: ShareUsagePortDeps): ShareActivityPor
|
|
|
158
175
|
and(
|
|
159
176
|
eq(telemetryUsageTotals.tenantId, tenantId),
|
|
160
177
|
eq(telemetryUsageTotals.identityType, "api_key"),
|
|
161
|
-
|
|
178
|
+
familyMembership(telemetryUsageTotals.entityId),
|
|
162
179
|
),
|
|
163
180
|
),
|
|
164
181
|
shouldMaskClientIp(preferences, tenantId),
|
|
@@ -63,6 +63,34 @@ export function etagKey(key: string): string {
|
|
|
63
63
|
return `${key}:etag`;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* In-memory fallback store used when no Redis client is configured (the
|
|
68
|
+
* embedded lite backend). Keyed by the same cache key, storing the value and
|
|
69
|
+
* its validator with a TTL. Bounded so a busy gateway cannot grow it without
|
|
70
|
+
* limit.
|
|
71
|
+
*/
|
|
72
|
+
const LOCAL_CACHE_MAX_ENTRIES = 2000;
|
|
73
|
+
const localCache = new Map<string, { at: number; value: string; etag: string }>();
|
|
74
|
+
|
|
75
|
+
function localGet(key: string): { value: string; etag: string } | undefined {
|
|
76
|
+
const entry = localCache.get(key);
|
|
77
|
+
if (entry === undefined) return undefined;
|
|
78
|
+
if (Date.now() - entry.at >= READ_THROUGH_DEFAULT_TTL_SECONDS * 1000) {
|
|
79
|
+
localCache.delete(key);
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
return { value: entry.value, etag: entry.etag };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function localSet(key: string, value: string, etag: string): void {
|
|
86
|
+
if (localCache.size >= LOCAL_CACHE_MAX_ENTRIES) {
|
|
87
|
+
// Evict the oldest entry (Map iterates in insertion order).
|
|
88
|
+
const oldest = localCache.keys().next();
|
|
89
|
+
if (!oldest.done) localCache.delete(oldest.value);
|
|
90
|
+
}
|
|
91
|
+
localCache.set(key, { at: Date.now(), value, etag });
|
|
92
|
+
}
|
|
93
|
+
|
|
66
94
|
/**
|
|
67
95
|
* Reads through the cache and returns a strong validator alongside the value.
|
|
68
96
|
*
|
|
@@ -74,7 +102,8 @@ export function etagKey(key: string): string {
|
|
|
74
102
|
* With no cache configured (`client === undefined`) the value is computed and
|
|
75
103
|
* the validator is still returned, so conditional requests keep working for
|
|
76
104
|
* the length of the client's own polling loop; only the cross-instance sharing
|
|
77
|
-
* is lost.
|
|
105
|
+
* is lost. A process-local Map serves the same TTL when Redis is absent, so a
|
|
106
|
+
* poller still gets cache hits.
|
|
78
107
|
*/
|
|
79
108
|
export async function cachedReadWithEtag<T>(
|
|
80
109
|
client: ReadThroughCacheClient | undefined,
|
|
@@ -83,8 +112,15 @@ export async function cachedReadWithEtag<T>(
|
|
|
83
112
|
ttlSeconds: number = READ_THROUGH_DEFAULT_TTL_SECONDS,
|
|
84
113
|
): Promise<CachedResult<T>> {
|
|
85
114
|
if (client === undefined) {
|
|
115
|
+
const entry = localGet(key);
|
|
116
|
+
if (entry !== undefined) {
|
|
117
|
+
return { value: JSON.parse(entry.value) as T, etag: entry.etag };
|
|
118
|
+
}
|
|
86
119
|
const fresh = await compute();
|
|
87
|
-
|
|
120
|
+
const bytes = JSON.stringify(fresh) ?? "null";
|
|
121
|
+
const etag = etagFor(bytes);
|
|
122
|
+
localSet(key, bytes, etag);
|
|
123
|
+
return { value: fresh, etag };
|
|
88
124
|
}
|
|
89
125
|
try {
|
|
90
126
|
const hit = await client.get(key);
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Tenant system-prompt injection contracts.
|
|
2
|
+
//
|
|
3
|
+
// Shared by the console API, the persistence store, and the dispatch-path
|
|
4
|
+
// transform — all Elysia-free so the dashboard bundle can import the types.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* One scope's prompt configuration. `enabled` is the master switch for the
|
|
8
|
+
* scope; `mode` decides how the text meets existing system content.
|
|
9
|
+
*/
|
|
10
|
+
export interface PromptSetting {
|
|
11
|
+
readonly enabled: boolean;
|
|
12
|
+
readonly mode: PromptMode;
|
|
13
|
+
readonly text: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const PROMPT_MODES = ["append", "override"] as const;
|
|
17
|
+
export type PromptMode = (typeof PROMPT_MODES)[number];
|
|
18
|
+
|
|
19
|
+
/** Per-provider entry: follow the tenant's global prompt, or carry a custom one. */
|
|
20
|
+
export interface ProviderPromptEntry {
|
|
21
|
+
readonly followGlobal: boolean;
|
|
22
|
+
/** Present and used only when `followGlobal` is false. */
|
|
23
|
+
readonly setting?: PromptSetting;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The full per-tenant configuration, one row in `tenant_system_prompts`. */
|
|
27
|
+
export interface TenantSystemPrompts {
|
|
28
|
+
/** `null` = never configured — distinct from an explicit disabled setting. */
|
|
29
|
+
readonly global: PromptSetting | null;
|
|
30
|
+
/** providerId → entry */
|
|
31
|
+
readonly providers: Readonly<Record<string, ProviderPromptEntry>>;
|
|
32
|
+
/** `${providerId}::${modelId}` → setting */
|
|
33
|
+
readonly models: Readonly<Record<string, PromptSetting>>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Empty configuration: nothing configured anywhere. */
|
|
37
|
+
export const EMPTY_TENANT_SYSTEM_PROMPTS: TenantSystemPrompts = {
|
|
38
|
+
global: null,
|
|
39
|
+
providers: {},
|
|
40
|
+
models: {},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Maximum stored text per scope (5 MB). Enforced at the console API. */
|
|
44
|
+
export const SYSTEM_PROMPT_TEXT_CAP_BYTES = 5_000_000;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Marker wrapping tenant text injected into a provider wire whose upstream
|
|
48
|
+
* validates the system prompt as a channel identity (the buddy family). The
|
|
49
|
+
* fixed identity prompt stays the opener; the tenant text rides after it
|
|
50
|
+
* inside the same turn, framed so the provider's own parser is not confused.
|
|
51
|
+
*/
|
|
52
|
+
export const SYSTEM_PROMPT_WRAPPER_PREFIX = "<cartethyia:system-prompt>";
|
|
53
|
+
export const SYSTEM_PROMPT_WRAPPER_SUFFIX = "</cartethyia:system-prompt>";
|
|
54
|
+
|
|
55
|
+
/** Maximum number of per-provider entries stored for one tenant. */
|
|
56
|
+
export const SYSTEM_PROMPT_PROVIDER_LIMIT = 256;
|
|
57
|
+
|
|
58
|
+
/** Maximum number of per-model entries stored for one tenant. */
|
|
59
|
+
export const SYSTEM_PROMPT_MODEL_LIMIT = 512;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Providers whose upstream validates the fixed system prompt as a channel
|
|
63
|
+
* identity. Their system turn must keep the fixed prompt as the opener, so
|
|
64
|
+
* tenant text is appended after it inside the same turn, wrapped in the
|
|
65
|
+
* gateway's prefix/suffix markers (see `SYSTEM_PROMPT_WRAPPER_PREFIX`).
|
|
66
|
+
* `override` mode is not honoured for these providers — it would strip the
|
|
67
|
+
* identity and the upstream would refuse the request.
|
|
68
|
+
*/
|
|
69
|
+
export const WRAPPED_INJECTION_PROVIDERS: ReadonlySet<string> = new Set([
|
|
70
|
+
"cb", // CodeBuddy International
|
|
71
|
+
"cbcn", // CodeBuddy CN
|
|
72
|
+
"workbuddy",
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
/** Whether a provider's tenant text rides wrapped after the fixed identity prompt. */
|
|
76
|
+
export function isWrappedInjectionProvider(providerId: string): boolean {
|
|
77
|
+
return WRAPPED_INJECTION_PROVIDERS.has(providerId);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Model-scope key for the `models` map. */
|
|
81
|
+
export function systemPromptModelKey(providerId: string, modelId: string): string {
|
|
82
|
+
return `${providerId}::${modelId}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Parses a model-scope key; null when the key is malformed. */
|
|
86
|
+
export function parseSystemPromptModelKey(key: string): { providerId: string; modelId: string } | null {
|
|
87
|
+
const index = key.indexOf("::");
|
|
88
|
+
if (index <= 0 || index === key.length - 2) return null;
|
|
89
|
+
return { providerId: key.slice(0, index), modelId: key.slice(index + 2) };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** True when the value is a well-formed prompt setting (shape only, not size). */
|
|
93
|
+
export function isPromptSetting(value: unknown): value is PromptSetting {
|
|
94
|
+
if (typeof value !== "object" || value === null) return false;
|
|
95
|
+
const candidate = value as Partial<PromptSetting>;
|
|
96
|
+
return (
|
|
97
|
+
typeof candidate.enabled === "boolean" &&
|
|
98
|
+
typeof candidate.mode === "string" &&
|
|
99
|
+
(PROMPT_MODES as readonly string[]).includes(candidate.mode) &&
|
|
100
|
+
typeof candidate.text === "string"
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// Console API for tenant system-prompt injection.
|
|
2
|
+
//
|
|
3
|
+
// GET /system-prompt → the stored configuration
|
|
4
|
+
// PUT /system-prompt → replace the configuration wholesale
|
|
5
|
+
// DELETE /system-prompt → clear it entirely
|
|
6
|
+
//
|
|
7
|
+
// The PUT replaces the row: the modal edits the whole three-scope shape at
|
|
8
|
+
// once, so a PATCH surface would add states the UI never produces. Text is
|
|
9
|
+
// capped at 5 MB per scope and the provider/model maps are bounded, matching
|
|
10
|
+
// the store's own defensive limits.
|
|
11
|
+
import { Elysia, t } from "elysia";
|
|
12
|
+
import type { ConsoleAccessResolver } from "../auth/access";
|
|
13
|
+
import { ConsoleDomainError, consoleErrorHandler, requireTenantScope } from "../shared/errors";
|
|
14
|
+
import type { AuditSink } from "../audit/contracts";
|
|
15
|
+
import type { CartethyiaDatabase } from "../../persistence/db-handle";
|
|
16
|
+
import {
|
|
17
|
+
clearTenantSystemPrompts,
|
|
18
|
+
readTenantSystemPrompts,
|
|
19
|
+
writeTenantSystemPrompts,
|
|
20
|
+
} from "../../persistence/system-prompt-store";
|
|
21
|
+
import {
|
|
22
|
+
EMPTY_TENANT_SYSTEM_PROMPTS,
|
|
23
|
+
PROMPT_MODES,
|
|
24
|
+
SYSTEM_PROMPT_MODEL_LIMIT,
|
|
25
|
+
SYSTEM_PROMPT_PROVIDER_LIMIT,
|
|
26
|
+
SYSTEM_PROMPT_TEXT_CAP_BYTES,
|
|
27
|
+
isPromptSetting,
|
|
28
|
+
parseSystemPromptModelKey,
|
|
29
|
+
type PromptSetting,
|
|
30
|
+
type ProviderPromptEntry,
|
|
31
|
+
type TenantSystemPrompts,
|
|
32
|
+
} from "./contracts";
|
|
33
|
+
|
|
34
|
+
/** Route-group context injected by the composition root. */
|
|
35
|
+
export interface SystemPromptRoutesConfig {
|
|
36
|
+
readonly db: CartethyiaDatabase;
|
|
37
|
+
readonly accessResolver: ConsoleAccessResolver;
|
|
38
|
+
readonly auditSink?: AuditSink;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Validates one scope setting, including the 5 MB text cap. */
|
|
42
|
+
function validateSetting(value: unknown, scopeLabel: string): PromptSetting {
|
|
43
|
+
if (!isPromptSetting(value)) {
|
|
44
|
+
throw new ConsoleDomainError("invalid_request", 400, `${scopeLabel} is not a valid prompt setting`);
|
|
45
|
+
}
|
|
46
|
+
const bytes = Buffer.byteLength(value.text, "utf8");
|
|
47
|
+
if (bytes > SYSTEM_PROMPT_TEXT_CAP_BYTES) {
|
|
48
|
+
throw new ConsoleDomainError("payload_too_large", 413, `${scopeLabel} exceeds the 5 MB limit`);
|
|
49
|
+
}
|
|
50
|
+
if (!(PROMPT_MODES as readonly string[]).includes(value.mode)) {
|
|
51
|
+
throw new ConsoleDomainError("invalid_request", 400, `${scopeLabel} has an invalid mode`);
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Validates the full PUT body into the stored shape. */
|
|
57
|
+
function validateBody(value: unknown): TenantSystemPrompts {
|
|
58
|
+
if (typeof value !== "object" || value === null) {
|
|
59
|
+
throw new ConsoleDomainError("invalid_request", 400, "Body must be an object");
|
|
60
|
+
}
|
|
61
|
+
const body = value as {
|
|
62
|
+
global?: unknown;
|
|
63
|
+
providers?: unknown;
|
|
64
|
+
models?: unknown;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const global =
|
|
68
|
+
body.global === null || body.global === undefined
|
|
69
|
+
? null
|
|
70
|
+
: validateSetting(body.global, "global");
|
|
71
|
+
|
|
72
|
+
if (body.providers !== undefined && typeof body.providers !== "object") {
|
|
73
|
+
throw new ConsoleDomainError("invalid_request", 400, "providers must be an object");
|
|
74
|
+
}
|
|
75
|
+
const rawProviders = (body.providers ?? {}) as Record<string, unknown>;
|
|
76
|
+
const providerIds = Object.keys(rawProviders);
|
|
77
|
+
if (providerIds.length > SYSTEM_PROMPT_PROVIDER_LIMIT) {
|
|
78
|
+
throw new ConsoleDomainError(
|
|
79
|
+
"invalid_request",
|
|
80
|
+
400,
|
|
81
|
+
`providers exceeds the ${SYSTEM_PROMPT_PROVIDER_LIMIT} entry limit`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const providers: Record<string, ProviderPromptEntry> = {};
|
|
85
|
+
for (const providerId of providerIds) {
|
|
86
|
+
const entry = rawProviders[providerId];
|
|
87
|
+
if (typeof entry !== "object" || entry === null) {
|
|
88
|
+
throw new ConsoleDomainError("invalid_request", 400, `provider ${providerId} entry must be an object`);
|
|
89
|
+
}
|
|
90
|
+
const candidate = entry as Partial<ProviderPromptEntry>;
|
|
91
|
+
if (typeof candidate.followGlobal !== "boolean") {
|
|
92
|
+
throw new ConsoleDomainError("invalid_request", 400, `provider ${providerId} needs followGlobal`);
|
|
93
|
+
}
|
|
94
|
+
if (candidate.followGlobal) {
|
|
95
|
+
providers[providerId] = { followGlobal: true };
|
|
96
|
+
} else {
|
|
97
|
+
providers[providerId] = {
|
|
98
|
+
followGlobal: false,
|
|
99
|
+
setting: validateSetting(candidate.setting, `provider ${providerId}`),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (body.models !== undefined && typeof body.models !== "object") {
|
|
105
|
+
throw new ConsoleDomainError("invalid_request", 400, "models must be an object");
|
|
106
|
+
}
|
|
107
|
+
const rawModels = (body.models ?? {}) as Record<string, unknown>;
|
|
108
|
+
const modelKeys = Object.keys(rawModels);
|
|
109
|
+
if (modelKeys.length > SYSTEM_PROMPT_MODEL_LIMIT) {
|
|
110
|
+
throw new ConsoleDomainError(
|
|
111
|
+
"invalid_request",
|
|
112
|
+
400,
|
|
113
|
+
`models exceeds the ${SYSTEM_PROMPT_MODEL_LIMIT} entry limit`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
const models: Record<string, PromptSetting> = {};
|
|
117
|
+
for (const key of modelKeys) {
|
|
118
|
+
if (parseSystemPromptModelKey(key) === null) {
|
|
119
|
+
throw new ConsoleDomainError("invalid_request", 400, `model key ${key} is malformed`);
|
|
120
|
+
}
|
|
121
|
+
models[key] = validateSetting(rawModels[key], `model ${key}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { global, providers, models };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The return type is inferred rather than annotated as `Elysia`: `.error()`
|
|
129
|
+
* widens the instance type with its handler's response envelope, so the
|
|
130
|
+
* annotated base type no longer matches the chained result (same reason the
|
|
131
|
+
* remote-support routes leave it inferred).
|
|
132
|
+
*/
|
|
133
|
+
export function createSystemPromptRoutes(config: SystemPromptRoutesConfig) {
|
|
134
|
+
return new Elysia()
|
|
135
|
+
.error(consoleErrorHandler("System prompt settings failed"))
|
|
136
|
+
.get("/system-prompt", async ({ request }) => {
|
|
137
|
+
const access = requireTenantScope(config.accessResolver(request), "dashboard:read");
|
|
138
|
+
return await readTenantSystemPrompts(config.db, access.tenantId);
|
|
139
|
+
})
|
|
140
|
+
.put(
|
|
141
|
+
"/system-prompt",
|
|
142
|
+
{ body: t.Any() },
|
|
143
|
+
async ({ request, body }) => {
|
|
144
|
+
const access = requireTenantScope(config.accessResolver(request), "dashboard:write");
|
|
145
|
+
const next = validateBody(body);
|
|
146
|
+
await writeTenantSystemPrompts(config.db, access.tenantId, next);
|
|
147
|
+
await config.auditSink?.record({
|
|
148
|
+
access,
|
|
149
|
+
action: "system_prompt.update",
|
|
150
|
+
target: "tenant_system_prompts",
|
|
151
|
+
detail: {
|
|
152
|
+
global: next.global !== null,
|
|
153
|
+
providers: Object.keys(next.providers).length,
|
|
154
|
+
models: Object.keys(next.models).length,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
return next;
|
|
158
|
+
},
|
|
159
|
+
)
|
|
160
|
+
.delete("/system-prompt", async ({ request }) => {
|
|
161
|
+
const access = requireTenantScope(config.accessResolver(request), "dashboard:write");
|
|
162
|
+
await clearTenantSystemPrompts(config.db, access.tenantId);
|
|
163
|
+
return EMPTY_TENANT_SYSTEM_PROMPTS;
|
|
164
|
+
});
|
|
165
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Elysia } from "elysia";
|
|
1
|
+
import { Elysia, type AnyElysia } from "elysia";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { consoleErrorHandler, requireTenantScope } from "../shared/errors";
|
|
4
4
|
import { consoleSseResponse, createConsoleSseStream } from "../observability/sse";
|
|
@@ -7,7 +7,10 @@ import type { ConsoleAccessResolver } from "../auth/access";
|
|
|
7
7
|
import { fetchLatestRelease, type LatestRelease } from "./repository-updates";
|
|
8
8
|
import { isNewerRelease } from "./version-compare";
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
// Memory is sampled per visible tab: ten seconds per stream made a console
|
|
11
|
+
// with many open tabs wake up constantly for numbers nobody is reading. 30 s
|
|
12
|
+
// is still responsive for a memory warning while cutting that work by three.
|
|
13
|
+
const MEMORY_SAMPLE_INTERVAL_MS = 30_000;
|
|
11
14
|
const UPDATE_SAMPLE_INTERVAL_MS = 60_000;
|
|
12
15
|
const HIGH_MEMORY_PERCENT = 85;
|
|
13
16
|
const HIGH_MEMORY_CLEAR_PERCENT = 80;
|
|
@@ -94,7 +97,7 @@ export function createUpdateRoutes(config: {
|
|
|
94
97
|
readonly accessResolver: ConsoleAccessResolver;
|
|
95
98
|
readonly observabilityStore: ObservabilityStore;
|
|
96
99
|
readonly fetchRelease?: () => Promise<LatestRelease | null>;
|
|
97
|
-
}):
|
|
100
|
+
}): AnyElysia {
|
|
98
101
|
const fetchRelease = config.fetchRelease ?? fetchLatestRelease;
|
|
99
102
|
return new Elysia()
|
|
100
103
|
.error(consoleErrorHandler("Update check failed"))
|
|
@@ -199,5 +202,5 @@ export function createUpdateRoutes(config: {
|
|
|
199
202
|
};
|
|
200
203
|
}),
|
|
201
204
|
);
|
|
202
|
-
})
|
|
205
|
+
});
|
|
203
206
|
}
|
package/src/main.ts
CHANGED
|
@@ -169,6 +169,17 @@ if (boot) {
|
|
|
169
169
|
globalThis.__cartethyiaSignalsRegistered = true;
|
|
170
170
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
171
171
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
172
|
+
// A rejected promise nothing awaited (a sweep, a cache warm, a read whose
|
|
173
|
+
// parameters a bug elsewhere ballooned) must not kill the listener: the
|
|
174
|
+
// default is fatal exit, which turned one query fault into a systemd
|
|
175
|
+
// restart loop. Logged and absorbed — the failure stays visible, the
|
|
176
|
+
// process stays up.
|
|
177
|
+
process.on("unhandledRejection", (reason) => {
|
|
178
|
+
log.error(
|
|
179
|
+
"[process] unhandled rejection (kept alive)",
|
|
180
|
+
reason instanceof Error ? reason : new Error(String(reason)),
|
|
181
|
+
);
|
|
182
|
+
});
|
|
172
183
|
// An in-place update signals SIGUSR2 before swapping the image so the old
|
|
173
184
|
// process drains with the `update` reason and its callers are told the
|
|
174
185
|
// replacement is seconds away instead of seeing a generic shutdown. Not
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Validated fetch: SSRF-guarded outbound fetch with optional pool-agent support.
|
|
2
2
|
import { GatewayError } from "../protocol/gateway-error";
|
|
3
|
-
import { Agent as HttpAgent } from "node:http";
|
|
4
|
-
import { Agent as HttpsAgent } from "node:https";
|
|
3
|
+
import { request as httpRequest, Agent as HttpAgent } from "node:http";
|
|
4
|
+
import { request as httpsRequest, Agent as HttpsAgent } from "node:https";
|
|
5
5
|
import { Agent as UndiciAgent, fetch as undiciFetch, type UndiciRequestInit } from "undici";
|
|
6
6
|
import { SocksProxyAgent } from "socks-proxy-agent";
|
|
7
7
|
import { resolveAllAddresses, validateResolvedAddresses } from "./ssrf";
|
|
@@ -167,6 +167,66 @@ async function dial(
|
|
|
167
167
|
});
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
/**
|
|
171
|
+
* One raw GET/HEAD dial that carries a body.
|
|
172
|
+
*
|
|
173
|
+
* The runtime-native fetch (`Bun`/undici) rejects `GET`/`HEAD` requests with a
|
|
174
|
+
* body — "request with GET/HEAD method cannot have body" — but some upstreams
|
|
175
|
+
* validate a signature over the body on a GET (Qoder's `/model/list` does).
|
|
176
|
+
* This is the Node `http`/`https` path for exactly that case. It runs only
|
|
177
|
+
* after the SSRF resolve/validate pass and only when no pool/relay agent owns
|
|
178
|
+
* the dial, so the security boundary is unchanged.
|
|
179
|
+
*/
|
|
180
|
+
function nodeDial(url: URL, init: RequestInit): Promise<Response> {
|
|
181
|
+
return new Promise((resolve, reject) => {
|
|
182
|
+
const mod = url.protocol === "http:" ? httpRequest : httpsRequest;
|
|
183
|
+
const headers: Record<string, string> = {};
|
|
184
|
+
{
|
|
185
|
+
const h = new Headers(init.headers);
|
|
186
|
+
h.forEach((value, key) => {
|
|
187
|
+
headers[key] = value;
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
let payload: Buffer | undefined;
|
|
191
|
+
const body = init.body;
|
|
192
|
+
if (typeof body === "string") {
|
|
193
|
+
payload = Buffer.from(body, "utf8");
|
|
194
|
+
} else if (Buffer.isBuffer(body)) {
|
|
195
|
+
payload = body;
|
|
196
|
+
} else if (body instanceof Uint8Array) {
|
|
197
|
+
payload = Buffer.from(body);
|
|
198
|
+
} else if (body !== null && body !== undefined) {
|
|
199
|
+
reject(new TypeError("nodeDial only supports string/Buffer/Uint8Array bodies"));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (payload !== undefined) headers["content-length"] = String(payload.byteLength);
|
|
203
|
+
const req = mod(
|
|
204
|
+
url,
|
|
205
|
+
{
|
|
206
|
+
method: (init.method ?? "GET").toUpperCase(),
|
|
207
|
+
headers,
|
|
208
|
+
...(init.signal === undefined ? {} : { signal: init.signal as AbortSignal }),
|
|
209
|
+
},
|
|
210
|
+
(res) => {
|
|
211
|
+
const chunks: Buffer[] = [];
|
|
212
|
+
res.on("data", (c: Buffer) => chunks.push(c));
|
|
213
|
+
res.on("end", () => {
|
|
214
|
+
resolve(
|
|
215
|
+
new Response(Buffer.concat(chunks), {
|
|
216
|
+
status: res.statusCode ?? 200,
|
|
217
|
+
...(res.statusMessage === undefined ? {} : { statusText: res.statusMessage }),
|
|
218
|
+
headers: res.headers as unknown as HeadersInit,
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
},
|
|
223
|
+
);
|
|
224
|
+
req.on("error", reject);
|
|
225
|
+
if (payload !== undefined) req.write(payload);
|
|
226
|
+
req.end();
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
170
230
|
export function createValidatedFetch(options: ValidatedFetchOptions = {}): ValidatedFetch {
|
|
171
231
|
const policy = options.policy ?? {};
|
|
172
232
|
const maxRedirects = options.maxRedirects ?? policy.maxRedirects ?? 3;
|
|
@@ -242,7 +302,18 @@ export function createValidatedFetch(options: ValidatedFetchOptions = {}): Valid
|
|
|
242
302
|
// serverless front door refuses (400).
|
|
243
303
|
response = await dial(relay.url, relay.init, fetchFn, undefined);
|
|
244
304
|
} else {
|
|
245
|
-
|
|
305
|
+
const method = (requestInit.method ?? "GET").toUpperCase();
|
|
306
|
+
const hasBody = requestInit.body !== undefined && requestInit.body !== null;
|
|
307
|
+
// The runtime fetch cannot carry a body on GET/HEAD, but Qoder's
|
|
308
|
+
// model-list endpoint requires a signed GET body. When no pool/relay
|
|
309
|
+
// owns the dial and no injected test fetch is present, dial through the
|
|
310
|
+
// Node http(s) client (post SSRF validation). Every other case keeps
|
|
311
|
+
// the existing fetch path.
|
|
312
|
+
if ((method === "GET" || method === "HEAD") && hasBody && !socksAgent && options.fetchFn === undefined) {
|
|
313
|
+
response = await nodeDial(url, requestInit);
|
|
314
|
+
} else {
|
|
315
|
+
response = await dial(url, requestInit, fetchFn, proxyUrl, socksAgent ? socksDispatcherFor(socksAgent) : undefined, socksFetch);
|
|
316
|
+
}
|
|
246
317
|
}
|
|
247
318
|
if (response.status < 300 || response.status >= 400) {
|
|
248
319
|
trackNetworkCall(url.hostname, performance.now() - startedAt);
|
|
@@ -264,7 +335,7 @@ export function createValidatedFetch(options: ValidatedFetchOptions = {}): Valid
|
|
|
264
335
|
if (isCrossOriginRedirect && (response.status === 307 || response.status === 308)) {
|
|
265
336
|
const method = (requestInit.method ?? "GET").toUpperCase();
|
|
266
337
|
if (method !== "GET" && method !== "HEAD" && requestInit.body !== undefined) {
|
|
267
|
-
throw new GatewayError("invalid_request", 400, "cross-origin redirect with body blocked");
|
|
338
|
+
throw new GatewayError("invalid_request", 400, "cross-origin redirect with body blocked (request {requestId}). Check the shape of what you sent — then resend 🪞");
|
|
268
339
|
}
|
|
269
340
|
}
|
|
270
341
|
url = nextUrl;
|