cartethyia 2.6.7 → 2.7.1

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 (335) hide show
  1. package/.env.example +1 -7
  2. package/README.md +25 -2
  3. package/dist/dashboard/assets/{BannedPage-Dskhyp0l.js → BannedPage-D-J75hBM.js} +1 -1
  4. package/dist/dashboard/assets/BannedPage-D-J75hBM.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolDetailPage--SJhk7qQ.js → CliToolDetailPage-CVe6RaBH.js} +1 -1
  6. package/dist/dashboard/assets/CliToolDetailPage-CVe6RaBH.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-C203VT-c.js → CliToolsPage-Dd8ZSamp.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-Dd8ZSamp.js.gz +0 -0
  9. package/dist/dashboard/assets/{CombosPage-BcS7kGG1.js → CombosPage-QoJ6foxo.js} +1 -1
  10. package/dist/dashboard/assets/CombosPage-QoJ6foxo.js.gz +0 -0
  11. package/dist/dashboard/assets/ConsoleLogPage-DpNRay4C.js +1 -0
  12. package/dist/dashboard/assets/ConsoleLogPage-DpNRay4C.js.gz +0 -0
  13. package/dist/dashboard/assets/{CustomizationPage-uujbBYwC.js → CustomizationPage-CKUFTcW4.js} +1 -1
  14. package/dist/dashboard/assets/CustomizationPage-CKUFTcW4.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-CzSLiDzr.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-CzSLiDzr.js.gz +0 -0
  17. package/dist/dashboard/assets/LoginPage-DubdQcvX.js +1 -0
  18. package/dist/dashboard/assets/LoginPage-DubdQcvX.js.gz +0 -0
  19. package/dist/dashboard/assets/{ModelPicker-CA1kLs45.js → ModelPicker-BH8Tehdi.js} +1 -1
  20. package/dist/dashboard/assets/ModelPicker-BH8Tehdi.js.gz +0 -0
  21. package/dist/dashboard/assets/OverviewPage-DuWzAQFL.js +1 -0
  22. package/dist/dashboard/assets/OverviewPage-DuWzAQFL.js.gz +0 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-BKEKe2Fm.js +4 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-BKEKe2Fm.js.gz +0 -0
  25. package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js +1 -0
  26. package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js.gz +0 -0
  27. package/dist/dashboard/assets/ProvidersPage-Bwotu8UJ.js +1 -0
  28. package/dist/dashboard/assets/ProvidersPage-Bwotu8UJ.js.gz +0 -0
  29. package/dist/dashboard/assets/ProxyPage-D7hpWbg9.js +9 -0
  30. package/dist/dashboard/assets/ProxyPage-D7hpWbg9.js.gz +0 -0
  31. package/dist/dashboard/assets/{QuotaPage-BueSWX5D.js → QuotaPage-ATpaxrbl.js} +1 -1
  32. package/dist/dashboard/assets/QuotaPage-ATpaxrbl.js.gz +0 -0
  33. package/dist/dashboard/assets/SettingsPage-CETophpx.js +1 -0
  34. package/dist/dashboard/assets/SettingsPage-CETophpx.js.gz +0 -0
  35. package/dist/dashboard/assets/SetupPage-BdgECjqq.js +1 -0
  36. package/dist/dashboard/assets/SetupPage-BdgECjqq.js.gz +0 -0
  37. package/dist/dashboard/assets/{SortableList-DrKeMcGN.js → SortableList-CAK-EN9z.js} +1 -1
  38. package/dist/dashboard/assets/SortableList-CAK-EN9z.js.gz +0 -0
  39. package/dist/dashboard/assets/{StudioPage-qjuYtccc.js → StudioPage-B7W2_RhT.js} +3 -3
  40. package/dist/dashboard/assets/StudioPage-B7W2_RhT.js.gz +0 -0
  41. package/dist/dashboard/assets/UsagePage-6T4EFDjW.js +56 -0
  42. package/dist/dashboard/assets/UsagePage-6T4EFDjW.js.gz +0 -0
  43. package/dist/dashboard/assets/{api-keys-C23S9sgp.js → api-keys-CUi6UXsR.js} +1 -1
  44. package/dist/dashboard/assets/api-keys-CUi6UXsR.js.gz +0 -0
  45. package/dist/dashboard/assets/app-KvsUJUMF.css +2 -0
  46. package/dist/dashboard/assets/app-KvsUJUMF.css.gz +0 -0
  47. package/dist/dashboard/assets/{assert-DU6qrMA_.js → assert-67xXH5rf.js} +1 -1
  48. package/dist/dashboard/assets/assert-67xXH5rf.js.gz +0 -0
  49. package/dist/dashboard/assets/{badge-Bkz_-RrM.js → badge-DC2L43Mj.js} +1 -1
  50. package/dist/dashboard/assets/badge-DC2L43Mj.js.gz +0 -0
  51. package/dist/dashboard/assets/{banner-C-7y3VKV.js → banner-BBVcb0J3.js} +1 -1
  52. package/dist/dashboard/assets/banner-BBVcb0J3.js.gz +0 -0
  53. package/dist/dashboard/assets/{card-BOPBuBzD.js → card-Cca1XC6G.js} +1 -1
  54. package/dist/dashboard/assets/card-Cca1XC6G.js.gz +0 -0
  55. package/dist/dashboard/assets/{cli-tools-BaFUuFFF.js → cli-tools-KvsWa1Jl.js} +1 -1
  56. package/dist/dashboard/assets/cli-tools-KvsWa1Jl.js.gz +0 -0
  57. package/dist/dashboard/assets/{clipboard-button-e-Mlk0FH.js → clipboard-button-DUzEbynq.js} +1 -1
  58. package/dist/dashboard/assets/clipboard-button-DUzEbynq.js.gz +0 -0
  59. package/dist/dashboard/assets/{dialog-Iec_ywbD.js → dialog-DFvWhwTa.js} +1 -1
  60. package/dist/dashboard/assets/dialog-DFvWhwTa.js.gz +0 -0
  61. package/dist/dashboard/assets/{drawer-Bs8MIIHc.js → drawer-CFM2sMX1.js} +1 -1
  62. package/dist/dashboard/assets/drawer-CFM2sMX1.js.gz +0 -0
  63. package/dist/dashboard/assets/{entry-DOddwoc0.js → entry-BLBLxX0h.js} +3 -3
  64. package/dist/dashboard/assets/entry-BLBLxX0h.js.gz +0 -0
  65. package/dist/dashboard/assets/{entry-DcobaKFu.js → entry-Bqy8RFvM.js} +1 -1
  66. package/dist/dashboard/assets/entry-Bqy8RFvM.js.gz +0 -0
  67. package/dist/dashboard/assets/{entry-BRszm4_Z.js → entry-_xHo5x6r.js} +1 -1
  68. package/dist/dashboard/assets/entry-_xHo5x6r.js.gz +0 -0
  69. package/dist/dashboard/assets/formatters-mpBPqAQ8.js +1 -0
  70. package/dist/dashboard/assets/formatters-mpBPqAQ8.js.gz +0 -0
  71. package/dist/dashboard/assets/{github-badge-BuDsrsPn.js → github-badge-C9e8Ts7e.js} +1 -1
  72. package/dist/dashboard/assets/github-badge-C9e8Ts7e.js.gz +0 -0
  73. package/dist/dashboard/assets/{index-BeLRhsmt.js → index-D6QzEC_n.js} +2 -2
  74. package/dist/dashboard/assets/index-D6QzEC_n.js.gz +0 -0
  75. package/dist/dashboard/assets/{inline-DToMICwl.js → inline-BFUcvllj.js} +1 -1
  76. package/dist/dashboard/assets/inline-BFUcvllj.js.gz +0 -0
  77. package/dist/dashboard/assets/{input-WlyHWeG2.js → input-C_ILAvBb.js} +1 -1
  78. package/dist/dashboard/assets/input-C_ILAvBb.js.gz +0 -0
  79. package/dist/dashboard/assets/{layout-DaJ5baJr.js → layout-CBIyT6Rt.js} +1 -1
  80. package/dist/dashboard/assets/layout-CBIyT6Rt.js.gz +0 -0
  81. package/dist/dashboard/assets/{list-row-B3oSK13z.js → list-row-ebJ_-Nsb.js} +1 -1
  82. package/dist/dashboard/assets/list-row-ebJ_-Nsb.js.gz +0 -0
  83. package/dist/dashboard/assets/{live-ta6GMb-8.js → live-D0mm--ME.js} +1 -1
  84. package/dist/dashboard/assets/live-D0mm--ME.js.gz +0 -0
  85. package/dist/dashboard/assets/minimize-2-D-MN8W8a.js +1 -0
  86. package/dist/dashboard/assets/minimize-2-D-MN8W8a.js.gz +0 -0
  87. package/dist/dashboard/assets/{mutation-g_fduiNw.js → mutation-sMD965oO.js} +1 -1
  88. package/dist/dashboard/assets/mutation-sMD965oO.js.gz +0 -0
  89. package/dist/dashboard/assets/{numbers-Dulqgys5.js → numbers-BO9dKaFr.js} +1 -1
  90. package/dist/dashboard/assets/numbers-BO9dKaFr.js.gz +0 -0
  91. package/dist/dashboard/assets/providers/inception.svg +5 -0
  92. package/dist/dashboard/assets/providers/inception.svg.gz +0 -0
  93. package/dist/dashboard/assets/{query-keys-UETqfytp.js → query-keys-CfL_Zdoh.js} +1 -1
  94. package/dist/dashboard/assets/query-keys-CfL_Zdoh.js.gz +0 -0
  95. package/dist/dashboard/assets/{routing-BPUo-W8d.js → routing-BnIpi1IJ.js} +1 -1
  96. package/dist/dashboard/assets/routing-BnIpi1IJ.js.gz +0 -0
  97. package/dist/dashboard/assets/{select-BfZ3SCTM.js → select-Cj5Nw43g.js} +1 -1
  98. package/dist/dashboard/assets/select-Cj5Nw43g.js.gz +0 -0
  99. package/dist/dashboard/assets/settings-BAFhDgX8.js +1 -0
  100. package/dist/dashboard/assets/settings-BAFhDgX8.js.gz +0 -0
  101. package/dist/dashboard/assets/{stack-B1w5BM0u.js → stack-EMrOMt4I.js} +1 -1
  102. package/dist/dashboard/assets/stack-EMrOMt4I.js.gz +0 -0
  103. package/dist/dashboard/assets/{state-DhZBfWgx.js → state-CWoo7ty5.js} +1 -1
  104. package/dist/dashboard/assets/state-CWoo7ty5.js.gz +0 -0
  105. package/dist/dashboard/assets/{switch-yD7D99Lg.js → switch-CCjT6IkF.js} +1 -1
  106. package/dist/dashboard/assets/switch-CCjT6IkF.js.gz +0 -0
  107. package/dist/dashboard/assets/{useMutation-EDFRkn1i.js → useMutation-DjkLHUcE.js} +1 -1
  108. package/dist/dashboard/assets/useMutation-DjkLHUcE.js.gz +0 -0
  109. package/dist/dashboard/index.html +1 -1
  110. package/dist/dashboard/index.html.gz +0 -0
  111. package/dist/dashboard/precompressed-assets.json +55 -54
  112. package/migrations/full-postgres/0024_add_pool_provider_cooldowns.sql +12 -0
  113. package/migrations/full-postgres/0025_add_pool_random_strategy.sql +3 -0
  114. package/migrations/lite-sqlite3/0024_add_pool_provider_cooldowns.sql +12 -0
  115. package/migrations/lite-sqlite3/0025_add_pool_random_strategy.sql +20 -0
  116. package/package.json +1 -1
  117. package/scripts/cli/console-menu.ts +5 -2
  118. package/scripts/cli/tray.ts +21 -6
  119. package/src/boot/app.ts +24 -62
  120. package/src/boot/dependencies.ts +732 -750
  121. package/src/config/cooldowns.ts +16 -0
  122. package/src/config/providers.ts +13 -1
  123. package/src/config/resources.ts +13 -80
  124. package/src/config/security.ts +2 -38
  125. package/src/config/spec.ts +16 -23
  126. package/src/console/console-router.ts +7 -7
  127. package/src/console/domain-registration.ts +19 -12
  128. package/src/console/providers/catalog/catalog-projections.ts +2 -6
  129. package/src/console/providers/catalog/contracts.ts +19 -0
  130. package/src/console/providers/catalog/provider-operations.ts +38 -0
  131. package/src/console/providers/catalog/routes.ts +14 -0
  132. package/src/console/providers/catalog/store-helpers.ts +326 -0
  133. package/src/console/providers/catalog/store.ts +1292 -1563
  134. package/src/console/quota/account-quota-global.ts +2 -2
  135. package/src/console/quota/account-quota-shared.ts +1 -1
  136. package/src/console/quota/account-quota-tenant.ts +35 -12
  137. package/src/console/quota/quota-view.ts +2 -2
  138. package/src/console/remote-support/contracts.ts +3 -1
  139. package/src/console/remote-support/service.ts +115 -6
  140. package/src/console/routing/pools/contracts.ts +41 -1
  141. package/src/console/routing/pools/operations.ts +183 -4
  142. package/src/console/routing/pools/relay-deploy.ts +17 -4
  143. package/src/console/routing/pools/routes.ts +30 -0
  144. package/src/console/routing/pools/store.ts +98 -8
  145. package/src/main.ts +16 -2
  146. package/src/network/outbound-fetch.ts +226 -365
  147. package/src/network/pool/agent.ts +8 -0
  148. package/src/network/pool/pool-state.ts +7 -0
  149. package/src/network/pool/selector.ts +110 -2
  150. package/src/network/pool-cooldown-store.ts +252 -0
  151. package/src/network/pool-health-machine.ts +5 -3
  152. package/src/network/pool-health.ts +36 -9
  153. package/src/observability/event-query.ts +57 -0
  154. package/src/observability/metrics.ts +15 -5
  155. package/src/observability/runtime-metrics.ts +30 -15
  156. package/src/persistence/health-event-flush.ts +2 -0
  157. package/src/persistence/schema.ts +19 -0
  158. package/src/persistence/usage-flush.ts +8 -3
  159. package/src/protocol/canonical-model.ts +1 -1
  160. package/src/protocol/dsml.ts +377 -0
  161. package/src/protocol/gateway-error.ts +0 -1
  162. package/src/{providers/provider-reasoning.ts → protocol/reasoning.ts} +10 -9
  163. package/src/protocol/request/chat.ts +7 -7
  164. package/src/protocol/request/codex.ts +2 -2
  165. package/src/protocol/request/gemini.ts +1 -1
  166. package/src/protocol/request/messages.ts +6 -6
  167. package/src/protocol/request/responses.ts +7 -7
  168. package/src/protocol/response/dsml.ts +12 -133
  169. package/src/{transport → protocol}/translation/attachment-integrity.ts +2 -2
  170. package/src/{transport → protocol}/translation/cache-controls.ts +1 -1
  171. package/src/{transport → protocol}/translation/capabilities.ts +2 -2
  172. package/src/{transport → protocol}/translation/normalize.ts +1 -1
  173. package/src/{transport → protocol}/translation/quirks.ts +1 -1
  174. package/src/{transport → protocol}/translation/search.ts +1 -1
  175. package/src/{transport → protocol}/translation/thinking.ts +23 -1
  176. package/src/{transport → protocol}/translation/tool-id.ts +1 -1
  177. package/src/{transport → protocol}/translation/tool-repair.ts +2 -2
  178. package/src/protocol/upstream-parsing.ts +176 -0
  179. package/src/providers/adapters/compatible-adapter.ts +11 -2
  180. package/src/providers/authentication/oauth-refresh-service.ts +9 -5
  181. package/src/providers/default-registry.ts +21 -5
  182. package/src/providers/discovery/probe-phases.ts +16 -3
  183. package/src/providers/discovery/probing-service.ts +4 -1
  184. package/src/providers/integrations/anthropic.ts +1 -1
  185. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -4
  186. package/src/providers/integrations/buddy/buddy-chat-shared.ts +141 -8
  187. package/src/providers/integrations/buddy/buddy-checkin.ts +5 -1
  188. package/src/providers/integrations/buddy/buddy-growth-shared.ts +0 -82
  189. package/src/providers/integrations/buddy/buddy-identity.ts +25 -0
  190. package/src/providers/integrations/buddy/buddy-server-tools.ts +240 -0
  191. package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +36 -0
  192. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +6 -31
  193. package/src/providers/integrations/buddy/codebuddy/provider.ts +2 -0
  194. package/src/providers/integrations/buddy/codebuddy/shared.ts +19 -4
  195. package/src/providers/integrations/buddy/turing-device-token.ts +262 -0
  196. package/src/providers/integrations/buddy/workbuddy/provider.ts +2 -0
  197. package/src/providers/integrations/buddy/workbuddy/shared.ts +56 -3
  198. package/src/providers/integrations/claude/claude.ts +1 -1
  199. package/src/providers/integrations/cloudflareai/cloudflareai.ts +1 -1
  200. package/src/providers/integrations/codex/codex-errors.ts +1 -1
  201. package/src/providers/integrations/codex/codex.ts +1 -1
  202. package/src/providers/integrations/commandcode/commandcode.ts +134 -104
  203. package/src/providers/integrations/commandcode/discovery.ts +6 -5
  204. package/src/providers/integrations/factory/factory.ts +1 -1
  205. package/src/providers/integrations/github/github.ts +1 -1
  206. package/src/providers/integrations/grok/grok.ts +1 -1
  207. package/src/providers/integrations/inception/provider.ts +91 -0
  208. package/src/providers/integrations/inferhub.ts +1 -1
  209. package/src/providers/integrations/kiro/kiro-request.ts +1 -1
  210. package/src/providers/integrations/mistral.ts +193 -19
  211. package/src/providers/integrations/opencode/fingerprint.ts +19 -2
  212. package/src/providers/integrations/opencode/prompt.ts +111 -0
  213. package/src/providers/integrations/opencode/provider.ts +101 -23
  214. package/src/providers/integrations/opencode/tool-definitions.ts +4 -4
  215. package/src/providers/integrations/qoder/qoder-checkin.ts +303 -0
  216. package/src/providers/integrations/qoder/qoder.ts +1641 -884
  217. package/src/providers/integrations/shared/install-id.ts +2 -1
  218. package/src/providers/integrations/trae/trae-cache.ts +110 -0
  219. package/src/providers/integrations/trae/trae-oauth.ts +29 -4
  220. package/src/providers/integrations/trae/trae-protocol.ts +2 -1
  221. package/src/providers/integrations/trae/trae.ts +106 -23
  222. package/src/providers/operations/account-error-classification.ts +1660 -0
  223. package/src/providers/operations/account-health-service.ts +621 -2097
  224. package/src/providers/operations/account-reset-service.ts +2 -0
  225. package/src/providers/provider-contracts.ts +0 -2
  226. package/src/providers/provider-metadata.ts +25 -4
  227. package/src/providers/provider-probe.ts +2 -2
  228. package/src/{console → providers}/quota/cache.ts +1 -1
  229. package/src/{console → providers}/quota/refresh.ts +5 -5
  230. package/src/security/abuse.ts +17 -0
  231. package/src/transport/dispatch/attempt-finalize.ts +114 -12
  232. package/src/transport/dispatch/attempt-loop.ts +3 -0
  233. package/src/transport/dispatch/empty-stream.ts +224 -217
  234. package/src/transport/dispatch/generation.ts +37 -55
  235. package/src/transport/dispatch/handler-deps.ts +32 -0
  236. package/src/transport/dispatch/leases.ts +9 -4
  237. package/src/transport/dispatch/proxy-request.ts +6 -30
  238. package/src/transport/dispatch/responses-compact.ts +22 -43
  239. package/src/transport/dispatch/streaming/finalize.ts +3 -0
  240. package/src/transport/dispatch/streaming/stream.ts +0 -8
  241. package/src/transport/dispatch/systemone.ts +22 -42
  242. package/src/transport/dispatch/token-count.ts +23 -39
  243. package/src/transport/dispatch/upstream.ts +1 -3
  244. package/src/transport/dispatch/websearch.ts +22 -41
  245. package/src/transport/failure-policy.ts +59 -178
  246. package/src/transport/guards/abuse-guard.ts +10 -0
  247. package/src/transport/guards/auth-guard.ts +0 -18
  248. package/src/transport/guards/error-lifecycle.ts +1 -1
  249. package/src/transport/guards/pipeline.ts +0 -6
  250. package/src/transport/guards/request-context.ts +18 -91
  251. package/src/transport/request/degradation.ts +20 -1
  252. package/src/transport/request/inflight.ts +4 -95
  253. package/src/transport/request/preparer.ts +6 -6
  254. package/src/transport/request/state.ts +66 -38
  255. package/src/transport/request/tenant-preferences.ts +1 -1
  256. package/src/transport/routing/route-catalog.ts +41 -31
  257. package/src/transport/routing/router.ts +2 -2
  258. package/src/transport/surface/messages/parse.ts +1 -1
  259. package/src/workers/buddy-growth-tasks.ts +346 -425
  260. package/src/workers/daily-checkin.ts +17 -6
  261. package/src/workers/quota-refresh-sweep.ts +2 -2
  262. package/src/workers/scheduler.ts +17 -1
  263. package/dist/dashboard/assets/BannedPage-Dskhyp0l.js.gz +0 -0
  264. package/dist/dashboard/assets/CliToolDetailPage--SJhk7qQ.js.gz +0 -0
  265. package/dist/dashboard/assets/CliToolsPage-C203VT-c.js.gz +0 -0
  266. package/dist/dashboard/assets/CombosPage-BcS7kGG1.js.gz +0 -0
  267. package/dist/dashboard/assets/ConsoleLogPage-ChmiSLwO.js +0 -1
  268. package/dist/dashboard/assets/ConsoleLogPage-ChmiSLwO.js.gz +0 -0
  269. package/dist/dashboard/assets/CustomizationPage-uujbBYwC.js.gz +0 -0
  270. package/dist/dashboard/assets/HealthEventsModal-fKAWd-OQ.js +0 -1
  271. package/dist/dashboard/assets/HealthEventsModal-fKAWd-OQ.js.gz +0 -0
  272. package/dist/dashboard/assets/LoginPage-DoOUthbG.js +0 -1
  273. package/dist/dashboard/assets/LoginPage-DoOUthbG.js.gz +0 -0
  274. package/dist/dashboard/assets/ModelPicker-CA1kLs45.js.gz +0 -0
  275. package/dist/dashboard/assets/OverviewPage-C5Z-UBz1.js +0 -1
  276. package/dist/dashboard/assets/OverviewPage-C5Z-UBz1.js.gz +0 -0
  277. package/dist/dashboard/assets/ProviderDetailPage-CFu1qGcT.js +0 -3
  278. package/dist/dashboard/assets/ProviderDetailPage-CFu1qGcT.js.gz +0 -0
  279. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js +0 -1
  280. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js.gz +0 -0
  281. package/dist/dashboard/assets/ProvidersPage-CnlJZbxF.js +0 -1
  282. package/dist/dashboard/assets/ProvidersPage-CnlJZbxF.js.gz +0 -0
  283. package/dist/dashboard/assets/ProxyPage-B9mBTweh.js +0 -7
  284. package/dist/dashboard/assets/ProxyPage-B9mBTweh.js.gz +0 -0
  285. package/dist/dashboard/assets/QuotaPage-BueSWX5D.js.gz +0 -0
  286. package/dist/dashboard/assets/SettingsPage-B0x00MVY.js +0 -1
  287. package/dist/dashboard/assets/SettingsPage-B0x00MVY.js.gz +0 -0
  288. package/dist/dashboard/assets/SetupPage-DQ9oDRpi.js +0 -1
  289. package/dist/dashboard/assets/SetupPage-DQ9oDRpi.js.gz +0 -0
  290. package/dist/dashboard/assets/SortableList-DrKeMcGN.js.gz +0 -0
  291. package/dist/dashboard/assets/StudioPage-qjuYtccc.js.gz +0 -0
  292. package/dist/dashboard/assets/UsagePage-Be7aNviQ.js +0 -56
  293. package/dist/dashboard/assets/UsagePage-Be7aNviQ.js.gz +0 -0
  294. package/dist/dashboard/assets/api-keys-C23S9sgp.js.gz +0 -0
  295. package/dist/dashboard/assets/app-BUSiYL6F.css +0 -2
  296. package/dist/dashboard/assets/app-BUSiYL6F.css.gz +0 -0
  297. package/dist/dashboard/assets/assert-DU6qrMA_.js.gz +0 -0
  298. package/dist/dashboard/assets/badge-Bkz_-RrM.js.gz +0 -0
  299. package/dist/dashboard/assets/banner-C-7y3VKV.js.gz +0 -0
  300. package/dist/dashboard/assets/card-BOPBuBzD.js.gz +0 -0
  301. package/dist/dashboard/assets/cli-tools-BaFUuFFF.js.gz +0 -0
  302. package/dist/dashboard/assets/clipboard-button-e-Mlk0FH.js.gz +0 -0
  303. package/dist/dashboard/assets/cog-CdWhsajs.js +0 -1
  304. package/dist/dashboard/assets/cog-CdWhsajs.js.gz +0 -0
  305. package/dist/dashboard/assets/dialog-Iec_ywbD.js.gz +0 -0
  306. package/dist/dashboard/assets/drawer-Bs8MIIHc.js.gz +0 -0
  307. package/dist/dashboard/assets/entry-BRszm4_Z.js.gz +0 -0
  308. package/dist/dashboard/assets/entry-DOddwoc0.js.gz +0 -0
  309. package/dist/dashboard/assets/entry-DcobaKFu.js.gz +0 -0
  310. package/dist/dashboard/assets/github-badge-BuDsrsPn.js.gz +0 -0
  311. package/dist/dashboard/assets/index-BeLRhsmt.js.gz +0 -0
  312. package/dist/dashboard/assets/inline-DToMICwl.js.gz +0 -0
  313. package/dist/dashboard/assets/input-WlyHWeG2.js.gz +0 -0
  314. package/dist/dashboard/assets/layout-DaJ5baJr.js.gz +0 -0
  315. package/dist/dashboard/assets/list-row-B3oSK13z.js.gz +0 -0
  316. package/dist/dashboard/assets/live-ta6GMb-8.js.gz +0 -0
  317. package/dist/dashboard/assets/model-bans-BmYTmbAk.js +0 -1
  318. package/dist/dashboard/assets/model-bans-BmYTmbAk.js.gz +0 -0
  319. package/dist/dashboard/assets/mutation-g_fduiNw.js.gz +0 -0
  320. package/dist/dashboard/assets/numbers-Dulqgys5.js.gz +0 -0
  321. package/dist/dashboard/assets/query-keys-UETqfytp.js.gz +0 -0
  322. package/dist/dashboard/assets/routing-BPUo-W8d.js.gz +0 -0
  323. package/dist/dashboard/assets/select-BfZ3SCTM.js.gz +0 -0
  324. package/dist/dashboard/assets/settings--3hxSyry.js +0 -1
  325. package/dist/dashboard/assets/settings--3hxSyry.js.gz +0 -0
  326. package/dist/dashboard/assets/stack-B1w5BM0u.js.gz +0 -0
  327. package/dist/dashboard/assets/state-DhZBfWgx.js.gz +0 -0
  328. package/dist/dashboard/assets/switch-yD7D99Lg.js.gz +0 -0
  329. package/dist/dashboard/assets/useMutation-EDFRkn1i.js.gz +0 -0
  330. package/src/console/admin/model-abuse/contracts.ts +0 -38
  331. package/src/console/admin/model-abuse/routes.ts +0 -97
  332. package/src/security/model-abuse.ts +0 -704
  333. package/src/transport/guards/concurrency-guard.ts +0 -125
  334. /package/dist/dashboard/assets/{app-MdVfwraB.js → app-BaWd0puM.js} +0 -0
  335. /package/dist/dashboard/assets/{app-MdVfwraB.js.gz → app-BaWd0puM.js.gz} +0 -0
@@ -1,184 +1,26 @@
1
1
  import { GatewayError, type GatewayErrorCode, type GatewayErrorOrigin } from "../protocol/gateway-error";
2
2
  import { isRecord } from "../protocol/codecs";
3
3
  import { resolveFallbackRetryBaseMs, resolveFallbackRetryCapMs } from "../config/timeouts";
4
+ import { parseUpstreamBackoff } from "../protocol/upstream-parsing";
4
5
 
5
- const MAX_UPSTREAM_ERROR_BYTES = 500;
6
- const MAX_COOLDOWN_MS = 24 * 3600 * 1000;
7
-
8
- /** Parses Retry-After seconds or an HTTP date, bounded to one day. */
9
- export function parseRetryAfter(value: string | null | undefined): number | null {
10
- if (!value) return null;
11
- const trimmed = value.trim();
12
- const numeric = Number(trimmed);
13
- if (Number.isFinite(numeric) && numeric > 0) return Math.min(MAX_COOLDOWN_MS, numeric * 1000);
14
- const parsed = Date.parse(trimmed);
15
- if (!Number.isFinite(parsed)) return null;
16
- const delay = parsed - Date.now();
17
- return delay > 0 ? Math.min(MAX_COOLDOWN_MS, delay) : null;
18
- }
19
-
20
- /**
21
- * Parses x-ratelimit-reset as epoch seconds or a relative delay.
22
- *
23
- * Returns `null` — not `0` — when the instant has already passed. This matters
24
- * because `parseUpstreamBackoff` chains these parsers with `??`, which falls
25
- * through only on null: a `0` from an elapsed `x-ratelimit-reset` (a cached error
26
- * body, clock skew, a stale edge response) stopped the chain before the
27
- * lower-priority headers that carried the real wait. The `0` was then written to
28
- * `details.retryAfterMs` and read by `classifyAccountError` as `headerCooldown`,
29
- * which outranks the provider's own message text — so both were discarded and the
30
- * account fell back to the generic default cooldown while the provider had parked
31
- * it for the rest of the window.
32
- *
33
- * `parseRetryAfter` above already returns `null` for an elapsed date; matching it
34
- * is what removes the asymmetry.
35
- */
36
- export function parseRateLimitReset(value: string | null | undefined): number | null {
37
- if (!value) return null;
38
- const numeric = Number(value.trim());
39
- if (!Number.isFinite(numeric) || numeric <= 0) return null;
40
- const target = numeric >= 1_000_000_000 ? numeric * 1000 : Date.now() + numeric * 1000;
41
- const remaining = target - Date.now();
42
- return remaining > 0 ? Math.min(MAX_COOLDOWN_MS, remaining) : null;
43
- }
44
-
45
- /** Parses a millisecond-denominated reset header (`retry-after-ms`, `x-ratelimit-reset-ms`). */
46
- export function parseResetAfterMs(value: string | null | undefined): number | null {
47
- if (!value) return null;
48
- const numeric = Number(value.trim());
49
- if (!Number.isFinite(numeric) || numeric <= 0) return null;
50
- return Math.min(MAX_COOLDOWN_MS, Math.round(numeric));
51
- }
52
-
53
- /**
54
- * Reads upstream backoff evidence in reference priority order: millisecond
55
- * variants first (`retry-after-ms`, then `x-ratelimit-reset-ms`), then the
56
- * classic `retry-after` / `x-ratelimit-reset` pair.
57
- */
58
- export function parseUpstreamBackoff(headers: {
59
- get(name: string): string | null;
60
- }): number | null {
61
- return (
62
- parseResetAfterMs(headers.get("retry-after-ms")) ??
63
- parseRetryAfter(headers.get("retry-after")) ??
64
- parseResetAfterMs(headers.get("x-ratelimit-reset-ms")) ??
65
- parseRateLimitReset(headers.get("x-ratelimit-reset")) ??
66
- parseRateLimitReset(headers.get("x-ratelimit-reset-requests")) ??
67
- parseRateLimitReset(headers.get("retry-after"))
68
- );
69
- }
70
-
71
- /**
72
- * Extracts bounded reset durations from provider messages when headers omit
73
- * them.
74
- *
75
- * Two shapes are recognized, because providers state resets both ways:
76
- *
77
- * - **Relative** — "quota will reset in 3 hours", "try again in 30s".
78
- * - **Absolute** — "your usage will reset at 2026-09-24 02:12:51 UTC+8"
79
- * (WorkBuddy/CodeBuddy `6004`). An absolute stamp is *not* a duration, and
80
- * the relative-only pattern silently missed it, so the account fell back to
81
- * the generic 15-minute default while the provider had actually parked it
82
- * for the rest of the window — the account kept re-entering rotation and
83
- * failing every request inside the stated reset window.
84
- */
85
6
  /**
86
- * One `amount unit` pair of a duration phrase: `4h`, `13m`, `2 hours`, `30s`.
7
+ * How much of the upstream's error body is kept and surfaced.
87
8
  *
88
- * The unit ends on `(?![a-z])`, not `\b`: a compact compound like `2h30m` puts
89
- * a digit straight after `h`, which is a word character, so a `\b` would refuse
90
- * to end the pair and the whole phrase would parse as null. `m(?!s|o)` is what
91
- * stops a bare `m` from swallowing `ms` (milliseconds) or `mo` (months),
92
- * neither of which is a reset duration.
9
+ * 500 bytes cut the CodeBuddy envelope mid-`extError`: the operator saw
10
+ * "request illegal" with none of the `displayMsg` prose that names the actual
11
+ * condition — "prompt is too long: 1048788 tokens > 1048576 maximum" — so a
12
+ * context overflow was indistinguishable from a policy refusal. The envelope
13
+ * runs a few KB; 4000 keeps the whole thing while bounding a hostile body.
93
14
  */
94
- const DURATION_PAIR_SOURCE = String.raw`(\d+(?:\.\d+)?)[\s-]*(hours?|hrs?|h|minutes?|mins?|m(?!s|o)|seconds?|secs?|s)(?![a-z])`;
95
-
96
- function durationUnitMs(unit: string): number {
97
- const first = unit.toLowerCase()[0];
98
- return first === "h" ? 3600_000 : first === "m" ? 60_000 : 1_000;
99
- }
100
-
101
- /**
102
- * Sums the `amount unit` pairs at the head of a duration phrase.
103
- *
104
- * Providers state a reset as one compound duration at least as often as a
105
- * single unit: a daily-limit 429 reads "Try again in 4h 13m". A single-pair
106
- * parser read only `4h` and dropped the 13 minutes, so the stored retry
107
- * deadline was earlier than the window the provider had actually stated and the
108
- * account re-entered rotation before it. Pairs are accepted only while they
109
- * continue the phrase — separated by whitespace, a comma, or `and` — so a
110
- * number that merely follows later in the sentence cannot be absorbed.
111
- */
112
- function parseCompoundDuration(text: string): number | null {
113
- const pairs = new RegExp(DURATION_PAIR_SOURCE, "gi");
114
- let totalMs = 0;
115
- let lastEnd = 0;
116
- let matched = false;
117
- for (let pair = pairs.exec(text); pair !== null; pair = pairs.exec(text)) {
118
- const gap = text.slice(lastEnd, pair.index);
119
- const continues = matched ? /^[\s,]*(?:and\s+)?$/i.test(gap) : /^\s*$/.test(gap);
120
- if (!continues) break;
121
- const amount = Number(pair[1]);
122
- if (!Number.isFinite(amount) || amount <= 0) break;
123
- totalMs += amount * durationUnitMs(pair[2] ?? "");
124
- matched = true;
125
- lastEnd = pair.index + pair[0].length;
126
- }
127
- return matched ? Math.round(totalMs) : null;
128
- }
129
-
130
- export function parseProviderResetDuration(message: string): number | null {
131
- const trigger =
132
- /(?:quota will reset|resets?|try again|retry(?:ing)?)\s+(?:in|after|over)\s+(?:a\s+)?(?:rolling\s+)?/i.exec(
133
- message,
134
- );
135
- if (trigger) {
136
- const parsed = parseCompoundDuration(message.slice(trigger.index + trigger[0].length));
137
- if (parsed !== null) return Math.min(MAX_COOLDOWN_MS, parsed);
138
- }
139
- return parseAbsoluteResetTimestamp(message);
140
- }
141
-
142
- /**
143
- * Reads an absolute reset instant out of a provider message, e.g.
144
- * `will reset at 2026-09-24 02:12:51 UTC+8`. Returns the delay from now,
145
- * bounded to one day, or null when no future timestamp is present.
146
- *
147
- * `Date.parse` cannot read the space-separated `YYYY-MM-DD HH:MM:SS` form with
148
- * a bare `UTC±H` offset, so the parts are normalized explicitly rather than
149
- * relying on engine-specific leniency.
150
- */
151
- export function parseAbsoluteResetTimestamp(message: string): number | null {
152
- const match = /(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})\s*(?:UTC|GMT)?\s*([+-]\d{1,2})?(?::?(\d{2}))?/i.exec(
153
- message,
154
- );
155
- if (!match) return null;
156
- const [, year, month, day, hour, minute, second, offsetHours, offsetMinutes] = match;
157
- if (!year || !month || !day || !hour || !minute || !second) return null;
158
- // A bare `UTC+8` means a whole-hour offset; `+08:30`-style halves are rare
159
- // but cheap to honor. No offset at all is read as UTC (providers that omit
160
- // it always label the stamp "UTC" in practice).
161
- const sign = offsetHours?.startsWith("-") ? -1 : 1;
162
- const offsetTotalMinutes =
163
- offsetHours === undefined
164
- ? 0
165
- : sign * (Math.abs(Number(offsetHours)) * 60 + Number(offsetMinutes ?? 0));
166
- const utcMs = Date.UTC(
167
- Number(year),
168
- Number(month) - 1,
169
- Number(day),
170
- Number(hour),
171
- Number(minute),
172
- Number(second),
173
- );
174
- if (!Number.isFinite(utcMs)) return null;
175
- const targetMs = utcMs - offsetTotalMinutes * 60_000;
176
- const delay = targetMs - Date.now();
177
- // A past stamp is not a usable reset (clock skew, stale cached error); fall
178
- // through to the caller's default instead of returning a negative delay.
179
- if (!Number.isFinite(delay) || delay <= 0) return null;
180
- return Math.min(MAX_COOLDOWN_MS, Math.round(delay));
181
- }
15
+ const MAX_UPSTREAM_ERROR_BYTES = 4000;
16
+ export {
17
+ parseRetryAfter,
18
+ parseRateLimitReset,
19
+ parseResetAfterMs,
20
+ parseUpstreamBackoff,
21
+ parseProviderResetDuration,
22
+ parseAbsoluteResetTimestamp,
23
+ } from "../protocol/upstream-parsing";
182
24
  /** Extracts a bounded, newline-free provider message from any error envelope. */
183
25
  export function extractUpstreamMessage(body: unknown): string {
184
26
  let raw = "";
@@ -544,7 +386,27 @@ export interface TerminalOutcome {
544
386
  readonly status: "cancelled" | "failed";
545
387
  readonly errorCategory: GatewayErrorCode | "unknown_error";
546
388
  readonly errorOrigin: GatewayErrorOrigin;
547
- }
389
+ /**
390
+ * The HTTP status the row should carry. `undefined` means the caller has a
391
+ * better one (the wire status of an actual response) and should keep it;
392
+ * a raw error has none, and the caller's own fallback was 500 — which
393
+ * read as a gateway fault for what was an upstream transport drop.
394
+ */
395
+ readonly httpStatus?: number;
396
+ }
397
+
398
+ /** The HTTP status each classifier-derived category carries when no wire response named one. */
399
+ const CATEGORY_HTTP_STATUS: Partial<Record<GatewayErrorCode | "unknown_error", number>> = {
400
+ // The upstream's transport died mid-response: a bad gateway, not a fault
401
+ // in this process.
402
+ transport_unavailable: 502,
403
+ platform_unavailable: 502,
404
+ deadline_exceeded: 504,
405
+ transport_closed: 499,
406
+ shutting_down: 503,
407
+ overloaded: 529,
408
+ unknown_error: 500,
409
+ };
548
410
 
549
411
  /**
550
412
  * Derives a failed attempt's status, error category, and error origin **from
@@ -559,11 +421,24 @@ export interface TerminalOutcome {
559
421
  * outcome carries the real code and origin.
560
422
  */
561
423
  export function classifyTerminalOutcome(error: unknown, signal: AbortSignal): TerminalOutcome {
424
+ const category = classifyTerminalCategory(error, signal);
425
+ const gateway = error instanceof GatewayError ? error : signal.reason instanceof GatewayError ? signal.reason : undefined;
562
426
  return {
563
427
  status: isClientCancellation(error, signal) ? "cancelled" : "failed",
564
- errorCategory: classifyTerminalCategory(error, signal),
565
- // A non-GatewayError outcome has no upstream origin to claim: it is ours.
566
- errorOrigin: error instanceof GatewayError ? error.origin : "cartethyia",
428
+ errorCategory: category,
429
+ // The classifier owns the blame. An upstream socket drop arrives as a
430
+ // raw runtime error — no `GatewayError` wraps it — and calling that ours
431
+ // made the row accuse the gateway for an upstream failure.
432
+ errorOrigin: gateway
433
+ ? gateway.origin
434
+ : category === "transport_unavailable"
435
+ ? "upstream"
436
+ : "cartethyia",
437
+ // A `GatewayError` carries its own status; a raw error takes the
438
+ // category's, so an upstream drop is 502 rather than the 500 fallback.
439
+ ...(gateway || CATEGORY_HTTP_STATUS[category] !== undefined
440
+ ? { httpStatus: gateway ? gateway.status : CATEGORY_HTTP_STATUS[category] }
441
+ : {}),
567
442
  };
568
443
  }
569
444
 
@@ -735,6 +610,12 @@ const QUOTA_ERROR_IDENTIFIERS: ReadonlySet<string> = new Set([
735
610
  "usage_not_included",
736
611
  "subscription:free-usage-exhausted",
737
612
  "resource_exhausted",
613
+ // Qoder answers an exhausted daily billing allowance with a bare numeric
614
+ // code: `{"code":"110","message":"Billing daily count exceeded"}`. The 403
615
+ // wrapper made the old status-only fallback read it as a credential fault;
616
+ // it is a spend/limit verdict, never an auth one, so it must classify as
617
+ // quota (a cooldown) instead of `auth_invalidated`.
618
+ "110",
738
619
  // InferHub publishes its own vocabulary in `error.code`. The two 403s below
739
620
  // are deliberate absences: `account_blocked` means the account was blocked
740
621
  // (an auth/entitlement verdict, not a spend limit) and
@@ -69,6 +69,16 @@ export function createIpAbuseProtectionMiddleware(deps: {
69
69
  throw peerAddressUnavailableError();
70
70
  const address = resolveClientIdentity(request, deps.trustedProxyBoundary, peer);
71
71
  const state = deps.stateStore.get(request);
72
+ // The identity middleware runs later in the plugin chain and resolves the
73
+ // same value from the same boundary; stashing it here means the request
74
+ // pays for one resolveClientIdentity instead of two, and the middleware can
75
+ // skip its own when it finds the stash.
76
+ if (state !== undefined) {
77
+ state.clientIdentity ??= {
78
+ address,
79
+ source: address === peer ? "tcp-peer" : "trusted-forwarded-header",
80
+ };
81
+ }
72
82
  try {
73
83
  await deps.ipAbuseProtection.checkBeforeAccess({
74
84
  identity: {
@@ -12,7 +12,6 @@ import {
12
12
  detectClientRouter,
13
13
  } from "../../security/client-router-fingerprint";
14
14
  import { createAccessDecision } from "../../security/access-control";
15
- import { modelAbuseBannedError, type ModelStrikeService } from "../../security/model-abuse";
16
15
 
17
16
  /**
18
17
  * Stateless Elysia `beforeHandle` gateway check: API-key authentication,
@@ -27,7 +26,6 @@ export function createApiKeyAuthenticationMiddleware(deps: {
27
26
  * assigned — so a banned caller's attempts produce no telemetry row and no
28
27
  * console error, which is exactly the noise this layer exists to stop.
29
28
  */
30
- readonly modelStrikes?: Pick<ModelStrikeService, "check">;
31
29
  /**
32
30
  * Tenant invoke kill switch. Checked after the key resolves, because the
33
31
  * tenant is a property of the key: the same gateway serves several tenants
@@ -105,22 +103,6 @@ export function createApiKeyAuthenticationMiddleware(deps: {
105
103
  "No API invocation access for this client.",
106
104
  { reason: "client_router_denied", clientRouter: denied },
107
105
  );
108
- // Abuse ban: refuse before parse/prepare and before authorization is
109
- // recorded, so a banned caller cannot keep producing failed rows. The ban
110
- // is keyed on the client address *and* the API key: one key can be shared
111
- // by every recipient of a share link, so refusing the address alone would
112
- // punish callers that did nothing, while refusing the key alone would let
113
- // the caller that probed mint a fresh one.
114
- if (deps.modelStrikes) {
115
- const state = deps.stateStore.get(request);
116
- const ip = state?.clientIdentity?.address;
117
- if (ip !== undefined) {
118
- const banned = await deps.modelStrikes
119
- .check({ ip, apiKeyId: authorization.id })
120
- .catch(() => false);
121
- if (banned) throw modelAbuseBannedError();
122
- }
123
- }
124
106
  deps.stateStore.require(request).authorization = authorization;
125
107
  })
126
108
  .as("plugin");
@@ -7,7 +7,7 @@ import type { TelemetryBatchBuffer, TelemetryEventInput } from "../../observabil
7
7
  import type { TelemetryPayloadCapture } from "../../observability/payload-capture";
8
8
  import { captureEarlyRejectionPayload } from "./early-rejection-capture";
9
9
  import type { CartethyiaDatabase } from "../../persistence/db-handle";
10
- import { reasoningEffortFromIntent } from "../../providers/provider-reasoning";
10
+ import { reasoningEffortFromIntent } from "../../protocol/reasoning";
11
11
  import { metrics } from "../../observability/metrics";
12
12
  import { computeTokensPerSec } from "../../observability/token-speed";
13
13
  import { pushStructuredConsoleLog } from "../../observability/log-ring";
@@ -10,7 +10,6 @@ import type { CanonicalAdapter, ShutdownDrainSource } from "../contracts";
10
10
  import { createApiKeyAuthenticationMiddleware } from "./auth-guard";
11
11
  import { createDependencyReadinessMiddleware } from "./readiness-guard";
12
12
  import { createIpAbuseProtectionMiddleware } from "./abuse-guard";
13
- import { createProcessConcurrencyMiddleware } from "./concurrency-guard";
14
13
  import { createHeadroomGuard } from "./headroom-guard";
15
14
  import {
16
15
  createErrorNormalizationMiddleware,
@@ -27,7 +26,6 @@ import type { TrustedProxyBoundary } from "../../config/network";
27
26
  import type { CartethyiaDatabase } from "../../persistence/db-handle";
28
27
  import type { RedisClient } from "../../persistence/redis";
29
28
  import type { TelemetryBatchBuffer } from "../../observability/telemetry-buffer";
30
- import type { ModelStrikeService } from "../../security/model-abuse";
31
29
  import type { RuntimeMemorySnapshot } from "../../observability/runtime-metrics";
32
30
  import {
33
31
  TelemetryPayloadCapture,
@@ -51,7 +49,6 @@ export interface TransportPipelineContext {
51
49
  readonly requestDeadlineMs?: number;
52
50
  readonly verifiedHttps?: boolean;
53
51
  /** Graduated strikes for repeated invalid-model requests. */
54
- readonly modelStrikes?: ModelStrikeService;
55
52
  readonly shutdownCoordinator?: ShutdownDrainSource;
56
53
  readonly telemetry?: TelemetryBatchBuffer;
57
54
  /**
@@ -96,7 +93,6 @@ export interface TransportPipeline {
96
93
  */
97
94
  export function createTransportPipeline(context: TransportPipelineContext): TransportPipeline {
98
95
  const stages: Elysia[] = [
99
- createProcessConcurrencyMiddleware(),
100
96
  createHeadroomGuard({
101
97
  store: context.stateStore,
102
98
  ...(context.memorySnapshot === undefined ? {} : { memorySnapshot: context.memorySnapshot }),
@@ -118,7 +114,6 @@ export function createTransportPipeline(context: TransportPipelineContext): Tran
118
114
  createApiKeyAuthenticationMiddleware({
119
115
  db: context.db,
120
116
  stateStore: context.stateStore,
121
- ...(context.modelStrikes ? { modelStrikes: context.modelStrikes } : {}),
122
117
  ...(context.isInvokeDisabled
123
118
  ? { isInvokeDisabled: context.isInvokeDisabled }
124
119
  : {}),
@@ -132,7 +127,6 @@ export function createTransportPipeline(context: TransportPipelineContext): Tran
132
127
  createProxyRoutePreparationMiddleware({
133
128
  stateStore: context.stateStore,
134
129
  preparer: context.preparer,
135
- ...(context.modelStrikes ? { modelStrikes: context.modelStrikes } : {}),
136
130
  }),
137
131
  ];
138
132
  const mountRoot = (app: Elysia<any, any, any, any, any, any, any, any>): void => {
@@ -14,51 +14,9 @@ import { pushStructuredConsoleLog } from "../../observability/log-ring";
14
14
  import { log } from "../../observability/logger";
15
15
  import { isJsonProxyRoutePath, isProxyDispatchRoute } from "./body-policy";
16
16
  import { isOpaqueBodyRoute } from "../contracts";
17
- import {
18
- modelAbuseBannedError,
19
- modelWarningMessage,
20
- type ModelAbuseOutcome,
21
- type ModelStrikeService,
22
- } from "../../security/model-abuse";
23
17
  import { peerAddressUnavailableError } from "./guard-errors";
24
18
 
25
- /**
26
- * Whether a thrown error is a *model* rejection the strike layer counts: the
27
- * key may not use the named model (`isModelAllowed` → 404), or the name
28
- * resolves to nothing (`modelNotFoundError` → 404). Both surface as
29
- * `model_not_found`; every other failure — an aborted request, a store outage,
30
- * a capability mismatch — is not the caller probing for models and is not
31
- * counted.
32
- */
33
- function isModelRejection(error: unknown): boolean {
34
- return error instanceof GatewayError && error.code === "model_not_found";
35
- }
36
-
37
- /**
38
- * Re-throws a model rejection carrying the escalating strike warning, so an
39
- * honest client that mistyped sees "Warning 1 of 3" instead of the same generic
40
- * 404 three times and then a ban. The code/status are unchanged — only the
41
- * message gains the warning, and the count rides in `details` for the console.
42
- */
43
- function withModelWarning(
44
- error: unknown,
45
- outcome: ModelAbuseOutcome,
46
- limit: number,
47
- opts?: { allowedModels?: readonly string[] | undefined },
48
- ): GatewayError {
49
- if (!(error instanceof GatewayError)) return error as GatewayError;
50
- const model = String(error.details.model ?? "");
51
- const message = modelWarningMessage(model, outcome, limit);
52
- const hint = opts?.allowedModels?.slice(0, 8);
53
- return new GatewayError(error.code, error.status, message, {
54
- ...error.details,
55
- strikes: outcome.strikes,
56
- strike_limit: limit,
57
19
 
58
- remaining_before_ban: Math.max(0, limit - outcome.strikes),
59
- ...(hint && hint.length > 0 ? { allowed_models_hint: hint } : {}),
60
- }, error.origin);
61
- }
62
20
 
63
21
  interface RequestApp {
64
22
  request(
@@ -71,7 +29,6 @@ export interface RequestContextDeps {
71
29
  readonly clock?: () => number;
72
30
  /** Explicit deadline override; defaults to `CARTETHYIA_UPSTREAM_TIMEOUT_MS` (120s). */
73
31
  readonly requestDeadlineMs?: number;
74
- readonly maxBodyBytes?: number;
75
32
  }
76
33
 
77
34
  /**
@@ -120,17 +77,25 @@ export function createClientIdentityMiddleware(deps: {
120
77
  // pipeline position still guarantees this executes before IP-abuse checks.
121
78
  return new Elysia()
122
79
  .beforeHandle(({ request, server }) => {
123
- const peer = deps.resolvePeerAddress
124
- ? deps.resolvePeerAddress(request)
125
- : (server?.requestIP(request)?.address ?? null);
126
- if (!peer)
127
- throw peerAddressUnavailableError();
128
- const address = resolveClientIdentity(request, deps.trustedProxyBoundary, peer);
129
80
  const state = deps.stateStore.require(request);
130
- state.clientIdentity = {
131
- address,
132
- source: address === peer ? "tcp-peer" : "trusted-forwarded-header",
133
- };
81
+ // The IP-abuse root hook already resolved the same identity from the
82
+ // same boundary (and only for /v1/*, exactly these routes); reuse it so
83
+ // a request pays one resolveClientIdentity instead of two. Fall back to
84
+ // resolving here when running in a reduced composition without the
85
+ // abuse guard.
86
+ if (!state.clientIdentity) {
87
+ const peer = deps.resolvePeerAddress
88
+ ? deps.resolvePeerAddress(request)
89
+ : (server?.requestIP(request)?.address ?? null);
90
+ if (!peer)
91
+ throw peerAddressUnavailableError();
92
+ const address = resolveClientIdentity(request, deps.trustedProxyBoundary, peer);
93
+ state.clientIdentity = {
94
+ address,
95
+ source: address === peer ? "tcp-peer" : "trusted-forwarded-header",
96
+ };
97
+ }
98
+ const address = state.clientIdentity.address;
134
99
  const path = state.ingressPath;
135
100
  if (!isProxyDispatchRoute(path)) return;
136
101
  // The ingress policy stage already decoded the body, so the client's
@@ -249,14 +214,6 @@ export function createCanonicalRequestMiddleware(deps: {
249
214
  export function createProxyRoutePreparationMiddleware(deps: {
250
215
  readonly stateStore: ProxyRequestStateStore;
251
216
  readonly preparer: ProxyRequestPreparer;
252
- /**
253
- * Graduated strikes for repeated invalid-model requests. The preparation step
254
- * is the single choke point where a request is rejected for naming a model the
255
- * key may not use (`isModelAllowed`) or a model that resolves to nothing
256
- * (`modelNotFoundError`), so it is where a strike is recorded and where a
257
- * valid model clears one.
258
- */
259
- readonly modelStrikes?: ModelStrikeService;
260
217
  }): Elysia {
261
218
  return new Elysia()
262
219
  .beforeHandle(async ({ request }) => {
@@ -268,15 +225,8 @@ export function createProxyRoutePreparationMiddleware(deps: {
268
225
  const state = deps.stateStore.require(request);
269
226
  if (!state.authorization || !state.canonicalRequest)
270
227
  throw new GatewayError("admission_unavailable", 503, "The request was not authenticated and parsed, so this route cannot be served. Resend the request.");
271
- const strikes = deps.modelStrikes;
272
- const ip = state.clientIdentity?.address;
273
228
  // The key rides along so a strike is charged to the caller that made it.
274
229
  // Without it, one recipient of a shared link could escalate a ban onto
275
- // every other caller behind the same address.
276
- const strikeIdentity =
277
- strikes && ip !== undefined
278
- ? { strikes, ip, apiKeyId: state.authorization?.id ?? null }
279
- : undefined;
280
230
  try {
281
231
  state.preparedRequest = await deps.preparer.prepare({
282
232
  canonicalRequest: state.canonicalRequest,
@@ -288,31 +238,8 @@ export function createProxyRoutePreparationMiddleware(deps: {
288
238
  : { clientUserAgent: state.clientUserAgent }),
289
239
  });
290
240
  } catch (error) {
291
- // Only a *model* rejection is a strike: an invalid model the key may not
292
- // use, or a name that resolves to nothing. A store outage, an aborted
293
- // request, or any other failure is not the caller probing for models.
294
- if (strikeIdentity && isModelRejection(error)) {
295
- const outcome = await strikeIdentity.strikes
296
- .noteInvalid({ ip: strikeIdentity.ip, apiKeyId: strikeIdentity.apiKeyId })
297
- .catch(() => null);
298
- if (outcome?.banned === true) throw modelAbuseBannedError();
299
- if (outcome) {
300
- const snap = state.authorization?.snapshot;
301
- const raw = snap?.model_list;
302
- const hint = Array.isArray(raw) ? (raw as readonly string[]) : raw instanceof Set ? [...raw] : undefined;
303
- throw withModelWarning(error, outcome, strikeIdentity.strikes.limit, {
304
- allowedModels: hint,
305
- });
306
- }
307
- }
308
241
  throw error;
309
242
  }
310
- // A valid model does not reset the invalid-attempt window. Old attempts
311
- // leave only when they age out of that window.
312
- if (strikeIdentity)
313
- void strikeIdentity.strikes
314
- .noteValid({ ip: strikeIdentity.ip })
315
- .catch(() => undefined);
316
243
  })
317
244
  .as("plugin");
318
245
  }
@@ -7,7 +7,7 @@
7
7
  * the planner warns (throttled here) when a request was dispatched degraded.
8
8
  */
9
9
  import type { CanonicalRequest, ContentPart } from "../../protocol/canonical-model";
10
- import { deriveRequiredCapabilities, type RequiredCapability } from "../translation/capabilities";
10
+ import { deriveRequiredCapabilities, type RequiredCapability } from "../../protocol/translation/capabilities";
11
11
 
12
12
  // Degradation is often sustained — a client that always asks for tools against
13
13
  // a tools-less route degrades every single request. An unthrottled warn would
@@ -261,6 +261,25 @@ export function* degradedRequestVariants(
261
261
  "audio",
262
262
  ] as RequiredCapability[];
263
263
  const ordered = priority.filter((c) => required.includes(c));
264
+ // Pre-pass: drop exactly one capability from the pristine request. The
265
+ // cumulative chain below can only ADD losses, so when the plan's real blocker
266
+ // is a late capability in this order (an image on a text-only model), every
267
+ // surviving chain variant has already lost `tools` as collateral — a
268
+ // tool-capable model then served a transcript of `[tool:<name>]` placeholders
269
+ // with no tool catalog at all. Trying each single loss first finds the
270
+ // smallest degradation that satisfies routing: `degraded: [cap]` alone is
271
+ // always lexicographically smaller than the chain's `[...earlier, cap]`, so
272
+ // a single-loss win must be preferred, never the reverse.
273
+ for (const cap of ordered) {
274
+ const singular = degradeRequestForCapability(original, cap);
275
+ if (singular) {
276
+ yield {
277
+ request: singular,
278
+ degraded: [cap],
279
+ required: required.filter((c) => c !== cap),
280
+ };
281
+ }
282
+ }
264
283
  let current: CanonicalRequest = original;
265
284
  const degraded: RequiredCapability[] = [];
266
285
  for (const cap of ordered) {