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
@@ -8,9 +8,12 @@
8
8
  * than inside it so the loop stays a policy *consumer* — one place decides
9
9
  * retryability (`../failure-policy.ts`), one place decides refresh and
10
10
  * cooldown eligibility (here).
11
+ *
12
+ * The egress-scoped half of the rate-limit decision lives in
13
+ * `../../network/rate-limit-verdict`; this module keeps only the credential-
14
+ * scoped predicates.
11
15
  */
12
16
  import { GatewayError } from "../../protocol/gateway-error";
13
- import { providerRateLimitIsIpScoped } from "../../providers/provider-metadata";
14
17
 
15
18
  /**
16
19
  * Returns true only when an upstream failure is evidence that the OAuth
@@ -32,69 +35,3 @@ export function isOAuthCredentialInvalidated(error: unknown): error is GatewayEr
32
35
  if (/web[ _-]?search|web[ _-]?fetch|x[ _-]?search|tool invocation|search backend/i.test(message)) return false;
33
36
  return error.details.credentialEvidence === true;
34
37
  }
35
-
36
- /**
37
- * A free-tier IP refusal: the upstream has flagged this egress address, not
38
- * this request.
39
- *
40
- * OpenCode answers a blocked address with `403` and its own
41
- * `FreeTierError`/`ModelDeprecated`-style body. The request was never read, so
42
- * retrying it — with the same tools, the same headers, or the same credential —
43
- * cannot succeed from the same address. Only a different egress can.
44
- *
45
- * This is deliberately narrower than "any 403": a content-policy rejection or a
46
- * licence refusal is also a 403 and must not retire a healthy proxy. The
47
- * provider must also declare `ipScopedRateLimit`, which is the same fact the
48
- * 429 path already relies on.
49
- */
50
- export function isFreeTierIpBlock(error: unknown, providerId: string): error is GatewayError {
51
- if (!(error instanceof GatewayError) || error.origin !== "upstream" || error.status !== 403) {
52
- return false;
53
- }
54
- if (!providerRateLimitIsIpScoped(providerId)) return false;
55
- const providerCode = typeof error.details.providerCode === "string" ? error.details.providerCode : "";
56
- const raw = String(error.details.raw ?? "");
57
- return (
58
- /freetier|free.?tier/i.test(providerCode) ||
59
- /FreeTierError|free tier can only be used/i.test(`${error.message} ${raw}`)
60
- );
61
- }
62
-
63
- /**
64
- * A provider-scoped 429, on a provider whose limits follow the egress address.
65
- *
66
- * Only then is the pool the thing that ran out: the upstream counted requests
67
- * from this address, so the next attempt must leave by a different one and
68
- * cooling this pool is the fix. `providerRateLimitIsIpScoped` holds that fact
69
- * per provider (see the metadata field) rather than inferring it from the
70
- * response — a 429 states which bucket was exhausted only in the provider's own
71
- * wording, and every account-keyed provider words it the same way.
72
- *
73
- * For an account-keyed provider the 429 belongs to the credential that was
74
- * dialed. The account health machine already records it and routes the next
75
- * request to a sibling, so cooling the pool as well only removed healthy
76
- * accounts from service: every account sharing that egress lost its route for
77
- * the cooldown window because one of them hit its own limit. A bare 429 with no
78
- * provider scope stays a request-level limit and never cools the pool.
79
- *
80
- * `providerId` is the caller's own candidate, not a field read off the error:
81
- * the in-stream error frames the protocol modules build carry no `providerId`
82
- * detail, so reading it from `details` would silently disable the IP-scoped
83
- * rule on exactly the streaming path it exists for.
84
- *
85
- * A type guard on `GatewayError`, so a caller that passes this check can hand
86
- * the same value to `flagPoolCooldown` without a second narrowing step.
87
- */
88
- export function shouldCooldownPool(error: unknown, providerId: string): error is GatewayError {
89
- if (
90
- !(
91
- error instanceof GatewayError &&
92
- error.origin === "upstream" &&
93
- error.status === 429 &&
94
- error.details.rateLimitScope === "provider"
95
- )
96
- ) {
97
- return false;
98
- }
99
- return providerRateLimitIsIpScoped(providerId);
100
- }
@@ -60,7 +60,7 @@ export function createDispatchStreamEncoder(
60
60
  throw new GatewayError(
61
61
  "capability_unsupported",
62
62
  400,
63
- `Unknown surface: ${surface}`,
63
+ `Unknown surface: ${surface} (request {requestId}). Know your tools — this one just isn't in the box 🧰`,
64
64
  { surface },
65
65
  );
66
66
  }
@@ -24,7 +24,7 @@ import type { Reservation } from "../../routing/route-model";
24
24
  import { repriceUsage } from "../../../providers/provider-usage";
25
25
  import { metrics } from "../../../observability/metrics";
26
26
  import { flagPoolBlocked, flagPoolCooldown } from "../../../network/pool-health";
27
- import { isFreeTierIpBlock, shouldCooldownPool } from "../retry-policy";
27
+ import { classifyEgressFailure } from "../../../network/rate-limit-verdict";
28
28
  import { releaseAttemptLeases } from "../leases";
29
29
  import type { AttemptLeaseOwnership } from "../leases";
30
30
  import { finalizeRequestTelemetry } from "../../guards/error-lifecycle";
@@ -186,7 +186,7 @@ export class StreamFinalizer {
186
186
  // origin is the upstream's: labelling it `cartethyia` reported a
187
187
  // provider that stopped sending as a gateway defect.
188
188
  this.input.state.abortController.abort(
189
- new GatewayError("deadline_exceeded", 504, "The upstream stopped sending data mid-stream. Retry; if it keeps happening, the account or pool is unstable.", {}, "upstream"),
189
+ new GatewayError("deadline_exceeded", 504, "The upstream stopped sending data mid-stream (request {requestId}). Retry; if it keeps happening, the account or pool is unstable. The connection ghosted us — come back in a moment 👻", {}, "upstream"),
190
190
  );
191
191
  }, timeoutMs);
192
192
  }
@@ -363,7 +363,7 @@ export class StreamFinalizer {
363
363
  if (drain) return drain;
364
364
  if (reason instanceof GatewayError && reason.code === "deadline_exceeded") return reason;
365
365
  if (reason instanceof DOMException && reason.name === "TimeoutError")
366
- return new GatewayError("deadline_exceeded", 504, "request deadline exceeded", {}, "cartethyia");
366
+ return new GatewayError("deadline_exceeded", 504, "request deadline exceeded (request {requestId}). Some things just take longer 🐢", {}, "cartethyia");
367
367
  return undefined;
368
368
  }
369
369
 
@@ -453,7 +453,7 @@ export class StreamFinalizer {
453
453
  ? new GatewayError(
454
454
  "transport_unavailable",
455
455
  502,
456
- "The upstream closed the stream before it reported a final result. Retry; if it keeps happening, the account or pool is unstable.",
456
+ "The upstream closed the stream before it reported a final result (request {requestId}). Retry; if it keeps happening, the account or pool is unstable. The connection ghosted us — come back in a moment 👻",
457
457
  {},
458
458
  "upstream",
459
459
  )
@@ -531,7 +531,7 @@ export class StreamFinalizer {
531
531
  ? new GatewayError(
532
532
  "deadline_exceeded",
533
533
  504,
534
- "request deadline exceeded",
534
+ "request deadline exceeded (request {requestId}). Some things just take longer 🐢",
535
535
  {},
536
536
  "cartethyia",
537
537
  )
@@ -579,10 +579,11 @@ export class StreamFinalizer {
579
579
  ...(pipeline.clientResponseText.length > 0 ? { clientResponseText: pipeline.clientResponseText } : {}),
580
580
  });
581
581
  this.streamSettled = true;
582
- if (candidate.networkPoolId && !cancelled && deps.poolSelector) {
583
- if (shouldCooldownPool(err, candidate.providerId)) {
582
+ if (candidate.networkPoolId && !cancelled && deps.poolSelector && err instanceof GatewayError) {
583
+ const egressVerdict = classifyEgressFailure(err, candidate.providerId);
584
+ if (egressVerdict.kind === "cooldown") {
584
585
  flagPoolCooldown(deps.poolSelector, deps.db, candidate.networkPoolId, candidate.providerId, err);
585
- } else if (isFreeTierIpBlock(err, candidate.providerId)) {
586
+ } else if (egressVerdict.kind === "blocked") {
586
587
  flagPoolBlocked(deps.poolSelector, deps.db, candidate.networkPoolId, candidate.providerId, err);
587
588
  }
588
589
  }
@@ -50,7 +50,9 @@ import {
50
50
  resolveCaptureModeForRequest,
51
51
  shouldCaptureProviderBodyForRequest,
52
52
  } from "../capture-policy";
53
- import { resolveTenantImageClampMaxBytes } from "../../request/tenant-preferences";
53
+ import { resolveTenantImageClampMaxBytes, readTenantSystemPromptsForDispatch } from "../../request/tenant-preferences";
54
+ import { resolveTenantSystemPrompt } from "../../request/system-prompt-injection";
55
+ import { isWrappedInjectionProvider } from "../../../console/system-prompt/contracts";
54
56
  import { proxySuccessHeaders, buildUpstreamDispatchContext } from "../upstream";
55
57
  import type { AttemptLeaseOwnership } from "../leases";
56
58
  import type { ProviderContinuationScope } from "../../../providers/provider-contracts";
@@ -217,6 +219,19 @@ export async function dispatchStreamingAttempt(input: StreamingDispatchInput): P
217
219
  ? {}
218
220
  : { imageClampMaxBytes: state.imageClampMaxBytes }),
219
221
  ...(conversationAffinity ? { conversationAffinity } : {}),
222
+ // Same wrapped-injection text the non-streaming path carries: providers
223
+ // whose upstream validates the fixed system prompt as a channel identity
224
+ // (the buddy family) receive the tenant prompt through dispatch context
225
+ // so the adapter can fold it into its fixed turn. `override` degrades to
226
+ // wrapped append there; stripping the identity would be refused outright.
227
+ ...(await (async () => {
228
+ if (!isWrappedInjectionProvider(candidate.provider_id)) return {};
229
+ const text = resolveTenantSystemPrompt(
230
+ await readTenantSystemPromptsForDispatch(deps.db, prepared.authorization.snapshot.tenant_id),
231
+ { providerId: candidate.provider_id, modelId: candidate.model_id },
232
+ )?.text;
233
+ return text === undefined ? {} : { tenantSystemPrompt: text };
234
+ })()),
220
235
  ...(candidate.user_agent === undefined ? {} : { userAgent: candidate.user_agent }),
221
236
  ...(wrappedOutboundFetch ? { outboundFetch: wrappedOutboundFetch } : {}),
222
237
  ...(outboundFetch ? { outboundAttachmentFetch: outboundFetch } : {}),
@@ -33,10 +33,10 @@ export function createSystemoneHandler(deps: SystemoneHandlerDeps) {
33
33
  const authorization = state.authorization;
34
34
  const body = state.ingressBody;
35
35
  if (!authorization || body === null || typeof body !== "object" || Array.isArray(body))
36
- throw new GatewayError("invalid_request", 400, "systemone request body must be a JSON object");
36
+ throw new GatewayError("invalid_request", 400, "systemone request body must be a JSON object (request {requestId}). Check the shape of what you sent — then resend 🪞");
37
37
  const decision = body as Record<string, unknown>;
38
38
  if (typeof decision.model !== "string" || decision.model.trim().length === 0)
39
- throw new GatewayError("invalid_request", 400, "model is required");
39
+ throw new GatewayError("invalid_request", 400, "model is required (request {requestId}). Check the shape of what you sent — then resend 🪞");
40
40
  // A thinking suffix (`model(high)`) is stripped for the same reason the
41
41
  // canonical preparer strips it: the route matches the model name against the
42
42
  // allowlist and the route plan, and `(high)` is not part of any registered
@@ -49,14 +49,14 @@ export function createSystemoneHandler(deps: SystemoneHandlerDeps) {
49
49
  // upstream's to validate. `state` and a `questions` object are what make a
50
50
  // System One request a System One request at all.
51
51
  if (decision.state === undefined || decision.state === null)
52
- throw new GatewayError("invalid_request", 400, "state is required");
52
+ throw new GatewayError("invalid_request", 400, "state is required (request {requestId}). Check the shape of what you sent — then resend 🪞");
53
53
  if (
54
54
  decision.questions === undefined ||
55
55
  decision.questions === null ||
56
56
  typeof decision.questions !== "object" ||
57
57
  Array.isArray(decision.questions)
58
58
  )
59
- throw new GatewayError("invalid_request", 400, "questions must be an object");
59
+ throw new GatewayError("invalid_request", 400, "questions must be an object (request {requestId}). Check the shape of what you sent — then resend 🪞");
60
60
  const prepared = await deps.proxyPreparer.prepareNativeService({
61
61
  model: bareModel,
62
62
  serviceKind: "systemone",
@@ -80,9 +80,9 @@ export function createSystemoneHandler(deps: SystemoneHandlerDeps) {
80
80
  // Lenient pool semantics: a missing slot tolerates direct egress.
81
81
  strictPoolSelection: false,
82
82
  exhaustedError: new GatewayError(
83
- "admission_unavailable",
83
+ "route_unavailable",
84
84
  503,
85
- "no eligible System One route",
85
+ "no eligible System One route (request {requestId}). The bouncer stepped out — come back shortly 🚪",
86
86
  ),
87
87
  prepare: async (candidate) => {
88
88
  const credential = candidate.provider_account_id
@@ -103,7 +103,7 @@ export function createSystemoneHandler(deps: SystemoneHandlerDeps) {
103
103
  throw new GatewayError(
104
104
  "capability_unsupported",
105
105
  400,
106
- "System One transport unavailable for this provider",
106
+ "System One transport unavailable for this provider (request {requestId}). Know your tools — this one just isn't in the box 🧰",
107
107
  );
108
108
  return { credential, adapter };
109
109
  },
@@ -148,7 +148,7 @@ export function createSystemoneHandler(deps: SystemoneHandlerDeps) {
148
148
  throw new GatewayError(
149
149
  response.status >= 500 ? "platform_unavailable" : "invalid_request",
150
150
  response.status,
151
- `System One upstream error: ${detail.slice(0, 240)}`,
151
+ `System One upstream error (request {requestId}): ${detail.slice(0, 240)}`,
152
152
  );
153
153
  }
154
154
  // Prefer the usage the decision endpoint reported; fall back to the
@@ -57,7 +57,7 @@ export function createTokenCountHandler(deps: TokenCountHandlerDeps) {
57
57
  throw new GatewayError(
58
58
  "invalid_request",
59
59
  400,
60
- "The count_tokens body must be a JSON object, the same shape POST /v1/messages accepts.",
60
+ "The count_tokens body must be a JSON object (request {requestId}), the same shape POST /v1/messages accepts. Check the shape of what you sent — then resend 🪞",
61
61
  );
62
62
  }
63
63
  const countBody = body as Record<string, unknown>;
@@ -66,16 +66,16 @@ export function createTokenCountHandler(deps: TokenCountHandlerDeps) {
66
66
  // upstream's own required fields, so the gateway does not restate its own
67
67
  // narrower rule and risk rejecting a body the provider would accept.
68
68
  if (typeof countBody.model !== "string" || countBody.model.trim().length === 0) {
69
- throw new GatewayError("invalid_request", 400, "A model is required: it names the provider to count with.");
69
+ throw new GatewayError("invalid_request", 400, "A model is required (request {requestId}): it names the provider to count with. Check the shape of what you sent — then resend 🪞");
70
70
  }
71
71
  if (!Array.isArray(countBody.messages)) {
72
- throw new GatewayError("invalid_request", 400, "messages must be an array — there is nothing to count without it.");
72
+ throw new GatewayError("invalid_request", 400, "messages must be an array (request {requestId}) — there is nothing to count without it. Check the shape of what you sent — then resend 🪞");
73
73
  }
74
74
  if (!authorization.snapshot.scopes?.includes("routing:invoke" as never)) {
75
75
  throw new GatewayError(
76
- "invalid_request",
76
+ "policy_rejected",
77
77
  403,
78
- "token counting not allowed for this key — enable routing:invoke on the API key",
78
+ "token counting not allowed for this key (request {requestId}) — enable routing:invoke on the API key. Not allowed by policy — no hard feelings 🫂",
79
79
  );
80
80
  }
81
81
 
@@ -111,9 +111,9 @@ export function createTokenCountHandler(deps: TokenCountHandlerDeps) {
111
111
  tenantId: prepared.authorization.tenantId,
112
112
  strictPoolSelection: false,
113
113
  exhaustedError: new GatewayError(
114
- "admission_unavailable",
114
+ "route_unavailable",
115
115
  503,
116
- "no eligible route can count tokens for this model",
116
+ "no eligible route can count tokens for this model (request {requestId}). The bouncer stepped out — come back shortly 🚪",
117
117
  ),
118
118
  prepare: async (candidate) => {
119
119
  const credential = candidate.provider_account_id
@@ -131,7 +131,7 @@ export function createTokenCountHandler(deps: TokenCountHandlerDeps) {
131
131
  };
132
132
  const adapter = await deps.resolveProviderAdapter?.(candidate.provider_id);
133
133
  if (!adapter) {
134
- throw new GatewayError("capability_unsupported", 400, "token counting is unavailable for this provider");
134
+ throw new GatewayError("capability_unsupported", 400, "token counting is unavailable for this provider (request {requestId}). Know your tools — this one just isn't in the box 🧰");
135
135
  }
136
136
  return { credential, adapter };
137
137
  },
@@ -146,7 +146,7 @@ export function createTokenCountHandler(deps: TokenCountHandlerDeps) {
146
146
 
147
147
  const adapter = context.adapter;
148
148
  if (typeof adapter.dispatchNativeService !== "function") {
149
- throw new GatewayError("capability_unsupported", 400, "token counting is unavailable for this provider");
149
+ throw new GatewayError("capability_unsupported", 400, "token counting is unavailable for this provider (request {requestId}). Know your tools — this one just isn't in the box 🧰");
150
150
  }
151
151
 
152
152
  const target: ProviderDispatchTarget = {
@@ -61,6 +61,13 @@ export function buildUpstreamDispatchContext(input: {
61
61
  * database handle.
62
62
  */
63
63
  readonly imageClampMaxBytes?: number;
64
+ /**
65
+ * Tenant system-prompt text resolved once per request (append/override
66
+ * injection). Carried through dispatch because the buddy adapters fold it
67
+ * into their fixed system turn after serialization — the canonical level
68
+ * cannot reach that turn.
69
+ */
70
+ readonly tenantSystemPrompt?: string;
64
71
  /** Opaque identity shared by retries of one logical gateway request. */
65
72
  readonly requestIdentity?: object;
66
73
  }): Record<string, unknown> {
@@ -101,6 +108,9 @@ export function buildUpstreamDispatchContext(input: {
101
108
  ...(input.imageClampMaxBytes === undefined
102
109
  ? {}
103
110
  : { image_clamp_max_bytes: input.imageClampMaxBytes }),
111
+ ...(input.tenantSystemPrompt === undefined
112
+ ? {}
113
+ : { tenant_system_prompt: input.tenantSystemPrompt }),
104
114
  ...(input.requestIdentity ? { request_identity: input.requestIdentity } : {}),
105
115
  } as Record<string, unknown>;
106
116
  }
@@ -50,10 +50,10 @@ export function createWebsearchHandler(deps: WebsearchHandlerDeps) {
50
50
  const authorization = state.authorization;
51
51
  const body = state.ingressBody;
52
52
  if (!authorization || body === null || typeof body !== "object" || Array.isArray(body))
53
- throw new GatewayError("invalid_request", 400, "The search body must be a JSON object. Send one with at least a model and a query.");
53
+ throw new GatewayError("invalid_request", 400, "The search body must be a JSON object (request {requestId}). Send one with at least a model and a query. Check the shape of what you sent — then resend 🪞");
54
54
  const search = body as Record<string, unknown>;
55
55
  if (typeof search.model !== "string" || search.model.trim().length === 0)
56
- throw new GatewayError("invalid_request", 400, "A model is required: it names the search provider to use.");
56
+ throw new GatewayError("invalid_request", 400, "A model is required (request {requestId}): it names the search provider to use. Check the shape of what you sent — then resend 🪞");
57
57
  // The search "model" names the provider (or an alias/combo of one). A
58
58
  // thinking suffix is stripped for the same reason the canonical preparer
59
59
  // strips it: it is not part of any registered id.
@@ -61,12 +61,12 @@ export function createWebsearchHandler(deps: WebsearchHandlerDeps) {
61
61
  const searchBody: Record<string, unknown> =
62
62
  bareModel === search.model ? search : { ...search, model: bareModel };
63
63
  if (!authorization.snapshot.scopes?.includes("search:invoke" as never)) {
64
- throw new GatewayError("invalid_request", 403, "search not allowed for this key — enable search:invoke on the API key");
64
+ throw new GatewayError("policy_rejected", 403, "search not allowed for this key (request {requestId}) — enable search:invoke on the API key. Not allowed by policy — no hard feelings 🫂");
65
65
  }
66
66
  // Trust boundary only: the query must be a non-empty string. The adapter
67
67
  // validates the remaining shape when it maps the body onto its provider.
68
68
  if (typeof search.query !== "string" || search.query.trim().length === 0)
69
- throw new GatewayError("invalid_request", 400, "query is required");
69
+ throw new GatewayError("invalid_request", 400, "query is required (request {requestId}). Check the shape of what you sent — then resend 🪞");
70
70
 
71
71
  const prepared = await deps.proxyPreparer.prepareNativeService({
72
72
  model: bareModel,
@@ -89,7 +89,7 @@ export function createWebsearchHandler(deps: WebsearchHandlerDeps) {
89
89
  candidates: prepared.candidates,
90
90
  tenantId: prepared.authorization.tenantId,
91
91
  strictPoolSelection: false,
92
- exhaustedError: new GatewayError("admission_unavailable", 503, "no eligible search route"),
92
+ exhaustedError: new GatewayError("route_unavailable", 503, "no eligible search route (request {requestId}). The bouncer stepped out — come back shortly 🚪"),
93
93
  prepare: async (candidate) => {
94
94
  const credential = candidate.provider_account_id
95
95
  ? await resolveCredentialForAccount(
@@ -109,7 +109,7 @@ export function createWebsearchHandler(deps: WebsearchHandlerDeps) {
109
109
  throw new GatewayError(
110
110
  "capability_unsupported",
111
111
  400,
112
- "web search is unavailable for this provider",
112
+ "web search is unavailable for this provider (request {requestId}). Know your tools — this one just isn't in the box 🧰",
113
113
  );
114
114
  return { credential, adapter };
115
115
  },
@@ -55,9 +55,9 @@ export function createDrainHandler(
55
55
  ): (context: { request: Request }) => Promise<Response> {
56
56
  return async ({ request }) => {
57
57
  if (!isLoopbackPeer(deps.resolvePeerAddress(request)))
58
- throw new GatewayError("invalid_request", 403, "drain is only available from loopback");
58
+ throw new GatewayError("policy_rejected", 403, "drain is only available from loopback (request {requestId}). This operation is only allowed from a trusted loopback connection — no hard feelings 🫂");
59
59
  if (!tokenMatches(request.headers.get("x-drain-token"), deps.token))
60
- throw new GatewayError("invalid_request", 401, "invalid drain token");
60
+ throw new GatewayError("authentication_failed", 401, "invalid drain token (request {requestId}). Wrong key, right door — check the token 🔑");
61
61
  // Acknowledge first, then drain: the response must be on the wire before the
62
62
  // shutdown path starts closing sockets, or the caller sees a dropped
63
63
  // connection instead of a 202.
@@ -167,7 +167,7 @@ export function statusToGatewayErrorCode(status: number): GatewayErrorCode {
167
167
  if (status === 413) return "request_too_large";
168
168
  if (status === 415) return "unsupported_media_type";
169
169
  if (status === 422) return "upstream_unprocessable";
170
- if (status === 529) return "capacity_exhausted";
170
+ if (status === 529) return "upstream_overloaded";
171
171
  if (status >= 500 && status <= 599) return "platform_unavailable";
172
172
  return "invalid_request";
173
173
  }
@@ -274,7 +274,9 @@ export function classifyUpstreamFailure(error: unknown): UpstreamFailurePolicy {
274
274
  error.code === "capability_unsupported" ||
275
275
  error.code === "model_not_found" ||
276
276
  error.code === "admission_unavailable" ||
277
+ error.code === "route_unavailable" ||
277
278
  error.code === "capacity_exhausted" ||
279
+ error.code === "upstream_overloaded" ||
278
280
  // A provider max-inflight rejection: the plan's next sibling account may
279
281
  // have a free slot, exactly as with an API-key concurrency rejection.
280
282
  error.code === "provider_capacity_exhausted" ||
@@ -286,6 +288,7 @@ export function classifyUpstreamFailure(error: unknown): UpstreamFailurePolicy {
286
288
  error.code === "accounts_unavailable" ||
287
289
  error.code === "accounts_rate_limited" ||
288
290
  error.code === "quota_exceeded" ||
291
+ error.code === "rate_limited" ||
289
292
  error.code === "authentication_failed" ||
290
293
  error.code === "proxy_auth_required" ||
291
294
  error.code === "deadline_exceeded" ||
@@ -427,10 +430,14 @@ const CATEGORY_HTTP_STATUS: Partial<Record<GatewayErrorCode | "unknown_error", n
427
430
  // in this process.
428
431
  transport_unavailable: 502,
429
432
  platform_unavailable: 502,
433
+ upstream_overloaded: 529,
430
434
  deadline_exceeded: 504,
431
435
  transport_closed: 499,
432
436
  shutting_down: 503,
433
437
  overloaded: 529,
438
+ route_unavailable: 503,
439
+ model_not_allowed: 403,
440
+ rate_limited: 429,
434
441
  unknown_error: 500,
435
442
  };
436
443
 
@@ -624,7 +631,7 @@ export function upstreamQueuedError(
624
631
  503,
625
632
  detail.length > 0
626
633
  ? detail
627
- : "The upstream has queued this request. Retry after the stated interval.",
634
+ : "The upstream has queued this request (request {requestId}); retry after the stated interval. You're in line — no cuts 💛",
628
635
  {
629
636
  providerId,
630
637
  reason: "upstream-queued",
@@ -816,7 +823,7 @@ export function classifyUpstreamError(
816
823
  }
817
824
  if (normalized !== undefined && CAPACITY_ERROR_IDENTIFIERS.has(normalized))
818
825
  return {
819
- code: "capacity_exhausted",
826
+ code: "upstream_overloaded",
820
827
  status: status === 503 || status === 529 ? status : 529,
821
828
  origin: "upstream",
822
829
  };
@@ -969,7 +976,7 @@ export async function throwIfHtmlResponse(response: Response): Promise<void> {
969
976
  throw new GatewayError(
970
977
  "transport_unavailable",
971
978
  502,
972
- `Upstream returned an HTML error page: ${summary}`.slice(0, 300),
979
+ `Upstream returned an HTML error page: ${summary} (request {requestId}). The provider's edge is having a moment — hang tight ☕️`.slice(0, 320),
973
980
  { upstreamStatus: response.status, contentType, accountScope: true },
974
981
  "upstream",
975
982
  );
@@ -1011,11 +1018,11 @@ export function fallbackRetryDelayMs(attemptIndex: number): number {
1011
1018
 
1012
1019
  /** Sleeps until the delay expires or the request is aborted. */
1013
1020
  export function sleep(ms: number, signal: AbortSignal): Promise<void> {
1014
- if (signal.aborted) return Promise.reject(new GatewayError("transport_closed", 499, "request was cancelled"));
1021
+ if (signal.aborted) return Promise.reject(new GatewayError("transport_closed", 499, "The request was cancelled (request {requestId}). Send it again whenever ready 🙃"));
1015
1022
  if (ms <= 0) return Promise.resolve();
1016
1023
  const { promise, resolve, reject } = Promise.withResolvers<void>();
1017
1024
  const timer = setTimeout(() => { signal.removeEventListener("abort", onAbort); resolve(); }, ms);
1018
- const onAbort = () => { clearTimeout(timer); reject(new GatewayError("transport_closed", 499, "request was cancelled")); };
1025
+ const onAbort = () => { clearTimeout(timer); reject(new GatewayError("transport_closed", 499, "The request was cancelled (request {requestId}). Send it again whenever ready 🙃")); };
1019
1026
  signal.addEventListener("abort", onAbort, { once: true });
1020
1027
  return promise;
1021
1028
  }
@@ -62,9 +62,9 @@ export function createApiKeyAuthenticationMiddleware(deps: {
62
62
  safe.name = error instanceof Error ? error.name : "Error";
63
63
  log.error("[auth] key lookup failed; treating as unavailable", safe);
64
64
  throw new GatewayError(
65
- "accounts_unavailable",
65
+ "admission_unavailable",
66
66
  503,
67
- "The gateway could not verify this API key because its credential store is unavailable. Retry shortly.",
67
+ "The gateway could not verify this API key because its credential store is unavailable (request {requestId}). Retry shortly. The bouncer stepped out — come back shortly 🚪",
68
68
  {},
69
69
  "cartethyia",
70
70
  );
@@ -76,7 +76,7 @@ export function createApiKeyAuthenticationMiddleware(deps: {
76
76
  scopes: authorization.scopes,
77
77
  });
78
78
  if (!decision.scopes.includes("routing:invoke"))
79
- throw new GatewayError("invalid_request", 403, "This API key is not allowed to invoke routing. Grant it the routing:invoke scope, or use a key that has it.");
79
+ throw new GatewayError("policy_rejected", 403, "This API key is not allowed to invoke routing (request {requestId}). Grant it the routing:invoke scope, or use a key that has it. Not allowed by policy — no hard feelings 🫂");
80
80
  // Kill switch: refuse before any upstream is touched. Distinct from the
81
81
  // scope check above, which is a property of the key — this is the tenant
82
82
  // having stopped all traffic, and it applies to every key the tenant owns.
@@ -86,7 +86,8 @@ export function createApiKeyAuthenticationMiddleware(deps: {
86
86
  throw new GatewayError(
87
87
  "platform_unavailable",
88
88
  503,
89
- "This gateway has invoke switched off. Nothing upstream is being dialed while the switch is off; turn it back on from the dashboard overview to resume routing.",
89
+ "This gateway has invoke switched off (request {requestId}). Nothing upstream is being dialed while the switch is off; turn it back on from the dashboard overview to resume routing. Flip the switch back on when ready 🔌",
90
+ { reason: "invoke_switched_off" },
90
91
  );
91
92
  // A key may refuse a downstream router it has been resold to. This is a
92
93
  // policy check on the resolved key, so it belongs with the other
@@ -100,7 +101,7 @@ export function createApiKeyAuthenticationMiddleware(deps: {
100
101
  throw new GatewayError(
101
102
  "client_router_denied",
102
103
  403,
103
- "No API invocation access for this client.",
104
+ "No API invocation access for this client (request {requestId}). This router isn't on the guest list — denied, but politely 🤗",
104
105
  { reason: "client_router_denied", clientRouter: denied },
105
106
  );
106
107
  deps.stateStore.require(request).authorization = authorization;
@@ -77,11 +77,11 @@ export async function readIngressBody(
77
77
  const isJsonRoute = isJsonProxyRoute(p);
78
78
  const contentType = request.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
79
79
  if (!contentType) {
80
- if (isJsonRoute) throw new GatewayError("unsupported_media_type", 415, "This endpoint reads JSON, so the request needs a content-type: application/json header.");
80
+ if (isJsonRoute) throw new GatewayError("unsupported_media_type", 415, "This endpoint reads JSON, so the request needs a content-type: application/json header (request {requestId}). We speak JSON here — send JSON 💬");
81
81
  return undefined;
82
82
  }
83
83
  if (contentType !== "application/json") {
84
- if (isJsonRoute) throw new GatewayError("unsupported_media_type", 415, `This endpoint reads JSON and cannot parse '${contentType}'. Send content-type: application/json.`);
84
+ if (isJsonRoute) throw new GatewayError("unsupported_media_type", 415, `This endpoint reads JSON and cannot parse '${contentType}' (request {requestId}). Send content-type: application/json. We speak JSON here — send JSON 💬`);
85
85
  return undefined;
86
86
  }
87
87
  const maxBytes = options.maxBodyBytes ?? 1_048_576;
@@ -91,7 +91,7 @@ export async function readIngressBody(
91
91
  throw bodyTooLargeError(maxBytes);
92
92
 
93
93
  const reader = request.body?.getReader();
94
- if (!reader) throw new GatewayError("invalid_request", 400, "The request body could not be read. Resend it with a JSON body.");
94
+ if (!reader) throw new GatewayError("invalid_request", 400, "The request body could not be read (request {requestId}). Resend it with a JSON body. Check the shape of what you sent — then resend 🪞");
95
95
  // Collect raw chunks then decode once. String `+=` on each chunk reallocates
96
96
  // the accumulating UTF-16 string (O(n²) for large Claude Code prompts);
97
97
  // Buffer.concat + one TextDecoder pass is linear and allocation-stable.
@@ -126,7 +126,7 @@ export async function readIngressBody(
126
126
  return body;
127
127
  } catch (error) {
128
128
  if (error instanceof GatewayError) throw error;
129
- throw new GatewayError("invalid_request", 400, "The request body is not valid JSON. Check for a trailing comma or an unclosed brace.");
129
+ throw new GatewayError("invalid_request", 400, "The request body is not valid JSON (request {requestId}). Check for a trailing comma or an unclosed brace. Check the shape of what you sent — then resend 🪞");
130
130
  }
131
131
  }
132
132
 
@@ -49,6 +49,18 @@ function retryAfterSeconds(error: GatewayError | undefined): string | undefined
49
49
  return undefined;
50
50
  }
51
51
 
52
+ /**
53
+ * Ensures a public message carries the request id. Builders write a
54
+ * `{requestId}` placeholder in the lead (the canonical convention); anything
55
+ * that predates the convention gets the id appended so every client-visible
56
+ * message is correlatable to the server log.
57
+ */
58
+ function applyRequestId(message: string, requestId: string): string {
59
+ if (message.includes("{requestId}")) return message.replaceAll("{requestId}", requestId);
60
+ if (message.includes("(request ")) return message;
61
+ return `${message} (request ${requestId})`;
62
+ }
63
+
52
64
  /**
53
65
  * Normalizes every thrown error (from any lifecycle hook or handler, on
54
66
  * `/v1/*` or `/console/api/*`) into the stable public `{ error: { code,
@@ -81,7 +93,8 @@ export function createErrorNormalizationMiddleware(deps: {
81
93
  gateway?.status ?? builtinStatus ?? (isBuiltinError || isInputError ? 400 : 500);
82
94
  const code =
83
95
  gateway?.code ?? builtinCode ?? (isBuiltinError || isInputError ? "invalid_request" : "internal_error");
84
- const message =
96
+ const requestId = state?.requestId ?? crypto.randomUUID();
97
+ const rawMessage =
85
98
  gateway !== undefined
86
99
  ? explainGatewayError(gateway)
87
100
  : isInputError && error instanceof Error
@@ -90,8 +103,9 @@ export function createErrorNormalizationMiddleware(deps: {
90
103
  code,
91
104
  isBuiltinError
92
105
  ? builtin?.message ?? "Unable to process the request as written."
93
- : "The gateway hit an internal error. Retry; if it persists, the server logs carry the request id.",
106
+ : "The gateway hit an internal error. Retry; if it persists, quote the request id to support. Even we have bad days — retry, and quote the id if it persists 🐻",
94
107
  );
108
+ const message = applyRequestId(rawMessage, requestId);
95
109
  const origin = gateway?.origin ?? "cartethyia";
96
110
  // `afterResponse` telemetry hook can enqueue it even when the request
97
111
  // never reached canonical parse / auth / preparation.
@@ -108,7 +122,7 @@ export function createErrorNormalizationMiddleware(deps: {
108
122
  if (!state?.canonicalRequest && !state?.authorization)
109
123
  metrics.proxy_requests_total.inc(1, { status: "rejected" });
110
124
  set.headers["cache-control"] = "no-store";
111
- set.headers["x-request-id"] = state?.requestId ?? crypto.randomUUID();
125
+ set.headers["x-request-id"] = requestId;
112
126
  Object.assign(set.headers, GATEWAY_SECURITY_HEADERS);
113
127
  if (deps.hsts) set.headers["strict-transport-security"] = "max-age=31536000";
114
128
  set.status = status;
@@ -128,6 +142,7 @@ export function createErrorNormalizationMiddleware(deps: {
128
142
  error: {
129
143
  origin,
130
144
  code,
145
+ requestId,
131
146
  message,
132
147
  ...(gateway
133
148
  ? { details: publicGatewayErrorDetails(gateway) }
@@ -8,15 +8,19 @@ import { GatewayError } from "../../protocol/gateway-error";
8
8
 
9
9
  /** Thrown when the request's client peer address cannot be established. */
10
10
  export function peerAddressUnavailableError(): GatewayError {
11
- return new GatewayError("admission_unavailable", 503, "client peer address unavailable");
11
+ return new GatewayError(
12
+ "admission_unavailable",
13
+ 503,
14
+ "client peer address unavailable (request {requestId}). We couldn't see who's knocking — retry 🚪",
15
+ );
12
16
  }
13
17
 
14
18
  /** Thrown when the request body exceeds the endpoint's byte budget. */
15
19
  export function bodyTooLargeError(maxBytes: number): GatewayError {
16
20
  return new GatewayError(
17
- "invalid_request",
21
+ "request_too_large",
18
22
  413,
19
- `The request body is larger than this endpoint accepts (${maxBytes} bytes). Shorten the prompt or split the request.`,
23
+ `The request body is larger than this endpoint accepts (${maxBytes} bytes) (request {requestId}). Shorten the prompt or split the request. We love long reads, but not that long 📚`,
20
24
  );
21
25
  }
22
26
 
@@ -90,7 +90,7 @@ export function createHeadroomGuard(deps: HeadroomGuardDeps): Elysia {
90
90
  throw new GatewayError(
91
91
  "overloaded",
92
92
  503,
93
- "The gateway is at capacity; retry shortly.",
93
+ "The gateway is at capacity (request {requestId}); retry shortly. It's not you, it's us — come back in a few minutes ☕️",
94
94
  {
95
95
  retryAfterMs: SHED_RETRY_MS,
96
96
  minFreeBytes: headroom,