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.
Files changed (249) hide show
  1. package/.env.example +2 -0
  2. package/dist/dashboard/assets/{BannedPage-XaUmiBrC.js → BannedPage-2TJD37Hb.js} +1 -1
  3. package/dist/dashboard/assets/BannedPage-2TJD37Hb.js.gz +0 -0
  4. package/dist/dashboard/assets/CliToolDetailPage-xefJNRmJ.js +1 -0
  5. package/dist/dashboard/assets/CliToolDetailPage-xefJNRmJ.js.gz +0 -0
  6. package/dist/dashboard/assets/CliToolsPage-Rj9iHQ3F.js +1 -0
  7. package/dist/dashboard/assets/CliToolsPage-Rj9iHQ3F.js.gz +0 -0
  8. package/dist/dashboard/assets/{CombosPage-CXGS_Ct-.js → CombosPage-JEbYJFxw.js} +1 -1
  9. package/dist/dashboard/assets/CombosPage-JEbYJFxw.js.gz +0 -0
  10. package/dist/dashboard/assets/{ConsoleLogPage-DGnGNYnM.js → ConsoleLogPage-C3xx_inM.js} +1 -1
  11. package/dist/dashboard/assets/ConsoleLogPage-C3xx_inM.js.gz +0 -0
  12. package/dist/dashboard/assets/CustomizationPage-Dw306gJU.js +1 -0
  13. package/dist/dashboard/assets/CustomizationPage-Dw306gJU.js.gz +0 -0
  14. package/dist/dashboard/assets/HealthEventsModal-jaBo1TWV.js +1 -0
  15. package/dist/dashboard/assets/HealthEventsModal-jaBo1TWV.js.gz +0 -0
  16. package/dist/dashboard/assets/{LoginPage-BdmRLnq2.js → LoginPage-C4w7vea-.js} +1 -1
  17. package/dist/dashboard/assets/LoginPage-C4w7vea-.js.gz +0 -0
  18. package/dist/dashboard/assets/{ModelPicker-BH8Tehdi.js → ModelPicker-BtPcAfot.js} +1 -1
  19. package/dist/dashboard/assets/ModelPicker-BtPcAfot.js.gz +0 -0
  20. package/dist/dashboard/assets/OverviewPage-CGC5gNs1.js +1 -0
  21. package/dist/dashboard/assets/OverviewPage-CGC5gNs1.js.gz +0 -0
  22. package/dist/dashboard/assets/ProviderDetailPage-DQcthNGt.js +4 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-DQcthNGt.js.gz +0 -0
  24. package/dist/dashboard/assets/ProvidersPage-0OsXRGXe.js +1 -0
  25. package/dist/dashboard/assets/ProvidersPage-0OsXRGXe.js.gz +0 -0
  26. package/dist/dashboard/assets/ProxyPage-DdPc8slb.js +9 -0
  27. package/dist/dashboard/assets/ProxyPage-DdPc8slb.js.gz +0 -0
  28. package/dist/dashboard/assets/{QuotaPage-7P2n--Fv.js → QuotaPage-Ci5O4BnH.js} +1 -1
  29. package/dist/dashboard/assets/QuotaPage-Ci5O4BnH.js.gz +0 -0
  30. package/dist/dashboard/assets/{SettingsPage-CxZ1fTkN.js → SettingsPage-CDZjTt87.js} +1 -1
  31. package/dist/dashboard/assets/SettingsPage-CDZjTt87.js.gz +0 -0
  32. package/dist/dashboard/assets/SetupPage-BQS0Rx_u.js +1 -0
  33. package/dist/dashboard/assets/SetupPage-BQS0Rx_u.js.gz +0 -0
  34. package/dist/dashboard/assets/{SortableList-Dqf4zpS-.js → SortableList-BAL5npBC.js} +1 -1
  35. package/dist/dashboard/assets/SortableList-BAL5npBC.js.gz +0 -0
  36. package/dist/dashboard/assets/{StudioPage-Byj2lAEm.js → StudioPage-TP7voAQ0.js} +3 -3
  37. package/dist/dashboard/assets/StudioPage-TP7voAQ0.js.gz +0 -0
  38. package/dist/dashboard/assets/{UsagePage-CTkLAuRc.js → UsagePage-Cj_fEzhg.js} +13 -13
  39. package/dist/dashboard/assets/UsagePage-Cj_fEzhg.js.gz +0 -0
  40. package/dist/dashboard/assets/{api-keys-CUi6UXsR.js → api-keys-MbI7e7rt.js} +1 -1
  41. package/dist/dashboard/assets/api-keys-MbI7e7rt.js.gz +0 -0
  42. package/dist/dashboard/assets/app-B2vyPoII.css +2 -0
  43. package/dist/dashboard/assets/app-B2vyPoII.css.gz +0 -0
  44. package/dist/dashboard/assets/arrow-up-59ms1iiT.js +1 -0
  45. package/dist/dashboard/assets/arrow-up-59ms1iiT.js.gz +0 -0
  46. package/dist/dashboard/assets/{assert-67xXH5rf.js → assert-C0YYuZJ4.js} +1 -1
  47. package/dist/dashboard/assets/assert-C0YYuZJ4.js.gz +0 -0
  48. package/dist/dashboard/assets/{badge-DC2L43Mj.js → badge-B_DMpMJN.js} +1 -1
  49. package/dist/dashboard/assets/badge-B_DMpMJN.js.gz +0 -0
  50. package/dist/dashboard/assets/{banner-BBVcb0J3.js → banner-DMrMPmth.js} +1 -1
  51. package/dist/dashboard/assets/banner-DMrMPmth.js.gz +0 -0
  52. package/dist/dashboard/assets/{card-Cca1XC6G.js → card-Be5Uzamx.js} +1 -1
  53. package/dist/dashboard/assets/card-Be5Uzamx.js.gz +0 -0
  54. package/dist/dashboard/assets/{cli-tools-KvsWa1Jl.js → cli-tools-muFRphDu.js} +1 -1
  55. package/dist/dashboard/assets/cli-tools-muFRphDu.js.gz +0 -0
  56. package/dist/dashboard/assets/{clipboard-button-DUzEbynq.js → clipboard-button-DOfrVmsi.js} +1 -1
  57. package/dist/dashboard/assets/clipboard-button-DOfrVmsi.js.gz +0 -0
  58. package/dist/dashboard/assets/{dialog-DFvWhwTa.js → dialog-C9omdjQt.js} +1 -1
  59. package/dist/dashboard/assets/dialog-C9omdjQt.js.gz +0 -0
  60. package/dist/dashboard/assets/drawer-Bm8WKaGY.js +1 -0
  61. package/dist/dashboard/assets/drawer-Bm8WKaGY.js.gz +0 -0
  62. package/dist/dashboard/assets/{entry-_xHo5x6r.js → entry-DS90gUks.js} +1 -1
  63. package/dist/dashboard/assets/entry-DS90gUks.js.gz +0 -0
  64. package/dist/dashboard/assets/{entry-ClCMCWWQ.js → entry-j40LJotO.js} +3 -3
  65. package/dist/dashboard/assets/entry-j40LJotO.js.gz +0 -0
  66. package/dist/dashboard/assets/{entry-Bqy8RFvM.js → entry-q5HwZ8nN.js} +1 -1
  67. package/dist/dashboard/assets/entry-q5HwZ8nN.js.gz +0 -0
  68. package/dist/dashboard/assets/{concurrency-BDF0ok7y.js → fingerprint-pattern-BWuHtbW4.js} +1 -1
  69. package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
  70. package/dist/dashboard/assets/{formatters-CWWZ-uCZ.js → formatters-BTScrQpe.js} +1 -1
  71. package/dist/dashboard/assets/formatters-BTScrQpe.js.gz +0 -0
  72. package/dist/dashboard/assets/{github-badge-C9e8Ts7e.js → github-badge-Orjuzemr.js} +1 -1
  73. package/dist/dashboard/assets/github-badge-Orjuzemr.js.gz +0 -0
  74. package/dist/dashboard/assets/{index-DFVFrnWW.js → index-cPrX5vn6.js} +2 -2
  75. package/dist/dashboard/assets/index-cPrX5vn6.js.gz +0 -0
  76. package/dist/dashboard/assets/{inline-BFUcvllj.js → inline-Dvux8w-0.js} +1 -1
  77. package/dist/dashboard/assets/inline-Dvux8w-0.js.gz +0 -0
  78. package/dist/dashboard/assets/{input-C_ILAvBb.js → input-CU1Rgf3p.js} +1 -1
  79. package/dist/dashboard/assets/input-CU1Rgf3p.js.gz +0 -0
  80. package/dist/dashboard/assets/{layout-CBIyT6Rt.js → layout-C_7A6H55.js} +1 -1
  81. package/dist/dashboard/assets/layout-C_7A6H55.js.gz +0 -0
  82. package/dist/dashboard/assets/{list-row-ebJ_-Nsb.js → list-row-CzFvogbj.js} +1 -1
  83. package/dist/dashboard/assets/list-row-CzFvogbj.js.gz +0 -0
  84. package/dist/dashboard/assets/{live-D0mm--ME.js → live-KaG15HLQ.js} +1 -1
  85. package/dist/dashboard/assets/live-KaG15HLQ.js.gz +0 -0
  86. package/dist/dashboard/assets/{mutation-sMD965oO.js → mutation-JJGQm8TD.js} +1 -1
  87. package/dist/dashboard/assets/mutation-JJGQm8TD.js.gz +0 -0
  88. package/dist/dashboard/assets/{numbers-BO9dKaFr.js → numbers-BNDZnsUE.js} +1 -1
  89. package/dist/dashboard/assets/numbers-BNDZnsUE.js.gz +0 -0
  90. package/dist/dashboard/assets/{query-keys-CfL_Zdoh.js → query-keys-CLaNz8Xz.js} +1 -1
  91. package/dist/dashboard/assets/query-keys-CLaNz8Xz.js.gz +0 -0
  92. package/dist/dashboard/assets/{routing-BnIpi1IJ.js → routing-BEH32QOC.js} +1 -1
  93. package/dist/dashboard/assets/routing-BEH32QOC.js.gz +0 -0
  94. package/dist/dashboard/assets/{select-DBNVLdUu.js → select-B3iTetHN.js} +1 -1
  95. package/dist/dashboard/assets/select-B3iTetHN.js.gz +0 -0
  96. package/dist/dashboard/assets/settings-2-Ds9DQp0t.js +1 -0
  97. package/dist/dashboard/assets/settings-2-Ds9DQp0t.js.gz +0 -0
  98. package/dist/dashboard/assets/{settings-BAFhDgX8.js → settings-Dt3Eqbnq.js} +1 -1
  99. package/dist/dashboard/assets/settings-Dt3Eqbnq.js.gz +0 -0
  100. package/dist/dashboard/assets/{stack-EMrOMt4I.js → stack-KrOrm7JZ.js} +1 -1
  101. package/dist/dashboard/assets/stack-KrOrm7JZ.js.gz +0 -0
  102. package/dist/dashboard/assets/{state-CWoo7ty5.js → state-DLS7IDMy.js} +1 -1
  103. package/dist/dashboard/assets/state-DLS7IDMy.js.gz +0 -0
  104. package/dist/dashboard/assets/{switch-CCjT6IkF.js → switch-42ItvbAs.js} +1 -1
  105. package/dist/dashboard/assets/switch-42ItvbAs.js.gz +0 -0
  106. package/dist/dashboard/assets/{useMutation-DjkLHUcE.js → useMutation-Dmnfh1Ue.js} +1 -1
  107. package/dist/dashboard/assets/useMutation-Dmnfh1Ue.js.gz +0 -0
  108. package/dist/dashboard/index.html +3 -2
  109. package/dist/dashboard/index.html.gz +0 -0
  110. package/dist/dashboard/precompressed-assets.json +117 -115
  111. package/migrations/full-postgres/0026_add_qoder_native_identity.sql +20 -0
  112. package/migrations/full-postgres/0027_qoder_identity_genuine_seed.sql +22 -0
  113. package/migrations/full-postgres/0028_add_family_template_key_telemetry.sql +40 -0
  114. package/migrations/full-postgres/0029_null_share_child_policy_columns.sql +38 -0
  115. package/migrations/full-postgres/0030_add_api_key_max_children.sql +10 -0
  116. package/migrations/full-postgres/0031_tenant_system_prompts.sql +16 -0
  117. package/migrations/lite-sqlite3/0026_add_qoder_native_identity.sql +15 -0
  118. package/migrations/lite-sqlite3/0027_qoder_identity_genuine_seed.sql +22 -0
  119. package/migrations/lite-sqlite3/0028_add_family_template_key_telemetry.sql +40 -0
  120. package/migrations/lite-sqlite3/0029_null_share_child_policy_columns.sql +38 -0
  121. package/migrations/lite-sqlite3/0030_add_api_key_max_children.sql +10 -0
  122. package/migrations/lite-sqlite3/0031_tenant_system_prompts.sql +10 -0
  123. package/package.json +1 -1
  124. package/src/config/network.ts +1 -1
  125. package/src/console/api-keys/bucket-spend.ts +7 -7
  126. package/src/console/api-keys/contracts.ts +423 -410
  127. package/src/console/api-keys/routes.ts +118 -19
  128. package/src/console/backup/export-endpoint.ts +8 -11
  129. package/src/console/backup/routes.ts +40 -8
  130. package/src/console/backup/service.ts +11 -6
  131. package/src/console/backup/store.ts +384 -88
  132. package/src/console/dashboard-assets.ts +16 -1
  133. package/src/console/domain-registration.ts +2 -0
  134. package/src/console/quota/account-quota-tenant.ts +8 -1
  135. package/src/console/quota/account-quota.ts +4 -1
  136. package/src/console/quota/native-identity.ts +189 -0
  137. package/src/console/remote-support/contracts.ts +12 -6
  138. package/src/console/remote-support/service.ts +2 -2
  139. package/src/console/share/share-router.ts +39 -6
  140. package/src/console/share/share-stats.ts +64 -25
  141. package/src/console/share/share-usage.ts +24 -7
  142. package/src/console/system-prompt/contracts.ts +102 -0
  143. package/src/console/system-prompt/routes.ts +165 -0
  144. package/src/main.ts +11 -0
  145. package/src/observability/event-query.ts +118 -7
  146. package/src/persistence/db-pg.ts +42 -0
  147. package/src/persistence/schema.ts +102 -0
  148. package/src/persistence/stores/api-key-store.ts +753 -581
  149. package/src/persistence/stores/share-store.ts +65 -13
  150. package/src/persistence/stores/telemetry-store.ts +57 -1
  151. package/src/persistence/system-prompt-store.ts +104 -0
  152. package/src/protocol/reasoning.ts +65 -1
  153. package/src/protocol/request/chat.ts +1 -0
  154. package/src/providers/integrations/antigravity/antigravity.ts +20 -23
  155. package/src/providers/integrations/buddy/buddy-chat-shared.ts +30 -2
  156. package/src/providers/integrations/buddy/buddy-media.ts +5 -1
  157. package/src/providers/integrations/buddy/codebuddy/provider.ts +5 -1
  158. package/src/providers/integrations/buddy/workbuddy/provider.ts +5 -1
  159. package/src/providers/integrations/cline/cline.ts +7 -0
  160. package/src/providers/integrations/github/github.ts +20 -1
  161. package/src/providers/integrations/hermes.ts +5 -1
  162. package/src/providers/integrations/kilo/kilo.ts +4 -0
  163. package/src/providers/integrations/openrouter/provider.ts +4 -0
  164. package/src/providers/integrations/qoder/qoder-checkin.ts +224 -51
  165. package/src/providers/integrations/qoder/qoder-native-identity-store.ts +156 -0
  166. package/src/providers/integrations/qoder/qoder-oauth.ts +12 -3
  167. package/src/providers/integrations/qoder/qoder-umid.ts +288 -0
  168. package/src/providers/integrations/qoder/qoder.ts +5 -3
  169. package/src/providers/operations/account-error-classification.ts +6 -8
  170. package/src/providers/operations/account-health-service.ts +14 -16
  171. package/src/providers/operations/client-versions.ts +37 -22
  172. package/src/providers/provider-contracts.ts +8 -0
  173. package/src/security/access-control.ts +19 -0
  174. package/src/security/api-key-auth.ts +14 -1
  175. package/src/transport/dispatch/proxy-request.ts +31 -2
  176. package/src/transport/dispatch/streaming/stream.ts +16 -1
  177. package/src/transport/dispatch/upstream.ts +10 -0
  178. package/src/transport/request/preparer.ts +17 -1
  179. package/src/transport/request/system-prompt-injection.ts +89 -0
  180. package/src/transport/request/tenant-preferences.ts +43 -0
  181. package/src/transport/routing/admission.ts +13 -3
  182. package/src/transport/routing/route-catalog.ts +7 -0
  183. package/src/transport/routing/route-model.ts +53 -0
  184. package/src/transport/routing/router.ts +306 -256
  185. package/src/workers/daily-checkin.ts +2 -0
  186. package/dist/dashboard/assets/BannedPage-XaUmiBrC.js.gz +0 -0
  187. package/dist/dashboard/assets/CliToolDetailPage-CqD7QiLM.js +0 -1
  188. package/dist/dashboard/assets/CliToolDetailPage-CqD7QiLM.js.gz +0 -0
  189. package/dist/dashboard/assets/CliToolsPage-0XUo4UMn.js +0 -1
  190. package/dist/dashboard/assets/CliToolsPage-0XUo4UMn.js.gz +0 -0
  191. package/dist/dashboard/assets/CombosPage-CXGS_Ct-.js.gz +0 -0
  192. package/dist/dashboard/assets/ConsoleLogPage-DGnGNYnM.js.gz +0 -0
  193. package/dist/dashboard/assets/CustomizationPage-DAG5J9EY.js +0 -1
  194. package/dist/dashboard/assets/CustomizationPage-DAG5J9EY.js.gz +0 -0
  195. package/dist/dashboard/assets/HealthEventsModal-BKtAPYVF.js +0 -1
  196. package/dist/dashboard/assets/HealthEventsModal-BKtAPYVF.js.gz +0 -0
  197. package/dist/dashboard/assets/LoginPage-BdmRLnq2.js.gz +0 -0
  198. package/dist/dashboard/assets/ModelPicker-BH8Tehdi.js.gz +0 -0
  199. package/dist/dashboard/assets/OverviewPage-CqMxibJB.js +0 -1
  200. package/dist/dashboard/assets/OverviewPage-CqMxibJB.js.gz +0 -0
  201. package/dist/dashboard/assets/ProviderDetailPage-CqMh2NRX.js +0 -4
  202. package/dist/dashboard/assets/ProviderDetailPage-CqMh2NRX.js.gz +0 -0
  203. package/dist/dashboard/assets/ProvidersPage-B9uWu4Nz.js +0 -1
  204. package/dist/dashboard/assets/ProvidersPage-B9uWu4Nz.js.gz +0 -0
  205. package/dist/dashboard/assets/ProxyPage-BoWOhpDO.js +0 -9
  206. package/dist/dashboard/assets/ProxyPage-BoWOhpDO.js.gz +0 -0
  207. package/dist/dashboard/assets/QuotaPage-7P2n--Fv.js.gz +0 -0
  208. package/dist/dashboard/assets/SettingsPage-CxZ1fTkN.js.gz +0 -0
  209. package/dist/dashboard/assets/SetupPage-Co12Zzaw.js +0 -1
  210. package/dist/dashboard/assets/SetupPage-Co12Zzaw.js.gz +0 -0
  211. package/dist/dashboard/assets/SortableList-Dqf4zpS-.js.gz +0 -0
  212. package/dist/dashboard/assets/StudioPage-Byj2lAEm.js.gz +0 -0
  213. package/dist/dashboard/assets/UsagePage-CTkLAuRc.js.gz +0 -0
  214. package/dist/dashboard/assets/api-keys-CUi6UXsR.js.gz +0 -0
  215. package/dist/dashboard/assets/app-KvsUJUMF.css +0 -2
  216. package/dist/dashboard/assets/app-KvsUJUMF.css.gz +0 -0
  217. package/dist/dashboard/assets/assert-67xXH5rf.js.gz +0 -0
  218. package/dist/dashboard/assets/badge-DC2L43Mj.js.gz +0 -0
  219. package/dist/dashboard/assets/banner-BBVcb0J3.js.gz +0 -0
  220. package/dist/dashboard/assets/card-Cca1XC6G.js.gz +0 -0
  221. package/dist/dashboard/assets/cli-tools-KvsWa1Jl.js.gz +0 -0
  222. package/dist/dashboard/assets/clipboard-button-DUzEbynq.js.gz +0 -0
  223. package/dist/dashboard/assets/concurrency-BDF0ok7y.js.gz +0 -0
  224. package/dist/dashboard/assets/dialog-DFvWhwTa.js.gz +0 -0
  225. package/dist/dashboard/assets/drawer-CFM2sMX1.js +0 -1
  226. package/dist/dashboard/assets/drawer-CFM2sMX1.js.gz +0 -0
  227. package/dist/dashboard/assets/entry-Bqy8RFvM.js.gz +0 -0
  228. package/dist/dashboard/assets/entry-ClCMCWWQ.js.gz +0 -0
  229. package/dist/dashboard/assets/entry-_xHo5x6r.js.gz +0 -0
  230. package/dist/dashboard/assets/formatters-CWWZ-uCZ.js.gz +0 -0
  231. package/dist/dashboard/assets/github-badge-C9e8Ts7e.js.gz +0 -0
  232. package/dist/dashboard/assets/index-DFVFrnWW.js.gz +0 -0
  233. package/dist/dashboard/assets/inline-BFUcvllj.js.gz +0 -0
  234. package/dist/dashboard/assets/input-C_ILAvBb.js.gz +0 -0
  235. package/dist/dashboard/assets/layout-CBIyT6Rt.js.gz +0 -0
  236. package/dist/dashboard/assets/list-row-ebJ_-Nsb.js.gz +0 -0
  237. package/dist/dashboard/assets/live-D0mm--ME.js.gz +0 -0
  238. package/dist/dashboard/assets/mutation-sMD965oO.js.gz +0 -0
  239. package/dist/dashboard/assets/numbers-BO9dKaFr.js.gz +0 -0
  240. package/dist/dashboard/assets/query-keys-CfL_Zdoh.js.gz +0 -0
  241. package/dist/dashboard/assets/routing-BnIpi1IJ.js.gz +0 -0
  242. package/dist/dashboard/assets/select-DBNVLdUu.js.gz +0 -0
  243. package/dist/dashboard/assets/settings-BAFhDgX8.js.gz +0 -0
  244. package/dist/dashboard/assets/stack-EMrOMt4I.js.gz +0 -0
  245. package/dist/dashboard/assets/state-CWoo7ty5.js.gz +0 -0
  246. package/dist/dashboard/assets/switch-CCjT6IkF.js.gz +0 -0
  247. package/dist/dashboard/assets/useMutation-DjkLHUcE.js.gz +0 -0
  248. /package/dist/dashboard/assets/{app-BaWd0puM.js → app-wvwCi4Eh.js} +0 -0
  249. /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
- ...(prepared.continuationAffinity === undefined ? {} : { borrowsHeldSession: true }),
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
- return affinity;
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
- return candidate.provider_account_id === undefined
27
- ? base
28
- : `${base}:${candidate.provider_account_id}`;
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";