cartethyia 2.7.1 → 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 (263) hide show
  1. package/.env.example +2 -0
  2. package/dist/dashboard/assets/{BannedPage-D-J75hBM.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-QoJ6foxo.js → CombosPage-JEbYJFxw.js} +1 -1
  9. package/dist/dashboard/assets/CombosPage-JEbYJFxw.js.gz +0 -0
  10. package/dist/dashboard/assets/{ConsoleLogPage-DpNRay4C.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-DubdQcvX.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-ATpaxrbl.js → QuotaPage-Ci5O4BnH.js} +1 -1
  29. package/dist/dashboard/assets/QuotaPage-Ci5O4BnH.js.gz +0 -0
  30. package/dist/dashboard/assets/{SettingsPage-CETophpx.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-CAK-EN9z.js → SortableList-BAL5npBC.js} +1 -1
  35. package/dist/dashboard/assets/SortableList-BAL5npBC.js.gz +0 -0
  36. package/dist/dashboard/assets/{StudioPage-B7W2_RhT.js → StudioPage-TP7voAQ0.js} +3 -3
  37. package/dist/dashboard/assets/StudioPage-TP7voAQ0.js.gz +0 -0
  38. package/dist/dashboard/assets/{UsagePage-6T4EFDjW.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-BLBLxX0h.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-mpBPqAQ8.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-D6QzEC_n.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-Cj5Nw43g.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/cli-tools/contracts.ts +10 -4
  133. package/src/console/dashboard-assets.ts +16 -1
  134. package/src/console/domain-registration.ts +2 -0
  135. package/src/console/quota/account-quota-tenant.ts +8 -1
  136. package/src/console/quota/account-quota.ts +4 -1
  137. package/src/console/quota/native-identity.ts +189 -0
  138. package/src/console/remote-support/contracts.ts +12 -6
  139. package/src/console/remote-support/service.ts +2 -2
  140. package/src/console/share/share-router.ts +39 -6
  141. package/src/console/share/share-stats.ts +64 -25
  142. package/src/console/share/share-usage.ts +24 -7
  143. package/src/console/system-prompt/contracts.ts +102 -0
  144. package/src/console/system-prompt/routes.ts +165 -0
  145. package/src/main.ts +11 -0
  146. package/src/network/outbound-fetch.ts +281 -226
  147. package/src/network/pool/agent.ts +7 -0
  148. package/src/observability/event-query.ts +118 -7
  149. package/src/persistence/db-pg.ts +42 -0
  150. package/src/persistence/schema.ts +102 -0
  151. package/src/persistence/stores/api-key-store.ts +753 -581
  152. package/src/persistence/stores/share-store.ts +65 -13
  153. package/src/persistence/stores/telemetry-store.ts +57 -1
  154. package/src/persistence/system-prompt-store.ts +104 -0
  155. package/src/protocol/reasoning.ts +65 -1
  156. package/src/protocol/request/chat.ts +1 -0
  157. package/src/providers/default-registry.ts +1 -0
  158. package/src/providers/integrations/antigravity/antigravity.ts +20 -23
  159. package/src/providers/integrations/buddy/buddy-chat-shared.ts +30 -2
  160. package/src/providers/integrations/buddy/buddy-media.ts +5 -1
  161. package/src/providers/integrations/buddy/codebuddy/provider.ts +5 -1
  162. package/src/providers/integrations/buddy/workbuddy/provider.ts +5 -1
  163. package/src/providers/integrations/cline/cline.ts +7 -0
  164. package/src/providers/integrations/github/github.ts +20 -1
  165. package/src/providers/integrations/hermes.ts +5 -1
  166. package/src/providers/integrations/kilo/kilo.ts +4 -0
  167. package/src/providers/integrations/ollamacloud-models.ts +50 -0
  168. package/src/providers/integrations/openrouter/provider.ts +4 -0
  169. package/src/providers/integrations/qoder/qoder-checkin.ts +224 -51
  170. package/src/providers/integrations/qoder/qoder-native-identity-store.ts +156 -0
  171. package/src/providers/integrations/qoder/qoder-oauth.ts +24 -4
  172. package/src/providers/integrations/qoder/qoder-realm.ts +20 -0
  173. package/src/providers/integrations/qoder/qoder-text-tools.ts +277 -0
  174. package/src/providers/integrations/qoder/qoder-umid.ts +288 -0
  175. package/src/providers/integrations/qoder/qoder.ts +1375 -1641
  176. package/src/providers/operations/account-error-classification.ts +1682 -1660
  177. package/src/providers/operations/account-health-service.ts +619 -621
  178. package/src/providers/operations/client-versions.ts +37 -22
  179. package/src/providers/operations/provider-catalog-service.ts +11 -1
  180. package/src/providers/provider-contracts.ts +15 -2
  181. package/src/security/access-control.ts +19 -0
  182. package/src/security/api-key-auth.ts +14 -1
  183. package/src/security/cli-client-fingerprint.ts +9 -2
  184. package/src/transport/dispatch/empty-stream.ts +224 -224
  185. package/src/transport/dispatch/proxy-request.ts +31 -2
  186. package/src/transport/dispatch/streaming/stream.ts +16 -1
  187. package/src/transport/dispatch/upstream.ts +10 -0
  188. package/src/transport/failure-policy.ts +33 -0
  189. package/src/transport/request/preparer.ts +17 -1
  190. package/src/transport/request/system-prompt-injection.ts +89 -0
  191. package/src/transport/request/tenant-preferences.ts +43 -0
  192. package/src/transport/routing/admission.ts +13 -3
  193. package/src/transport/routing/route-catalog.ts +41 -12
  194. package/src/transport/routing/route-model.ts +53 -0
  195. package/src/transport/routing/router.ts +306 -228
  196. package/src/transport/surface/responses/encode.ts +2 -3
  197. package/src/transport/surface/responses/parse.ts +49 -7
  198. package/src/types/undici.d.ts +33 -0
  199. package/src/workers/daily-checkin.ts +2 -0
  200. package/dist/dashboard/assets/BannedPage-D-J75hBM.js.gz +0 -0
  201. package/dist/dashboard/assets/CliToolDetailPage-CVe6RaBH.js +0 -1
  202. package/dist/dashboard/assets/CliToolDetailPage-CVe6RaBH.js.gz +0 -0
  203. package/dist/dashboard/assets/CliToolsPage-Dd8ZSamp.js +0 -1
  204. package/dist/dashboard/assets/CliToolsPage-Dd8ZSamp.js.gz +0 -0
  205. package/dist/dashboard/assets/CombosPage-QoJ6foxo.js.gz +0 -0
  206. package/dist/dashboard/assets/ConsoleLogPage-DpNRay4C.js.gz +0 -0
  207. package/dist/dashboard/assets/CustomizationPage-CKUFTcW4.js +0 -1
  208. package/dist/dashboard/assets/CustomizationPage-CKUFTcW4.js.gz +0 -0
  209. package/dist/dashboard/assets/HealthEventsModal-CzSLiDzr.js +0 -1
  210. package/dist/dashboard/assets/HealthEventsModal-CzSLiDzr.js.gz +0 -0
  211. package/dist/dashboard/assets/LoginPage-DubdQcvX.js.gz +0 -0
  212. package/dist/dashboard/assets/ModelPicker-BH8Tehdi.js.gz +0 -0
  213. package/dist/dashboard/assets/OverviewPage-DuWzAQFL.js +0 -1
  214. package/dist/dashboard/assets/OverviewPage-DuWzAQFL.js.gz +0 -0
  215. package/dist/dashboard/assets/ProviderDetailPage-BKEKe2Fm.js +0 -4
  216. package/dist/dashboard/assets/ProviderDetailPage-BKEKe2Fm.js.gz +0 -0
  217. package/dist/dashboard/assets/ProvidersPage-Bwotu8UJ.js +0 -1
  218. package/dist/dashboard/assets/ProvidersPage-Bwotu8UJ.js.gz +0 -0
  219. package/dist/dashboard/assets/ProxyPage-D7hpWbg9.js +0 -9
  220. package/dist/dashboard/assets/ProxyPage-D7hpWbg9.js.gz +0 -0
  221. package/dist/dashboard/assets/QuotaPage-ATpaxrbl.js.gz +0 -0
  222. package/dist/dashboard/assets/SettingsPage-CETophpx.js.gz +0 -0
  223. package/dist/dashboard/assets/SetupPage-BdgECjqq.js +0 -1
  224. package/dist/dashboard/assets/SetupPage-BdgECjqq.js.gz +0 -0
  225. package/dist/dashboard/assets/SortableList-CAK-EN9z.js.gz +0 -0
  226. package/dist/dashboard/assets/StudioPage-B7W2_RhT.js.gz +0 -0
  227. package/dist/dashboard/assets/UsagePage-6T4EFDjW.js.gz +0 -0
  228. package/dist/dashboard/assets/api-keys-CUi6UXsR.js.gz +0 -0
  229. package/dist/dashboard/assets/app-KvsUJUMF.css +0 -2
  230. package/dist/dashboard/assets/app-KvsUJUMF.css.gz +0 -0
  231. package/dist/dashboard/assets/assert-67xXH5rf.js.gz +0 -0
  232. package/dist/dashboard/assets/badge-DC2L43Mj.js.gz +0 -0
  233. package/dist/dashboard/assets/banner-BBVcb0J3.js.gz +0 -0
  234. package/dist/dashboard/assets/card-Cca1XC6G.js.gz +0 -0
  235. package/dist/dashboard/assets/cli-tools-KvsWa1Jl.js.gz +0 -0
  236. package/dist/dashboard/assets/clipboard-button-DUzEbynq.js.gz +0 -0
  237. package/dist/dashboard/assets/concurrency-BDF0ok7y.js.gz +0 -0
  238. package/dist/dashboard/assets/dialog-DFvWhwTa.js.gz +0 -0
  239. package/dist/dashboard/assets/drawer-CFM2sMX1.js +0 -1
  240. package/dist/dashboard/assets/drawer-CFM2sMX1.js.gz +0 -0
  241. package/dist/dashboard/assets/entry-BLBLxX0h.js.gz +0 -0
  242. package/dist/dashboard/assets/entry-Bqy8RFvM.js.gz +0 -0
  243. package/dist/dashboard/assets/entry-_xHo5x6r.js.gz +0 -0
  244. package/dist/dashboard/assets/formatters-mpBPqAQ8.js.gz +0 -0
  245. package/dist/dashboard/assets/github-badge-C9e8Ts7e.js.gz +0 -0
  246. package/dist/dashboard/assets/index-D6QzEC_n.js.gz +0 -0
  247. package/dist/dashboard/assets/inline-BFUcvllj.js.gz +0 -0
  248. package/dist/dashboard/assets/input-C_ILAvBb.js.gz +0 -0
  249. package/dist/dashboard/assets/layout-CBIyT6Rt.js.gz +0 -0
  250. package/dist/dashboard/assets/list-row-ebJ_-Nsb.js.gz +0 -0
  251. package/dist/dashboard/assets/live-D0mm--ME.js.gz +0 -0
  252. package/dist/dashboard/assets/mutation-sMD965oO.js.gz +0 -0
  253. package/dist/dashboard/assets/numbers-BO9dKaFr.js.gz +0 -0
  254. package/dist/dashboard/assets/query-keys-CfL_Zdoh.js.gz +0 -0
  255. package/dist/dashboard/assets/routing-BnIpi1IJ.js.gz +0 -0
  256. package/dist/dashboard/assets/select-Cj5Nw43g.js.gz +0 -0
  257. package/dist/dashboard/assets/settings-BAFhDgX8.js.gz +0 -0
  258. package/dist/dashboard/assets/stack-EMrOMt4I.js.gz +0 -0
  259. package/dist/dashboard/assets/state-CWoo7ty5.js.gz +0 -0
  260. package/dist/dashboard/assets/switch-CCjT6IkF.js.gz +0 -0
  261. package/dist/dashboard/assets/useMutation-DjkLHUcE.js.gz +0 -0
  262. /package/dist/dashboard/assets/{app-BaWd0puM.js → app-wvwCi4Eh.js} +0 -0
  263. /package/dist/dashboard/assets/{app-BaWd0puM.js.gz → app-wvwCi4Eh.js.gz} +0 -0
@@ -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
  /**
@@ -35,7 +35,19 @@ import { providerHasAdapterUserAgent, providerUsesBespokeWire } from "../../prov
35
35
 
36
36
  const CLAUDE_MODEL_FAMILIES = new Set(["opus", "sonnet", "haiku", "fable", "mythos"]);
37
37
 
38
+ /** Codex's built-in (non-routeable) model spellings worth matching. */
39
+ const CODEX_REVIEW_MODELS = new Set(["codex-auto-review"]);
40
+
38
41
  export function cliMappingSourceKeys(toolId: string, sourceModel: string): readonly string[] {
42
+ if (toolId === "codex") {
43
+ const normalized = sourceModel.trim().toLowerCase();
44
+ if (CODEX_REVIEW_MODELS.has(normalized)) {
45
+ // Guardian's auto-review model is one fixed spelling; surface the full
46
+ // spelling plus the bare family so a slot mapped to either one hits.
47
+ return [...new Set([sourceModel, "codex-auto-review"])];
48
+ }
49
+ return [sourceModel];
50
+ }
39
51
  if (toolId !== "claude") return [sourceModel];
40
52
  const normalized = sourceModel.trim().toLowerCase();
41
53
  const family =
@@ -199,23 +211,33 @@ export function capabilityProfileCacheSize(): number {
199
211
  return capabilityProfileCache.size;
200
212
  }
201
213
 
202
- /** Resolves route identity only for bundled adapters without a native User-Agent builder. */
214
+ /** Resolves route identity: bundled adapters without a native User-Agent builder, plus custom providers. */
203
215
  export function resolveRouteUserAgent(
204
216
  providerId: string,
205
217
  tenantUserAgent: string | undefined,
206
218
  globalUserAgent: string | undefined,
207
219
  ): string | undefined {
208
- if (!isBundledProviderId(providerId) || providerHasAdapterUserAgent(providerId)) return undefined;
209
- // A provider whose operator set no identity of their own gets the Claude Code
210
- // CLI identity, resolved live so the stamped version tracks discovery. This
211
- // used to be a frozen `codex_cli_rs` literal, which named a different vendor's
212
- // client on requests that mostly reached neither.
213
- const selected = resolveTenantOverride(
214
- tenantUserAgent,
215
- globalUserAgent,
216
- buildClaudeCliUserAgent(),
217
- );
218
- return selected.trim() === "" ? undefined : selected;
220
+ if (isBundledProviderId(providerId)) {
221
+ // A bundled adapter that frames its own identity headers never consumes a
222
+ // route User-Agent (Command Code, Qoder, buddy, ...), so it never gets one
223
+ // — the control is hidden for it too.
224
+ if (providerHasAdapterUserAgent(providerId)) return undefined;
225
+ // Bundled API-key providers: an operator who set no identity of their own
226
+ // gets the Claude Code CLI identity, resolved live so the stamped version
227
+ // tracks discovery. This used to be a frozen `codex_cli_rs` literal, which
228
+ // named a different vendor's client on requests that mostly reached neither.
229
+ const selected = resolveTenantOverride(
230
+ tenantUserAgent,
231
+ globalUserAgent,
232
+ buildClaudeCliUserAgent(),
233
+ );
234
+ return selected.trim() === "" ? undefined : selected;
235
+ }
236
+ // Custom (BYOK) providers: only an explicit operator choice rides on the
237
+ // route; the adapter owns the default otherwise (see `registerByokProviders`).
238
+ // An explicit empty choice ("None") is preserved so the adapter can honor it.
239
+ const selected = resolveTenantOverride(tenantUserAgent, globalUserAgent, undefined);
240
+ return selected === undefined ? undefined : selected.trim();
219
241
  }
220
242
 
221
243
  /** One persisted model row ready for candidate construction. */
@@ -648,6 +670,8 @@ class RouteCatalogRepository {
648
670
  ...(model.outputLimit == null ? {} : { output_limit: model.outputLimit }),
649
671
  capability_profile: capabilityProfile,
650
672
  tenant_id: tId,
673
+ // Public credential-less candidates have no account to own them.
674
+ owner_tenant_id: null,
651
675
  requires_account: false as const,
652
676
  network_pool_ids: tenantPools.ids,
653
677
  network_pool_required: true,
@@ -674,6 +698,8 @@ class RouteCatalogRepository {
674
698
  ...(model.outputLimit == null ? {} : { output_limit: model.outputLimit }),
675
699
  capability_profile: capabilityProfile,
676
700
  tenant_id: providerTenantId,
701
+ // Public credential-less candidates have no account to own them.
702
+ owner_tenant_id: null,
677
703
  ...(providerRequiresAccount
678
704
  ? { health_status: "disabled" as const }
679
705
  : { requires_account: false as const }),
@@ -716,6 +742,9 @@ class RouteCatalogRepository {
716
742
  ...(routeUserAgent === undefined ? {} : { user_agent: routeUserAgent }),
717
743
  tenant_id: rowTenantId,
718
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,
719
748
  // The plan gate is per (account, model): the model may be refused on
720
749
  // a free plan and the account may be on one. Both ride here because
721
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";