cartethyia 2.7.2 → 2.7.3
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-2TJD37Hb.js} +1 -1
- package/dist/dashboard/assets/BannedPage-2TJD37Hb.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-xefJNRmJ.js +1 -0
- package/dist/dashboard/assets/CliToolDetailPage-xefJNRmJ.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-Rj9iHQ3F.js +1 -0
- package/dist/dashboard/assets/CliToolsPage-Rj9iHQ3F.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-CXGS_Ct-.js → CombosPage-JEbYJFxw.js} +1 -1
- package/dist/dashboard/assets/CombosPage-JEbYJFxw.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-DGnGNYnM.js → ConsoleLogPage-C3xx_inM.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-C3xx_inM.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-Dw306gJU.js +1 -0
- package/dist/dashboard/assets/CustomizationPage-Dw306gJU.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-jaBo1TWV.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-jaBo1TWV.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-BdmRLnq2.js → LoginPage-C4w7vea-.js} +1 -1
- package/dist/dashboard/assets/LoginPage-C4w7vea-.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-BH8Tehdi.js → ModelPicker-BtPcAfot.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-BtPcAfot.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-CGC5gNs1.js +1 -0
- package/dist/dashboard/assets/OverviewPage-CGC5gNs1.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-DQcthNGt.js +4 -0
- package/dist/dashboard/assets/ProviderDetailPage-DQcthNGt.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-0OsXRGXe.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-0OsXRGXe.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-DdPc8slb.js +9 -0
- package/dist/dashboard/assets/ProxyPage-DdPc8slb.js.gz +0 -0
- package/dist/dashboard/assets/{QuotaPage-7P2n--Fv.js → QuotaPage-Ci5O4BnH.js} +1 -1
- package/dist/dashboard/assets/QuotaPage-Ci5O4BnH.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-CxZ1fTkN.js → SettingsPage-CDZjTt87.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-CDZjTt87.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-BQS0Rx_u.js +1 -0
- package/dist/dashboard/assets/SetupPage-BQS0Rx_u.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-Dqf4zpS-.js → SortableList-BAL5npBC.js} +1 -1
- package/dist/dashboard/assets/SortableList-BAL5npBC.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-Byj2lAEm.js → StudioPage-TP7voAQ0.js} +3 -3
- package/dist/dashboard/assets/StudioPage-TP7voAQ0.js.gz +0 -0
- package/dist/dashboard/assets/{UsagePage-CTkLAuRc.js → UsagePage-Cj_fEzhg.js} +13 -13
- package/dist/dashboard/assets/UsagePage-Cj_fEzhg.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-CUi6UXsR.js → api-keys-MbI7e7rt.js} +1 -1
- package/dist/dashboard/assets/api-keys-MbI7e7rt.js.gz +0 -0
- package/dist/dashboard/assets/app-B2vyPoII.css +2 -0
- package/dist/dashboard/assets/app-B2vyPoII.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-67xXH5rf.js → assert-C0YYuZJ4.js} +1 -1
- package/dist/dashboard/assets/assert-C0YYuZJ4.js.gz +0 -0
- package/dist/dashboard/assets/{badge-DC2L43Mj.js → badge-B_DMpMJN.js} +1 -1
- package/dist/dashboard/assets/badge-B_DMpMJN.js.gz +0 -0
- package/dist/dashboard/assets/{banner-BBVcb0J3.js → banner-DMrMPmth.js} +1 -1
- package/dist/dashboard/assets/banner-DMrMPmth.js.gz +0 -0
- package/dist/dashboard/assets/{card-Cca1XC6G.js → card-Be5Uzamx.js} +1 -1
- package/dist/dashboard/assets/card-Be5Uzamx.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-KvsWa1Jl.js → cli-tools-muFRphDu.js} +1 -1
- package/dist/dashboard/assets/cli-tools-muFRphDu.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-DUzEbynq.js → clipboard-button-DOfrVmsi.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-DOfrVmsi.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-DFvWhwTa.js → dialog-C9omdjQt.js} +1 -1
- package/dist/dashboard/assets/dialog-C9omdjQt.js.gz +0 -0
- package/dist/dashboard/assets/drawer-Bm8WKaGY.js +1 -0
- package/dist/dashboard/assets/drawer-Bm8WKaGY.js.gz +0 -0
- package/dist/dashboard/assets/{entry-_xHo5x6r.js → entry-DS90gUks.js} +1 -1
- package/dist/dashboard/assets/entry-DS90gUks.js.gz +0 -0
- package/dist/dashboard/assets/{entry-ClCMCWWQ.js → entry-j40LJotO.js} +3 -3
- package/dist/dashboard/assets/entry-j40LJotO.js.gz +0 -0
- package/dist/dashboard/assets/{entry-Bqy8RFvM.js → entry-q5HwZ8nN.js} +1 -1
- package/dist/dashboard/assets/entry-q5HwZ8nN.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-BTScrQpe.js} +1 -1
- package/dist/dashboard/assets/formatters-BTScrQpe.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-C9e8Ts7e.js → github-badge-Orjuzemr.js} +1 -1
- package/dist/dashboard/assets/github-badge-Orjuzemr.js.gz +0 -0
- package/dist/dashboard/assets/{index-DFVFrnWW.js → index-cPrX5vn6.js} +2 -2
- package/dist/dashboard/assets/index-cPrX5vn6.js.gz +0 -0
- package/dist/dashboard/assets/{inline-BFUcvllj.js → inline-Dvux8w-0.js} +1 -1
- package/dist/dashboard/assets/inline-Dvux8w-0.js.gz +0 -0
- package/dist/dashboard/assets/{input-C_ILAvBb.js → input-CU1Rgf3p.js} +1 -1
- package/dist/dashboard/assets/input-CU1Rgf3p.js.gz +0 -0
- package/dist/dashboard/assets/{layout-CBIyT6Rt.js → layout-C_7A6H55.js} +1 -1
- package/dist/dashboard/assets/layout-C_7A6H55.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-ebJ_-Nsb.js → list-row-CzFvogbj.js} +1 -1
- package/dist/dashboard/assets/list-row-CzFvogbj.js.gz +0 -0
- package/dist/dashboard/assets/{live-D0mm--ME.js → live-KaG15HLQ.js} +1 -1
- package/dist/dashboard/assets/live-KaG15HLQ.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-sMD965oO.js → mutation-JJGQm8TD.js} +1 -1
- package/dist/dashboard/assets/mutation-JJGQm8TD.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-BO9dKaFr.js → numbers-BNDZnsUE.js} +1 -1
- package/dist/dashboard/assets/numbers-BNDZnsUE.js.gz +0 -0
- package/dist/dashboard/assets/{query-keys-CfL_Zdoh.js → query-keys-CLaNz8Xz.js} +1 -1
- package/dist/dashboard/assets/query-keys-CLaNz8Xz.js.gz +0 -0
- package/dist/dashboard/assets/{routing-BnIpi1IJ.js → routing-BEH32QOC.js} +1 -1
- package/dist/dashboard/assets/routing-BEH32QOC.js.gz +0 -0
- package/dist/dashboard/assets/{select-DBNVLdUu.js → select-B3iTetHN.js} +1 -1
- package/dist/dashboard/assets/select-B3iTetHN.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-BAFhDgX8.js → settings-Dt3Eqbnq.js} +1 -1
- package/dist/dashboard/assets/settings-Dt3Eqbnq.js.gz +0 -0
- package/dist/dashboard/assets/{stack-EMrOMt4I.js → stack-KrOrm7JZ.js} +1 -1
- package/dist/dashboard/assets/stack-KrOrm7JZ.js.gz +0 -0
- package/dist/dashboard/assets/{state-CWoo7ty5.js → state-DLS7IDMy.js} +1 -1
- package/dist/dashboard/assets/state-DLS7IDMy.js.gz +0 -0
- package/dist/dashboard/assets/{switch-CCjT6IkF.js → switch-42ItvbAs.js} +1 -1
- package/dist/dashboard/assets/switch-42ItvbAs.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-DjkLHUcE.js → useMutation-Dmnfh1Ue.js} +1 -1
- package/dist/dashboard/assets/useMutation-Dmnfh1Ue.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 +117 -115
- package/migrations/full-postgres/0026_add_qoder_native_identity.sql +20 -0
- package/migrations/full-postgres/0027_qoder_identity_genuine_seed.sql +22 -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/lite-sqlite3/0026_add_qoder_native_identity.sql +15 -0
- package/migrations/lite-sqlite3/0027_qoder_identity_genuine_seed.sql +22 -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/package.json +1 -1
- package/src/config/network.ts +1 -1
- package/src/console/api-keys/bucket-spend.ts +7 -7
- package/src/console/api-keys/contracts.ts +423 -410
- package/src/console/api-keys/routes.ts +118 -19
- package/src/console/backup/export-endpoint.ts +8 -11
- package/src/console/backup/routes.ts +40 -8
- package/src/console/backup/service.ts +11 -6
- package/src/console/backup/store.ts +384 -88
- package/src/console/dashboard-assets.ts +16 -1
- package/src/console/domain-registration.ts +2 -0
- package/src/console/quota/account-quota-tenant.ts +8 -1
- package/src/console/quota/account-quota.ts +4 -1
- 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/share/share-router.ts +39 -6
- package/src/console/share/share-stats.ts +64 -25
- package/src/console/share/share-usage.ts +24 -7
- package/src/console/system-prompt/contracts.ts +102 -0
- package/src/console/system-prompt/routes.ts +165 -0
- package/src/main.ts +11 -0
- package/src/observability/event-query.ts +118 -7
- package/src/persistence/db-pg.ts +42 -0
- package/src/persistence/schema.ts +102 -0
- package/src/persistence/stores/api-key-store.ts +753 -581
- package/src/persistence/stores/share-store.ts +65 -13
- package/src/persistence/stores/telemetry-store.ts +57 -1
- package/src/persistence/system-prompt-store.ts +104 -0
- package/src/protocol/reasoning.ts +65 -1
- package/src/protocol/request/chat.ts +1 -0
- package/src/providers/integrations/antigravity/antigravity.ts +20 -23
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +30 -2
- package/src/providers/integrations/buddy/buddy-media.ts +5 -1
- package/src/providers/integrations/buddy/codebuddy/provider.ts +5 -1
- package/src/providers/integrations/buddy/workbuddy/provider.ts +5 -1
- package/src/providers/integrations/cline/cline.ts +7 -0
- package/src/providers/integrations/github/github.ts +20 -1
- package/src/providers/integrations/hermes.ts +5 -1
- package/src/providers/integrations/kilo/kilo.ts +4 -0
- package/src/providers/integrations/openrouter/provider.ts +4 -0
- package/src/providers/integrations/qoder/qoder-checkin.ts +224 -51
- package/src/providers/integrations/qoder/qoder-native-identity-store.ts +156 -0
- package/src/providers/integrations/qoder/qoder-oauth.ts +12 -3
- package/src/providers/integrations/qoder/qoder-umid.ts +288 -0
- package/src/providers/integrations/qoder/qoder.ts +5 -3
- 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/provider-contracts.ts +8 -0
- package/src/security/access-control.ts +19 -0
- package/src/security/api-key-auth.ts +14 -1
- package/src/transport/dispatch/proxy-request.ts +31 -2
- package/src/transport/dispatch/streaming/stream.ts +16 -1
- package/src/transport/dispatch/upstream.ts +10 -0
- package/src/transport/request/preparer.ts +17 -1
- 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 +7 -0
- package/src/transport/routing/route-model.ts +53 -0
- package/src/transport/routing/router.ts +306 -256
- 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.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 +0 -1
- 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.gz +0 -0
- 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.gz +0 -0
- 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.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.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.gz +0 -0
- 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.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/routing-BnIpi1IJ.js.gz +0 -0
- package/dist/dashboard/assets/select-DBNVLdUu.js.gz +0 -0
- 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-wvwCi4Eh.js} +0 -0
- /package/dist/dashboard/assets/{app-BaWd0puM.js.gz → app-wvwCi4Eh.js.gz} +0 -0
|
@@ -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
|
+
}
|
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
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
* latency arrays are never returned to JavaScript;
|
|
18
18
|
* - `isGatewayError()` semantics live in one shared SQL predicate.
|
|
19
19
|
*/
|
|
20
|
-
import { and, desc, eq, gte, inArray, isNotNull, sql } from "drizzle-orm";
|
|
20
|
+
import { and, desc, eq, gte, inArray, isNotNull, or, sql, type SQL } from "drizzle-orm";
|
|
21
21
|
import type { CartethyiaDatabase } from "../persistence/db-handle";
|
|
22
|
-
import { telemetryEvents } from "../persistence/schema";
|
|
22
|
+
import { apiKeys, telemetryEvents } from "../persistence/schema";
|
|
23
23
|
import type { TelemetryEventRecord } from "./event-log";
|
|
24
24
|
import { gatewayErrorSql } from "./telemetry-status";
|
|
25
25
|
|
|
@@ -52,6 +52,17 @@ export interface EventListQuery {
|
|
|
52
52
|
readonly until?: Date;
|
|
53
53
|
readonly apiKeyIds?: readonly string[];
|
|
54
54
|
readonly accountIds?: readonly string[];
|
|
55
|
+
/**
|
|
56
|
+
* The share-template key whose family (the template itself plus every child
|
|
57
|
+
* key it issued) scopes the read. Rendered as an `EXISTS` subquery against
|
|
58
|
+
* `api_keys` rather than an id list: a family is minted by the tenant and
|
|
59
|
+
* can grow to whatever size the tenant drives it to, so enumerating its ids
|
|
60
|
+
* in JavaScript to feed a bind list would scale the query's wire size with
|
|
61
|
+
* the family instead of with the result. The subquery keeps the same
|
|
62
|
+
* membership semantics — template id or any child of it — for any family
|
|
63
|
+
* size.
|
|
64
|
+
*/
|
|
65
|
+
readonly familyApiKeyId?: string;
|
|
55
66
|
readonly requestId?: string;
|
|
56
67
|
/** Several request ids at once — the backup restore's "already present" check. */
|
|
57
68
|
readonly requestIds?: readonly string[];
|
|
@@ -231,6 +242,94 @@ function scoped(tenantId: string, since?: Date) {
|
|
|
231
242
|
return since === undefined ? tenant : and(tenant, gte(telemetryEvents.createdAt, since));
|
|
232
243
|
}
|
|
233
244
|
|
|
245
|
+
/**
|
|
246
|
+
* Thrown when a membership filter outruns the guard below. This is a handled
|
|
247
|
+
* query error, not a protocol fault: callers surface it as a request failure
|
|
248
|
+
* instead of crashing the process with a PostgreSQL wire error that systemd
|
|
249
|
+
* would only restart into.
|
|
250
|
+
*/
|
|
251
|
+
export class TelemetryQueryInputError extends Error {
|
|
252
|
+
constructor(message: string) {
|
|
253
|
+
super(message);
|
|
254
|
+
this.name = "TelemetryQueryInputError";
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* The deduplicated id count a list membership filter accepts before refusing.
|
|
260
|
+
*
|
|
261
|
+
* `idMembershipFilter` binds the whole set as one array parameter, so even a
|
|
262
|
+
* six-figure set is wire-safe; this guard is not a protocol necessity but a
|
|
263
|
+
* runaway-collector tripwire — a bug elsewhere (an unbounded accumulator, a
|
|
264
|
+
* vanished scope) must fail as a query error instead of materializing an
|
|
265
|
+
* unbounded parameter. Families larger than this must use the `familyApiKeyId`
|
|
266
|
+
* filter, which never materializes ids.
|
|
267
|
+
*/
|
|
268
|
+
const MAX_MEMBERSHIP_IDS = 200_000;
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* The chunk size for the SQLite rendering of a list membership filter.
|
|
272
|
+
*
|
|
273
|
+
* SQLite has no array parameters, so the set has to come back as individual
|
|
274
|
+
* placeholders; 500 per chunk sits far below every build's
|
|
275
|
+
* SQLITE_MAX_VARIABLE_NUMBER while keeping the OR of chunks — semantically
|
|
276
|
+
* the identical membership test — to a handful of clauses for realistic sets.
|
|
277
|
+
*/
|
|
278
|
+
const SQLITE_IDS_PER_CHUNK = 500;
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Membership of `column` in a caller-supplied id set, safe at any set size.
|
|
282
|
+
*
|
|
283
|
+
* Expanding the members into one bind placeholder each breaks the wire twice
|
|
284
|
+
* over: PostgreSQL caps a Bind message at 65,535 parameters, and node-postgres
|
|
285
|
+
* miscounts long placeholder lists anyway (`08P01 bind message has N parameter
|
|
286
|
+
* formats but 0 parameters` — the shape that crash-looped the dev deployment,
|
|
287
|
+
* whose share family carried ~90k minted child keys). Instead the set is
|
|
288
|
+
* deduplicated and handed to the server as a single array parameter
|
|
289
|
+
* (`= ANY(…::uuid[])`), or, on SQLite, to chunks of individual placeholders;
|
|
290
|
+
* membership semantics are unchanged in both renderings.
|
|
291
|
+
*/
|
|
292
|
+
function idMembershipFilter(
|
|
293
|
+
db: CartethyiaDatabase,
|
|
294
|
+
column: typeof telemetryEvents.apiKeyId | typeof telemetryEvents.accountId | typeof telemetryEvents.requestId,
|
|
295
|
+
values: readonly string[],
|
|
296
|
+
cast: "uuid[]" | "text[]",
|
|
297
|
+
): SQL {
|
|
298
|
+
const unique = [...new Set(values)];
|
|
299
|
+
if (unique.length > MAX_MEMBERSHIP_IDS) {
|
|
300
|
+
throw new TelemetryQueryInputError(
|
|
301
|
+
`telemetry filter exceeds ${MAX_MEMBERSHIP_IDS} distinct ids (got ${unique.length}); scope the read by family or tenant instead`,
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
if (unique.length === 0) return sql`false`;
|
|
305
|
+
if (isPostgres(db)) {
|
|
306
|
+
return sql`${column} = ANY(${unique}::${sql.raw(cast)})`;
|
|
307
|
+
}
|
|
308
|
+
if (unique.length <= SQLITE_IDS_PER_CHUNK) return inArray(column, unique);
|
|
309
|
+
const chunks: SQL[] = [];
|
|
310
|
+
for (let start = 0; start < unique.length; start += SQLITE_IDS_PER_CHUNK) {
|
|
311
|
+
chunks.push(inArray(column, unique.slice(start, start + SQLITE_IDS_PER_CHUNK)));
|
|
312
|
+
}
|
|
313
|
+
return or(...chunks) as SQL;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Membership of `api_key_id` in the family of one share template — the
|
|
318
|
+
* template itself plus every key it issued.
|
|
319
|
+
*
|
|
320
|
+
* The primary predicate is the denormalized `family_template_key_id` column:
|
|
321
|
+
* an event is in the family when it was stamped with the template at write
|
|
322
|
+
* time. That makes the read independent of the key row surviving — a revoked
|
|
323
|
+
* or removed key keeps its history. Events written before the column existed
|
|
324
|
+
* (or whose key was hard-deleted before the backfill) fall back to an
|
|
325
|
+
* `EXISTS` against `api_keys`, so the migration window is still counted when
|
|
326
|
+
* the key row is present. The tenant predicate is carried by every read
|
|
327
|
+
* already, so a child in another tenant still cannot match.
|
|
328
|
+
*/
|
|
329
|
+
function familyKeyCondition(templateKeyId: string): SQL {
|
|
330
|
+
return sql`(${telemetryEvents.familyTemplateKeyId} = ${templateKeyId} or (${telemetryEvents.familyTemplateKeyId} is null and exists (select 1 from ${apiKeys} where ${apiKeys.id} = ${telemetryEvents.apiKeyId} and (${apiKeys.id} = ${templateKeyId} or ${apiKeys.parentKeyId} = ${templateKeyId}))))`;
|
|
331
|
+
}
|
|
332
|
+
|
|
234
333
|
/**
|
|
235
334
|
* Bounded detail/list read — the only place rows are materialized.
|
|
236
335
|
*
|
|
@@ -249,16 +348,19 @@ export async function queryTelemetryEvents(
|
|
|
249
348
|
conditions.push(sql`${telemetryEvents.createdAt} < ${query.until}`);
|
|
250
349
|
}
|
|
251
350
|
if (query.apiKeyIds !== undefined) {
|
|
252
|
-
conditions.push(
|
|
351
|
+
conditions.push(idMembershipFilter(db, telemetryEvents.apiKeyId, query.apiKeyIds, "uuid[]"));
|
|
352
|
+
}
|
|
353
|
+
if (query.familyApiKeyId !== undefined) {
|
|
354
|
+
conditions.push(familyKeyCondition(query.familyApiKeyId));
|
|
253
355
|
}
|
|
254
356
|
if (query.accountIds !== undefined) {
|
|
255
|
-
conditions.push(
|
|
357
|
+
conditions.push(idMembershipFilter(db, telemetryEvents.accountId, query.accountIds, "uuid[]"));
|
|
256
358
|
}
|
|
257
359
|
if (query.requestId !== undefined) {
|
|
258
360
|
conditions.push(eq(telemetryEvents.requestId, query.requestId));
|
|
259
361
|
}
|
|
260
362
|
if (query.requestIds !== undefined) {
|
|
261
|
-
conditions.push(
|
|
363
|
+
conditions.push(idMembershipFilter(db, telemetryEvents.requestId, query.requestIds, "text[]"));
|
|
262
364
|
}
|
|
263
365
|
if (query.id !== undefined) {
|
|
264
366
|
conditions.push(eq(telemetryEvents.id, query.id));
|
|
@@ -638,14 +740,23 @@ export async function healthTelemetry(
|
|
|
638
740
|
export async function sumTelemetryTokens(
|
|
639
741
|
db: CartethyiaDatabase,
|
|
640
742
|
tenantId: string,
|
|
641
|
-
filters: {
|
|
743
|
+
filters: {
|
|
744
|
+
readonly apiKeyIds?: readonly string[];
|
|
745
|
+
/** The share-template family filter; see {@link EventListQuery.familyApiKeyId}. */
|
|
746
|
+
readonly familyKeyId?: string;
|
|
747
|
+
readonly since: Date;
|
|
748
|
+
readonly until?: Date;
|
|
749
|
+
},
|
|
642
750
|
): Promise<number> {
|
|
643
751
|
const conditions = [scoped(tenantId, filters.since)];
|
|
644
752
|
if (filters.until !== undefined) {
|
|
645
753
|
conditions.push(sql`${telemetryEvents.createdAt} < ${filters.until}`);
|
|
646
754
|
}
|
|
647
755
|
if (filters.apiKeyIds !== undefined && filters.apiKeyIds.length > 0) {
|
|
648
|
-
conditions.push(
|
|
756
|
+
conditions.push(idMembershipFilter(db, telemetryEvents.apiKeyId, filters.apiKeyIds, "uuid[]"));
|
|
757
|
+
}
|
|
758
|
+
if (filters.familyKeyId !== undefined) {
|
|
759
|
+
conditions.push(familyKeyCondition(filters.familyKeyId));
|
|
649
760
|
}
|
|
650
761
|
const [row] = await db
|
|
651
762
|
.select({
|
package/src/persistence/db-pg.ts
CHANGED
|
@@ -108,6 +108,44 @@ export function poolMaxFromEnv(): number {
|
|
|
108
108
|
return value;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
/** Target number of idle connections the pool shrinks to (default 2). */
|
|
112
|
+
export function poolIdleFloorFromEnv(): number {
|
|
113
|
+
const raw = process.env.DATABASE_POOL_IDLE_FLOOR ?? "2";
|
|
114
|
+
const value = Number(raw);
|
|
115
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
116
|
+
throw new Error(`DATABASE_POOL_IDLE_FLOOR must be a non-negative integer (got "${raw}")`);
|
|
117
|
+
}
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Periodically closes idle clients above the floor so a low-traffic process
|
|
123
|
+
* does not hold `DATABASE_POOL_MAX` backends open for nothing. node-postgres
|
|
124
|
+
* only closes an idle client after `idleTimeoutMillis` (60s here); this gives
|
|
125
|
+
* the pool a faster, explicit shrink path while still keeping the floor warm.
|
|
126
|
+
*/
|
|
127
|
+
export function startPoolShrinker(pool: Pool, floor: number): () => void {
|
|
128
|
+
const interval = setInterval(() => {
|
|
129
|
+
try {
|
|
130
|
+
const idle = pool.idleCount;
|
|
131
|
+
const excess = idle - floor;
|
|
132
|
+
if (excess <= 0) return;
|
|
133
|
+
// Close the excess idle clients in order; `release(true)` destroys the
|
|
134
|
+
// client instead of returning it to the pool.
|
|
135
|
+
for (let index = 0; index < excess; index += 1) {
|
|
136
|
+
pool
|
|
137
|
+
.connect()
|
|
138
|
+
.then((client) => client.release(true))
|
|
139
|
+
.catch(() => undefined);
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
// The pool is best-effort; a failed shrink must never crash a request.
|
|
143
|
+
}
|
|
144
|
+
}, 30_000);
|
|
145
|
+
interval.unref();
|
|
146
|
+
return () => clearInterval(interval);
|
|
147
|
+
}
|
|
148
|
+
|
|
111
149
|
declare global {
|
|
112
150
|
// eslint-disable-next-line no-var -- globalThis augmentation requires `var`
|
|
113
151
|
var __cartethyiaPool: Pool | undefined;
|
|
@@ -147,6 +185,10 @@ export function getPool(): Pool {
|
|
|
147
185
|
pool.on("error", (err) => {
|
|
148
186
|
log.error("[postgres] pool error (idle client)", err);
|
|
149
187
|
});
|
|
188
|
+
// Idle-floor shrinker: keeps the pool warm at the floor instead of parking
|
|
189
|
+
// a full `max` worth of backends. Tied to the pool's lifetime, so a hot
|
|
190
|
+
// reload that replaces the pool re-registers it.
|
|
191
|
+
startPoolShrinker(pool, poolIdleFloorFromEnv());
|
|
150
192
|
globalThis.__cartethyiaPool = pool;
|
|
151
193
|
return pool;
|
|
152
194
|
}
|
|
@@ -183,6 +183,31 @@ export const providers = pgTable(
|
|
|
183
183
|
// tenant-owned/BYOK. The concurrency ceiling lives in provider routing
|
|
184
184
|
// (`provider_routing_settings.max_inflight`) and network pools
|
|
185
185
|
// (`network_pools.max_inflight`); there is no per-account override.
|
|
186
|
+
/**
|
|
187
|
+
* A Qoder native machine identity (UMID) bound to one account.
|
|
188
|
+
*
|
|
189
|
+
* The device identity is a deterministic function of a seed file
|
|
190
|
+
* (`~/.config/.locale_cfg`): the `runtime-info` bridge reads that seed and
|
|
191
|
+
* derives `machineToken`/`machineType`/`machineCode`. The server gates the
|
|
192
|
+
* daily 100-credit campaign on a *genuine, non-VM* device identity, so each
|
|
193
|
+
* account gets its own minted seed (a distinct "device").
|
|
194
|
+
*
|
|
195
|
+
* `seed` is the exact seed-file content the identity was minted from, stored
|
|
196
|
+
* so the same device identity can be replayed (restore seed → bridge → same
|
|
197
|
+
* token). It is not a credential, but it is the per-account device material
|
|
198
|
+
* and must never be shared between accounts. `machineToken` is the unique
|
|
199
|
+
* device key: two accounts sharing a token are the same device to the server.
|
|
200
|
+
*/
|
|
201
|
+
export interface NativeIdentityRecord {
|
|
202
|
+
readonly machineToken: string;
|
|
203
|
+
readonly machineType: string;
|
|
204
|
+
readonly machineCode: string;
|
|
205
|
+
/** The seed-file content this identity was minted from, if captured. */
|
|
206
|
+
readonly seed?: string;
|
|
207
|
+
/** How this identity was produced: the genuine SG bridge or the fallback. */
|
|
208
|
+
readonly source?: "runtime-info" | "generated";
|
|
209
|
+
}
|
|
210
|
+
|
|
186
211
|
export const providerAccounts = pgTable("provider_accounts", {
|
|
187
212
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
188
213
|
providerId: text("provider_id")
|
|
@@ -270,6 +295,18 @@ export const providerAccounts = pgTable("provider_accounts", {
|
|
|
270
295
|
*/
|
|
271
296
|
quotaPlan: text("quota_plan"),
|
|
272
297
|
|
|
298
|
+
/**
|
|
299
|
+
* Qoder native machine identity bound to this account, as one JSON object
|
|
300
|
+
* (see {@link NativeIdentityRecord}). `null` means the account has no bound
|
|
301
|
+
* identity: the check-in mints one on first use.
|
|
302
|
+
*
|
|
303
|
+
* Deliberately a dedicated column, not a key inside `auth_state`: the
|
|
304
|
+
* device token is queryable ("which device is taken by whom") and enforced
|
|
305
|
+
* unique by the partial index below, and it must survive backup/restore
|
|
306
|
+
* like any other non-secret column.
|
|
307
|
+
*/
|
|
308
|
+
nativeIdentity: jsonb("native_identity").$type<NativeIdentityRecord | null>(),
|
|
309
|
+
|
|
273
310
|
},
|
|
274
311
|
(table) => [
|
|
275
312
|
uniqueIndex("provider_accounts_identity_uidx").on(
|
|
@@ -290,6 +327,17 @@ export const providerAccounts = pgTable("provider_accounts", {
|
|
|
290
327
|
// predicate that does not constrain `provider_id`. This one matches the
|
|
291
328
|
// hot listing path.
|
|
292
329
|
index("provider_accounts_tenant_id_idx").on(table.tenantId),
|
|
330
|
+
// One Qoder device identity per account: the partial unique index on the
|
|
331
|
+
// extracted `machineToken` guarantees two accounts never share a device,
|
|
332
|
+
// which is what makes the daily 100-credit grant reusable per account
|
|
333
|
+
// instead of colliding on a device identity. `NULL` rows (unbound
|
|
334
|
+
// accounts) are excluded so the uniqueness only applies to bound
|
|
335
|
+
// identities. The Postgres migration spells the expression
|
|
336
|
+
// `(native_identity ->> 'machineToken')`; the SQLite migration spells it
|
|
337
|
+
// `json_extract(native_identity, '$.machineToken')`.
|
|
338
|
+
uniqueIndex("provider_accounts_native_identity_token_uidx")
|
|
339
|
+
.on(sql`(${table.nativeIdentity} ->> 'machineToken')`)
|
|
340
|
+
.where(sql`${table.nativeIdentity} IS NOT NULL`),
|
|
293
341
|
],
|
|
294
342
|
);
|
|
295
343
|
|
|
@@ -766,6 +814,13 @@ export const apiKeys = pgTable(
|
|
|
766
814
|
.notNull()
|
|
767
815
|
.default(0),
|
|
768
816
|
maxConcurrentRequests: integer("max_concurrent_requests"),
|
|
817
|
+
/**
|
|
818
|
+
* Slot cap for a share template: the maximum number of concurrently
|
|
819
|
+
* active child keys it may mint. `null` = unlimited (legacy behavior).
|
|
820
|
+
* Enforced at mint time in `issueSharedApiKey`; the console exposes it as
|
|
821
|
+
* the template's "slots".
|
|
822
|
+
*/
|
|
823
|
+
maxChildren: integer("max_children"),
|
|
769
824
|
/**
|
|
770
825
|
* `whitelist` = only the names in `modelList` may be used (empty = all);
|
|
771
826
|
* `blacklist` = the names in `modelList` are refused (empty = none). One
|
|
@@ -1126,6 +1181,26 @@ export const consoleSettings = pgTable("console_settings", {
|
|
|
1126
1181
|
updatedAt: updatedAtColumn(),
|
|
1127
1182
|
});
|
|
1128
1183
|
|
|
1184
|
+
// Tenant-scoped system prompt injection (append/override). One row per
|
|
1185
|
+
// tenant; `global` NULL means never configured, distinct from an explicit
|
|
1186
|
+
// disabled setting. Providers/models are JSON object maps keyed by
|
|
1187
|
+
// providerId and `providerId::modelId` respectively — see
|
|
1188
|
+
// `src/console/system-prompt/contracts.ts` for the value shapes and the
|
|
1189
|
+
// resolution rules implemented by the dispatch-path transform.
|
|
1190
|
+
export const tenantSystemPrompts = pgTable("tenant_system_prompts", {
|
|
1191
|
+
tenantId: tenantRefPrimaryKey(),
|
|
1192
|
+
global: jsonb("global").$type<import("../console/system-prompt/contracts").PromptSetting | null>(),
|
|
1193
|
+
providers: jsonb("providers")
|
|
1194
|
+
.notNull()
|
|
1195
|
+
.default({})
|
|
1196
|
+
.$type<Record<string, import("../console/system-prompt/contracts").ProviderPromptEntry>>(),
|
|
1197
|
+
models: jsonb("models")
|
|
1198
|
+
.notNull()
|
|
1199
|
+
.default({})
|
|
1200
|
+
.$type<Record<string, import("../console/system-prompt/contracts").PromptSetting>>(),
|
|
1201
|
+
updatedAt: updatedAtColumn(),
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1129
1204
|
// InferHub `config_read` key. One row for the whole deployment.
|
|
1130
1205
|
//
|
|
1131
1206
|
// Deliberately NOT `provider_accounts.auth_state`: that field is documented as
|
|
@@ -1229,6 +1304,14 @@ export const telemetryEvents = pgTable(
|
|
|
1229
1304
|
networkPoolId: uuid("network_pool_id"),
|
|
1230
1305
|
endpoint: text("endpoint"),
|
|
1231
1306
|
apiKeyId: uuid("api_key_id"),
|
|
1307
|
+
/**
|
|
1308
|
+
* The share-template key this event belongs to (the template itself, or
|
|
1309
|
+
* the parent of the child key that carried the request). Denormalized at
|
|
1310
|
+
* write time so family rollups survive the key row being deleted — an
|
|
1311
|
+
* `EXISTS` join against `api_keys` tied history to key lifetime. NULL for
|
|
1312
|
+
* requests that ran under no share template.
|
|
1313
|
+
*/
|
|
1314
|
+
familyTemplateKeyId: uuid("family_template_key_id"),
|
|
1232
1315
|
userAgent: text("user_agent"),
|
|
1233
1316
|
clientIp: text("client_ip"),
|
|
1234
1317
|
latencyMs: integer("latency_ms"),
|
|
@@ -1317,6 +1400,14 @@ export const telemetryEvents = pgTable(
|
|
|
1317
1400
|
desc(table.createdAt),
|
|
1318
1401
|
desc(table.id),
|
|
1319
1402
|
),
|
|
1403
|
+
// Share-family rollup: equality on the denormalized template, keyset
|
|
1404
|
+
// range+order. Backs both the family totals and the hourly chart reads.
|
|
1405
|
+
index("telemetry_events_tenant_family_created_id_idx").on(
|
|
1406
|
+
table.tenantId,
|
|
1407
|
+
table.familyTemplateKeyId,
|
|
1408
|
+
desc(table.createdAt),
|
|
1409
|
+
desc(table.id),
|
|
1410
|
+
),
|
|
1320
1411
|
index("telemetry_events_request_id_idx").on(table.requestId),
|
|
1321
1412
|
index("telemetry_events_tenant_account_created_idx").on(
|
|
1322
1413
|
table.tenantId,
|
|
@@ -1373,6 +1464,13 @@ export const telemetryUsageTotals = pgTable(
|
|
|
1373
1464
|
tenantId: tenantRefRequired(),
|
|
1374
1465
|
identityType: text("identity_type").$type<TelemetryUsageIdentityType>().notNull(),
|
|
1375
1466
|
entityId: uuid("entity_id").notNull(),
|
|
1467
|
+
/**
|
|
1468
|
+
* For `api_key` identities, the share-template key this key belongs to
|
|
1469
|
+
* (the template itself, or the parent of a minted child). Denormalized so
|
|
1470
|
+
* family reads do not depend on the key row surviving; NULL for `account`
|
|
1471
|
+
* identities and for keys outside a share family.
|
|
1472
|
+
*/
|
|
1473
|
+
familyTemplateKeyId: uuid("family_template_key_id"),
|
|
1376
1474
|
requests: bigint("requests", { mode: "number" }).notNull().default(0),
|
|
1377
1475
|
errors: bigint("errors", { mode: "number" }).notNull().default(0),
|
|
1378
1476
|
inputTokens: bigint("input_tokens", { mode: "number" }).notNull().default(0),
|
|
@@ -1389,6 +1487,10 @@ export const telemetryUsageTotals = pgTable(
|
|
|
1389
1487
|
"telemetry_usage_totals_identity_type_check",
|
|
1390
1488
|
sql`${table.identityType} IN ('account', 'api_key')`,
|
|
1391
1489
|
),
|
|
1490
|
+
index("telemetry_usage_totals_tenant_family_idx").on(
|
|
1491
|
+
table.tenantId,
|
|
1492
|
+
table.familyTemplateKeyId,
|
|
1493
|
+
),
|
|
1392
1494
|
],
|
|
1393
1495
|
);
|
|
1394
1496
|
|