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
@@ -46,6 +46,17 @@ async function traeVersion(response: Response): Promise<string | null> {
46
46
  /"insert":"((?:January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{1,2},\s*\d{4}\s*\([^)]*\))\\n"/g;
47
47
  for (const match of text.matchAll(heading)) {
48
48
  const window = text.slice(match.index ?? 0, (match.index ?? 0) + 900);
49
+ // A release line may name a *range* ("TRAE v3.6.1 ~ 3.6.4 are released"),
50
+ // and the newest entry is the newest build in it. Taking the first number
51
+ // reported 3.6.1 while 3.6.4 was already shipping — and on an entry whose
52
+ // heading carried no version at all the old pattern matched nothing, so
53
+ // discovery fell through to the next (older) entry and the pin drifted
54
+ // backwards on every refresh.
55
+ const range = window.match(/(\d+\.\d+\.\d+)\s*[~–-]\s*(\d+\.\d+\.\d+)/);
56
+ if (range) {
57
+ const high = range[2] ?? range[1];
58
+ if (high && isSemverish(high)) return high.trim();
59
+ }
49
60
  const found = window.match(/TraeCode\s+v?(\d+\.\d+\.\d+)|v?(\d+\.\d+\.\d+)\s+is\s+released/i);
50
61
  const version = found?.[1] ?? found?.[2];
51
62
  if (version && isSemverish(version)) return version.trim();
@@ -221,7 +232,7 @@ const KIRO_NODE_VERSION = "24.18.0";
221
232
  export const VERSION_SOURCES = {
222
233
  antigravity: {
223
234
  key: "antigravity",
224
- fallback: "2.24.0",
235
+ fallback: "2.28.2",
225
236
  sources: [
226
237
  {
227
238
  url: "https://antigravity-hub-auto-updater-974169037036.us-central1.run.app/manifest/latest-arm64-mac.yml",
@@ -231,7 +242,7 @@ export const VERSION_SOURCES = {
231
242
  },
232
243
  qoder: {
233
244
  key: "qoder",
234
- fallback: "1.1.74",
245
+ fallback: "1.1.81",
235
246
  sources: [
236
247
  {
237
248
  url: "https://registry.npmjs.org/@qoder-ai/qodercli/latest",
@@ -243,18 +254,21 @@ export const VERSION_SOURCES = {
243
254
  key: "opencode",
244
255
  // The v1 package (`opencode-ai`) stopped at 1.18. The v2 line publishes
245
256
  // under `@opencode/cli`, and this feed is what its own installer reads.
246
- fallback: "2.0.32",
257
+ fallback: "2.2.2",
247
258
  minVersion: "2.0.0",
248
259
  sources: [{ url: "https://opencode.ai/update/api/latest/cli/npm" }],
249
260
  },
250
261
  commandcode: {
251
262
  key: "commandcode",
252
- fallback: "1.84.0",
263
+ // npm `command-code` latest as of 2026-10-30. The CLI sends this as
264
+ // `x-command-code-version`, so a stale fallback presents a client build
265
+ // the upstream no longer ships — raise it whenever the package moves.
266
+ fallback: "1.88.2",
253
267
  sources: [{ url: "https://registry.npmjs.org/command-code/latest" }],
254
268
  },
255
269
  grok: {
256
270
  key: "grok",
257
- fallback: "1.0.62",
271
+ fallback: "1.0.70",
258
272
  sources: [
259
273
  { url: "https://storage.googleapis.com/grok-build-public-artifacts/cli/stable" },
260
274
  { url: "https://registry.npmjs.org/@xai-official/grok/latest" },
@@ -262,7 +276,7 @@ export const VERSION_SOURCES = {
262
276
  },
263
277
  clineClient: {
264
278
  key: "cline-client",
265
- fallback: "4.1.25",
279
+ fallback: "4.1.26",
266
280
  minVersion: "4.1.23",
267
281
  sources: [
268
282
  {
@@ -273,22 +287,22 @@ export const VERSION_SOURCES = {
273
287
  },
274
288
  clineSdk: {
275
289
  key: "cline-sdk",
276
- fallback: "0.0.95",
290
+ fallback: "0.2.0",
277
291
  sources: [{ url: "https://registry.npmjs.org/@cline/sdk/latest", extract: clineSdkVersion }],
278
292
  },
279
293
  clineCli: {
280
294
  key: "cline-cli",
281
- fallback: "3.0.73",
295
+ fallback: "3.0.74",
282
296
  sources: [{ url: "https://registry.npmjs.org/cline/latest", extract: clineSdkVersion }],
283
297
  },
284
298
  codex: {
285
299
  key: "codex",
286
- fallback: "0.163.0",
300
+ fallback: "0.166.0",
287
301
  sources: [{ url: "https://registry.npmjs.org/@openai/codex/latest" }],
288
302
  },
289
303
  workbuddyClient: {
290
304
  key: "workbuddy-client",
291
- fallback: "5.8.4.41366630",
305
+ fallback: "5.9.4.42593613",
292
306
  sources: [
293
307
  {
294
308
  url: "https://www.workbuddy.ai/v2/update?platform=workbuddy-win32-x64-user",
@@ -302,7 +316,7 @@ export const VERSION_SOURCES = {
302
316
  },
303
317
  workbuddyCnClient: {
304
318
  key: "workbuddy-cn-client",
305
- fallback: "5.8.3.41334564",
319
+ fallback: "5.9.3.42398134",
306
320
  sources: [
307
321
  {
308
322
  url: "https://www.workbuddy.cn/v2/update?platform=workbuddy-win32-x64-user",
@@ -316,7 +330,7 @@ export const VERSION_SOURCES = {
316
330
  },
317
331
  workbuddyCli: {
318
332
  key: "workbuddy",
319
- fallback: "2.169.0",
333
+ fallback: "2.178.1",
320
334
  sources: [
321
335
  { url: "https://registry.npmjs.org/@tencent-ai/codebuddy-code/latest" },
322
336
  { url: "https://registry.npmmirror.com/@tencent-ai/codebuddy-code/latest" },
@@ -329,7 +343,7 @@ export const VERSION_SOURCES = {
329
343
  },
330
344
  kiro: {
331
345
  key: "kiro",
332
- fallback: "1.3.11",
346
+ fallback: "1.4.62",
333
347
  sources: [
334
348
  {
335
349
  // Kiro's downloads page exposes no stable version string. Homebrew
@@ -343,7 +357,7 @@ export const VERSION_SOURCES = {
343
357
  },
344
358
  codebuddy: {
345
359
  key: "codebuddy",
346
- fallback: "2.169.0",
360
+ fallback: "2.178.1",
347
361
  sources: [
348
362
  { url: "https://registry.npmjs.org/@tencent-ai/codebuddy-code/latest" },
349
363
  { url: "https://registry.npmmirror.com/@tencent-ai/codebuddy-code/latest" },
@@ -366,7 +380,7 @@ export const VERSION_SOURCES = {
366
380
  // Current `@anthropic-ai/claude-code` release, so the billing
367
381
  // `cc_version=` suffix and the `claude-cli/` User-Agent stay on what
368
382
  // upstream ships.
369
- fallback: "2.1.302",
383
+ fallback: "2.1.314",
370
384
  sources: [{ url: "https://registry.npmjs.org/@anthropic-ai/claude-code/latest" }],
371
385
  },
372
386
  claudeSdk: {
@@ -393,20 +407,21 @@ export const VERSION_SOURCES = {
393
407
  },
394
408
  kilo: {
395
409
  key: "kilo-code",
396
- fallback: "7.8.14",
410
+ fallback: "7.8.25",
397
411
  sources: [{ url: "https://open-vsx.org/api/kilocode/kilo-code" }],
398
412
  },
399
413
  muse: {
400
414
  key: "muse-code",
401
- fallback: "1.4.10-R6427.1",
415
+ fallback: "1.4.14-R7309.1",
402
416
  sources: [{ url: "https://api.meta.ai/muse-code/channels/muse-stable" }],
403
417
  },
404
418
  trae: {
405
419
  key: "trae",
406
- // Fallback = the donor's last observed client version. Discovery replaces
407
- // it within one monitor tick; it is only what a cold process sends before
420
+ // Fallback = the newest release the changelog names, which is the *high*
421
+ // end of its newest line ("TRAE v3.6.1 ~ 3.6.4"). Discovery replaces it
422
+ // within one monitor tick; it is only what a cold process sends before
408
423
  // the first successful fetch.
409
- fallback: "3.6.2",
424
+ fallback: "3.6.4",
410
425
  sources: [
411
426
  {
412
427
  url: "https://docs.trae.ai/ide/changelog?_lang=en",
@@ -419,7 +434,7 @@ export const VERSION_SOURCES = {
419
434
  // Fallback = the last observed wire version. Discovery replaces it within
420
435
  // one monitor tick; it is only what a cold process sends before the first
421
436
  // successful fetch.
422
- fallback: "cli-2026.10.19-6bbf2c4",
437
+ fallback: "cli-3.27.22-b1aa50c",
423
438
  sources: [
424
439
  {
425
440
  url: "https://cursor.com/api/download?platform=win32-x64-user&releaseTrack=stable",
@@ -433,7 +448,7 @@ export const VERSION_SOURCES = {
433
448
  // comment this replaced claimed no semver existed and pinned a date tag
434
449
  // (`v2026.9.24`) as the version — a tag, not a release, and not the
435
450
  // identity the client sends.
436
- fallback: "0.22.2",
451
+ fallback: "0.22.5",
437
452
  sources: [
438
453
  {
439
454
  url: "https://api.github.com/repos/NousResearch/hermes-agent/releases/latest",
@@ -290,10 +290,10 @@ export async function registerByokProviders(
290
290
  try {
291
291
  providerId = parseProviderId(row.id);
292
292
  } catch {
293
- throw new GatewayError("invalid_request", 400, `invalid BYOK provider ID: ${row.id}`);
293
+ throw new GatewayError("invalid_request", 400, `invalid BYOK provider ID: ${row.id} (request {requestId}). Check the shape of what you sent — then resend 🪞`);
294
294
  }
295
295
  if (isBundledProviderId(providerId)) {
296
- throw new GatewayError("invalid_request", 409, `BYOK provider ID collides with a built-in: ${row.id}`, {
296
+ throw new GatewayError("invalid_request", 409, `BYOK provider ID collides with a built-in: ${row.id} (request {requestId}). This name's taken — pick another alias ✨`, {
297
297
  provider_id: providerId,
298
298
  });
299
299
  }
@@ -309,14 +309,14 @@ export async function registerByokProviders(
309
309
  }
310
310
  } catch (error) {
311
311
  if (error instanceof GatewayError) throw error;
312
- throw new GatewayError("invalid_request", 400, `invalid BYOK base URL for provider ${row.id}`);
312
+ throw new GatewayError("invalid_request", 400, `invalid BYOK base URL for provider ${row.id} (request {requestId}). Check the shape of what you sent — then resend 🪞`);
313
313
  }
314
314
  // IP literals are rejected at registration; DNS names are revalidated at dispatch.
315
315
  if (isIP(parsedUrl.hostname) !== 0 && !isAddressAllowed(parsedUrl.hostname, ssrfPolicy)) {
316
316
  throw new GatewayError(
317
317
  "invalid_request",
318
318
  400,
319
- `BYOK provider ${row.id} host is blocked (private/unsafe address)`,
319
+ `BYOK provider ${row.id} host is blocked (private/unsafe address) (request {requestId}). All roads blocked — time to find a new route 🗺️`,
320
320
  );
321
321
  }
322
322
  const defaultPort = parsedUrl.protocol === "http:" ? 80 : 443;
@@ -9,8 +9,20 @@ import type { OAuthRefreshService, OAuthTokenRefresher } from "../authentication
9
9
  import { CredentialResolver, type CredentialAlternative } from "../provider-credential-resolver";
10
10
  import { parseProviderId, type CredentialKind } from "../provider-contracts";
11
11
  import type { ResolvedCredential } from "../provider-contracts";
12
- import { record } from "../authentication/oauth-flow-store";
13
12
  import { refreshLeadMs } from "./oauth-refresh-lead";
13
+ import { asJsonbRecord } from "../../shared/coerce";
14
+
15
+ /**
16
+ * The `auth_state` JSONB column arrives as a POJO from Postgres (drizzle
17
+ * jsonb) but as a JSON STRING from SQLite (the native column stores text).
18
+ * The resolver's `record()` helper rejects strings, so a SQLite read dropped
19
+ * the auth state entirely — Qoder dispatch then saw no userId/machineId and
20
+ * failed with 401 "missing its user identity". Parse the string form once
21
+ * here so every backend hands the resolver the same POJO shape.
22
+ */
23
+ function jsonbRecord(value: unknown): Record<string, unknown> | undefined {
24
+ return asJsonbRecord(value);
25
+ }
14
26
 
15
27
  // ── Credential cache ─────────────────────────────────────────────────────────
16
28
  /**
@@ -229,7 +241,7 @@ export async function resolveCredentialForAccount(
229
241
  throw new GatewayError(
230
242
  "authentication_failed",
231
243
  401,
232
- "provider credential cannot be decrypted; re-save the account credential",
244
+ "provider credential cannot be decrypted (request {requestId}); re-save the account credential. Wrong key, right door — check the credential 🔑",
233
245
  {
234
246
  providerId: row.providerId,
235
247
  accountId: row.id,
@@ -253,7 +265,7 @@ export async function resolveCredentialForAccount(
253
265
  }
254
266
  }
255
267
 
256
- const authState = record(row.authState);
268
+ const authState = jsonbRecord(row.authState);
257
269
  const alternative: CredentialAlternative = {
258
270
  provider_id: parseProviderId(row.providerId),
259
271
  account_id: row.id,
@@ -319,7 +331,7 @@ export async function resolveAccountCredentialsForExport(
319
331
  await oauth.refreshService.ensureFreshAccessToken(accountId, refresher);
320
332
  account = await loadAccountWithFreshness(db, accountId);
321
333
  if (!account) {
322
- throw new GatewayError("admission_unavailable", 503, "This provider account no longer exists, so its credential cannot be read. Re-select the account, or point the route at a live one.", {
334
+ throw new GatewayError("admission_unavailable", 503, "This provider account no longer exists, so its credential cannot be read (request {requestId}). Re-select the account, or point the route at a live one. The account went cold — reconnect or resend 🔌", {
323
335
  provider_id: providerId,
324
336
  account_id: accountId,
325
337
  });
@@ -17,7 +17,7 @@ export function createUpstreamDeadlineLifecycle(
17
17
  new GatewayError(
18
18
  "deadline_exceeded",
19
19
  504,
20
- "upstream request deadline exceeded",
20
+ "upstream request deadline exceeded (request {requestId}). Some things just take longer 🐢",
21
21
  {},
22
22
  "upstream",
23
23
  ),
@@ -52,11 +52,11 @@ export function abortGatewayError(
52
52
  const reason = lifecycle.signal.reason ?? requestSignal?.reason;
53
53
  if (reason instanceof GatewayError) return reason;
54
54
  if (reason instanceof DOMException && reason.name === "TimeoutError")
55
- return new GatewayError("deadline_exceeded", 504, "request deadline exceeded", {}, "cartethyia");
55
+ return new GatewayError("deadline_exceeded", 504, "request deadline exceeded (request {requestId}). Some things just take longer 🐢", {}, "cartethyia");
56
56
  const abortLike =
57
57
  (error instanceof DOMException && error.name === "AbortError") ||
58
58
  (error instanceof Error && error.name === "AbortError");
59
59
  if (lifecycle.signal.aborted || requestSignal?.aborted || abortLike)
60
- return new GatewayError("transport_closed", 499, "request was cancelled");
60
+ return new GatewayError("transport_closed", 499, "The request was cancelled (request {requestId}). Send it again whenever ready 🙃");
61
61
  return undefined;
62
62
  }
@@ -42,12 +42,56 @@ export function readCredentialSecret(credential: ResolvedCredential, errorMessag
42
42
  throw new GatewayError(
43
43
  "authentication_failed",
44
44
  401,
45
- errorMessage ?? "Provider credential secret is missing or empty",
45
+ `${errorMessage ?? "Provider credential secret is missing or empty"} (request {requestId}). Wrong key, right door — check the credential 🔑`,
46
46
  );
47
47
  }
48
48
  return raw;
49
49
  }
50
50
 
51
+ /**
52
+ * Asserts a credential is of the required kind, throwing a typed rejection
53
+ * otherwise. Providers disagree on the kind they accept (`oauth` vs
54
+ * `api_key` vs `scoped_access_token`), and each hand-rolled its own check +
55
+ * message. This centralizes the common pattern so a mis-bound credential reads
56
+ * the same everywhere, preserving the existing `invalid_request`/400 wire
57
+ * contract.
58
+ */
59
+ export function assertCredentialKind(
60
+ credential: ResolvedCredential,
61
+ expected: CredentialKind | readonly CredentialKind[],
62
+ providerLabel: string,
63
+ ): void {
64
+ const expectedKinds = Array.isArray(expected) ? expected : [expected];
65
+ if (expectedKinds.includes(credential.credential_kind)) return;
66
+ throw new GatewayError(
67
+ "invalid_request",
68
+ 400,
69
+ `${providerLabel}: ${expectedKinds.join(" or ")} credential required (got credential_kind=${credential.credential_kind}) (request {requestId}). Wrong key, right door — check the credential 🔑`,
70
+ { expected_credential_kind: expectedKinds },
71
+ );
72
+ }
73
+
74
+ /**
75
+ * Asserts a candidate's wire family is one of the ones the provider serves,
76
+ * throwing `capability_unsupported` otherwise. Every provider hand-rolled this
77
+ * guard with the same shape; this centralizes the pattern so a mis-routed
78
+ * candidate reads the same everywhere.
79
+ */
80
+ export function assertWireFamily(
81
+ candidate: ProviderDispatchTarget,
82
+ allowed: WireFamily | readonly WireFamily[],
83
+ providerLabel: string,
84
+ ): void {
85
+ const allowedFamilies = Array.isArray(allowed) ? allowed : [allowed];
86
+ if (allowedFamilies.includes(candidate.wire_family)) return;
87
+ throw new GatewayError(
88
+ "capability_unsupported",
89
+ 400,
90
+ `${providerLabel} supports ${allowedFamilies.join("/")} only, got ${candidate.wire_family} (request {requestId}). Know your tools — this one just isn't in the box 🧰`,
91
+ { expected_wire_families: allowedFamilies, got_wire_family: candidate.wire_family },
92
+ );
93
+ }
94
+
51
95
  /** Fetch capability that validates DNS answers and redirects before each request. */
52
96
  export type ValidatedOutboundFetch = (
53
97
  input: RequestInfo | URL,
@@ -155,6 +199,14 @@ export interface ProviderDispatchContext {
155
199
  * and hold no database handle.
156
200
  */
157
201
  readonly image_clamp_max_bytes?: number | undefined;
202
+ /**
203
+ * Tenant system-prompt text resolved once per request (append/override
204
+ * injection). The buddy adapters fold it into their fixed system turn after
205
+ * serialization, wrapped in the gateway's marker prefix/suffix, so the
206
+ * upstream's channel-identity validation still sees its prompt as the
207
+ * opener.
208
+ */
209
+ readonly tenant_system_prompt?: string | undefined;
158
210
  /** Opaque identity shared by retries of one logical gateway request. */
159
211
  readonly request_identity?: object | undefined;
160
212
  /**
@@ -344,7 +396,7 @@ export interface WebSearchOutcome {
344
396
  function normalizeProviderSlug(id: string): string {
345
397
  const trimmed = id.trim().toLowerCase();
346
398
  if (!/^[a-z][a-z0-9-]{0,63}$/.test(trimmed)) {
347
- throw new GatewayError("invalid_request", 400, "Provider ID must be a lowercase slug");
399
+ throw new GatewayError("invalid_request", 400, "Provider ID must be a lowercase slug (request {requestId}). Check the shape of what you sent — then resend 🪞");
348
400
  }
349
401
  return trimmed;
350
402
  }
@@ -371,7 +423,7 @@ export function parseCustomProviderId(value: string): CustomProviderId {
371
423
  throw new GatewayError(
372
424
  "slug_reserved",
373
425
  409,
374
- "The provider ID is reserved for a builtin provider",
426
+ "The provider ID is reserved for a builtin provider (request {requestId}). This name's taken — pick another alias ✨",
375
427
  { provider_id: normalized },
376
428
  );
377
429
  }
@@ -68,7 +68,7 @@ export class CredentialResolver {
68
68
  throw new GatewayError(
69
69
  "invalid_request",
70
70
  401,
71
- "No usable upstream credential is configured for the selected provider",
71
+ "No usable upstream credential is configured for the selected provider (request {requestId}). Wrong key, right door — check the credential 🔑",
72
72
  { provider_id: providerId },
73
73
  );
74
74
  }
@@ -89,7 +89,7 @@ function assertAdapterIdentity(expected: ProviderId, adapter: ProviderAdapter):
89
89
  // error: `invalid_request` would blame the caller and invite a byte-identical
90
90
  // retry. `internal_error` (500) says what is true — the gateway is
91
91
  // misconfigured and the request never had a chance.
92
- throw new GatewayError("internal_error", 500, "Loaded adapter ID does not match registration", {
92
+ throw new GatewayError("internal_error", 500, "Loaded adapter ID does not match registration (request {requestId}). Even we have bad days — check the registry wiring 🐻", {
93
93
  provider_id: expected,
94
94
  adapter_id: adapter.provider_id,
95
95
  });
@@ -122,12 +122,12 @@ export class ProviderRegistry {
122
122
  register(registration: ProviderRegistration): void {
123
123
  const providerId = parseProviderId(registration.provider_id);
124
124
  if (this.#registrations.has(providerId)) {
125
- throw new GatewayError("invalid_request", 409, "Provider is already registered", {
125
+ throw new GatewayError("invalid_request", 409, "Provider is already registered (request {requestId}). This name's taken — pick another alias ✨", {
126
126
  provider_id: providerId,
127
127
  });
128
128
  }
129
129
  if (providerId !== registration.provider_id) {
130
- throw new GatewayError("internal_error", 500, "Provider registration ID is not normalized", {
130
+ throw new GatewayError("internal_error", 500, "Provider registration ID is not normalized (request {requestId}). Even we have bad days — check the registry wiring 🐻", {
131
131
  provider_id: registration.provider_id,
132
132
  });
133
133
  }
@@ -158,7 +158,7 @@ export class ProviderRegistry {
158
158
  upsert(registration: ProviderRegistration): boolean {
159
159
  const providerId = parseProviderId(registration.provider_id);
160
160
  if (providerId !== registration.provider_id) {
161
- throw new GatewayError("internal_error", 500, "Provider registration ID is not normalized", {
161
+ throw new GatewayError("internal_error", 500, "Provider registration ID is not normalized (request {requestId}). Even we have bad days — check the registry wiring 🐻", {
162
162
  provider_id: registration.provider_id,
163
163
  });
164
164
  }
@@ -58,20 +58,27 @@ export function totalRemainingCredit(
58
58
  * independent quota windows, or `null` when neither is reported.
59
59
  *
60
60
  * Pooled credits are summed before converting to percent, so one exhausted
61
- * bonus pack cannot hide balance in another. Independent windows keep their
62
- * own remaining percent and the lowest one gates the account.
61
+ * bonus pack cannot hide balance in another. A pooled window without a ceiling
62
+ * (Command Code credits) contributes its absolute remaining to the credit sum
63
+ * but cannot produce a percent, so it is not counted in the percent ratio.
64
+ * Independent windows keep their own remaining percent and the lowest one
65
+ * gates the account.
63
66
  */
64
67
  export function totalRemainingPercent(
65
68
  windows: readonly ProviderQuotaWindow[],
66
69
  ): number | null {
67
70
  let creditLimit = 0;
68
71
  let creditRemaining = 0;
72
+ let hasPooledLimit = false;
69
73
  let independentPercent: number | null = null;
70
74
  for (const window of windows) {
71
75
  if (window.accounting === "pooled") {
72
76
  const balance = creditWindowBalance(window);
73
77
  if (balance !== null) {
74
- creditLimit += balance.limit;
78
+ if (balance.limit !== null) {
79
+ creditLimit += balance.limit;
80
+ hasPooledLimit = true;
81
+ }
75
82
  creditRemaining += balance.remaining;
76
83
  }
77
84
  continue;
@@ -85,8 +92,13 @@ export function totalRemainingPercent(
85
92
  independentPercent =
86
93
  independentPercent === null ? clamped : Math.min(independentPercent, clamped);
87
94
  }
88
- const creditPercent =
89
- creditLimit > 0 ? Math.min(100, Math.max(0, (creditRemaining / creditLimit) * 100)) : null;
95
+ // A pooled window with a reported ceiling (including a 0 ceiling — a spent
96
+ // balance) always defines a percent: 0 ceiling means 0% remaining.
97
+ const creditPercent = hasPooledLimit
98
+ ? creditLimit > 0
99
+ ? Math.min(100, Math.max(0, (creditRemaining / creditLimit) * 100))
100
+ : 0
101
+ : null;
90
102
  if (creditPercent === null) return independentPercent;
91
103
  if (independentPercent === null) return creditPercent;
92
104
  return Math.min(creditPercent, independentPercent);
@@ -94,20 +106,22 @@ export function totalRemainingPercent(
94
106
 
95
107
  function creditWindowBalance(
96
108
  window: ProviderQuotaWindow,
97
- ): { readonly limit: number; readonly remaining: number } | null {
109
+ ): { readonly limit: number | null; readonly remaining: number } | null {
110
+ // A ceiling of 0 is a spent balance, not an absence of data; only a window
111
+ // with no remaining figure at all is skipped. `limit` may be null (Command
112
+ // Code reports credits without a ceiling) — the remaining total still counts.
98
113
  const limit =
99
- typeof window.limit === "number" && Number.isFinite(window.limit) && window.limit > 0
114
+ typeof window.limit === "number" && Number.isFinite(window.limit) && window.limit >= 0
100
115
  ? window.limit
101
116
  : null;
102
- if (limit === null) return null;
103
117
  let remaining: number | null = null;
104
118
  if (typeof window.remaining === "number" && Number.isFinite(window.remaining)) {
105
119
  remaining = window.remaining;
106
- } else if (typeof window.used === "number" && Number.isFinite(window.used)) {
120
+ } else if (typeof window.used === "number" && Number.isFinite(window.used) && limit !== null) {
107
121
  remaining = limit - window.used;
108
122
  }
109
123
  if (remaining === null) return null;
110
- return { limit, remaining: Math.min(limit, Math.max(0, remaining)) };
124
+ return { limit, remaining: Math.max(0, remaining) };
111
125
  }
112
126
 
113
127
  const TIMEOUT_MS = 15_000;
@@ -16,7 +16,7 @@ import { type FetchLike, type ProviderQuotaResult } from "./quota-contracts";
16
16
  import type { ProviderId } from "../provider-contracts";
17
17
  import type { ProviderRegistry } from "../provider-registry";
18
18
  import { GLOBAL_QUOTA_LENS, setCachedQuota } from "./cache";
19
- import { record } from "../authentication/oauth-flow-store";
19
+ import { asJsonbRecord } from "../../shared/coerce";
20
20
 
21
21
  /** Why an account quota refresh failed; `null` means the fetch succeeded. */
22
22
  export type QuotaRefreshFailureCode = "missing_credential" | "quota_fetch_failed";
@@ -355,7 +355,7 @@ async function runQuotaRefresh(
355
355
  ? (deps.markApiKeyCredential?.(target.providerId, credential) ?? credential)
356
356
  : credential;
357
357
  try {
358
- const authState = record(target.authState);
358
+ const authState = asJsonbRecord(target.authState);
359
359
  quota = await fetchProviderQuota(deps.providerRegistry, target.providerId, marked, fetcher, {
360
360
  // Carried so a collector can reuse a reading captured from real
361
361
  // traffic rather than spending a request to rediscover it.
@@ -53,7 +53,7 @@ function stringField(record: Record<string, unknown> | undefined, key: string):
53
53
  export function searchParamsFromBody(body: Record<string, unknown>): SearchParams {
54
54
  const query = typeof body.query === "string" ? body.query.trim() : "";
55
55
  if (query.length === 0)
56
- throw new GatewayError("invalid_request", 400, "search query is required");
56
+ throw new GatewayError("invalid_request", 400, "search query is required (request {requestId}). Check the shape of what you sent — then resend 🪞");
57
57
  const rawMax = body.max_results;
58
58
  const maxResults =
59
59
  typeof rawMax === "number" && Number.isFinite(rawMax)
@@ -90,7 +90,7 @@ export function createSearchAdapter(spec: SearchProviderSpec): ProviderAdapter {
90
90
  throw new GatewayError(
91
91
  "capability_unsupported",
92
92
  400,
93
- `${spec.provider_id} serves web search, not a chat wire`,
93
+ `${spec.provider_id} serves web search, not a chat wire (request {requestId}). Know your tools — this one just isn't in the box 🧰`,
94
94
  );
95
95
  },
96
96
  async websearch(
@@ -113,7 +113,7 @@ export function createSearchAdapter(spec: SearchProviderSpec): ProviderAdapter {
113
113
  throw new GatewayError(
114
114
  "platform_unavailable",
115
115
  502,
116
- `${spec.provider_id} search unreachable: ${error instanceof Error ? error.message : String(error)}`,
116
+ `${spec.provider_id} search unreachable (request {requestId}): ${error instanceof Error ? error.message : String(error)}. The connection ghosted us — come back in a moment 👻`,
117
117
  );
118
118
  }
119
119
  if (!res.ok) {
@@ -121,7 +121,7 @@ export function createSearchAdapter(spec: SearchProviderSpec): ProviderAdapter {
121
121
  throw new GatewayError(
122
122
  res.status >= 500 ? "platform_unavailable" : "invalid_request",
123
123
  res.status,
124
- `${spec.provider_id} search error: ${detail.slice(0, 240)}`,
124
+ `${spec.provider_id} search error (request {requestId}): ${detail.slice(0, 240)}. The upstream came back empty-handed — retry 🌙`,
125
125
  );
126
126
  }
127
127
  const raw = await res.text();
@@ -135,7 +135,7 @@ export function createSearchAdapter(spec: SearchProviderSpec): ProviderAdapter {
135
135
  throw new GatewayError(
136
136
  "platform_unavailable",
137
137
  502,
138
- `${spec.provider_id} search returned an unparseable body`,
138
+ `${spec.provider_id} search returned an unparseable body (request {requestId}). The upstream came back empty-handed — retry 🌙`,
139
139
  );
140
140
  }
141
141
  const results = spec.normalize(data);
@@ -399,7 +399,7 @@ export class IpAbuseProtectionService {
399
399
  readonly signal?: AbortSignal;
400
400
  }): Promise<void> {
401
401
  if (input.signal?.aborted) {
402
- throw new GatewayError("admission_unavailable", 503, "The abuse check never ran because the request was already cancelled.", {
402
+ throw new GatewayError("admission_unavailable", 503, "The abuse check never ran because the request was already cancelled (request {requestId}). Send it again whenever ready 🙃", {
403
403
  reason: "admission-unavailable",
404
404
  });
405
405
  }
@@ -436,7 +436,7 @@ export class IpAbuseProtectionService {
436
436
 
437
437
  if (outcome.banned) {
438
438
  metrics.cartethyia_ip_abuse_refusals_total.inc(1, { reason: "ip-banned" });
439
- throw new GatewayError("quota_exceeded", 429, "This address is temporarily banned for too many failed requests. Wait for the ban to lift, then slow down.", {
439
+ throw new GatewayError("rate_limited", 429, "This address is temporarily banned for too many failed requests (request {requestId}). Wait for the ban to lift, then slow down. You've maxed this window — pace yourself ⏳", {
440
440
  // Distinguishes this from the rate-limited 429 below and from an API
441
441
  // key's own budget. It used to be "admission-unavailable", which is
442
442
  // the label for an *unreachable* store — so every refusal from this
@@ -454,7 +454,7 @@ export class IpAbuseProtectionService {
454
454
 
455
455
  if (outcome.count > this.maxRequestsPerWindow) {
456
456
  metrics.cartethyia_ip_abuse_refusals_total.inc(1, { reason: "ip-rate-limited" });
457
- throw new GatewayError("quota_exceeded", 429, "This address is sending requests faster than the abuse limit allows. Back off and retry after the window rolls over.", {
457
+ throw new GatewayError("rate_limited", 429, "This address is sending requests faster than the abuse limit allows (request {requestId}). Back off and retry after the window rolls over. You've maxed this window — pace yourself ⏳", {
458
458
  reason: "ip-rate-limited",
459
459
  ip,
460
460
  route: input.route,
@@ -464,7 +464,7 @@ export class IpAbuseProtectionService {
464
464
  }
465
465
  } catch (err) {
466
466
  if (err instanceof GatewayError) throw err;
467
- throw new GatewayError("admission_unavailable", 503, "The abuse-tracking store is unreachable, so requests from this address cannot be checked. Retry shortly.", {
467
+ throw new GatewayError("admission_unavailable", 503, "The abuse-tracking store is unreachable, so requests from this address cannot be checked (request {requestId}). Retry shortly. The bouncer stepped out — come back shortly 🚪", {
468
468
  reason: "admission-unavailable",
469
469
  cause: (err as Error).message,
470
470
  });
@@ -1,5 +1,24 @@
1
1
  // Shared authorization primitives: closed scope model and immutable access decisions.
2
2
 
3
+ /**
4
+ * The single identity of "who is speaking" — resolved once at auth and carried
5
+ * through planning, admission, pool selection, and telemetry so every layer
6
+ * reasons about the same caller.
7
+ *
8
+ * - `apiKeyId` — the credential that authenticated.
9
+ * - `admissionIdentity` — the quota/concurrency owner (the share parent for a
10
+ * child key; otherwise the key itself).
11
+ * - `tenantId` — the tenant the request runs as.
12
+ *
13
+ * Provider/account/model are added by the candidate, not here: the caller
14
+ * identity is stable regardless of what serves the request.
15
+ */
16
+ export interface CallerIdentity {
17
+ readonly apiKeyId: string;
18
+ readonly admissionIdentity: string;
19
+ readonly tenantId: string;
20
+ }
21
+
3
22
  /**
4
23
  * Closed set of authorization scopes.
5
24
  * - `routing:invoke`: May call only /v1/* gateway routes within holder's tenant.