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
@@ -267,6 +267,16 @@ export class PrometheusRegistry {
267
267
  readonly cartethyia_redis_up: GaugeMetric;
268
268
  readonly proxy_requests_total: CounterMetric;
269
269
  readonly proxy_admission_total: CounterMetric;
270
+ /**
271
+ * Refusals from the per-IP abuse layer, by reason.
272
+ *
273
+ * These are the only 429s this process produces before dispatch, and they
274
+ * were invisible: a client-facing 429 could come from here, from admission,
275
+ * or from the upstream, and nothing recorded which. `ip-rate-limited` is the
276
+ * sliding window and `ip-banned` the escalation — different remedies, so
277
+ * they must not share a number.
278
+ */
279
+ readonly cartethyia_ip_abuse_refusals_total: CounterMetric;
270
280
  readonly proxy_in_flight: GaugeMetric;
271
281
  readonly proxy_request_latency_ms: HistogramMetric;
272
282
  readonly cartethyia_telemetry_buffered: GaugeMetric;
@@ -280,7 +290,6 @@ export class PrometheusRegistry {
280
290
  readonly cartethyia_quota_cache_entries: GaugeMetric;
281
291
  readonly cartethyia_model_catalog_entries: GaugeMetric;
282
292
  readonly cartethyia_process_in_flight: GaugeMetric;
283
- readonly cartethyia_process_concurrency_limit: GaugeMetric;
284
293
  readonly cartethyia_telemetry_backlog_items: GaugeMetric;
285
294
  readonly cartethyia_telemetry_backlog_bytes: GaugeMetric;
286
295
  readonly cartethyia_proxy_dial_proxy_resolved_total: CounterMetric;
@@ -321,6 +330,11 @@ export class PrometheusRegistry {
321
330
  "Admission decisions by reason",
322
331
  ["reason"],
323
332
  );
333
+ this.cartethyia_ip_abuse_refusals_total = this.counter(
334
+ "cartethyia_ip_abuse_refusals_total",
335
+ "Requests refused by the per-IP abuse limit, by reason",
336
+ ["reason"],
337
+ );
324
338
  this.proxy_in_flight = this.gauge(
325
339
  "proxy_in_flight",
326
340
  "Provider dispatch requests currently in flight on this process",
@@ -358,10 +372,6 @@ export class PrometheusRegistry {
358
372
  "cartethyia_process_in_flight",
359
373
  "Gateway requests currently admitted into this process",
360
374
  );
361
- this.cartethyia_process_concurrency_limit = this.gauge(
362
- "cartethyia_process_concurrency_limit",
363
- "Effective in-flight ceiling for this process, derived from the memory limit",
364
- );
365
375
  this.cartethyia_telemetry_backlog_items = this.gauge(
366
376
  "cartethyia_telemetry_backlog_items",
367
377
  "Telemetry events queued for the next drain",
@@ -8,6 +8,7 @@
8
8
  * layers, keeping the observability boundary one-directional.
9
9
  */
10
10
  import { readFileSync } from "node:fs";
11
+ import os from "node:os";
11
12
  import { metrics } from "./metrics";
12
13
  import { trackMemoryUsage } from "./performance-metrics";
13
14
  import { log } from "./logger";
@@ -66,12 +67,34 @@ const PRESSURE_GC_WINDOW_MS = 60_000;
66
67
  /** Fraction of the resolved limit at which memory pressure is reported. */
67
68
  const PRESSURE_THRESHOLD = 0.8;
68
69
  /**
69
- * Fallback limit used when neither an override nor a cgroup limit is
70
- * discoverable (native development, macOS, Windows). Without it the pressure
71
- * check bails and `Bun.gc` never runs, so an idle process holds its startup
72
- * peak forever. 512 MiB keeps reclaim active without assuming a container.
70
+ * Fallback limit when neither an override nor a cgroup limit is discoverable
71
+ * (native development, macOS, Windows).
72
+ *
73
+ * A fixed 512 MiB made every machine without a cgroup look like a 512 MiB
74
+ * container. On a desktop running the gateway natively that is far below what
75
+ * the machine has, so the pressure check fired against a number that described
76
+ * nothing — "RSS 430 MiB exceeds 80% of the 512 MiB limit" on a machine with
77
+ * gigabytes free — and the concurrency cap derived from it admitted a handful
78
+ * of requests while every tenant-level setting sat unreachable above it.
79
+ *
80
+ * The host's real memory is the honest stand-in. It is what the process can
81
+ * actually grow into here, and it is what "no container limit" means.
73
82
  */
74
- const DEFAULT_MEMORY_LIMIT_BYTES = 512 * 1024 * 1024;
83
+ function defaultMemoryLimitBytes(): number {
84
+ try {
85
+ const total = os.totalmem();
86
+ if (Number.isFinite(total) && total > 0) return total;
87
+ } catch {
88
+ // Platform without totalmem; fall through to the last-resort constant.
89
+ }
90
+ return LAST_RESORT_MEMORY_LIMIT_BYTES;
91
+ }
92
+
93
+ /**
94
+ * Used only when the host's memory cannot be read at all. Deliberately large
95
+ * so an unmeasurable host is not treated as a small container.
96
+ */
97
+ const LAST_RESORT_MEMORY_LIMIT_BYTES = 4 * 1024 * 1024 * 1024;
75
98
  /** Cooldown between idle full collections. */
76
99
  const IDLE_GC_WINDOW_MS = 5 * 60_000;
77
100
  /** RSS retained above the post-collection baseline before another idle collection runs. */
@@ -88,7 +111,7 @@ const IDLE_GC_MIN_GROWTH_BYTES = 32 * 1024 * 1024;
88
111
  export function resolveMemoryLimitBytes(): number | undefined {
89
112
  const override = resolveMemoryLimitOverrideBytes();
90
113
  if (override !== undefined) return override;
91
- if (process.platform !== "linux") return DEFAULT_MEMORY_LIMIT_BYTES;
114
+ if (process.platform !== "linux") return defaultMemoryLimitBytes();
92
115
  for (const path of [
93
116
  "/sys/fs/cgroup/memory.max",
94
117
  "/sys/fs/cgroup/memory/memory.limit_in_bytes",
@@ -103,7 +126,7 @@ export function resolveMemoryLimitBytes(): number | undefined {
103
126
  // File absent (cgroup v1 vs v2) or unreadable — try the next source.
104
127
  }
105
128
  }
106
- return DEFAULT_MEMORY_LIMIT_BYTES;
129
+ return defaultMemoryLimitBytes();
107
130
  }
108
131
 
109
132
  /** Samples process memory and bounded-collection sizes into the metric registry. */
@@ -120,8 +143,6 @@ export class RuntimeMetricsSampler {
120
143
  * first sample runs; consumers must treat that as "unknown", not as zero.
121
144
  */
122
145
  private snapshot: RuntimeMemorySnapshot | undefined;
123
- /** The effective concurrency ceiling, published alongside the gauges. */
124
- private concurrencyLimit: number | undefined;
125
146
 
126
147
  /**
127
148
  * Binds the live in-flight count after the request state store exists. The
@@ -149,9 +170,6 @@ export class RuntimeMetricsSampler {
149
170
  * limit, which is known here, but the guard that owns it is constructed in
150
171
  * the transport layer.
151
172
  */
152
- bindConcurrencyLimit(limit: number): void {
153
- this.concurrencyLimit = limit;
154
- }
155
173
 
156
174
  constructor(
157
175
  private readonly sources: RuntimeMetricSources,
@@ -198,9 +216,6 @@ export class RuntimeMetricsSampler {
198
216
  metrics.cartethyia_telemetry_backlog_items.set(backlog.items);
199
217
  metrics.cartethyia_telemetry_backlog_bytes.set(backlog.bytes);
200
218
  }
201
- if (this.concurrencyLimit !== undefined) {
202
- metrics.cartethyia_process_concurrency_limit.set(this.concurrencyLimit);
203
- }
204
219
 
205
220
  this.checkPressure(memory.rss);
206
221
  this.checkIdleReclaim(memory.rss);
@@ -15,6 +15,7 @@
15
15
  // Same shape as `persistence/usage-flush.ts`: enqueue and return, a timer
16
16
  // flushes, shutdown flushes what remains. A crash loses the unflushed rows and
17
17
  // nothing else — the durable state routing depends on was never in here.
18
+ import { randomUUID } from "node:crypto";
18
19
  import type { CartethyiaDatabase } from "./db-handle";
19
20
  import { healthEvents } from "./schema";
20
21
 
@@ -141,6 +142,7 @@ export class HealthEventFlushBuffer {
141
142
  try {
142
143
  await this.db.insert(healthEvents).values(
143
144
  batch.map((row) => ({
145
+ id: randomUUID(),
144
146
  entityKind: row.entityKind,
145
147
  accountId: row.accountId ?? null,
146
148
  networkPoolId: row.networkPoolId ?? null,
@@ -490,6 +490,24 @@ export const networkPools = pgTable("network_pools", {
490
490
  /** The endpoint the verdict was measured against. Compared on read, so an
491
491
  * edited endpoint reports "never probed" instead of a stale verdict. */
492
492
  providerProbeEndpoint: text("provider_probe_endpoint"),
493
+ /**
494
+ * The database mirror of the pool selector's provider cooldown state, one
495
+ * entry per cooling provider keyed by its lowercased id:
496
+ * `{ "opencode": { "until": <epoch ms>, "reason": "...", "blocked": false } }`.
497
+ *
498
+ * The selector's own map is process-local (mirrored to Redis when present),
499
+ * so a restart dropped every flag while the upstream kept refusing the
500
+ * egress — dispatch re-picked a spent pool and the console showed nothing.
501
+ * The dispatch-side flaggers write each marker here, and boot reads this
502
+ * column back into the selector, so the state survives a restart the way the
503
+ * upstream's memory of the address does.
504
+ *
505
+ * `null` means no provider was ever flagged. Expired entries are left inert
506
+ * until the next write for the pool rather than swept eagerly: a marker with
507
+ * `until` in the past is skipped by every reader.
508
+ */
509
+ providerCooldowns: jsonb("provider_cooldowns")
510
+ .$type<Record<string, { until: number; reason: string; blocked?: boolean }>>(),
493
511
  /** Metered egress totals, flushed from the in-memory socket counters on
494
512
  * every pool-touching write so the quota bar survives a restart. */
495
513
  bytesSentTotal: bigint("bytes_sent_total", { mode: "number" }).notNull().default(0),
@@ -676,6 +694,7 @@ export const providerRoutingSettings = pgTable(
676
694
  export const poolRoutingStrategy = pgEnum("pool_routing_strategy", [
677
695
  "least_loaded",
678
696
  "round_robin",
697
+ "random",
679
698
  ]);
680
699
  export type PoolRoutingStrategy = (typeof poolRoutingStrategy.enumValues)[number];
681
700
 
@@ -145,13 +145,18 @@ export class UsageFlushBuffer {
145
145
  * cost the others their delta. A key whose write fails goes back into the
146
146
  * pending map, added to anything enqueued while the flush ran, so a
147
147
  * transient failure retries instead of dropping.
148
+ *
149
+ * `force` bypasses the outage backoff. The scheduled timer never passes it —
150
+ * a failing database should not be probed on every tick — but `stop({flush:
151
+ * true})` must, or a shutdown during a backoff window would return without
152
+ * even attempting to persist the pending deltas.
148
153
  */
149
- async flush(): Promise<void> {
154
+ async flush(options?: { force?: boolean }): Promise<void> {
150
155
  if (this.flushing || this.pending.size === 0) return;
151
156
  // Backing off during a sustained outage: without this the writer probed a
152
157
  // failing database on every tick, adding write load to the very overload
153
158
  // that caused the failure.
154
- if (Date.now() < this.backoffUntil) return;
159
+ if (Date.now() < this.backoffUntil && options?.force !== true) return;
155
160
  this.flushing = true;
156
161
  const batch = new Map(this.pending);
157
162
  this.pending.clear();
@@ -210,7 +215,7 @@ export class UsageFlushBuffer {
210
215
  clearTimeout(this.timer);
211
216
  this.timer = undefined;
212
217
  }
213
- if (options?.flush) await this.flush();
218
+ if (options?.flush) await this.flush({ force: true });
214
219
  }
215
220
 
216
221
  private arm(): void {
@@ -52,7 +52,7 @@ export type ServiceKind = (typeof SERVICE_KINDS)[number];
52
52
  * their membership set from it, and the probe route's accepted subset is
53
53
  * declared separately because it is deliberately narrower.
54
54
  */
55
- export const REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
55
+ export const REASONING_EFFORTS = ["none", "instant", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
56
56
 
57
57
  /** One canonical reasoning effort. */
58
58
  export type ReasoningEffort = (typeof REASONING_EFFORTS)[number];
@@ -0,0 +1,377 @@
1
+ /**
2
+ * Canonical DSML (DeepSeek Markup Language) tool-call recovery.
3
+ *
4
+ * DeepSeek-family models occasionally write their tool calls as XML-like DSML
5
+ * markup inside the plain-text content stream instead of the structured
6
+ * `tool_calls` field. Two upstream families in this gateway see it:
7
+ *
8
+ * - CodeBuddy/WorkBuddy emit strict blocks (`<|DSML| calls>…</|DSML| calls>`)
9
+ * that must be parsed exactly, with declared-tool allowlisting.
10
+ * - Qoder emits looser blocks (`<[^>]*DSML[^>]*calls>…`) that tolerate any
11
+ * intermediate characters in the tag, and its parameter values are plain
12
+ * strings.
13
+ *
14
+ * The two share the same recovery idea, so the mechanics live here once and
15
+ * each caller picks its dialect through options. The defaults reproduce the
16
+ * CodeBuddy dialect exactly (`app/dsml.py` in codebuddy2api); Qoder passes
17
+ * `fuzzy: true` and its id factory, mirroring `qoder_proxy.py`.
18
+ */
19
+ import { randomUUID } from "node:crypto";
20
+
21
+ export interface DsmlToolCall {
22
+ readonly id: string;
23
+ readonly name: string;
24
+ readonly arguments: string;
25
+ }
26
+
27
+ export interface DsmlRecovery {
28
+ readonly calls: readonly DsmlToolCall[];
29
+ readonly clean: string;
30
+ }
31
+
32
+ export interface DsmlParseOptions {
33
+ /** Separator spellings a strict block may use (CodeBuddy dialect). */
34
+ readonly separators?: readonly string[];
35
+ /** Block names a strict opener may name. */
36
+ readonly blockNames?: readonly string[];
37
+ /** When true, match the Qoder fuzzy form (`<[^>]*DSML[^>]*…`). */
38
+ readonly fuzzy?: boolean;
39
+ /** How parameter values decode: `json` (CodeBuddy) or `string` (Qoder). */
40
+ readonly params?: "json" | "string";
41
+ /** Call id factory. Defaults to sequential for strict, uuid for fuzzy. */
42
+ readonly id?: (call: { readonly name: string }, index: number, seq: number) => string;
43
+ /** Non-empty allowlist makes the strict dialect reject undeclared names. */
44
+ readonly allowedNames?: ReadonlySet<string>;
45
+ /**
46
+ * When true, JSON stringifies argument objects with recursively sorted keys,
47
+ * matching the reference bridge (`json.dumps(..., sort_keys=True)`). The
48
+ * strict dialect defaults to true; the fuzzy dialect never sorts, matching
49
+ * its reference's `json.dumps(params)`.
50
+ */
51
+ readonly sortKeys?: boolean;
52
+ /**
53
+ * Character cap for an unclosed strict block. Past it, the block is treated
54
+ * as plain text and passed through instead of buffered indefinitely — same
55
+ * fail-open as `MAX_BLOCK_BYTES` in the reference bridge (256 KB).
56
+ */
57
+ readonly maxBlockBytes?: number;
58
+ }
59
+
60
+ const DEFAULT_SEPARATORS = ["||", "|", "|"] as const;
61
+ const DEFAULT_BLOCK_NAMES = ["calls", "function_calls"] as const;
62
+ const DEFAULT_MAX_BLOCK_BYTES = 256 * 1024;
63
+ const TOOL_NAME = /^[A-Za-z0-9_][A-Za-z0-9_.:-]{0,63}$/;
64
+
65
+ /** Escapes regex metacharacters so a `|` separator is literal, not alternation. */
66
+ export function escapeRegExp(value: string): string {
67
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
68
+ }
69
+
70
+ function openers(separators: readonly string[], blockNames: readonly string[]): readonly string[] {
71
+ return separators.flatMap((separator) => blockNames.map((name) => `<${separator}DSML${separator} ${name}>`));
72
+ }
73
+
74
+ function sequentialId(_call: { readonly name: string }, _index: number, seq: number): string {
75
+ return `call_dsml_${seq}_${_call.name}`;
76
+ }
77
+
78
+ function uuidId(_call: { readonly name: string }, _index: number, _seq: number): string {
79
+ return `call_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
80
+ }
81
+
82
+ export interface DsmlParseResult {
83
+ readonly text: string;
84
+ readonly calls: readonly DsmlToolCall[];
85
+ }
86
+
87
+ /**
88
+ * Streaming strict-prefix parser (CodeBuddy dialect). Buffers text until a
89
+ * complete block is seen, emits clean prose as it settles, and recovers calls
90
+ * from complete blocks only. Incomplete blocks stay text; an unclosed block
91
+ * past `maxBlockBytes` passes through as text instead of buffering forever.
92
+ * With no allowlist the generic name rule applies; with one, undeclared names
93
+ * reject the whole block. The caller feeds chunks with `push` and drains the
94
+ * tail with `flush`.
95
+ */
96
+ export class DsmlStreamParser {
97
+ readonly #separators: readonly string[];
98
+ readonly #blockNames: readonly string[];
99
+ readonly #openers: readonly string[];
100
+ readonly #allowedNames: ReadonlySet<string>;
101
+ readonly #params: "json" | "string";
102
+ readonly #sortKeys: boolean;
103
+ readonly #maxBlockBytes: number;
104
+ readonly #id: (call: { readonly name: string }, index: number, seq: number) => string;
105
+ #pending = "";
106
+ #callSequence = 0;
107
+
108
+ constructor(options: DsmlParseOptions = {}) {
109
+ this.#separators = options.separators ?? DEFAULT_SEPARATORS;
110
+ this.#blockNames = options.blockNames ?? DEFAULT_BLOCK_NAMES;
111
+ this.#openers = openers(this.#separators, this.#blockNames);
112
+ this.#allowedNames = options.allowedNames ?? new Set();
113
+ this.#params = options.params ?? "json";
114
+ this.#sortKeys = options.sortKeys ?? true;
115
+ this.#maxBlockBytes = options.maxBlockBytes ?? DEFAULT_MAX_BLOCK_BYTES;
116
+ this.#id = options.id ?? sequentialId;
117
+ }
118
+
119
+ push(chunk: string): DsmlParseResult {
120
+ this.#pending += chunk;
121
+ let cursor = 0;
122
+ let visible = "";
123
+ const calls: DsmlToolCall[] = [];
124
+ for (;;) {
125
+ let openIndex = -1;
126
+ let opener = "";
127
+ for (const candidate of this.#openers) {
128
+ const index = this.#pending.indexOf(candidate, cursor);
129
+ if (index >= 0 && (openIndex < 0 || index < openIndex)) {
130
+ openIndex = index;
131
+ opener = candidate;
132
+ }
133
+ }
134
+ if (openIndex < 0) {
135
+ const safeEnd = this.#settledEnd(cursor);
136
+ visible += this.#pending.slice(cursor, safeEnd);
137
+ cursor = safeEnd;
138
+ break;
139
+ }
140
+ visible += this.#pending.slice(cursor, openIndex);
141
+ const separator = this.#separators.find((value) => opener.startsWith(`<${value}DSML`));
142
+ const blockName = this.#blockNames.find((value) => opener.endsWith(` ${value}>`));
143
+ if (separator === undefined || blockName === undefined) {
144
+ visible += "<";
145
+ cursor = openIndex + 1;
146
+ continue;
147
+ }
148
+ const close = `</${separator}DSML${separator} ${blockName}>`;
149
+ const closeIndex = this.#pending.indexOf(close, openIndex + opener.length);
150
+ if (closeIndex < 0) {
151
+ // An unclosed block past the cap is not markup — pass it through as
152
+ // text instead of buffering it indefinitely. Same fail-open as
153
+ // `feed()` in the reference bridge's `MAX_BLOCK_BYTES` guard.
154
+ if (this.#pending.length - openIndex > this.#maxBlockBytes) {
155
+ visible += this.#pending.slice(openIndex);
156
+ cursor = this.#pending.length;
157
+ break;
158
+ }
159
+ cursor = openIndex;
160
+ break;
161
+ }
162
+ const end = closeIndex + close.length;
163
+ const block = this.#pending.slice(openIndex, end);
164
+ const parsed = parseStrictBlock(block, opener, close, separator, this.#allowedNames, this.#params, this.#sortKeys);
165
+ if (parsed === undefined) visible += block;
166
+ else {
167
+ parsed.forEach((call, index) => {
168
+ calls.push({ ...call, id: this.#id(call, index, this.#callSequence++) });
169
+ });
170
+ }
171
+ cursor = end;
172
+ }
173
+ this.#pending = this.#pending.slice(cursor);
174
+ return { text: visible, calls };
175
+ }
176
+
177
+ flush(): DsmlParseResult {
178
+ const text = this.#pending;
179
+ this.#pending = "";
180
+ return { text, calls: [] };
181
+ }
182
+
183
+ #settledEnd(start: number): number {
184
+ const remaining = this.#pending.slice(start);
185
+ const maxOpener = this.#openers.reduce((max, opener) => Math.max(max, opener.length - 1), 0);
186
+ for (let length = Math.min(maxOpener, remaining.length); length > 0; length -= 1) {
187
+ const suffix = remaining.slice(-length);
188
+ if (this.#openers.some((opener) => opener.startsWith(suffix))) return this.#pending.length - length;
189
+ }
190
+ return this.#pending.length;
191
+ }
192
+ }
193
+
194
+ function parseStrictBlock(
195
+ block: string,
196
+ opener: string,
197
+ close: string,
198
+ separator: string,
199
+ allowedNames: ReadonlySet<string>,
200
+ params: "json" | "string",
201
+ sortKeys: boolean,
202
+ ): Omit<DsmlToolCall, "id">[] | undefined {
203
+ const inner = block.slice(opener.length, -close.length);
204
+ const pipe = escapeRegExp(separator);
205
+ const invokePattern = new RegExp(`<${pipe}DSML${pipe}\\s+invoke\\s+name="([^"]*)"\\s*>`, "g");
206
+ const calls: Omit<DsmlToolCall, "id">[] = [];
207
+ // The allowlist is strict only when the caller knows the declared tools: a
208
+ // caller without one (a direct parse) keeps the generic name rule, while a
209
+ // dispatch that declared its tools must never execute a name it did not offer.
210
+ const strict = allowedNames.size > 0;
211
+ let cursor = 0;
212
+ for (const invoke of inner.matchAll(invokePattern)) {
213
+ const name = invoke[1] ?? "";
214
+ if (!TOOL_NAME.test(name) || (strict && !allowedNames.has(name)) || invoke.index === undefined) return undefined;
215
+ if (inner.slice(cursor, invoke.index).trim().length > 0) return undefined;
216
+ const bodyStart = invoke.index + invoke[0].length;
217
+ const invokeClose = `</${separator}DSML${separator} invoke>`;
218
+ const invokeEnd = inner.indexOf(invokeClose, bodyStart);
219
+ if (invokeEnd < 0) return undefined;
220
+ const parameterPattern = new RegExp(`<${pipe}DSML${pipe}\\s+parameter\\s+name="([^"]*)"(?:\\s+string="(true|false)")?\\s*>`, "g");
221
+ const body = inner.slice(bodyStart, invokeEnd);
222
+ const args: Record<string, unknown> = {};
223
+ let parameterCursor = 0;
224
+ for (const parameter of body.matchAll(parameterPattern)) {
225
+ const key = parameter[1] ?? "";
226
+ if (key.length === 0 || parameter.index === undefined || body.slice(parameterCursor, parameter.index).trim().length > 0) return undefined;
227
+ const valueStart = parameter.index + parameter[0].length;
228
+ const parameterClose = `</${separator}DSML${separator} parameter>`;
229
+ const valueEnd = body.indexOf(parameterClose, valueStart);
230
+ if (valueEnd < 0) return undefined;
231
+ const raw = body.slice(valueStart, valueEnd);
232
+ if (params === "string") args[key] = raw;
233
+ else if (parameter[2] === "true") args[key] = raw;
234
+ else {
235
+ try { args[key] = JSON.parse(raw); }
236
+ catch { args[key] = raw; }
237
+ }
238
+ parameterCursor = valueEnd + parameterClose.length;
239
+ }
240
+ if (body.slice(parameterCursor).trim().length > 0) return undefined;
241
+ calls.push({ name, arguments: JSON.stringify(sortKeys ? normalizeForSort(args) : args) });
242
+ cursor = invokeEnd + invokeClose.length;
243
+ }
244
+ return calls.length > 0 && inner.slice(cursor).trim().length === 0 ? calls : undefined;
245
+ }
246
+
247
+ /**
248
+ * Returns `value` with plain-object keys recursively sorted, mirroring the
249
+ * reference bridge's `json.dumps(..., sort_keys=True)`. Arrays keep their
250
+ * order — sorting applies to object keys only.
251
+ */
252
+ function normalizeForSort(value: unknown): unknown {
253
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return value;
254
+ const record = value as Record<string, unknown>;
255
+ const out: Record<string, unknown> = {};
256
+ for (const key of Object.keys(record).sort()) out[key] = normalizeForSort(record[key]);
257
+ return out;
258
+ }
259
+
260
+ /** The Qoder fuzzy block scanner, mirroring the reference bridge's regexes. */
261
+ const FUZZY_CALLS_RE = /<[^>]*DSML[^>]*calls>([\s\S]*?)<\/[^>]*DSML[^>]*calls>/;
262
+ const FUZZY_INVOKE_RE = /<[^>]*DSML[^>]*invoke\s+name=["']([^"']+)["']>([\s\S]*?)<\/[^>]*invoke>/g;
263
+ const FUZZY_PARAM_RE = /<[^>]*DSML[^>]*parameter\s+name=["']([^"']+)["'][^>]*>([\s\S]*?)<\/[^>]*parameter>/g;
264
+
265
+ function parseFuzzyDsml(text: string, options: DsmlParseOptions): DsmlRecovery {
266
+ const id = options.id ?? uuidId;
267
+ if (text.length === 0 || !text.includes("DSML")) return { calls: [], clean: text };
268
+
269
+ const callsMatch = FUZZY_CALLS_RE.exec(text);
270
+ let block: string;
271
+ let clean: string;
272
+
273
+ if (callsMatch) {
274
+ block = callsMatch[1] ?? "";
275
+ const before = text.slice(0, callsMatch.index).trimEnd();
276
+ const after = text.slice(callsMatch.index + callsMatch[0].length).trimStart();
277
+ clean = (before + " " + after).trim();
278
+ } else {
279
+ // No <calls> wrapper (issue #51): find bare <invoke> blocks in the whole
280
+ // text, then cut the prose off at the last `<` before the first invoke —
281
+ // exclusive of the invoke's own tag start, exactly like the reference's
282
+ // `text.rfind("<", 0, first_index)`.
283
+ FUZZY_INVOKE_RE.lastIndex = 0;
284
+ const first = FUZZY_INVOKE_RE.exec(text);
285
+ if (!first) return { calls: [], clean: text };
286
+ block = text;
287
+ const cut = text.lastIndexOf("<", first.index - 1);
288
+ clean = text.slice(0, cut >= 0 ? cut : first.index).trim();
289
+ }
290
+
291
+ const calls: DsmlToolCall[] = [];
292
+ FUZZY_INVOKE_RE.lastIndex = 0;
293
+ let inv: RegExpExecArray | null;
294
+ let seq = 0;
295
+ while ((inv = FUZZY_INVOKE_RE.exec(block)) !== null) {
296
+ const params: Record<string, string> = {};
297
+ FUZZY_PARAM_RE.lastIndex = 0;
298
+ let par: RegExpExecArray | null;
299
+ while ((par = FUZZY_PARAM_RE.exec(inv[2] ?? "")) !== null) {
300
+ params[par[1] ?? ""] = (par[2] ?? "").trim();
301
+ }
302
+ const call = { name: inv[1] ?? "", arguments: JSON.stringify(params) };
303
+ calls.push({ ...call, id: id(call, calls.length, seq++) });
304
+ }
305
+
306
+ return { calls, clean };
307
+ }
308
+
309
+ /**
310
+ * Recovers DSML tool calls from one plain-text string using the Qoder fuzzy
311
+ * dialect. Always returns `{ calls, clean }` — no-DSML input returns the text
312
+ * unchanged with no calls.
313
+ */
314
+ export function parseFuzzyDsmlToolCalls(text: string): DsmlRecovery {
315
+ return parseFuzzyDsml(text, {});
316
+ }
317
+
318
+ /**
319
+ * Recovers DSML tool calls from one plain-text string. The dialect is chosen
320
+ * by `options.fuzzy`; each dialect keeps its exact public shape:
321
+ *
322
+ * - strict (CodeBuddy): returns `{ text, calls }`, with `text` the clean
323
+ * prose; `undefined` when nothing was recovered.
324
+ * - fuzzy (Qoder): returns `{ clean, calls }`, with no-DSML input returning
325
+ * `{ calls: [], clean: text }`.
326
+ *
327
+ * Callers that need the streaming strict parser use {@link DsmlStreamParser}.
328
+ */
329
+ export function parseDsmlToolCalls(
330
+ text: string,
331
+ options: DsmlParseOptions = {},
332
+ ): DsmlRecovery | DsmlParseResult | undefined {
333
+ if (options.fuzzy === true) return parseFuzzyDsml(text, options);
334
+ if (!text.includes("DSML")) return undefined;
335
+ const parser = new DsmlStreamParser(options);
336
+ const first = parser.push(text);
337
+ const last = parser.flush();
338
+ const recovered = [...first.calls, ...last.calls];
339
+ return recovered.length === 0 ? undefined : { text: `${first.text}${last.text}`.trim(), calls: recovered };
340
+ }
341
+
342
+ /**
343
+ * The holdable tail of a content chunk: a short suffix that looks like the
344
+ * start of a DSML tag and may finish in the next frame. Empty when there is
345
+ * nothing worth holding. Mirrors `_dsml_tail_hold` in the reference bridge.
346
+ */
347
+ export function dsmlTailHold(text: string): string {
348
+ const at = text.lastIndexOf("<");
349
+ if (at < 0 || text.length - at > 24) return "";
350
+ const tail = text.slice(at);
351
+ // A complete tag already — nothing to wait for.
352
+ if (tail.includes(">")) return "";
353
+ const body = tail.replace(/\uFF5C/g, "|").toUpperCase().slice(1).replace(/^\//, "");
354
+ if (body.length === 0) return tail; // bare "<" or "</"
355
+ if (!body.startsWith("|")) return "";
356
+ const rest = body.slice(1);
357
+ if (rest.length === 0) return tail;
358
+ if ("DSML".startsWith(rest) || rest.startsWith("DSML")) return tail;
359
+ return "";
360
+ }
361
+
362
+ const DSML_CLOSE_TAG_RE = /<\/(?:[^>]*DSML[^>]*|parameter|invoke|calls|tool_calls|function_calls)\s*>\s*$/i;
363
+
364
+ /**
365
+ * Removes leftover DSML closing tags from the tail of a text. Anchored to the
366
+ * end and looped, so several trailing tags come off one per pass; a closing
367
+ * tag in the middle of a text is not residue and is preserved.
368
+ */
369
+ export function stripDsmlCloseTags(text: string): string {
370
+ let previous: string | null = null;
371
+ let out = text.trimEnd();
372
+ while (previous !== out) {
373
+ previous = out;
374
+ out = out.replace(DSML_CLOSE_TAG_RE, "").trimEnd();
375
+ }
376
+ return out;
377
+ }
@@ -52,7 +52,6 @@ export type GatewayErrorCode =
52
52
  | "proxy_unreachable"
53
53
  | "tool_call_loop_detected"
54
54
  | "client_router_denied"
55
- | "model_abuse_banned"
56
55
  | "shutting_down"
57
56
  | "restart_for_update";
58
57
 
@@ -1,6 +1,6 @@
1
- import type { CanonicalRequest, ReasoningEffort, ReasoningIntent } from "../protocol/canonical-model";
2
- import { budgetToLevel } from "../transport/translation/thinking";
3
- import { isRecord } from "../protocol/codecs";
1
+ import type { CanonicalRequest, ReasoningEffort, ReasoningIntent } from "./canonical-model";
2
+ import { budgetToLevel } from "./translation/thinking";
3
+ import { isRecord } from "./codecs";
4
4
 
5
5
  /**
6
6
  * The effective reasoning effort for a canonical intent, deriving one from an
@@ -132,12 +132,13 @@ const CLAMPED_EFFORT_MIN_OUTPUT_TOKENS = 16_384;
132
132
  const BUDDY_LEVEL_RANK: Readonly<Record<string, number>> = {
133
133
  off: 0,
134
134
  none: 0,
135
- minimal: 1,
136
- low: 2,
137
- medium: 3,
138
- high: 4,
139
- xhigh: 5,
140
- max: 6,
135
+ instant: 1,
136
+ minimal: 2,
137
+ low: 3,
138
+ medium: 4,
139
+ high: 5,
140
+ xhigh: 6,
141
+ max: 7,
141
142
  };
142
143
 
143
144
  /** Builds the Anthropic thinking block from canonical reasoning intent. */
@@ -3,18 +3,18 @@
3
3
  */
4
4
  import type { CanonicalRequest, ResponseFormat } from "../canonical-model";
5
5
  import { isRecord } from "../codecs";
6
- import { markLatestBreakpoint } from "../../transport/translation/cache-controls";
7
- import { isWebSearchTool } from "../../transport/translation/search";
8
- import { pickWireSupportedControls } from "../../transport/translation/capabilities";
9
- import { normalizeWireMaxTokens } from "../../transport/translation/normalize";
10
- import { resolveWireMaxTokens } from "../../transport/translation/quirks";
6
+ import { markLatestBreakpoint } from "../../protocol/translation/cache-controls";
7
+ import { isWebSearchTool } from "../../protocol/translation/search";
8
+ import { pickWireSupportedControls } from "../../protocol/translation/capabilities";
9
+ import { normalizeWireMaxTokens } from "../../protocol/translation/normalize";
10
+ import { resolveWireMaxTokens } from "../../protocol/translation/quirks";
11
11
  import {
12
12
  clampReasoningEffort,
13
13
  resolveSupportedReasoningEfforts,
14
- } from "../../transport/translation/thinking";
14
+ } from "../../protocol/translation/thinking";
15
15
  import { isClaudeBillingHeaderText, resolveImageSource } from "../codecs";
16
16
  import { resolvePromptCacheKey } from "../../providers/operations/session-resolution";
17
- import { reasoningEffortFromIntent } from "../../providers/provider-reasoning";
17
+ import { reasoningEffortFromIntent } from "../../protocol/reasoning";
18
18
  import { parseChatResponseToEvents, decodeChatSseStream } from "../response/chat";
19
19
 
20
20
  /**