cartethyia 2.7.2 → 2.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (405) hide show
  1. package/.env.example +2 -0
  2. package/dist/dashboard/assets/{BannedPage-XaUmiBrC.js → BannedPage-ChG5svFb.js} +1 -1
  3. package/dist/dashboard/assets/BannedPage-ChG5svFb.js.gz +0 -0
  4. package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js +1 -0
  5. package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js.gz +0 -0
  6. package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js +1 -0
  7. package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js.gz +0 -0
  8. package/dist/dashboard/assets/{CombosPage-CXGS_Ct-.js → CombosPage-DCgm5g6g.js} +1 -1
  9. package/dist/dashboard/assets/CombosPage-DCgm5g6g.js.gz +0 -0
  10. package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js +1 -0
  11. package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js.gz +0 -0
  12. package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js +1 -0
  13. package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js.gz +0 -0
  14. package/dist/dashboard/assets/{HealthEventsModal-BKtAPYVF.js → HealthEventsModal-DzSWzE_j.js} +1 -1
  15. package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js.gz +0 -0
  16. package/dist/dashboard/assets/{LoginPage-BdmRLnq2.js → LoginPage-U6AKaJcF.js} +1 -1
  17. package/dist/dashboard/assets/LoginPage-U6AKaJcF.js.gz +0 -0
  18. package/dist/dashboard/assets/{ModelPicker-BH8Tehdi.js → ModelPicker-BMS2t8BA.js} +1 -1
  19. package/dist/dashboard/assets/ModelPicker-BMS2t8BA.js.gz +0 -0
  20. package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js +1 -0
  21. package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js.gz +0 -0
  22. package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js +5 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js.gz +0 -0
  24. package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js +1 -0
  25. package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js.gz +0 -0
  26. package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js +9 -0
  27. package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js.gz +0 -0
  28. package/dist/dashboard/assets/QuotaPage-L0skzML8.js +1 -0
  29. package/dist/dashboard/assets/QuotaPage-L0skzML8.js.gz +0 -0
  30. package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js +1 -0
  31. package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js.gz +0 -0
  32. package/dist/dashboard/assets/SetupPage-f0DE1h0-.js +1 -0
  33. package/dist/dashboard/assets/SetupPage-f0DE1h0-.js.gz +0 -0
  34. package/dist/dashboard/assets/{SortableList-Dqf4zpS-.js → SortableList-ClzpwyET.js} +1 -1
  35. package/dist/dashboard/assets/SortableList-ClzpwyET.js.gz +0 -0
  36. package/dist/dashboard/assets/{StudioPage-Byj2lAEm.js → StudioPage-Cq5tW7hW.js} +3 -3
  37. package/dist/dashboard/assets/StudioPage-Cq5tW7hW.js.gz +0 -0
  38. package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js +56 -0
  39. package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js.gz +0 -0
  40. package/dist/dashboard/assets/api-keys-C38iXoO4.js +1 -0
  41. package/dist/dashboard/assets/api-keys-C38iXoO4.js.gz +0 -0
  42. package/dist/dashboard/assets/app-a159k0ub.css +2 -0
  43. package/dist/dashboard/assets/app-a159k0ub.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-DbRA8EG2.js +15 -0
  47. package/dist/dashboard/assets/assert-DbRA8EG2.js.gz +0 -0
  48. package/dist/dashboard/assets/{badge-DC2L43Mj.js → badge-D9CS6dZk.js} +1 -1
  49. package/dist/dashboard/assets/badge-D9CS6dZk.js.gz +0 -0
  50. package/dist/dashboard/assets/{banner-BBVcb0J3.js → banner-DympBNq4.js} +1 -1
  51. package/dist/dashboard/assets/banner-DympBNq4.js.gz +0 -0
  52. package/dist/dashboard/assets/{card-Cca1XC6G.js → card-ibRWU_uT.js} +1 -1
  53. package/dist/dashboard/assets/card-ibRWU_uT.js.gz +0 -0
  54. package/dist/dashboard/assets/cli-tools-63qBGaRw.js +1 -0
  55. package/dist/dashboard/assets/cli-tools-63qBGaRw.js.gz +0 -0
  56. package/dist/dashboard/assets/{clipboard-button-DUzEbynq.js → clipboard-button-DJm-9pz7.js} +1 -1
  57. package/dist/dashboard/assets/clipboard-button-DJm-9pz7.js.gz +0 -0
  58. package/dist/dashboard/assets/{dialog-DFvWhwTa.js → dialog-8fiE6tec.js} +1 -1
  59. package/dist/dashboard/assets/dialog-8fiE6tec.js.gz +0 -0
  60. package/dist/dashboard/assets/drawer-CqGYeOrd.js +1 -0
  61. package/dist/dashboard/assets/drawer-CqGYeOrd.js.gz +0 -0
  62. package/dist/dashboard/assets/{entry-Bqy8RFvM.js → entry-BSAsYsmV.js} +1 -1
  63. package/dist/dashboard/assets/entry-BSAsYsmV.js.gz +0 -0
  64. package/dist/dashboard/assets/entry-CiIQULZD.js +4 -0
  65. package/dist/dashboard/assets/entry-CiIQULZD.js.gz +0 -0
  66. package/dist/dashboard/assets/entry-Daz0lJ-W.js +1 -0
  67. package/dist/dashboard/assets/entry-Daz0lJ-W.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-D-MTJxMr.js} +1 -1
  71. package/dist/dashboard/assets/formatters-D-MTJxMr.js.gz +0 -0
  72. package/dist/dashboard/assets/{github-badge-C9e8Ts7e.js → github-badge-CPK47xQx.js} +1 -1
  73. package/dist/dashboard/assets/github-badge-CPK47xQx.js.gz +0 -0
  74. package/dist/dashboard/assets/{index-DFVFrnWW.js → index-DBNPOfhV.js} +2 -2
  75. package/dist/dashboard/assets/index-DBNPOfhV.js.gz +0 -0
  76. package/dist/dashboard/assets/{inline-BFUcvllj.js → inline-BT0zntEr.js} +1 -1
  77. package/dist/dashboard/assets/inline-BT0zntEr.js.gz +0 -0
  78. package/dist/dashboard/assets/{input-C_ILAvBb.js → input-Da2ZViWg.js} +1 -1
  79. package/dist/dashboard/assets/input-Da2ZViWg.js.gz +0 -0
  80. package/dist/dashboard/assets/{layout-CBIyT6Rt.js → layout-BXNeLw6M.js} +1 -1
  81. package/dist/dashboard/assets/layout-BXNeLw6M.js.gz +0 -0
  82. package/dist/dashboard/assets/{list-row-ebJ_-Nsb.js → list-row-BycOrrIV.js} +1 -1
  83. package/dist/dashboard/assets/list-row-BycOrrIV.js.gz +0 -0
  84. package/dist/dashboard/assets/live-DCQNZpF7.js +1 -0
  85. package/dist/dashboard/assets/live-DCQNZpF7.js.gz +0 -0
  86. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js +1 -0
  87. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js.gz +0 -0
  88. package/dist/dashboard/assets/{mutation-sMD965oO.js → mutation-Rr0TIvcj.js} +1 -1
  89. package/dist/dashboard/assets/mutation-Rr0TIvcj.js.gz +0 -0
  90. package/dist/dashboard/assets/network-9Yj2ZyLo.js +1 -0
  91. package/dist/dashboard/assets/network-9Yj2ZyLo.js.gz +0 -0
  92. package/dist/dashboard/assets/{numbers-BO9dKaFr.js → numbers-CEagj6ZZ.js} +2 -2
  93. package/dist/dashboard/assets/numbers-CEagj6ZZ.js.gz +0 -0
  94. package/dist/dashboard/assets/{query-keys-CfL_Zdoh.js → query-keys-BloKtaXJ.js} +1 -1
  95. package/dist/dashboard/assets/query-keys-BloKtaXJ.js.gz +0 -0
  96. package/dist/dashboard/assets/quota-Caw7wqci.js +1 -0
  97. package/dist/dashboard/assets/quota-Caw7wqci.js.gz +0 -0
  98. package/dist/dashboard/assets/routing-CO9zCUus.js +1 -0
  99. package/dist/dashboard/assets/routing-CO9zCUus.js.gz +0 -0
  100. package/dist/dashboard/assets/{select-DBNVLdUu.js → select-BIYIBkMU.js} +2 -2
  101. package/dist/dashboard/assets/select-BIYIBkMU.js.gz +0 -0
  102. package/dist/dashboard/assets/settings-2-Ds9DQp0t.js +1 -0
  103. package/dist/dashboard/assets/settings-2-Ds9DQp0t.js.gz +0 -0
  104. package/dist/dashboard/assets/settings-jS3-PNSA.js +1 -0
  105. package/dist/dashboard/assets/settings-jS3-PNSA.js.gz +0 -0
  106. package/dist/dashboard/assets/{stack-EMrOMt4I.js → stack-DfgnFDWY.js} +1 -1
  107. package/dist/dashboard/assets/stack-DfgnFDWY.js.gz +0 -0
  108. package/dist/dashboard/assets/{state-CWoo7ty5.js → state-CkfdYWel.js} +1 -1
  109. package/dist/dashboard/assets/state-CkfdYWel.js.gz +0 -0
  110. package/dist/dashboard/assets/{switch-CCjT6IkF.js → switch-SYm237HG.js} +1 -1
  111. package/dist/dashboard/assets/switch-SYm237HG.js.gz +0 -0
  112. package/dist/dashboard/assets/{useMutation-DjkLHUcE.js → useMutation-BQqCRNIi.js} +1 -1
  113. package/dist/dashboard/assets/useMutation-BQqCRNIi.js.gz +0 -0
  114. package/dist/dashboard/index.html +3 -2
  115. package/dist/dashboard/index.html.gz +0 -0
  116. package/dist/dashboard/precompressed-assets.json +118 -115
  117. package/migrations/full-postgres/0026_add_qoder_native_identity.sql +20 -0
  118. package/migrations/full-postgres/0027_qoder_identity_genuine_seed.sql +21 -0
  119. package/migrations/full-postgres/0028_add_family_template_key_telemetry.sql +40 -0
  120. package/migrations/full-postgres/0029_null_share_child_policy_columns.sql +38 -0
  121. package/migrations/full-postgres/0030_add_api_key_max_children.sql +10 -0
  122. package/migrations/full-postgres/0031_tenant_system_prompts.sql +16 -0
  123. package/migrations/full-postgres/0032_add_provider_pool_accounts.sql +53 -0
  124. package/migrations/full-postgres/0033_add_api_key_ban.sql +9 -0
  125. package/migrations/full-postgres/0034_add_api_key_ban_until.sql +7 -0
  126. package/migrations/full-postgres/0035_align_pool_account_status_enum.sql +21 -0
  127. package/migrations/lite-sqlite3/0026_add_qoder_native_identity.sql +15 -0
  128. package/migrations/lite-sqlite3/0027_qoder_identity_genuine_seed.sql +21 -0
  129. package/migrations/lite-sqlite3/0028_add_family_template_key_telemetry.sql +40 -0
  130. package/migrations/lite-sqlite3/0029_null_share_child_policy_columns.sql +38 -0
  131. package/migrations/lite-sqlite3/0030_add_api_key_max_children.sql +10 -0
  132. package/migrations/lite-sqlite3/0031_tenant_system_prompts.sql +10 -0
  133. package/migrations/lite-sqlite3/0032_add_provider_pool_accounts.sql +44 -0
  134. package/migrations/lite-sqlite3/0033_add_api_key_ban.sql +8 -0
  135. package/migrations/lite-sqlite3/0034_add_api_key_ban_until.sql +5 -0
  136. package/migrations/lite-sqlite3/0035_align_pool_account_status_enum.sql +6 -0
  137. package/package.json +2 -1
  138. package/src/boot/app.ts +9 -8
  139. package/src/config/network.ts +7 -3
  140. package/src/console/api-keys/bucket-spend.ts +7 -7
  141. package/src/console/api-keys/contracts.ts +475 -410
  142. package/src/console/api-keys/routes.ts +211 -23
  143. package/src/console/audit/routes.ts +3 -3
  144. package/src/console/auth/account-reset.ts +4 -4
  145. package/src/console/auth/session.ts +3 -3
  146. package/src/console/backup/contracts.ts +2 -0
  147. package/src/console/backup/export-endpoint.ts +14 -17
  148. package/src/console/backup/routes.ts +43 -11
  149. package/src/console/backup/service.ts +11 -6
  150. package/src/console/backup/store.ts +384 -88
  151. package/src/console/cli-tools/routes.ts +3 -3
  152. package/src/console/console-router-guards.ts +2 -2
  153. package/src/console/console-router.ts +4 -4
  154. package/src/console/dashboard-assets.ts +16 -1
  155. package/src/console/data-management/routes.ts +3 -3
  156. package/src/console/domain-registration.ts +4 -2
  157. package/src/console/observability/contracts.ts +4 -0
  158. package/src/console/observability/live.ts +3 -3
  159. package/src/console/observability/logs.ts +3 -3
  160. package/src/console/observability/performance.ts +3 -3
  161. package/src/console/observability/routes.ts +3 -3
  162. package/src/console/observability/store.ts +16 -16
  163. package/src/console/providers/catalog/contracts.ts +43 -22
  164. package/src/console/providers/catalog/provider-operations.ts +17 -2
  165. package/src/console/providers/catalog/routes.ts +3 -3
  166. package/src/console/providers/catalog/store-helpers.ts +349 -326
  167. package/src/console/providers/catalog/store.ts +1523 -1292
  168. package/src/console/providers/detail/routes.ts +3 -3
  169. package/src/console/providers/oauth/routes.ts +3 -3
  170. package/src/console/quota/account-quota-tenant.ts +34 -2
  171. package/src/console/quota/account-quota.ts +4 -1
  172. package/src/console/quota/account-test-egress.ts +4 -4
  173. package/src/console/quota/native-identity.ts +189 -0
  174. package/src/console/remote-support/contracts.ts +12 -6
  175. package/src/console/remote-support/service.ts +2 -2
  176. package/src/console/routing/model/routes.ts +3 -3
  177. package/src/console/routing/pools/contracts.ts +11 -8
  178. package/src/console/routing/pools/operations.ts +14 -7
  179. package/src/console/routing/pools/probe-verdict.ts +8 -3
  180. package/src/console/routing/pools/relay-deploy.ts +9 -9
  181. package/src/console/routing/pools/routes.ts +3 -3
  182. package/src/console/routing/pools/store.ts +21 -1
  183. package/src/console/settings/routes.ts +3 -3
  184. package/src/console/share/share-router.ts +51 -18
  185. package/src/console/share/share-stats.ts +158 -49
  186. package/src/console/share/share-usage.ts +24 -7
  187. package/src/console/shared/read-through-cache.ts +38 -2
  188. package/src/console/system-prompt/contracts.ts +102 -0
  189. package/src/console/system-prompt/routes.ts +165 -0
  190. package/src/console/updates/routes.ts +7 -4
  191. package/src/main.ts +11 -0
  192. package/src/network/outbound-fetch.ts +75 -4
  193. package/src/network/pool/resolver.ts +39 -39
  194. package/src/network/pool-cooldown-store.ts +71 -16
  195. package/src/network/pool-health-machine.ts +21 -1
  196. package/src/network/rate-limit-verdict.ts +68 -0
  197. package/src/network/ssrf.ts +4 -4
  198. package/src/observability/event-query.ts +437 -22
  199. package/src/observability/telemetry-status.ts +1 -1
  200. package/src/persistence/db-pg.ts +42 -0
  201. package/src/persistence/db-sqlite.ts +7 -2
  202. package/src/persistence/readiness.ts +18 -6
  203. package/src/persistence/schema.ts +162 -1
  204. package/src/persistence/stores/api-key-store.ts +849 -581
  205. package/src/persistence/stores/share-store.ts +65 -13
  206. package/src/persistence/stores/telemetry-store.ts +58 -2
  207. package/src/persistence/system-prompt-store.ts +104 -0
  208. package/src/protocol/codecs.ts +6 -6
  209. package/src/protocol/gateway-error.ts +6 -3
  210. package/src/protocol/image-generation.ts +2 -2
  211. package/src/protocol/reasoning.ts +65 -1
  212. package/src/protocol/request/chat.ts +1 -0
  213. package/src/protocol/response/chat.ts +2 -2
  214. package/src/protocol/response/codex.ts +1 -1
  215. package/src/protocol/response/gemini.ts +1 -1
  216. package/src/protocol/response/messages.ts +5 -5
  217. package/src/protocol/response/responses.ts +2 -2
  218. package/src/protocol/sse-decoder.ts +14 -3
  219. package/src/protocol/translation/capabilities.ts +2 -2
  220. package/src/protocol/upstream/send-claude-messages.ts +3 -3
  221. package/src/providers/adapters/base-adapter.ts +1 -1
  222. package/src/providers/adapters/compatible-adapter.ts +2 -2
  223. package/src/providers/authentication/oauth-refresh-service.ts +2 -2
  224. package/src/providers/configured-provider.ts +1 -1
  225. package/src/providers/discovery/probe-phases.ts +2 -2
  226. package/src/providers/discovery/probing-service.ts +12 -12
  227. package/src/providers/integrations/agentrouter/agentrouter.ts +2 -3
  228. package/src/providers/integrations/alibabacp/alibabacp.ts +2 -2
  229. package/src/providers/integrations/anthropic.ts +4 -11
  230. package/src/providers/integrations/antigravity/antigravity.ts +33 -47
  231. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -1
  232. package/src/providers/integrations/buddy/buddy-chat-shared.ts +30 -2
  233. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +37 -6
  234. package/src/providers/integrations/buddy/buddy-identity.ts +22 -1
  235. package/src/providers/integrations/buddy/buddy-media.ts +21 -17
  236. package/src/providers/integrations/buddy/codebuddy/provider.ts +6 -2
  237. package/src/providers/integrations/buddy/workbuddy/provider.ts +6 -2
  238. package/src/providers/integrations/claude/claude-betas.ts +1 -1
  239. package/src/providers/integrations/claude/claude-credentials.ts +1 -1
  240. package/src/providers/integrations/claude/claude-slow-lane-retry.ts +1 -1
  241. package/src/providers/integrations/claude/claude.ts +3 -10
  242. package/src/providers/integrations/cline/cline.ts +8 -1
  243. package/src/providers/integrations/cloudflareai/cloudflareai.ts +4 -7
  244. package/src/providers/integrations/codex/codex-errors.ts +1 -1
  245. package/src/providers/integrations/codex/codex.ts +14 -15
  246. package/src/providers/integrations/commandcode/commandcode.ts +4 -4
  247. package/src/providers/integrations/commandcode/quota.ts +5 -1
  248. package/src/providers/integrations/cursor/cursor-http2.ts +11 -11
  249. package/src/providers/integrations/cursor/cursor.ts +15 -15
  250. package/src/providers/integrations/factory/factory.ts +4 -4
  251. package/src/providers/integrations/gemini.ts +11 -11
  252. package/src/providers/integrations/github/github.ts +26 -7
  253. package/src/providers/integrations/grok/grok.ts +2 -6
  254. package/src/providers/integrations/hermes.ts +5 -1
  255. package/src/providers/integrations/inferhub.ts +4 -11
  256. package/src/providers/integrations/kilo/kilo.ts +4 -0
  257. package/src/providers/integrations/kimi/kimi.ts +4 -11
  258. package/src/providers/integrations/kiro/kiro.ts +10 -10
  259. package/src/providers/integrations/muse/muse.ts +6 -9
  260. package/src/providers/integrations/openrouter/provider.ts +4 -0
  261. package/src/providers/integrations/perplexity.ts +2 -2
  262. package/src/providers/integrations/qoder/qoder-catalog.ts +47 -9
  263. package/src/providers/integrations/qoder/qoder-checkin.ts +242 -51
  264. package/src/providers/integrations/qoder/qoder-native-identity-store.ts +157 -0
  265. package/src/providers/integrations/qoder/qoder-oauth.ts +12 -3
  266. package/src/providers/integrations/qoder/qoder-realm.ts +15 -0
  267. package/src/providers/integrations/qoder/qoder-umid.ts +287 -0
  268. package/src/providers/integrations/qoder/qoder-virtual.ts +2 -2
  269. package/src/providers/integrations/qoder/qoder.ts +192 -24
  270. package/src/providers/integrations/tokenharbor.ts +2 -2
  271. package/src/providers/integrations/trae/trae.ts +6 -9
  272. package/src/providers/integrations/zcode/oauth.ts +1 -1
  273. package/src/providers/operations/account-error-classification.ts +6 -8
  274. package/src/providers/operations/account-health-service.ts +14 -16
  275. package/src/providers/operations/client-versions.ts +37 -22
  276. package/src/providers/operations/provider-catalog-service.ts +4 -4
  277. package/src/providers/operations/provider-credential-service.ts +16 -4
  278. package/src/providers/operations/upstream-deadline.ts +3 -3
  279. package/src/providers/provider-contracts.ts +55 -3
  280. package/src/providers/provider-credential-resolver.ts +1 -1
  281. package/src/providers/provider-registry.ts +4 -4
  282. package/src/providers/quota/quota-contracts.ts +24 -10
  283. package/src/providers/quota/refresh.ts +2 -2
  284. package/src/providers/search/search-provider.ts +5 -5
  285. package/src/security/abuse.ts +4 -4
  286. package/src/security/access-control.ts +19 -0
  287. package/src/security/admission/reasons.ts +17 -17
  288. package/src/security/api-key-auth.ts +18 -5
  289. package/src/shared/coerce.ts +19 -0
  290. package/src/transport/dispatch/attempt-finalize.ts +4 -3
  291. package/src/transport/dispatch/attempt-loop.ts +10 -7
  292. package/src/transport/dispatch/empty-stream.ts +6 -6
  293. package/src/transport/dispatch/generation.ts +9 -9
  294. package/src/transport/dispatch/leases.ts +8 -8
  295. package/src/transport/dispatch/proxy-request.ts +38 -9
  296. package/src/transport/dispatch/responses-compact.ts +11 -11
  297. package/src/transport/dispatch/retry-policy.ts +4 -67
  298. package/src/transport/dispatch/stream-bridge.ts +1 -1
  299. package/src/transport/dispatch/streaming/finalize.ts +9 -8
  300. package/src/transport/dispatch/streaming/stream.ts +16 -1
  301. package/src/transport/dispatch/systemone.ts +8 -8
  302. package/src/transport/dispatch/token-count.ts +9 -9
  303. package/src/transport/dispatch/upstream.ts +10 -0
  304. package/src/transport/dispatch/websearch.ts +6 -6
  305. package/src/transport/drain-endpoint.ts +2 -2
  306. package/src/transport/failure-policy.ts +13 -6
  307. package/src/transport/guards/auth-guard.ts +6 -5
  308. package/src/transport/guards/body-policy.ts +4 -4
  309. package/src/transport/guards/error-lifecycle.ts +18 -3
  310. package/src/transport/guards/guard-errors.ts +7 -3
  311. package/src/transport/guards/headroom-guard.ts +1 -1
  312. package/src/transport/guards/pipeline.ts +6 -6
  313. package/src/transport/guards/readiness-guard.ts +16 -2
  314. package/src/transport/guards/request-context.ts +3 -3
  315. package/src/transport/request/preparer.ts +37 -21
  316. package/src/transport/request/state.ts +4 -7
  317. package/src/transport/request/system-prompt-injection.ts +89 -0
  318. package/src/transport/request/tenant-preferences.ts +43 -0
  319. package/src/transport/routing/admission.ts +13 -3
  320. package/src/transport/routing/route-catalog.ts +104 -2
  321. package/src/transport/routing/route-model.ts +60 -7
  322. package/src/transport/routing/router.ts +306 -256
  323. package/src/transport/shutdown-notice.ts +19 -7
  324. package/src/transport/surface/completion.ts +1 -1
  325. package/src/transport/surface/messages/encode.ts +1 -1
  326. package/src/workers/daily-checkin.ts +2 -0
  327. package/dist/dashboard/assets/BannedPage-XaUmiBrC.js.gz +0 -0
  328. package/dist/dashboard/assets/CliToolDetailPage-CqD7QiLM.js +0 -1
  329. package/dist/dashboard/assets/CliToolDetailPage-CqD7QiLM.js.gz +0 -0
  330. package/dist/dashboard/assets/CliToolsPage-0XUo4UMn.js +0 -1
  331. package/dist/dashboard/assets/CliToolsPage-0XUo4UMn.js.gz +0 -0
  332. package/dist/dashboard/assets/CombosPage-CXGS_Ct-.js.gz +0 -0
  333. package/dist/dashboard/assets/ConsoleLogPage-DGnGNYnM.js +0 -1
  334. package/dist/dashboard/assets/ConsoleLogPage-DGnGNYnM.js.gz +0 -0
  335. package/dist/dashboard/assets/CustomizationPage-DAG5J9EY.js +0 -1
  336. package/dist/dashboard/assets/CustomizationPage-DAG5J9EY.js.gz +0 -0
  337. package/dist/dashboard/assets/HealthEventsModal-BKtAPYVF.js.gz +0 -0
  338. package/dist/dashboard/assets/LoginPage-BdmRLnq2.js.gz +0 -0
  339. package/dist/dashboard/assets/ModelPicker-BH8Tehdi.js.gz +0 -0
  340. package/dist/dashboard/assets/OverviewPage-CqMxibJB.js +0 -1
  341. package/dist/dashboard/assets/OverviewPage-CqMxibJB.js.gz +0 -0
  342. package/dist/dashboard/assets/ProviderDetailPage-CqMh2NRX.js +0 -4
  343. package/dist/dashboard/assets/ProviderDetailPage-CqMh2NRX.js.gz +0 -0
  344. package/dist/dashboard/assets/ProvidersPage-B9uWu4Nz.js +0 -1
  345. package/dist/dashboard/assets/ProvidersPage-B9uWu4Nz.js.gz +0 -0
  346. package/dist/dashboard/assets/ProxyPage-BoWOhpDO.js +0 -9
  347. package/dist/dashboard/assets/ProxyPage-BoWOhpDO.js.gz +0 -0
  348. package/dist/dashboard/assets/QuotaPage-7P2n--Fv.js +0 -1
  349. package/dist/dashboard/assets/QuotaPage-7P2n--Fv.js.gz +0 -0
  350. package/dist/dashboard/assets/SettingsPage-CxZ1fTkN.js +0 -1
  351. package/dist/dashboard/assets/SettingsPage-CxZ1fTkN.js.gz +0 -0
  352. package/dist/dashboard/assets/SetupPage-Co12Zzaw.js +0 -1
  353. package/dist/dashboard/assets/SetupPage-Co12Zzaw.js.gz +0 -0
  354. package/dist/dashboard/assets/SortableList-Dqf4zpS-.js.gz +0 -0
  355. package/dist/dashboard/assets/StudioPage-Byj2lAEm.js.gz +0 -0
  356. package/dist/dashboard/assets/UsagePage-CTkLAuRc.js +0 -56
  357. package/dist/dashboard/assets/UsagePage-CTkLAuRc.js.gz +0 -0
  358. package/dist/dashboard/assets/api-keys-CUi6UXsR.js +0 -1
  359. package/dist/dashboard/assets/api-keys-CUi6UXsR.js.gz +0 -0
  360. package/dist/dashboard/assets/app-KvsUJUMF.css +0 -2
  361. package/dist/dashboard/assets/app-KvsUJUMF.css.gz +0 -0
  362. package/dist/dashboard/assets/assert-67xXH5rf.js +0 -1
  363. package/dist/dashboard/assets/assert-67xXH5rf.js.gz +0 -0
  364. package/dist/dashboard/assets/badge-DC2L43Mj.js.gz +0 -0
  365. package/dist/dashboard/assets/banner-BBVcb0J3.js.gz +0 -0
  366. package/dist/dashboard/assets/card-Cca1XC6G.js.gz +0 -0
  367. package/dist/dashboard/assets/cli-tools-KvsWa1Jl.js +0 -1
  368. package/dist/dashboard/assets/cli-tools-KvsWa1Jl.js.gz +0 -0
  369. package/dist/dashboard/assets/clipboard-button-DUzEbynq.js.gz +0 -0
  370. package/dist/dashboard/assets/concurrency-BDF0ok7y.js.gz +0 -0
  371. package/dist/dashboard/assets/dialog-DFvWhwTa.js.gz +0 -0
  372. package/dist/dashboard/assets/drawer-CFM2sMX1.js +0 -1
  373. package/dist/dashboard/assets/drawer-CFM2sMX1.js.gz +0 -0
  374. package/dist/dashboard/assets/entry-Bqy8RFvM.js.gz +0 -0
  375. package/dist/dashboard/assets/entry-ClCMCWWQ.js +0 -4
  376. package/dist/dashboard/assets/entry-ClCMCWWQ.js.gz +0 -0
  377. package/dist/dashboard/assets/entry-_xHo5x6r.js +0 -1
  378. package/dist/dashboard/assets/entry-_xHo5x6r.js.gz +0 -0
  379. package/dist/dashboard/assets/formatters-CWWZ-uCZ.js.gz +0 -0
  380. package/dist/dashboard/assets/github-badge-C9e8Ts7e.js.gz +0 -0
  381. package/dist/dashboard/assets/index-DFVFrnWW.js.gz +0 -0
  382. package/dist/dashboard/assets/inline-BFUcvllj.js.gz +0 -0
  383. package/dist/dashboard/assets/input-C_ILAvBb.js.gz +0 -0
  384. package/dist/dashboard/assets/layout-CBIyT6Rt.js.gz +0 -0
  385. package/dist/dashboard/assets/list-row-ebJ_-Nsb.js.gz +0 -0
  386. package/dist/dashboard/assets/live-D0mm--ME.js +0 -1
  387. package/dist/dashboard/assets/live-D0mm--ME.js.gz +0 -0
  388. package/dist/dashboard/assets/minimize-2-D-MN8W8a.js +0 -1
  389. package/dist/dashboard/assets/minimize-2-D-MN8W8a.js.gz +0 -0
  390. package/dist/dashboard/assets/mutation-sMD965oO.js.gz +0 -0
  391. package/dist/dashboard/assets/numbers-BO9dKaFr.js.gz +0 -0
  392. package/dist/dashboard/assets/query-keys-CfL_Zdoh.js.gz +0 -0
  393. package/dist/dashboard/assets/quota-DZGyp0N5.js +0 -1
  394. package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
  395. package/dist/dashboard/assets/routing-BnIpi1IJ.js +0 -1
  396. package/dist/dashboard/assets/routing-BnIpi1IJ.js.gz +0 -0
  397. package/dist/dashboard/assets/select-DBNVLdUu.js.gz +0 -0
  398. package/dist/dashboard/assets/settings-BAFhDgX8.js +0 -1
  399. package/dist/dashboard/assets/settings-BAFhDgX8.js.gz +0 -0
  400. package/dist/dashboard/assets/stack-EMrOMt4I.js.gz +0 -0
  401. package/dist/dashboard/assets/state-CWoo7ty5.js.gz +0 -0
  402. package/dist/dashboard/assets/switch-CCjT6IkF.js.gz +0 -0
  403. package/dist/dashboard/assets/useMutation-DjkLHUcE.js.gz +0 -0
  404. /package/dist/dashboard/assets/{app-BaWd0puM.js → app-BayTTy01.js} +0 -0
  405. /package/dist/dashboard/assets/{app-BaWd0puM.js.gz → app-BayTTy01.js.gz} +0 -0
@@ -19,26 +19,26 @@ const REASON_META: Record<
19
19
  }
20
20
  > = {
21
21
  "rpm-exhausted": {
22
- message: "This key has used its requests-per-minute allowance. Wait for the window to roll over, or raise the key's rpm limit.",
23
- code: "quota_exceeded",
22
+ message: "This key has used its requests-per-minute allowance (request {requestId}). Wait for the window to roll over, or raise the key's rpm limit. You've maxed this window — pace yourself ⏳",
23
+ code: "rate_limited",
24
24
  status: 429,
25
25
  metricLabel: "rpm_exhausted",
26
26
  },
27
27
  "daily-token-limit": {
28
- message: "This key has used today's token allowance. It resets at the start of the next day, or raise the key's daily limit.",
29
- code: "quota_exceeded",
28
+ message: "This key has used today's token allowance (request {requestId}). It resets at the start of the next day, or raise the key's daily limit. Pace yourself — it resets tomorrow ⏳",
29
+ code: "rate_limited",
30
30
  status: 429,
31
31
  metricLabel: "daily",
32
32
  },
33
33
  "monthly-token-limit": {
34
- message: "This key has used this month's token allowance. It resets at the start of the next month, or raise the key's monthly limit.",
35
- code: "quota_exceeded",
34
+ message: "This key has used this month's token allowance (request {requestId}). It resets at the start of the next month, or raise the key's monthly limit. Saving up for next month? It resets then ⏳",
35
+ code: "rate_limited",
36
36
  status: 429,
37
37
  metricLabel: "monthly",
38
38
  },
39
39
  "lifetime-token-budget": {
40
- message: "This key has spent its lifetime token budget. Raise the budget, or issue a new key.",
41
- code: "quota_exceeded",
40
+ message: "This key has spent its lifetime token budget (request {requestId}). Raise the budget, or issue a new key. The piggy bank is empty — top it up or mint a new key 🐷",
41
+ code: "rate_limited",
42
42
  status: 429,
43
43
  metricLabel: "lifetime",
44
44
  },
@@ -47,27 +47,27 @@ const REASON_META: Record<
47
47
  // max-inflight. The two used to be indistinguishable and both named the
48
48
  // provider, sending operators to the wrong setting after a rejection. The
49
49
  // `details` carry the limit so a client can tell which one fired.
50
- message: "This API key is already serving its maximum number of concurrent requests. Retry shortly, or raise the key's concurrency limit.",
50
+ message: "This API key is already serving its maximum number of concurrent requests (request {requestId}). Retry shortly, or raise the key's concurrency limit. It'll free up in a moment 🌊",
51
51
  code: "capacity_exhausted",
52
52
  status: 429,
53
53
  metricLabel: "concurrency",
54
54
  },
55
55
  "tenant-capacity-exhausted": {
56
- message: "This tenant is already serving its maximum number of concurrent requests. Retry shortly, or raise the tenant's concurrency limit.",
56
+ message: "This tenant is already serving its maximum number of concurrent requests (request {requestId}). Retry shortly, or raise the tenant's concurrency limit. The tenant is maxed out right now — chill 🏠",
57
57
  code: "tenant_capacity_exhausted",
58
58
  status: 429,
59
59
  metricLabel: "tenant_capacity",
60
60
  },
61
61
  "model-not-allowed": {
62
- message: "This key is not allowed to use this model under its allowlist. Add the model to the key, or use one it already allows.",
63
- code: "model_not_found",
64
- status: 404,
62
+ message: "This key is not allowed to use this model under its allowlist (request {requestId}). Add the model to the key, or use one it already allows. The door's locked for this one — check the allowlist 🔒",
63
+ code: "model_not_allowed",
64
+ status: 403,
65
65
  metricLabel: "model_not_allowed",
66
66
  },
67
67
  "model-denied": {
68
- message: "This key's denylist blocks this model. Remove the model from the denylist, or use a different model.",
69
- code: "model_not_found",
70
- status: 404,
68
+ message: "This key's denylist blocks this model (request {requestId}). Remove the model from the denylist, or use a different model. This one's on the banned list — pick another 🚫",
69
+ code: "model_not_allowed",
70
+ status: 403,
71
71
  metricLabel: "model_not_allowed",
72
72
  },
73
73
  "admission-unavailable": {
@@ -76,7 +76,7 @@ const REASON_META: Record<
76
76
  // `invalid_counter`, `unknown_reservation`, `invalid_actual_usage`). The
77
77
  // second is not an outage, so the text must not claim one — it names the
78
78
  // effect the client can act on, and the diagnostic stays in `details.reason`.
79
- message: "The gateway could not admit this request. Retry shortly; if it persists, check the instance's Redis or coordination backend.",
79
+ message: "The gateway could not admit this request (request {requestId}). Retry shortly; if it persists, check the instance's Redis or coordination backend. The bouncer stepped out — come back shortly 🚪",
80
80
  code: "admission_unavailable",
81
81
  status: 503,
82
82
  metricLabel: "admission_unavailable",
@@ -6,7 +6,7 @@ import { DrizzleApiKeyStore } from "../persistence/stores/api-key-store";
6
6
  import { GatewayError } from "../protocol/gateway-error";
7
7
  import { hashSecret } from "./crypto";
8
8
  import { AUTH_CACHE_TTL_MS, createApiKeyAuthorizationCache } from "./api-key-auth-cache";
9
- import type { AccessScope } from "./access-control";
9
+ import type { AccessScope, CallerIdentity } from "./access-control";
10
10
  import type { ApiKeyModelAccessMode } from "../persistence/schema";
11
11
 
12
12
  // ── Authorization cache ──────────────────────────────────────────────────────
@@ -222,6 +222,12 @@ export interface ResolvedApiKey {
222
222
  readonly modelPrefix?: string;
223
223
  /** Share-template id whose remote CLI mappings are inherited by this child. */
224
224
  readonly cliMappingOwnerId?: string;
225
+ /**
226
+ * Single "who is speaking" identity derived at auth. Carried through
227
+ * planning, admission, pool selection, and telemetry so every gate reasons
228
+ * about the same caller.
229
+ */
230
+ readonly caller: CallerIdentity;
225
231
  }
226
232
 
227
233
  /**
@@ -233,7 +239,7 @@ export interface ResolvedApiKey {
233
239
  * indistinguishable — distinguishing them would let a caller confirm that a
234
240
  * guessed token once existed — so there is no variant carrying a reason.
235
241
  */
236
- const INVALID_KEY_MESSAGE = "Invalid API key. Check the key you are sending, or create a new one in API Keys and send it as: Authorization: Bearer <key>.";
242
+ const INVALID_KEY_MESSAGE = "Invalid API key (request {requestId}). Check the key you are sending, or create a new one in API Keys and send it as: Authorization: Bearer <key>. Wrong key, right door — double-check and come back 🔑";
237
243
 
238
244
  export function unknownApiKeyError(): GatewayError {
239
245
  return new GatewayError("authentication_failed", 401, INVALID_KEY_MESSAGE);
@@ -252,7 +258,7 @@ export function modelsListApiKeyError(): GatewayError {
252
258
  return new GatewayError(
253
259
  "authentication_failed",
254
260
  401,
255
- "Invalid API key. Rotate the default Cartethyia API key in API Keys and send the new key as: Authorization: Bearer <key>.",
261
+ "Invalid API key (request {requestId}). Rotate the default Cartethyia API key in API Keys and send the new key as: Authorization: Bearer <key>. Wrong key, right door — rotate and try again 🔑",
256
262
  );
257
263
  }
258
264
 
@@ -273,12 +279,12 @@ export function requestToken(headers: Headers | Record<string, string>): string
273
279
  // *different* credentials stay a conflict — accepting whichever came first
274
280
  // would silently decide the request's identity.
275
281
  if (bearerMatch?.[1] === apiKey) return apiKey;
276
- throw new GatewayError("invalid_request", 400, "The request sends two different credentials. Send exactly one key, either as Authorization: Bearer <key> or as the api-key header.");
282
+ throw new GatewayError("invalid_request", 400, "The request sends two different credentials (request {requestId}). Send exactly one key, either as Authorization: Bearer <key> or as the api-key header. Pick one key and stick with it 🤝");
277
283
  }
278
284
  if (hasApiKey) return apiKey;
279
285
  const token = bearerMatch?.[1];
280
286
  if (token === undefined)
281
- throw new GatewayError("invalid_request", 401, "The request carries no usable credential. Send the key as: Authorization: Bearer <key>.");
287
+ throw new GatewayError("authentication_failed", 401, "The request carries no usable credential (request {requestId}). Send the key as: Authorization: Bearer <key>. We need a key to let you in 🔑");
282
288
  return token;
283
289
  }
284
290
 
@@ -398,6 +404,13 @@ export async function resolveApiKeyAuthorization(
398
404
  snapshot,
399
405
  ...(policy.modelPrefix ? { modelPrefix: policy.modelPrefix } : {}),
400
406
  ...(parent?.keyMode === "share" ? { cliMappingOwnerId: parent.id } : {}),
407
+ caller: {
408
+ apiKeyId: row.id,
409
+ // The admission identity is the family owner for a share child — the
410
+ // same id every counter/lease in the system keys against.
411
+ admissionIdentity: snapshot.admission_identity ?? row.id,
412
+ tenantId: row.tenantId,
413
+ },
401
414
  };
402
415
  authorizationCache.rememberPositive(hash, value.id, value, Date.now());
403
416
  // Shared copy for peer processes, keyed by the token hash so a lookup can
@@ -13,6 +13,25 @@ export function asRecord(value: unknown): Record<string, unknown> | undefined {
13
13
  : undefined;
14
14
  }
15
15
 
16
+ /**
17
+ * Narrow a stored `auth_state`/JSONB value to a plain object. Postgres'
18
+ * drizzle `jsonb` arrives as a POJO; SQLite's text-backed column arrives as
19
+ * a JSON STRING. Parse the string form so both backends hand downstream
20
+ * readers the same shape — otherwise auth-state-dependent providers (Qoder's
21
+ * userId/machineId, Trae's region, …) silently lose their identity on the
22
+ * SQLite backend.
23
+ */
24
+ export function asJsonbRecord(value: unknown): Record<string, unknown> | undefined {
25
+ if (typeof value === "string") {
26
+ try {
27
+ return asRecord(JSON.parse(value));
28
+ } catch {
29
+ return undefined;
30
+ }
31
+ }
32
+ return asRecord(value);
33
+ }
34
+
16
35
  /** Narrow to a finite number, else `undefined`. `NaN`/`Infinity` are not numbers here. */
17
36
  export function asNumber(value: unknown): number | undefined {
18
37
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;
@@ -425,7 +425,7 @@ export function terminalFailure(
425
425
  return new GatewayError(
426
426
  "transport_unavailable",
427
427
  502,
428
- "The upstream closed the response without reporting a result. Retry; if it keeps happening, the account or pool is unstable.",
428
+ "The upstream closed the response without reporting a result (request {requestId}). Retry; if it keeps happening, the account or pool is unstable. The connection ghosted us — come back in a moment 👻",
429
429
  {},
430
430
  "upstream",
431
431
  );
@@ -437,7 +437,7 @@ export function terminalFailure(
437
437
  const details: Record<string, unknown> = {};
438
438
  if (terminal.provider_stop_reason) details["provider_code"] = terminal.provider_stop_reason;
439
439
  if (terminal.stop_details) Object.assign(details, terminal.stop_details);
440
- const message = typeof details["message"] === "string" ? details["message"] : "upstream request failed";
440
+ const message = typeof details["message"] === "string" ? details["message"] : "upstream request failed (request {requestId}). The upstream came back empty-handed — retry 🌙";
441
441
  // Preserve the upstream's error code when the provider embedded it in
442
442
  // `stop_details` (Cursor writes `quota_exceeded` and `authentication_failed`
443
443
  // there). Wrapping every terminal failure as `transport_unavailable` discarded
@@ -460,7 +460,7 @@ export function terminalFailure(
460
460
  // reports it as truncation instead.
461
461
  return options.truncated === true
462
462
  ? undefined
463
- : new GatewayError("transport_closed", 499, "request was cancelled");
463
+ : new GatewayError("transport_closed", 499, "The request was cancelled (request {requestId}). Send it again whenever ready 🙃");
464
464
  }
465
465
  return undefined;
466
466
  }
@@ -823,6 +823,7 @@ export async function completeAttempt(
823
823
  (completion.error instanceof ProxyConnectRefusalError ? "network" : undefined);
824
824
  await recordPoolDispatchOutcome(completion.db, completion.networkPoolId, {
825
825
  succeeded: completion.status === "completed",
826
+ ...(completion.providerId === undefined ? {} : { providerId: completion.providerId }),
826
827
  ...(completion.error === undefined ? {} : { error: completion.error }),
827
828
  // A CONNECT refusal is the proxy's own answer, given before any
828
829
  // upstream was dialed, so it is a network fault even though the
@@ -41,7 +41,8 @@ import {
41
41
  type ProviderExchangeCapture,
42
42
  } from "./attempt-finalize";
43
43
  import { repriceUsage } from "../../providers/provider-usage";
44
- import { isFreeTierIpBlock, isOAuthCredentialInvalidated, shouldCooldownPool } from "./retry-policy";
44
+ import { classifyEgressFailure } from "../../network/rate-limit-verdict";
45
+ import { isOAuthCredentialInvalidated } from "./retry-policy";
45
46
  import { drainAbortReason } from "../shutdown-notice";
46
47
 
47
48
  /**
@@ -316,7 +317,7 @@ export async function runAttemptLoop<TResult, TAdapter>(
316
317
  };
317
318
  try {
318
319
  if (state.abortController.signal.aborted)
319
- throw new GatewayError("transport_closed", 499, "request was cancelled");
320
+ throw new GatewayError("transport_closed", 499, "The request was cancelled (request {requestId}). Send it again whenever ready 🙃");
320
321
  const { credential, adapter } = await input.prepare(candidate);
321
322
  const host = input.resolveHost?.(candidate);
322
323
  // Single attempt-lease manager owns acquisition order (admission →
@@ -353,9 +354,10 @@ export async function runAttemptLoop<TResult, TAdapter>(
353
354
  });
354
355
  } catch (error) {
355
356
  // A drain aborts the controller, so a mid-flight attempt unwinds here as
356
- // an abort. Surface the typed shutdown error (503 shutting_down /
357
- // restart_for_update) instead of an opaque `transport_closed`, so the
358
- // client's JSON error — not just the SSE path — names the real cause.
357
+ // an abort. Surface the typed shutdown error (503 shutting_down, reason
358
+ // restart_for_update for an update drain) instead of an opaque
359
+ // `transport_closed`, so the client's JSON error — not just the SSE path —
360
+ // names the real cause.
359
361
  const drain = drainAbortReason(state.abortController.signal.reason);
360
362
  if (drain !== undefined) error = drain;
361
363
  lastError = error;
@@ -374,11 +376,12 @@ export async function runAttemptLoop<TResult, TAdapter>(
374
376
  // single candidate carrying every pool). Judging it there is what made a
375
377
  // refused egress end the request instead of failing over to a healthy
376
378
  // one.
377
- const rateLimitedPool = !cancelled && shouldCooldownPool(error, candidate.provider_id);
379
+ const egressVerdict = !cancelled ? classifyEgressFailure(error, candidate.provider_id) : { kind: "none" } as const;
378
380
  // A free-tier refusal is only ever this provider's: the guard is gated on
379
381
  // `ipScopedRateLimit` in the provider metadata, so a 403 from any other
380
382
  // upstream still means what it always did and never retires an egress.
381
- const blockedPool = !cancelled && isFreeTierIpBlock(error, candidate.provider_id);
383
+ const rateLimitedPool = egressVerdict.kind === "cooldown";
384
+ const blockedPool = egressVerdict.kind === "blocked";
382
385
  const poolScopedFailure =
383
386
  (rateLimitedPool || blockedPool) &&
384
387
  networkPoolId !== undefined &&
@@ -107,7 +107,7 @@ export async function fetchWithEmptyStreamRecovery(
107
107
  const sleep = deps.policy?.sleep ?? abortableSleep;
108
108
 
109
109
  if (endpoints.length === 0) {
110
- throw new GatewayError("platform_unavailable", 502, "No endpoints available", {}, "upstream");
110
+ throw new GatewayError("platform_unavailable", 502, "No endpoints available (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
111
111
  }
112
112
 
113
113
  for (let endpointIndex = 0; endpointIndex < endpoints.length; endpointIndex++) {
@@ -118,7 +118,7 @@ export async function fetchWithEmptyStreamRecovery(
118
118
  for (let replay = 0; replay <= replaysAllowed; replay++) {
119
119
  // Check abort before each fetch
120
120
  if (deps.signal.aborted) {
121
- throw new GatewayError("transport_closed", 499, "Aborted");
121
+ throw new GatewayError("transport_closed", 499, "The request was aborted (request {requestId}). Send it again whenever ready 🙃");
122
122
  }
123
123
 
124
124
  // Check deadline floor
@@ -128,7 +128,7 @@ export async function fetchWithEmptyStreamRecovery(
128
128
  throw new GatewayError(
129
129
  "platform_unavailable",
130
130
  502,
131
- "Deadline exhausted before empty-stream replay",
131
+ "Deadline exhausted before empty-stream replay (request {requestId}). Some things just take longer 🐢",
132
132
  {},
133
133
  "upstream",
134
134
  );
@@ -181,7 +181,7 @@ export async function fetchWithEmptyStreamRecovery(
181
181
  throw new GatewayError(
182
182
  "platform_unavailable",
183
183
  502,
184
- "All endpoints returned empty streams",
184
+ "All endpoints returned empty streams (request {requestId}). The upstream came back empty-handed — retry 🌙",
185
185
  {},
186
186
  "upstream",
187
187
  );
@@ -204,11 +204,11 @@ async function fetchWithTimeout(
204
204
  // It must surface as a deadline failure, never as a `transport_closed`
205
205
  // 499 — that code is reserved for genuine client cancellations and would
206
206
  // otherwise hide a real upstream wait behind "cancelled by client".
207
- reject(new GatewayError("deadline_exceeded", 504, "First event timeout", {}, "upstream"));
207
+ reject(new GatewayError("deadline_exceeded", 504, "First event timeout (request {requestId}). Some things just take longer 🐢", {}, "upstream"));
208
208
  }, timeoutMs);
209
209
  signal.addEventListener("abort", () => {
210
210
  clearTimeout(timer);
211
- reject(new GatewayError("transport_closed", 499, "Aborted"));
211
+ reject(new GatewayError("transport_closed", 499, "The request was aborted (request {requestId}). Send it again whenever ready 🙃"));
212
212
  }, { once: true });
213
213
  });
214
214
 
@@ -163,18 +163,18 @@ export function createNativeSurfaceHandler(
163
163
  throw new GatewayError(
164
164
  "invalid_request",
165
165
  400,
166
- `The ${noun} body must be a JSON object. Send one with at least a model and a prompt.`,
166
+ `The ${noun} body must be a JSON object (request {requestId}). Send one with at least a model and a prompt. Check the shape of what you sent — then resend 🪞`,
167
167
  );
168
168
  }
169
169
  const generation = body as Record<string, unknown>;
170
170
  if (typeof generation.model !== "string" || generation.model.trim().length === 0) {
171
- throw new GatewayError("invalid_request", 400, `A model is required: it names the ${noun} provider to use.`);
171
+ throw new GatewayError("invalid_request", 400, `A model is required (request {requestId}): it names the ${noun} provider to use. Check the shape of what you sent — then resend 🪞`);
172
172
  }
173
173
  if (!authorization.snapshot.scopes?.includes("routing:invoke" as never)) {
174
174
  throw new GatewayError(
175
- "invalid_request",
175
+ "policy_rejected",
176
176
  403,
177
- `${noun} generation not allowed for this key — enable routing:invoke on the API key`,
177
+ `${noun} generation not allowed for this key (request {requestId}) — enable routing:invoke on the API key. Not allowed by policy — no hard feelings 🫂`,
178
178
  );
179
179
  }
180
180
  // Trust boundary only. Keep accepting `messages` for chat-backed providers
@@ -183,7 +183,7 @@ export function createNativeSurfaceHandler(
183
183
  const hasPrompt = typeof generation.prompt === "string" && generation.prompt.trim().length > 0;
184
184
  const hasMessages = Array.isArray(generation.messages) && generation.messages.length > 0;
185
185
  if (!hasPrompt && !hasMessages) {
186
- throw new GatewayError("invalid_request", 400, "prompt is required");
186
+ throw new GatewayError("invalid_request", 400, "prompt is required (request {requestId}). Check the shape of what you sent — then resend 🪞");
187
187
  }
188
188
  if (generation.n !== undefined) {
189
189
  const n = generation.n;
@@ -191,7 +191,7 @@ export function createNativeSurfaceHandler(
191
191
  throw new GatewayError(
192
192
  "invalid_request",
193
193
  400,
194
- `n must be an integer between 1 and ${MAX_ITEMS_PER_REQUEST}`,
194
+ `n must be an integer between 1 and ${MAX_ITEMS_PER_REQUEST} (request {requestId}). Check the shape of what you sent — then resend 🪞`,
195
195
  );
196
196
  }
197
197
  }
@@ -219,7 +219,7 @@ export function createNativeSurfaceHandler(
219
219
  candidates: prepared.candidates,
220
220
  tenantId: prepared.authorization.tenantId,
221
221
  strictPoolSelection: false,
222
- exhaustedError: new GatewayError("admission_unavailable", 503, `no eligible ${noun} generation route`),
222
+ exhaustedError: new GatewayError("route_unavailable", 503, `no eligible ${noun} generation route (request {requestId}). The bouncer stepped out — come back shortly 🚪`),
223
223
  prepare: async (candidate) => {
224
224
  const credential = candidate.provider_account_id
225
225
  ? await resolveCredentialForAccount(
@@ -239,7 +239,7 @@ export function createNativeSurfaceHandler(
239
239
  throw new GatewayError(
240
240
  "capability_unsupported",
241
241
  400,
242
- `${noun} generation is unavailable for this provider`,
242
+ `${noun} generation is unavailable for this provider (request {requestId}). Know your tools — this one just isn't in the box 🧰`,
243
243
  );
244
244
  }
245
245
  return { credential, adapter };
@@ -344,7 +344,7 @@ export function createNativeSurfaceHandler(
344
344
  new GatewayError(
345
345
  "capability_unsupported",
346
346
  400,
347
- `${noun} generation is unavailable for this provider`,
347
+ `${noun} generation is unavailable for this provider (request {requestId}). Know your tools — this one just isn't in the box 🧰`,
348
348
  ),
349
349
  )
350
350
  : adapter
@@ -141,7 +141,7 @@ async function acquirePoolSlot(
141
141
  throw new GatewayError(
142
142
  "proxy_pool_unhealthy",
143
143
  503,
144
- "Every proxy pool this route requires is unhealthy or disabled. Repair a pool, or relax the route's pool requirement.",
144
+ "Every proxy pool this route requires is unhealthy or disabled (request {requestId}). Repair a pool, or relax the route's pool requirement. This bridge looks shaky — repair or reroute 🔧",
145
145
  { reason: "no_healthy_pool" },
146
146
  "network",
147
147
  );
@@ -172,7 +172,7 @@ async function acquirePoolSlot(
172
172
  throw new GatewayError(
173
173
  "proxy_pool_unavailable",
174
174
  503,
175
- "No active proxy pool is available because coordination is unavailable.",
175
+ "No active proxy pool is available because coordination is unavailable (request {requestId}). The bouncer stepped out — come back shortly 🚪",
176
176
  { reason: "coordination_unavailable" },
177
177
  "network",
178
178
  );
@@ -219,14 +219,14 @@ async function acquirePoolSlot(
219
219
  failure.reason === "at_capacity" || failure.reason === "provider_at_capacity" ? 429 : 503;
220
220
  const message =
221
221
  failure.reason === "at_capacity"
222
- ? "Every proxy pool for this provider is at its concurrency limit. Retry shortly, or add a pool or raise its capacity."
222
+ ? "Every proxy pool for this provider is at its concurrency limit (request {requestId}). Retry shortly, or add a pool or raise its capacity. The pools are full right now — chill 🌊"
223
223
  : failure.reason === "provider_at_capacity"
224
- ? "Every proxy pool has reached this provider's per-proxy concurrency limit. Retry shortly, raise the limit in the provider's routing settings, or add a pool."
224
+ ? "Every proxy pool has reached this provider's per-proxy concurrency limit (request {requestId}). Retry shortly, raise the limit in the provider's routing settings, or add a pool. The proxies are working overtime — hang tight ⚡️"
225
225
  : failure.reason === "blocked"
226
- ? "All proxy pools are saturated for this provider: the upstream refuses every egress configured for it. Replace a blocked proxy, or add one the upstream has not flagged."
226
+ ? "All proxy pools are saturated for this provider (request {requestId}): the upstream refuses every egress configured for it. Replace a blocked proxy, or add one the upstream has not flagged. All roads blocked — time to find a new route 🗺️"
227
227
  : failure.reason === "cooldown"
228
- ? "Every proxy pool for this provider is cooling down after upstream failures. Retry once the cooldown resets."
229
- : "No active proxy pool is configured for this tenant and provider. Add one, or route this provider through direct egress.";
228
+ ? "Every proxy pool for this provider is cooling down after upstream failures (request {requestId}). Retry once the cooldown resets. The proxies are catching their breath — zzz 💤"
229
+ : "No active proxy pool is configured for this tenant and provider (request {requestId}). Add one, or route this provider through direct egress. No bridge to cross — configure a pool first 🏗️";
230
230
  throw new GatewayError(code, status, message, {
231
231
  pools: failure.pools,
232
232
  // Only measured evidence: a cooldown's real reset instant. The
@@ -279,7 +279,7 @@ export async function acquireAttemptLeases(
279
279
  : await acquirePoolSlot(candidate, source, input.poolSelector, input.strictPoolSelection);
280
280
  if (slot) partial.proxySlot = slot;
281
281
  if (input.strictPoolSelection && input.signal.aborted)
282
- throw new GatewayError("transport_closed", 499, "request was cancelled");
282
+ throw new GatewayError("transport_closed", 499, "The request was cancelled (request {requestId}). Send it again whenever ready 🙃");
283
283
  partial.reservation = await source.routingEngine.reserve({
284
284
  ...source.plan,
285
285
  candidates: [candidate],
@@ -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";
@@ -113,8 +116,28 @@ export async function handleProviderProxyRequest(
113
116
  const state = deps.stateStore.require(request);
114
117
  const prepared = state.preparedRequest;
115
118
  if (!prepared)
116
- throw new GatewayError("admission_unavailable", 503, "The request was not prepared, so no route can be dialed. Resend the request.");
119
+ throw new GatewayError("route_unavailable", 503, "The request was not prepared, so no route can be dialed (request {requestId}). Resend the request. The bouncer stepped out — resend shortly 🚪");
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,20 +176,25 @@ 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,
161
189
  strictPoolSelection: true,
162
190
  resolveHost: (candidate) => deps.byokUpstreamHosts?.get(candidate.provider_id),
163
- exhaustedError: new GatewayError("admission_unavailable", 503, "No account could be leased for this request. Retry shortly; if it persists, add an account or check that one is healthy."),
191
+ exhaustedError: new GatewayError("route_unavailable", 503, "No account could be leased for this request (request {requestId}). Retry shortly; if it persists, add an account or check that one is healthy. The bouncer stepped out — come back shortly 🚪"),
164
192
  prepare: async (candidate) => {
165
193
  if (!candidate.provider_account_id && candidate.requires_account !== false)
166
194
  throw new GatewayError(
167
- "admission_unavailable",
195
+ "route_unavailable",
168
196
  503,
169
- "This route has no usable credential. Save one on the account, or route to a provider whose account is configured.",
197
+ "This route has no usable credential (request {requestId}). Save one on the account, or route to a provider whose account is configured. Wrong key, right door — check the credential 🔑",
170
198
  );
171
199
  const credential = candidate.provider_account_id
172
200
  ? await resolveCredentialForAccount(
@@ -183,13 +211,13 @@ export async function handleProviderProxyRequest(
183
211
  };
184
212
  const adapter = await deps.resolveProviderAdapter?.(candidate.provider_id);
185
213
  if (!adapter)
186
- throw new GatewayError("admission_unavailable", 503, "provider adapter not registered");
214
+ throw new GatewayError("route_unavailable", 503, "provider adapter not registered (request {requestId}). Even we have bad days — check the registry wiring 🐻");
187
215
  if (!isBundledProviderId(candidate.provider_id)) {
188
216
  if (!deps.byokUpstreamHosts?.get(candidate.provider_id) || !deps.networkBindingFactory)
189
217
  throw new GatewayError(
190
- "admission_unavailable",
218
+ "route_unavailable",
191
219
  503,
192
- "validated network binding is required for configurable upstreams",
220
+ "validated network binding is required for configurable upstreams (request {requestId}). No bridge to cross — configure a pool first 🏗️",
193
221
  );
194
222
  }
195
223
  return { credential, adapter };
@@ -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
  ? {
@@ -27,10 +27,10 @@ export function createResponsesCompactHandler(deps: ResponsesCompactHandlerDeps)
27
27
  const authorization = state.authorization;
28
28
  const body = state.ingressBody;
29
29
  if (!authorization || body === null || typeof body !== "object" || Array.isArray(body))
30
- throw new GatewayError("invalid_request", 400, "compact request body must be a JSON object");
30
+ throw new GatewayError("invalid_request", 400, "compact request body must be a JSON object (request {requestId}). Check the shape of what you sent — then resend 🪞");
31
31
  const compact = body as Record<string, unknown>;
32
32
  if (typeof compact.model !== "string" || compact.model.trim().length === 0)
33
- throw new GatewayError("invalid_request", 400, "model is required");
33
+ throw new GatewayError("invalid_request", 400, "model is required (request {requestId}). Check the shape of what you sent — then resend 🪞");
34
34
  // A thinking suffix (`model(high)`) is stripped here for the same reason the
35
35
  // canonical preparer strips it: this route matches the model name against
36
36
  // the allowlist and the route plan, and `(high)` is not part of any
@@ -43,18 +43,18 @@ export function createResponsesCompactHandler(deps: ResponsesCompactHandlerDeps)
43
43
  const compactBody: Record<string, unknown> =
44
44
  compactModel === compact.model ? compact : { ...compact, model: compactModel };
45
45
  if (!(typeof compact.input === "string" || Array.isArray(compact.input)))
46
- throw new GatewayError("invalid_request", 400, "input must be a string or array");
46
+ throw new GatewayError("invalid_request", 400, "input must be a string or array (request {requestId}). Check the shape of what you sent — then resend 🪞");
47
47
  if ("instructions" in compact && typeof compact.instructions !== "string")
48
- throw new GatewayError("invalid_request", 400, "instructions must be a string");
48
+ throw new GatewayError("invalid_request", 400, "instructions must be a string (request {requestId}). Check the shape of what you sent — then resend 🪞");
49
49
  if (compact.stream === true)
50
50
  throw new GatewayError(
51
51
  "capability_unsupported",
52
52
  400,
53
- "Responses compact does not support streaming",
53
+ "Responses compact does not support streaming (request {requestId}). Know your tools — this one just isn't in the box 🧰",
54
54
  );
55
55
  const adapter = (await deps.resolveProviderAdapter?.("codex")) as CodexCompactAdapter | undefined;
56
56
  if (!adapter || typeof adapter.compact !== "function")
57
- throw new GatewayError("admission_unavailable", 503, "Codex compact transport unavailable");
57
+ throw new GatewayError("admission_unavailable", 503, "Codex compact transport unavailable (request {requestId}). The bouncer stepped out — come back shortly 🚪");
58
58
  const prepared = await deps.proxyPreparer.prepareNativeCompact({
59
59
  model: compactModel,
60
60
  authorization,
@@ -77,16 +77,16 @@ export function createResponsesCompactHandler(deps: ResponsesCompactHandlerDeps)
77
77
  // Lenient pool semantics: a missing slot tolerates direct egress.
78
78
  strictPoolSelection: false,
79
79
  exhaustedError: new GatewayError(
80
- "admission_unavailable",
80
+ "route_unavailable",
81
81
  503,
82
- "no eligible Codex route",
82
+ "no eligible Codex route (request {requestId}). The bouncer stepped out — come back shortly 🚪",
83
83
  ),
84
84
  prepare: async (candidate) => {
85
85
  if (!candidate.provider_account_id)
86
86
  throw new GatewayError(
87
- "admission_unavailable",
87
+ "route_unavailable",
88
88
  503,
89
- "Codex compact requires an OAuth account",
89
+ "Codex compact requires an OAuth account (request {requestId}). Wrong key, right door — check the credential 🔑",
90
90
  );
91
91
  const credential = await resolveCredentialForAccount(
92
92
  deps.db,
@@ -100,7 +100,7 @@ export function createResponsesCompactHandler(deps: ResponsesCompactHandlerDeps)
100
100
  throw new GatewayError(
101
101
  "capability_unsupported",
102
102
  400,
103
- "Responses compact requires a Codex ChatGPT OAuth account",
103
+ "Responses compact requires a Codex ChatGPT OAuth account (request {requestId}). Wrong key, right door — check the credential 🔑",
104
104
  );
105
105
  return { credential, adapter };
106
106
  },