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
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { isBundledProviderId } from "../../providers/provider-contracts";
|
|
2
2
|
import type { ProviderDispatchContext, ProviderId, ProviderAdapter } from "../../providers/provider-contracts";
|
|
3
3
|
import { GatewayError } from "../../protocol/gateway-error";
|
|
4
|
+
import { isWrappedInjectionProvider } from "../../console/system-prompt/contracts";
|
|
5
|
+
import { resolveTenantSystemPrompt } from "../request/system-prompt-injection";
|
|
6
|
+
import { readTenantSystemPromptsForDispatch } from "../request/tenant-preferences";
|
|
4
7
|
import type { CanonicalEvent } from "../../protocol/canonical-model";
|
|
5
8
|
import { resolveCredentialForAccount } from "../../providers/operations/provider-credential-service";
|
|
6
9
|
import type { ByokUpstreamHost } from "../../providers/operations/provider-catalog-service";
|
|
@@ -115,6 +118,26 @@ export async function handleProviderProxyRequest(
|
|
|
115
118
|
if (!prepared)
|
|
116
119
|
throw new GatewayError("admission_unavailable", 503, "The request was not prepared, so no route can be dialed. Resend the request.");
|
|
117
120
|
const canonicalRequest = await applyTenantPreferences(prepared, deps.db);
|
|
121
|
+
// Tenant system-prompt injection, resolved once per request. For providers
|
|
122
|
+
// whose upstream validates the fixed system prompt as a channel identity
|
|
123
|
+
// (the buddy family), the text cannot ride the canonical `system` array —
|
|
124
|
+
// the adapter replaces it — so it travels through dispatch context and the
|
|
125
|
+
// adapter folds it into its fixed turn, wrapped in the gateway's markers.
|
|
126
|
+
// `override` degrades to wrapped append there: stripping the identity would
|
|
127
|
+
// have the upstream refuse the request outright.
|
|
128
|
+
const candidateForPrompt = prepared.candidate;
|
|
129
|
+
const tenantSystemPromptText = isWrappedInjectionProvider(candidateForPrompt.provider_id)
|
|
130
|
+
? resolveTenantSystemPrompt(
|
|
131
|
+
await readTenantSystemPromptsForDispatch(
|
|
132
|
+
deps.db,
|
|
133
|
+
prepared.authorization.snapshot.tenant_id,
|
|
134
|
+
),
|
|
135
|
+
{
|
|
136
|
+
providerId: candidateForPrompt.provider_id,
|
|
137
|
+
modelId: candidateForPrompt.model_id,
|
|
138
|
+
},
|
|
139
|
+
)?.text
|
|
140
|
+
: undefined;
|
|
118
141
|
// Per-tenant timeout, applied as soon as the tenant is known. The deadline
|
|
119
142
|
// was armed at admission with the deployment default; this re-arms it.
|
|
120
143
|
await applyTenantDeadlineOverride(
|
|
@@ -153,8 +176,13 @@ export async function handleProviderProxyRequest(
|
|
|
153
176
|
estimatedOutputTokens: prepared.estimatedOutputTokens,
|
|
154
177
|
authorizationSnapshot: prepared.authorization.snapshot,
|
|
155
178
|
// A continuation borrows the held Cursor Run: one upstream turn must not
|
|
156
|
-
// occupy two account/pool concurrency slots.
|
|
157
|
-
|
|
179
|
+
// occupy two account/pool concurrency slots. A rebaseable continuation
|
|
180
|
+
// (complete history, pinned account no longer routable) does NOT borrow —
|
|
181
|
+
// it starts a fresh Run on a newly selected account, so it leases like a
|
|
182
|
+
// normal request.
|
|
183
|
+
...(prepared.continuationAffinity === undefined || prepared.continuationAffinity.rebaseable
|
|
184
|
+
? {}
|
|
185
|
+
: { borrowsHeldSession: true }),
|
|
158
186
|
},
|
|
159
187
|
candidates: attemptCandidates,
|
|
160
188
|
tenantId: prepared.authorization.tenantId,
|
|
@@ -231,6 +259,7 @@ export async function handleProviderProxyRequest(
|
|
|
231
259
|
? {}
|
|
232
260
|
: { imageClampMaxBytes: state.imageClampMaxBytes }),
|
|
233
261
|
...(conversationAffinity ? { conversationAffinity } : {}),
|
|
262
|
+
...(tenantSystemPromptText === undefined ? {} : { tenantSystemPrompt: tenantSystemPromptText }),
|
|
234
263
|
...(candidate.user_agent === undefined ? {} : { userAgent: candidate.user_agent }),
|
|
235
264
|
...(deps.networkBindingFactory
|
|
236
265
|
? {
|
|
@@ -50,7 +50,9 @@ import {
|
|
|
50
50
|
resolveCaptureModeForRequest,
|
|
51
51
|
shouldCaptureProviderBodyForRequest,
|
|
52
52
|
} from "../capture-policy";
|
|
53
|
-
import { resolveTenantImageClampMaxBytes } from "../../request/tenant-preferences";
|
|
53
|
+
import { resolveTenantImageClampMaxBytes, readTenantSystemPromptsForDispatch } from "../../request/tenant-preferences";
|
|
54
|
+
import { resolveTenantSystemPrompt } from "../../request/system-prompt-injection";
|
|
55
|
+
import { isWrappedInjectionProvider } from "../../../console/system-prompt/contracts";
|
|
54
56
|
import { proxySuccessHeaders, buildUpstreamDispatchContext } from "../upstream";
|
|
55
57
|
import type { AttemptLeaseOwnership } from "../leases";
|
|
56
58
|
import type { ProviderContinuationScope } from "../../../providers/provider-contracts";
|
|
@@ -217,6 +219,19 @@ export async function dispatchStreamingAttempt(input: StreamingDispatchInput): P
|
|
|
217
219
|
? {}
|
|
218
220
|
: { imageClampMaxBytes: state.imageClampMaxBytes }),
|
|
219
221
|
...(conversationAffinity ? { conversationAffinity } : {}),
|
|
222
|
+
// Same wrapped-injection text the non-streaming path carries: providers
|
|
223
|
+
// whose upstream validates the fixed system prompt as a channel identity
|
|
224
|
+
// (the buddy family) receive the tenant prompt through dispatch context
|
|
225
|
+
// so the adapter can fold it into its fixed turn. `override` degrades to
|
|
226
|
+
// wrapped append there; stripping the identity would be refused outright.
|
|
227
|
+
...(await (async () => {
|
|
228
|
+
if (!isWrappedInjectionProvider(candidate.provider_id)) return {};
|
|
229
|
+
const text = resolveTenantSystemPrompt(
|
|
230
|
+
await readTenantSystemPromptsForDispatch(deps.db, prepared.authorization.snapshot.tenant_id),
|
|
231
|
+
{ providerId: candidate.provider_id, modelId: candidate.model_id },
|
|
232
|
+
)?.text;
|
|
233
|
+
return text === undefined ? {} : { tenantSystemPrompt: text };
|
|
234
|
+
})()),
|
|
220
235
|
...(candidate.user_agent === undefined ? {} : { userAgent: candidate.user_agent }),
|
|
221
236
|
...(wrappedOutboundFetch ? { outboundFetch: wrappedOutboundFetch } : {}),
|
|
222
237
|
...(outboundFetch ? { outboundAttachmentFetch: outboundFetch } : {}),
|
|
@@ -61,6 +61,13 @@ export function buildUpstreamDispatchContext(input: {
|
|
|
61
61
|
* database handle.
|
|
62
62
|
*/
|
|
63
63
|
readonly imageClampMaxBytes?: number;
|
|
64
|
+
/**
|
|
65
|
+
* Tenant system-prompt text resolved once per request (append/override
|
|
66
|
+
* injection). Carried through dispatch because the buddy adapters fold it
|
|
67
|
+
* into their fixed system turn after serialization — the canonical level
|
|
68
|
+
* cannot reach that turn.
|
|
69
|
+
*/
|
|
70
|
+
readonly tenantSystemPrompt?: string;
|
|
64
71
|
/** Opaque identity shared by retries of one logical gateway request. */
|
|
65
72
|
readonly requestIdentity?: object;
|
|
66
73
|
}): Record<string, unknown> {
|
|
@@ -101,6 +108,9 @@ export function buildUpstreamDispatchContext(input: {
|
|
|
101
108
|
...(input.imageClampMaxBytes === undefined
|
|
102
109
|
? {}
|
|
103
110
|
: { image_clamp_max_bytes: input.imageClampMaxBytes }),
|
|
111
|
+
...(input.tenantSystemPrompt === undefined
|
|
112
|
+
? {}
|
|
113
|
+
: { tenant_system_prompt: input.tenantSystemPrompt }),
|
|
104
114
|
...(input.requestIdentity ? { request_identity: input.requestIdentity } : {}),
|
|
105
115
|
} as Record<string, unknown>;
|
|
106
116
|
}
|
|
@@ -2,6 +2,7 @@ import { GatewayError } from "../../protocol/gateway-error";
|
|
|
2
2
|
import type { CanonicalRequest, ServiceKind } from "../../protocol/canonical-model";
|
|
3
3
|
import { toolResultParts } from "../../protocol/canonical-model";
|
|
4
4
|
import type { ApiKeyAdmissionService } from "../../security/admission/service";
|
|
5
|
+
import type { CallerIdentity } from "../../security/access-control";
|
|
5
6
|
import type { RouteCandidate as RouteCandidate, InMemoryRouteSnapshotService, RoutePlan } from "../routing/route-model";
|
|
6
7
|
import type { RouteContinuationAffinity } from "../routing/route-model";
|
|
7
8
|
import { resolveAliasTarget, type RoutingEngine } from "../routing/router";
|
|
@@ -80,6 +81,8 @@ export function resolveContinuationAffinity(
|
|
|
80
81
|
apiKeyId: authorization.id,
|
|
81
82
|
};
|
|
82
83
|
let affinity: RouteContinuationAffinity | undefined;
|
|
84
|
+
let anyResolved = false;
|
|
85
|
+
let allResolved = true;
|
|
83
86
|
for (const callId of ids) {
|
|
84
87
|
// The registry resolves by tenant + API key + provider + account, so a call
|
|
85
88
|
// id replayed from another key or tenant resolves nothing.
|
|
@@ -91,6 +94,7 @@ export function resolveContinuationAffinity(
|
|
|
91
94
|
// through the ids that still resolve; when none do, the ordinary routing
|
|
92
95
|
// path decides (a Cursor adapter may rebase the full history into a
|
|
93
96
|
// fresh Run).
|
|
97
|
+
allResolved = false;
|
|
94
98
|
continue;
|
|
95
99
|
}
|
|
96
100
|
if (affinity !== undefined && affinity.providerAccountId !== record.providerAccountId) {
|
|
@@ -101,12 +105,21 @@ export function resolveContinuationAffinity(
|
|
|
101
105
|
{ callIds: [...ids] },
|
|
102
106
|
);
|
|
103
107
|
}
|
|
108
|
+
anyResolved = true;
|
|
104
109
|
affinity = {
|
|
105
110
|
providerId: record.providerId,
|
|
106
111
|
providerAccountId: record.providerAccountId,
|
|
112
|
+
rebaseable: false,
|
|
107
113
|
};
|
|
108
114
|
}
|
|
109
|
-
|
|
115
|
+
if (affinity !== undefined) {
|
|
116
|
+
// Rebaseable when every supplied call id resolved to a live pending call
|
|
117
|
+
// (so none is owed to another account) — or when none resolved and the
|
|
118
|
+
// whole history is present. In both cases the conversation is complete and
|
|
119
|
+
// may travel as history to a fresh Run.
|
|
120
|
+
return { ...affinity, rebaseable: anyResolved ? allResolved : true };
|
|
121
|
+
}
|
|
122
|
+
return undefined;
|
|
110
123
|
}
|
|
111
124
|
|
|
112
125
|
/** Looks up one pending call across every provider that records continuations. */
|
|
@@ -137,6 +150,8 @@ export interface PreparedProxyRequest {
|
|
|
137
150
|
* second account and pool slot.
|
|
138
151
|
*/
|
|
139
152
|
readonly continuationAffinity?: RouteContinuationAffinity;
|
|
153
|
+
/** Single "who is speaking" identity, derived at auth. */
|
|
154
|
+
readonly caller: CallerIdentity;
|
|
140
155
|
readonly estimatedInputTokens: number;
|
|
141
156
|
readonly estimatedOutputTokens: number;
|
|
142
157
|
readonly deadlineMs: number;
|
|
@@ -407,6 +422,7 @@ export class ProxyRequestPreparer {
|
|
|
407
422
|
degradedCapabilities: degraded,
|
|
408
423
|
plan,
|
|
409
424
|
...(continuationAffinity === undefined ? {} : { continuationAffinity }),
|
|
425
|
+
caller: authorization.caller,
|
|
410
426
|
estimatedInputTokens,
|
|
411
427
|
estimatedOutputTokens,
|
|
412
428
|
deadlineMs: input.deadlineMs,
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tenant system-prompt injection at the canonical level.
|
|
3
|
+
*
|
|
4
|
+
* Runs inside `applyTenantPreferences`, after reasoning/thinking shaping and
|
|
5
|
+
* before RTK/PonyTail, so the injected text composes with (and is then
|
|
6
|
+
* appended-to by) the compression directive. Wire-agnostic: one transform
|
|
7
|
+
* covers messages/chat/responses/gemini/codex/kiro; provider-native
|
|
8
|
+
* injection (Claude identity/billing prepend, buddy fixed system turn,
|
|
9
|
+
* antigravity identity) happens later, in the adapters, and is untouched.
|
|
10
|
+
*
|
|
11
|
+
* Resolution is most-specific-wins: model > provider > global, where a
|
|
12
|
+
* provider either follows the tenant's global prompt or carries a custom one.
|
|
13
|
+
* Failure is non-fatal — an unusable configuration must not fail a
|
|
14
|
+
* dispatchable request.
|
|
15
|
+
*/
|
|
16
|
+
import type { CanonicalRequest } from "../../protocol/canonical-model";
|
|
17
|
+
import type { ServiceKind } from "../../protocol/canonical-model";
|
|
18
|
+
import type {
|
|
19
|
+
PromptSetting,
|
|
20
|
+
TenantSystemPrompts,
|
|
21
|
+
} from "../../console/system-prompt/contracts";
|
|
22
|
+
import {
|
|
23
|
+
systemPromptModelKey,
|
|
24
|
+
} from "../../console/system-prompt/contracts";
|
|
25
|
+
|
|
26
|
+
/** The route facts the resolution needs. */
|
|
27
|
+
export interface SystemPromptRoute {
|
|
28
|
+
readonly providerId: string;
|
|
29
|
+
readonly modelId: string;
|
|
30
|
+
readonly serviceKind: ServiceKind;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resolves the effective prompt for a route: the most-specific enabled scope
|
|
35
|
+
* wins. A provider scope honours its `followGlobal` toggle; model scope is
|
|
36
|
+
* always explicit. Returns `undefined` when nothing applies.
|
|
37
|
+
*/
|
|
38
|
+
export function resolveTenantSystemPrompt(
|
|
39
|
+
prompts: TenantSystemPrompts | undefined,
|
|
40
|
+
route: Pick<SystemPromptRoute, "providerId" | "modelId">,
|
|
41
|
+
): PromptSetting | undefined {
|
|
42
|
+
if (prompts === undefined) return undefined;
|
|
43
|
+
const model = prompts.models[systemPromptModelKey(route.providerId, route.modelId)];
|
|
44
|
+
if (model !== undefined && model.enabled) return model;
|
|
45
|
+
const provider = prompts.providers[route.providerId];
|
|
46
|
+
if (provider !== undefined) {
|
|
47
|
+
if (provider.followGlobal) {
|
|
48
|
+
return prompts.global !== null && prompts.global.enabled ? prompts.global : undefined;
|
|
49
|
+
}
|
|
50
|
+
if (provider.setting !== undefined && provider.setting.enabled) return provider.setting;
|
|
51
|
+
}
|
|
52
|
+
return prompts.global !== null && prompts.global.enabled ? prompts.global : undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Marker text guard so retries never double-inject. */
|
|
56
|
+
function carriesText(system: readonly { kind: string }[] | undefined, text: string): boolean {
|
|
57
|
+
return (system ?? []).some((part) => part.kind === "text" && (part as { text?: string }).text === text);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Applies the resolved tenant prompt to the canonical request. Appends after
|
|
62
|
+
* existing system content (or replaces the caller's system content for
|
|
63
|
+
* `override` — provider-native identity injection later re-adds the fixed
|
|
64
|
+
* provider prompt, so override is never a bare wire).
|
|
65
|
+
*/
|
|
66
|
+
export function applyInjectSystemPrompt(
|
|
67
|
+
request: CanonicalRequest,
|
|
68
|
+
prompts: TenantSystemPrompts | undefined,
|
|
69
|
+
route: SystemPromptRoute,
|
|
70
|
+
): CanonicalRequest {
|
|
71
|
+
// Non-chat native protocols have no system surface to inject into.
|
|
72
|
+
if (route.serviceKind !== "llm") return request;
|
|
73
|
+
const setting = resolveTenantSystemPrompt(prompts, route);
|
|
74
|
+
if (setting === undefined || setting.text.length === 0) return request;
|
|
75
|
+
|
|
76
|
+
const text = setting.text;
|
|
77
|
+
// `override` replaces only the CALLER's system content: the adapters later
|
|
78
|
+
// prepend their mandatory identity/billing blocks (claude, antigravity) or
|
|
79
|
+
// install their fixed system turn (buddy), so the wire always keeps the
|
|
80
|
+
// provider-native prompt. For wrapped-injection providers the override
|
|
81
|
+
// cannot survive the replace at all — the identity must stay the opener —
|
|
82
|
+
// so it degrades to append there.
|
|
83
|
+
const existing = request.system ?? [];
|
|
84
|
+
if (setting.mode === "override" && existing.length > 0) {
|
|
85
|
+
return { ...request, system: [{ kind: "text", text }] };
|
|
86
|
+
}
|
|
87
|
+
if (carriesText(existing, text)) return request;
|
|
88
|
+
return { ...request, system: [...existing, { kind: "text", text }] };
|
|
89
|
+
}
|
|
@@ -12,9 +12,42 @@ import type { CanonicalRequest } from "../../protocol/canonical-model";
|
|
|
12
12
|
import type { PreparedProxyRequest } from "./preparer";
|
|
13
13
|
import { normalizeThinkingConfig } from "../../protocol/translation/thinking";
|
|
14
14
|
import { compressRequest } from "./rtk/compress-request";
|
|
15
|
+
import { applyInjectSystemPrompt } from "./system-prompt-injection";
|
|
15
16
|
import { preferencesReaderFor } from "../../persistence/tenant-preferences";
|
|
17
|
+
import {
|
|
18
|
+
readTenantSystemPrompts,
|
|
19
|
+
type StoredTenantSystemPrompts,
|
|
20
|
+
} from "../../persistence/system-prompt-store";
|
|
21
|
+
import { currentSettingsRevision } from "../../persistence/settings-revision";
|
|
22
|
+
import { TtlCache } from "../../persistence/ttl-cache";
|
|
16
23
|
import { resolveUpstreamTimeoutMs } from "../../config/timeouts";
|
|
17
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Revision-keyed cached read of the tenant's system-prompt configuration, the
|
|
27
|
+
* same convergence mechanism as the runtime-preferences reader: a mutation
|
|
28
|
+
* bumps the settings revision, which changes every cache key, so writes are
|
|
29
|
+
* visible to the dispatch path immediately without a per-request round trip.
|
|
30
|
+
*/
|
|
31
|
+
const systemPromptCache = new TtlCache<StoredTenantSystemPrompts>({
|
|
32
|
+
ttlMs: 5_000,
|
|
33
|
+
maxEntries: 128,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
async function readTenantSystemPromptsCached(
|
|
37
|
+
db: CartethyiaDatabase,
|
|
38
|
+
tenantId: string,
|
|
39
|
+
): Promise<StoredTenantSystemPrompts> {
|
|
40
|
+
const cacheKey = `${tenantId}:${currentSettingsRevision()}`;
|
|
41
|
+
const cached = systemPromptCache.get(cacheKey);
|
|
42
|
+
if (cached !== undefined) return cached;
|
|
43
|
+
const prompts = await readTenantSystemPrompts(db, tenantId);
|
|
44
|
+
systemPromptCache.set(cacheKey, prompts);
|
|
45
|
+
return prompts;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Cached read for the dispatch path (same revision-keyed cache). */
|
|
49
|
+
export { readTenantSystemPromptsCached as readTenantSystemPromptsForDispatch };
|
|
50
|
+
|
|
18
51
|
/**
|
|
19
52
|
* Re-arms the request deadline from the tenant's timeout override.
|
|
20
53
|
*
|
|
@@ -91,6 +124,16 @@ export async function applyTenantPreferences(
|
|
|
91
124
|
const next = normalizeThinkingConfig(canonicalRequest);
|
|
92
125
|
if (next !== canonicalRequest) canonicalRequest = next;
|
|
93
126
|
}
|
|
127
|
+
// Tenant system-prompt injection runs before compression: the PonyTail
|
|
128
|
+
// directive must stay the tail of the system content, and the injected
|
|
129
|
+
// prompt is itself stable per scope, so the prefix-cache invariant holds.
|
|
130
|
+
const prompts = await readTenantSystemPromptsCached(db, tenantId);
|
|
131
|
+
const candidate = prepared.candidate;
|
|
132
|
+
canonicalRequest = applyInjectSystemPrompt(canonicalRequest, prompts, {
|
|
133
|
+
providerId: candidate.provider_id,
|
|
134
|
+
modelId: candidate.model_id,
|
|
135
|
+
serviceKind: candidate.service_kind ?? "llm",
|
|
136
|
+
});
|
|
94
137
|
if (prefs) {
|
|
95
138
|
// When the routed model is Responses (codex/openai/muse-spark) and the
|
|
96
139
|
// client hit a Chat surface, force the configured summary mode so the
|
|
@@ -17,15 +17,25 @@ import type {
|
|
|
17
17
|
/**
|
|
18
18
|
* The admission bucket one candidate draws from.
|
|
19
19
|
*
|
|
20
|
+
* Bucket identity is `provider:model[:account][:ownerTenant]`:
|
|
21
|
+
* - A candidate with no account (credential-less provider) buckets per
|
|
22
|
+
* provider:model.
|
|
23
|
+
* - A **shared** account (`owner_tenant_id` null) is one bucket across all
|
|
24
|
+
* tenants — the account is pool-wide, so its concurrency ceiling is global.
|
|
25
|
+
* - A **tenant-owned** account is scoped to its owner tenant, so two tenants
|
|
26
|
+
* cannot drain each other's ceiling on an account only one of them owns.
|
|
27
|
+
*
|
|
20
28
|
* Exported so the router can order candidates by the same counters admission
|
|
21
29
|
* enforces against: a second copy of this format would drift and quietly
|
|
22
30
|
* deprioritize the wrong accounts.
|
|
23
31
|
*/
|
|
24
32
|
export function admissionKey(candidate: RouteCandidate): string {
|
|
25
33
|
const base = `${candidate.provider_id}:${candidate.model_id}`;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
if (candidate.provider_account_id === undefined) return base;
|
|
35
|
+
const account = `${base}:${candidate.provider_account_id}`;
|
|
36
|
+
return candidate.owner_tenant_id === undefined || candidate.owner_tenant_id === null
|
|
37
|
+
? account
|
|
38
|
+
: `${account}:${candidate.owner_tenant_id}`;
|
|
29
39
|
}
|
|
30
40
|
|
|
31
41
|
/**
|
|
@@ -670,6 +670,8 @@ class RouteCatalogRepository {
|
|
|
670
670
|
...(model.outputLimit == null ? {} : { output_limit: model.outputLimit }),
|
|
671
671
|
capability_profile: capabilityProfile,
|
|
672
672
|
tenant_id: tId,
|
|
673
|
+
// Public credential-less candidates have no account to own them.
|
|
674
|
+
owner_tenant_id: null,
|
|
673
675
|
requires_account: false as const,
|
|
674
676
|
network_pool_ids: tenantPools.ids,
|
|
675
677
|
network_pool_required: true,
|
|
@@ -696,6 +698,8 @@ class RouteCatalogRepository {
|
|
|
696
698
|
...(model.outputLimit == null ? {} : { output_limit: model.outputLimit }),
|
|
697
699
|
capability_profile: capabilityProfile,
|
|
698
700
|
tenant_id: providerTenantId,
|
|
701
|
+
// Public credential-less candidates have no account to own them.
|
|
702
|
+
owner_tenant_id: null,
|
|
699
703
|
...(providerRequiresAccount
|
|
700
704
|
? { health_status: "disabled" as const }
|
|
701
705
|
: { requires_account: false as const }),
|
|
@@ -738,6 +742,9 @@ class RouteCatalogRepository {
|
|
|
738
742
|
...(routeUserAgent === undefined ? {} : { user_agent: routeUserAgent }),
|
|
739
743
|
tenant_id: rowTenantId,
|
|
740
744
|
provider_account_id: account.id,
|
|
745
|
+
// The account's owning tenant (null = shared pool-wide) — the bucket
|
|
746
|
+
// key for admission, distinct from the requesting tenant.
|
|
747
|
+
owner_tenant_id: account.tenantId ?? null,
|
|
741
748
|
// The plan gate is per (account, model): the model may be refused on
|
|
742
749
|
// a free plan and the account may be on one. Both ride here because
|
|
743
750
|
// `plan()` sees the candidate and nothing else.
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// Routing contracts and snapshots.
|
|
2
2
|
import { GatewayError } from "../../protocol/gateway-error";
|
|
3
3
|
import { type ServiceKind, type WireFamily } from "../../protocol/canonical-model";
|
|
4
|
+
import type { CallerIdentity } from "../../security/access-control";
|
|
5
|
+
|
|
6
|
+
export type { CallerIdentity };
|
|
4
7
|
|
|
5
8
|
|
|
6
9
|
export type RoutingRevision = number;
|
|
@@ -57,6 +60,14 @@ export interface RouteCandidate {
|
|
|
57
60
|
readonly network_pool_provider_limit?: number;
|
|
58
61
|
/** `null`/`undefined` = globally routable; a tenant id = routable only for that tenant's requests (BYOK). */
|
|
59
62
|
readonly tenant_id?: string | null;
|
|
63
|
+
/**
|
|
64
|
+
* The tenant that owns this candidate's account (`provider_accounts.tenant_id`),
|
|
65
|
+
* or null for a shared pool-wide account. Distinct from `tenant_id` (the
|
|
66
|
+
* requesting tenant). Used to scope the admission bucket: a shared account
|
|
67
|
+
* is one bucket across tenants; a tenant-owned account is scoped to its
|
|
68
|
+
* owner so two tenants cannot drain each other's ceiling.
|
|
69
|
+
*/
|
|
70
|
+
readonly owner_tenant_id?: string | null;
|
|
60
71
|
/** `false` only for a genuinely credential-less provider (e.g. OpenCode
|
|
61
72
|
* Free) dispatched with zero `provider_accounts` rows — the handler skips
|
|
62
73
|
* `resolveCredentialForAccount` and dispatches with a `"none"` credential.
|
|
@@ -80,6 +91,11 @@ export interface RouteCandidate {
|
|
|
80
91
|
* "free": guessing free here would defeat the gate it exists to serve.
|
|
81
92
|
*/
|
|
82
93
|
readonly account_plan?: string | null;
|
|
94
|
+
/** Usage-floor ceiling from Routing Strategy; read live at plan time. */
|
|
95
|
+
readonly usage_floor?: number;
|
|
96
|
+
readonly usage_floor_unit?: "credit" | "percent";
|
|
97
|
+
/** Live quota windows (cache) the floor check evaluates. */
|
|
98
|
+
readonly quota_windows?: readonly unknown[];
|
|
83
99
|
}
|
|
84
100
|
|
|
85
101
|
/**
|
|
@@ -163,6 +179,16 @@ export interface PoolRoutingSetting {
|
|
|
163
179
|
export interface RouteContinuationAffinity {
|
|
164
180
|
readonly providerId: string;
|
|
165
181
|
readonly providerAccountId: string;
|
|
182
|
+
/**
|
|
183
|
+
* True when the request's tool-result ids are all already answered by the
|
|
184
|
+
* conversation history (a replay), so no call is still owed to the pinned
|
|
185
|
+
* account. In that case, if the pinned account is no longer routable for
|
|
186
|
+
* the requested model, the request may safely rebase to a fresh Run on an
|
|
187
|
+
* eligible account instead of being refused — the recorded exchange travels
|
|
188
|
+
* as history. When false, the pin is a hard constraint: a live pending call
|
|
189
|
+
* must reach the exact account that owns it.
|
|
190
|
+
*/
|
|
191
|
+
readonly rebaseable: boolean;
|
|
166
192
|
}
|
|
167
193
|
|
|
168
194
|
export interface RoutePlan {
|
|
@@ -171,6 +197,33 @@ export interface RoutePlan {
|
|
|
171
197
|
readonly requested_model: string;
|
|
172
198
|
readonly resolved_model: string;
|
|
173
199
|
readonly provider_id: string;
|
|
200
|
+
/**
|
|
201
|
+
* The system decision behind each candidate, keyed by the candidate's
|
|
202
|
+
* admission bucket. Written once at plan time; every later layer (dispatch,
|
|
203
|
+
* telemetry) reads this instead of re-deriving its own ordering.
|
|
204
|
+
*/
|
|
205
|
+
readonly decisions: ReadonlyMap<string, RoutingDecision>;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** How eligible a candidate is and why — written once, read by every layer. */
|
|
209
|
+
export const ROUTING_RANK = {
|
|
210
|
+
FRESH: 0,
|
|
211
|
+
SLOW: 1,
|
|
212
|
+
COOLING: 2,
|
|
213
|
+
SATURATED: 3,
|
|
214
|
+
} as const;
|
|
215
|
+
export type RoutingRank = (typeof ROUTING_RANK)[keyof typeof ROUTING_RANK];
|
|
216
|
+
|
|
217
|
+
export interface RoutingDecision {
|
|
218
|
+
readonly eligible: boolean;
|
|
219
|
+
readonly rank: RoutingRank;
|
|
220
|
+
readonly reason: string;
|
|
221
|
+
readonly health_status?: string;
|
|
222
|
+
readonly cooldown_until?: number;
|
|
223
|
+
readonly floor_blocked?: boolean;
|
|
224
|
+
readonly plan_gate_blocked?: boolean;
|
|
225
|
+
readonly capability_blocked?: boolean;
|
|
226
|
+
readonly saturated?: boolean;
|
|
174
227
|
}
|
|
175
228
|
|
|
176
229
|
import type { ComboStrategy, PoolRoutingStrategy, ProviderRoutingStrategy } from "../../persistence/schema";
|