cartethyia 2.4.6 → 2.5.0

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 (302) hide show
  1. package/README.md +45 -13
  2. package/dist/dashboard/assets/BannedPage-9igfctfZ.js +1 -0
  3. package/dist/dashboard/assets/BannedPage-9igfctfZ.js.gz +0 -0
  4. package/dist/dashboard/assets/{CliToolDetailPage-Bcy150Jo.js → CliToolDetailPage-ZRr2K2sF.js} +1 -1
  5. package/dist/dashboard/assets/CliToolDetailPage-ZRr2K2sF.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolsPage-DyNXefph.js → CliToolsPage-D3xqZbN-.js} +1 -1
  7. package/dist/dashboard/assets/CliToolsPage-D3xqZbN-.js.gz +0 -0
  8. package/dist/dashboard/assets/CombosPage-CVnQPBBz.js +1 -0
  9. package/dist/dashboard/assets/CombosPage-CVnQPBBz.js.gz +0 -0
  10. package/dist/dashboard/assets/{ConsoleLogPage-CATpAEOz.js → ConsoleLogPage-u6W1UXSc.js} +1 -1
  11. package/dist/dashboard/assets/ConsoleLogPage-u6W1UXSc.js.gz +0 -0
  12. package/dist/dashboard/assets/{CustomizationPage-D6jEU7nL.js → CustomizationPage-CTTpCb25.js} +1 -1
  13. package/dist/dashboard/assets/CustomizationPage-CTTpCb25.js.gz +0 -0
  14. package/dist/dashboard/assets/{HealthEventsModal-CZkpn0a7.js → HealthEventsModal-C5XpWlvy.js} +1 -1
  15. package/dist/dashboard/assets/HealthEventsModal-C5XpWlvy.js.gz +0 -0
  16. package/dist/dashboard/assets/{LoginPage-BJlMVN4U.js → LoginPage-DHoB3Agn.js} +1 -1
  17. package/dist/dashboard/assets/LoginPage-DHoB3Agn.js.gz +0 -0
  18. package/dist/dashboard/assets/{ModelPicker-Br3CXEau.js → ModelPicker-oQ-SDXp8.js} +1 -1
  19. package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js.gz +0 -0
  20. package/dist/dashboard/assets/OverviewPage-BrY4SgIt.js +1 -0
  21. package/dist/dashboard/assets/OverviewPage-BrY4SgIt.js.gz +0 -0
  22. package/dist/dashboard/assets/ProviderDetailPage-D9VSt7T_.js +3 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-D9VSt7T_.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js +1 -0
  25. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js.gz +0 -0
  26. package/dist/dashboard/assets/ProvidersPage-Df77V5I5.js +1 -0
  27. package/dist/dashboard/assets/ProvidersPage-Df77V5I5.js.gz +0 -0
  28. package/dist/dashboard/assets/ProxyPage-CDfxP7AC.js +7 -0
  29. package/dist/dashboard/assets/ProxyPage-CDfxP7AC.js.gz +0 -0
  30. package/dist/dashboard/assets/QuotaPage-CY4o2t4p.js +1 -0
  31. package/dist/dashboard/assets/QuotaPage-CY4o2t4p.js.gz +0 -0
  32. package/dist/dashboard/assets/{SettingsPage-BHW4Zt06.js → SettingsPage-CvjZxHE4.js} +1 -1
  33. package/dist/dashboard/assets/SettingsPage-CvjZxHE4.js.gz +0 -0
  34. package/dist/dashboard/assets/{SetupPage-VOYC_lIa.js → SetupPage-D3nza7LH.js} +1 -1
  35. package/dist/dashboard/assets/SetupPage-D3nza7LH.js.gz +0 -0
  36. package/dist/dashboard/assets/SortableList-Dtt12WT1.js +1 -0
  37. package/dist/dashboard/assets/SortableList-Dtt12WT1.js.gz +0 -0
  38. package/dist/dashboard/assets/{StudioPage-BNIDLiU6.js → StudioPage-CbK3hXvU.js} +4 -4
  39. package/dist/dashboard/assets/StudioPage-CbK3hXvU.js.gz +0 -0
  40. package/dist/dashboard/assets/UsagePage-BQlWbWg2.js +56 -0
  41. package/dist/dashboard/assets/UsagePage-BQlWbWg2.js.gz +0 -0
  42. package/dist/dashboard/assets/{api-keys-BETfaMYZ.js → api-keys-BmDbsior.js} +1 -1
  43. package/dist/dashboard/assets/api-keys-BmDbsior.js.gz +0 -0
  44. package/dist/dashboard/assets/app-BbpSarVV.css +2 -0
  45. package/dist/dashboard/assets/app-BbpSarVV.css.gz +0 -0
  46. package/dist/dashboard/assets/assert-BHfaei-X.js +1 -0
  47. package/dist/dashboard/assets/assert-BHfaei-X.js.gz +0 -0
  48. package/dist/dashboard/assets/{badge-BTyGk_DL.js → badge-DpXT4Ixt.js} +1 -1
  49. package/dist/dashboard/assets/badge-DpXT4Ixt.js.gz +0 -0
  50. package/dist/dashboard/assets/{banner-bujTWYM4.js → banner-CSAAWLJv.js} +1 -1
  51. package/dist/dashboard/assets/banner-CSAAWLJv.js.gz +0 -0
  52. package/dist/dashboard/assets/{card-B0Bnx5CA.js → card-DEhRXXw4.js} +1 -1
  53. package/dist/dashboard/assets/card-DEhRXXw4.js.gz +0 -0
  54. package/dist/dashboard/assets/{cli-tools-DE2ug812.js → cli-tools-B0wBq-RL.js} +1 -1
  55. package/dist/dashboard/assets/cli-tools-B0wBq-RL.js.gz +0 -0
  56. package/dist/dashboard/assets/{clipboard-button-R_rArH8S.js → clipboard-button-DOo6EU8O.js} +1 -1
  57. package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js.gz +0 -0
  58. package/dist/dashboard/assets/{dialog-BWIP_EvW.js → dialog-BvQeBMfP.js} +1 -1
  59. package/dist/dashboard/assets/dialog-BvQeBMfP.js.gz +0 -0
  60. package/dist/dashboard/assets/{drawer-BKcgVugF.js → drawer-CMwPXwQB.js} +1 -1
  61. package/dist/dashboard/assets/drawer-CMwPXwQB.js.gz +0 -0
  62. package/dist/dashboard/assets/{entry-CcWkGR4a.js → entry-D4O2229X.js} +1 -1
  63. package/dist/dashboard/assets/{entry-CcWkGR4a.js.gz → entry-D4O2229X.js.gz} +0 -0
  64. package/dist/dashboard/assets/{entry-kTiTFrvh.js → entry-DKvr5Q9K.js} +1 -1
  65. package/dist/dashboard/assets/entry-DKvr5Q9K.js.gz +0 -0
  66. package/dist/dashboard/assets/entry-FQCejoDY.js +4 -0
  67. package/dist/dashboard/assets/entry-FQCejoDY.js.gz +0 -0
  68. package/dist/dashboard/assets/{github-badge-Booc5mNe.js → github-badge-zliLvBwr.js} +1 -1
  69. package/dist/dashboard/assets/github-badge-zliLvBwr.js.gz +0 -0
  70. package/dist/dashboard/assets/{index-D70pChfn.js → index-CXm3FIyt.js} +2 -2
  71. package/dist/dashboard/assets/index-CXm3FIyt.js.gz +0 -0
  72. package/dist/dashboard/assets/{inline-D1TCqM2D.js → inline-D1IlZZ8H.js} +1 -1
  73. package/dist/dashboard/assets/inline-D1IlZZ8H.js.gz +0 -0
  74. package/dist/dashboard/assets/{input-B9oyU93y.js → input-BpVZdrbc.js} +1 -1
  75. package/dist/dashboard/assets/input-BpVZdrbc.js.gz +0 -0
  76. package/dist/dashboard/assets/{layout-BArzXirQ.js → layout-BioLW7gN.js} +1 -1
  77. package/dist/dashboard/assets/layout-BioLW7gN.js.gz +0 -0
  78. package/dist/dashboard/assets/{list-row-BVZzaoHF.js → list-row-CwVcGAIc.js} +1 -1
  79. package/dist/dashboard/assets/list-row-CwVcGAIc.js.gz +0 -0
  80. package/dist/dashboard/assets/{live-EmuIayqp.js → live-yfEJFF9T.js} +1 -1
  81. package/dist/dashboard/assets/live-yfEJFF9T.js.gz +0 -0
  82. package/dist/dashboard/assets/{model-bans-CSPQ9xB0.js → model-bans-CT-TxSNX.js} +1 -1
  83. package/dist/dashboard/assets/model-bans-CT-TxSNX.js.gz +0 -0
  84. package/dist/dashboard/assets/providers/cursor.webp +0 -0
  85. package/dist/dashboard/assets/providers/trae.webp +0 -0
  86. package/dist/dashboard/assets/{routing-BExgxxBU.js → routing-Cs6jl49L.js} +1 -1
  87. package/dist/dashboard/assets/routing-Cs6jl49L.js.gz +0 -0
  88. package/dist/dashboard/assets/select-C0Rer3gF.js +41 -0
  89. package/dist/dashboard/assets/select-C0Rer3gF.js.gz +0 -0
  90. package/dist/dashboard/assets/settings-CfcTcOyU.js +1 -0
  91. package/dist/dashboard/assets/settings-CfcTcOyU.js.gz +0 -0
  92. package/dist/dashboard/assets/{stack-Ca5HxI5i.js → stack-C9_uUjaA.js} +1 -1
  93. package/dist/dashboard/assets/stack-C9_uUjaA.js.gz +0 -0
  94. package/dist/dashboard/assets/{state-CzUPfLXo.js → state-DA0B95UQ.js} +1 -1
  95. package/dist/dashboard/assets/state-DA0B95UQ.js.gz +0 -0
  96. package/dist/dashboard/assets/{switch-DOUIky_i.js → switch-Cittxy-x.js} +1 -1
  97. package/dist/dashboard/assets/switch-Cittxy-x.js.gz +0 -0
  98. package/dist/dashboard/assets/{toast-C76WrMhc.js → toast-CNt_jzO4.js} +1 -1
  99. package/dist/dashboard/assets/toast-CNt_jzO4.js.gz +0 -0
  100. package/dist/dashboard/assets/{useMutation-D87FZwH0.js → useMutation-BQKMcJ1H.js} +1 -1
  101. package/dist/dashboard/assets/useMutation-BQKMcJ1H.js.gz +0 -0
  102. package/dist/dashboard/index.html +1 -1
  103. package/dist/dashboard/index.html.gz +0 -0
  104. package/dist/dashboard/precompressed-assets.json +51 -52
  105. package/migrations/full-postgres/0015_add_payload_stages.sql +18 -0
  106. package/migrations/full-postgres/0016_add_share_link_slug.sql +20 -0
  107. package/migrations/full-postgres/0017_add_models_pro_only.sql +18 -0
  108. package/migrations/full-postgres/0018_add_accounts_quota_plan.sql +18 -0
  109. package/migrations/full-postgres/0019_add_models_free_tier.sql +15 -0
  110. package/migrations/lite-sqlite3/0015_add_payload_stages.sql +18 -0
  111. package/migrations/lite-sqlite3/0016_add_share_link_slug.sql +20 -0
  112. package/migrations/lite-sqlite3/0017_add_models_pro_only.sql +18 -0
  113. package/migrations/lite-sqlite3/0018_add_accounts_quota_plan.sql +18 -0
  114. package/migrations/lite-sqlite3/0019_add_models_free_tier.sql +15 -0
  115. package/package.json +3 -2
  116. package/scripts/cli/command/console-menu.ts +30 -11
  117. package/scripts/cli/command/start.ts +77 -13
  118. package/scripts/cli/command/update.ts +1 -1
  119. package/scripts/cli/gateway/instance-lock.ts +105 -0
  120. package/scripts/cli/gateway/spawn.ts +173 -101
  121. package/scripts/cli/ui/background.ts +110 -49
  122. package/src/boot/app.ts +20 -6
  123. package/src/boot/dependencies.ts +19 -0
  124. package/src/boot/elysia-typebox-bridge.ts +37 -0
  125. package/src/boot/lifecycle.ts +2 -0
  126. package/src/config/cooldowns.ts +138 -138
  127. package/src/config/providers.ts +5 -5
  128. package/src/config/runtime.ts +19 -3
  129. package/src/config/spec.ts +9 -0
  130. package/src/console/api-keys/contracts.ts +414 -391
  131. package/src/console/api-keys/routes.ts +880 -870
  132. package/src/console/auth/account-reset-store.ts +14 -0
  133. package/src/console/auth/account-reset.ts +3 -2
  134. package/src/console/auth/setup-service.ts +6 -3
  135. package/src/console/domain-registration.ts +9 -2
  136. package/src/console/observability/contracts.ts +475 -455
  137. package/src/console/observability/store.ts +1015 -952
  138. package/src/console/providers/catalog/contracts.ts +31 -0
  139. package/src/console/providers/catalog/provider-operations.ts +8 -0
  140. package/src/console/providers/catalog/store.ts +1532 -1525
  141. package/src/console/providers/oauth/callback-listener.ts +18 -3
  142. package/src/console/providers/oauth/routes.ts +38 -15
  143. package/src/console/quota/refresh.ts +19 -0
  144. package/src/console/updates/routes.ts +162 -4
  145. package/src/main.ts +3 -0
  146. package/src/network/http2-bidi.ts +85 -0
  147. package/src/network/outbound-fetch.ts +1 -26
  148. package/src/network/pool/agent.ts +478 -460
  149. package/src/network/pool/resolver.ts +1104 -544
  150. package/src/network/pool-health-machine.ts +12 -5
  151. package/src/observability/payload-capture.ts +344 -324
  152. package/src/persistence/db-pg.ts +9 -3
  153. package/src/persistence/db-sqlite.ts +497 -495
  154. package/src/persistence/health-event-flush.ts +137 -0
  155. package/src/persistence/migrate.ts +181 -129
  156. package/src/persistence/schema.ts +1360 -1295
  157. package/src/persistence/settings-revision.ts +25 -0
  158. package/src/persistence/shared-cache.ts +217 -0
  159. package/src/persistence/stores/share-store.ts +556 -528
  160. package/src/persistence/usage-flush.ts +61 -1
  161. package/src/protocol/canonical-model.ts +23 -1
  162. package/src/protocol/response/chat.ts +143 -10
  163. package/src/protocol/response/omp-tool-call-markup.ts +221 -0
  164. package/src/providers/default-registry.ts +173 -1
  165. package/src/providers/discovery/openai-model-discovery.ts +39 -3
  166. package/src/providers/discovery/openrouter-free-roster.ts +98 -0
  167. package/src/providers/discovery/probing-service.ts +1022 -1001
  168. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +39 -0
  169. package/src/providers/integrations/buddy/buddy-chat-shared.ts +913 -914
  170. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +18 -5
  171. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +4 -0
  172. package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -175
  173. package/src/providers/integrations/buddy/codebuddy/shared.ts +15 -6
  174. package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -204
  175. package/src/providers/integrations/buddy/workbuddy/shared.ts +3 -3
  176. package/src/providers/integrations/claude/claude.ts +606 -582
  177. package/src/providers/integrations/cline/cline-quota.ts +18 -16
  178. package/src/providers/integrations/cline/cline.ts +539 -468
  179. package/src/providers/integrations/commandcode/commandcode.ts +675 -541
  180. package/src/providers/integrations/commandcode/discovery.ts +116 -0
  181. package/src/providers/integrations/commandcode/quota.ts +160 -0
  182. package/src/providers/integrations/cursor/cursor-auth.ts +418 -0
  183. package/src/providers/integrations/cursor/cursor-catalog.ts +368 -0
  184. package/src/providers/integrations/cursor/cursor-http2.ts +300 -0
  185. package/src/providers/integrations/cursor/cursor-identity.ts +92 -0
  186. package/src/providers/integrations/cursor/cursor-request.ts +471 -0
  187. package/src/providers/integrations/cursor/cursor-session-manager.ts +281 -0
  188. package/src/providers/integrations/cursor/cursor-shared.ts +108 -0
  189. package/src/providers/integrations/cursor/cursor.ts +960 -0
  190. package/src/providers/integrations/cursor/protocol/framing.ts +129 -0
  191. package/src/providers/integrations/cursor/protocol/messages.ts +2355 -0
  192. package/src/providers/integrations/cursor/protocol/struct.ts +305 -0
  193. package/src/providers/integrations/github/github.ts +4 -4
  194. package/src/providers/integrations/hermes.ts +79 -8
  195. package/src/providers/integrations/kilo/kilo-discovery.ts +10 -1
  196. package/src/providers/integrations/kilo/kilo.ts +153 -146
  197. package/src/providers/integrations/opencode/provider.ts +532 -538
  198. package/src/providers/integrations/openrouter/provider.ts +139 -281
  199. package/src/providers/integrations/qoder/qoder-catalog.ts +450 -0
  200. package/src/providers/integrations/qoder/qoder-oauth.ts +310 -257
  201. package/src/providers/integrations/qoder/qoder-realm.ts +106 -0
  202. package/src/providers/integrations/qoder/qoder-virtual.ts +77 -0
  203. package/src/providers/integrations/qoder/qoder.ts +865 -730
  204. package/src/providers/integrations/trae/trae-catalog.ts +190 -0
  205. package/src/providers/integrations/trae/trae-headers.ts +149 -0
  206. package/src/providers/integrations/trae/trae-oauth.ts +773 -0
  207. package/src/providers/integrations/trae/trae-protocol.ts +400 -0
  208. package/src/providers/integrations/trae/trae-realm.ts +220 -0
  209. package/src/providers/integrations/trae/trae.ts +458 -0
  210. package/src/providers/operations/account-health-service.ts +107 -10
  211. package/src/providers/operations/client-versions.ts +133 -6
  212. package/src/providers/operations/provider-catalog-seeder.ts +95 -86
  213. package/src/providers/operations/provider-credential-service.ts +46 -7
  214. package/src/providers/provider-contracts.ts +76 -0
  215. package/src/providers/provider-metadata.ts +78 -6
  216. package/src/providers/provider-model-definition.ts +373 -349
  217. package/src/security/api-key-auth.ts +126 -8
  218. package/src/transport/contracts.ts +3 -0
  219. package/src/transport/dispatch/attempt-finalize.ts +32 -1
  220. package/src/transport/dispatch/attempt-loop.ts +530 -515
  221. package/src/transport/dispatch/generation.ts +88 -21
  222. package/src/transport/dispatch/leases.ts +99 -2
  223. package/src/transport/dispatch/proxy-request.ts +60 -3
  224. package/src/transport/dispatch/streaming/finalize.ts +16 -1
  225. package/src/transport/dispatch/streaming/stream.ts +22 -2
  226. package/src/transport/dispatch/upstream.ts +19 -1
  227. package/src/transport/guards/auth-guard.ts +26 -1
  228. package/src/transport/guards/body-policy.ts +5 -4
  229. package/src/transport/request/preparer.ts +103 -0
  230. package/src/transport/request/state.ts +14 -0
  231. package/src/transport/routing/plan-gate.ts +45 -0
  232. package/src/transport/routing/route-catalog.ts +799 -777
  233. package/src/transport/routing/route-model.ts +52 -1
  234. package/src/transport/routing/router.ts +51 -0
  235. package/dist/dashboard/assets/BannedPage-QhvbJB9o.js +0 -1
  236. package/dist/dashboard/assets/BannedPage-QhvbJB9o.js.gz +0 -0
  237. package/dist/dashboard/assets/CliToolDetailPage-Bcy150Jo.js.gz +0 -0
  238. package/dist/dashboard/assets/CliToolsPage-DyNXefph.js.gz +0 -0
  239. package/dist/dashboard/assets/CombosPage-Cak0ljnO.js +0 -1
  240. package/dist/dashboard/assets/CombosPage-Cak0ljnO.js.gz +0 -0
  241. package/dist/dashboard/assets/ConsoleLogPage-CATpAEOz.js.gz +0 -0
  242. package/dist/dashboard/assets/CustomizationPage-D6jEU7nL.js.gz +0 -0
  243. package/dist/dashboard/assets/HealthEventsModal-CZkpn0a7.js.gz +0 -0
  244. package/dist/dashboard/assets/LoginPage-BJlMVN4U.js.gz +0 -0
  245. package/dist/dashboard/assets/ModelPicker-Br3CXEau.js.gz +0 -0
  246. package/dist/dashboard/assets/OverviewPage-CQxXbo2g.js +0 -1
  247. package/dist/dashboard/assets/OverviewPage-CQxXbo2g.js.gz +0 -0
  248. package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js +0 -3
  249. package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js.gz +0 -0
  250. package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js +0 -1
  251. package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js.gz +0 -0
  252. package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js +0 -1
  253. package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js.gz +0 -0
  254. package/dist/dashboard/assets/ProxyPage-CXiumA53.js +0 -7
  255. package/dist/dashboard/assets/ProxyPage-CXiumA53.js.gz +0 -0
  256. package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js +0 -1
  257. package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js.gz +0 -0
  258. package/dist/dashboard/assets/SettingsPage-BHW4Zt06.js.gz +0 -0
  259. package/dist/dashboard/assets/SetupPage-VOYC_lIa.js.gz +0 -0
  260. package/dist/dashboard/assets/SortableList-DpOTnGuH.js +0 -1
  261. package/dist/dashboard/assets/SortableList-DpOTnGuH.js.gz +0 -0
  262. package/dist/dashboard/assets/StudioPage-BNIDLiU6.js.gz +0 -0
  263. package/dist/dashboard/assets/UsagePage-BAFaSDQV.js +0 -56
  264. package/dist/dashboard/assets/UsagePage-BAFaSDQV.js.gz +0 -0
  265. package/dist/dashboard/assets/api-keys-BETfaMYZ.js.gz +0 -0
  266. package/dist/dashboard/assets/app-BLCvGdzb.css +0 -2
  267. package/dist/dashboard/assets/app-BLCvGdzb.css.gz +0 -0
  268. package/dist/dashboard/assets/assert-7J6_LVD3.js +0 -1
  269. package/dist/dashboard/assets/assert-7J6_LVD3.js.gz +0 -0
  270. package/dist/dashboard/assets/badge-BTyGk_DL.js.gz +0 -0
  271. package/dist/dashboard/assets/banner-bujTWYM4.js.gz +0 -0
  272. package/dist/dashboard/assets/card-B0Bnx5CA.js.gz +0 -0
  273. package/dist/dashboard/assets/cli-tools-DE2ug812.js.gz +0 -0
  274. package/dist/dashboard/assets/clipboard-button-R_rArH8S.js.gz +0 -0
  275. package/dist/dashboard/assets/dialog-BWIP_EvW.js.gz +0 -0
  276. package/dist/dashboard/assets/drawer-BKcgVugF.js.gz +0 -0
  277. package/dist/dashboard/assets/entry-7dpgvpJ3.js +0 -4
  278. package/dist/dashboard/assets/entry-7dpgvpJ3.js.gz +0 -0
  279. package/dist/dashboard/assets/entry-kTiTFrvh.js.gz +0 -0
  280. package/dist/dashboard/assets/github-badge-Booc5mNe.js.gz +0 -0
  281. package/dist/dashboard/assets/index-D70pChfn.js.gz +0 -0
  282. package/dist/dashboard/assets/inline-D1TCqM2D.js.gz +0 -0
  283. package/dist/dashboard/assets/input-B9oyU93y.js.gz +0 -0
  284. package/dist/dashboard/assets/layout-BArzXirQ.js.gz +0 -0
  285. package/dist/dashboard/assets/list-row-BVZzaoHF.js.gz +0 -0
  286. package/dist/dashboard/assets/live-EmuIayqp.js.gz +0 -0
  287. package/dist/dashboard/assets/model-bans-CSPQ9xB0.js.gz +0 -0
  288. package/dist/dashboard/assets/routing-BExgxxBU.js.gz +0 -0
  289. package/dist/dashboard/assets/select-C9mMoEpJ.js +0 -41
  290. package/dist/dashboard/assets/select-C9mMoEpJ.js.gz +0 -0
  291. package/dist/dashboard/assets/settings-C4_Gn_wq.js +0 -1
  292. package/dist/dashboard/assets/settings-C4_Gn_wq.js.gz +0 -0
  293. package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js +0 -1
  294. package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js.gz +0 -0
  295. package/dist/dashboard/assets/stack-Ca5HxI5i.js.gz +0 -0
  296. package/dist/dashboard/assets/state-CzUPfLXo.js.gz +0 -0
  297. package/dist/dashboard/assets/switch-DOUIky_i.js.gz +0 -0
  298. package/dist/dashboard/assets/toast-C76WrMhc.js.gz +0 -0
  299. package/dist/dashboard/assets/useMutation-D87FZwH0.js.gz +0 -0
  300. package/src/network/tls/bun-client-hello.ts +0 -112
  301. /package/dist/dashboard/assets/{app-Cjq7nrwu.js → app-C-Y4oZT0.js} +0 -0
  302. /package/dist/dashboard/assets/{app-Cjq7nrwu.js.gz → app-C-Y4oZT0.js.gz} +0 -0
@@ -80,11 +80,30 @@ async function writeLifetimeDeltas(
80
80
  const FLUSH_STATEMENT_MAX_KEYS = 500;
81
81
  /** Max distinct keys retained across windows; oldest dropped first. */
82
82
  const FLUSH_PENDING_MAX_KEYS = 5000;
83
+
84
+ /**
85
+ * Consecutive fully-failed flushes before the writer backs off.
86
+ *
87
+ * A database that is down stays down: retrying every `intervalMs` means a
88
+ * write attempt per second against a backend that is already overloaded, which
89
+ * is load the operator cannot distinguish from real traffic and which delays
90
+ * recovery. After this many failures the buffer stops probing and waits out a
91
+ * cooldown instead.
92
+ */
93
+ const FLUSH_FAILURE_STREAK_LIMIT = 3;
94
+ /** How long a failing writer waits before probing again. */
95
+ const FLUSH_BACKOFF_MS = 15_000;
83
96
  export class UsageFlushBuffer {
84
97
  private readonly pending = new Map<string, number>();
85
98
  private timer: ReturnType<typeof setTimeout> | undefined;
86
99
  private flushing = false;
87
100
  private stopped = false;
101
+ /** Consecutive flushes that failed outright; drives the backoff below. */
102
+ private failureStreak = 0;
103
+ /** Wall-clock time before which no flush is attempted. */
104
+ private backoffUntil = 0;
105
+ /** Deltas abandoned because the buffer could not persist them. */
106
+ private droppedDelta = 0;
88
107
 
89
108
  constructor(
90
109
  private readonly db: CartethyiaDatabase,
@@ -104,6 +123,19 @@ export class UsageFlushBuffer {
104
123
  }
105
124
  }
106
125
 
126
+ /**
127
+ * Deltas recorded but not yet persisted, for one key.
128
+ *
129
+ * Exposed so a reader of `lifetime_tokens_consumed` can add what is in
130
+ * flight to what Postgres last stored. Without it such a reader saw a value
131
+ * that was behind by up to one flush interval, and — because the flush is
132
+ * coalescing by design — behind by the largest amount exactly when the key
133
+ * was busiest.
134
+ */
135
+ pendingFor(apiKeyId: string): number {
136
+ return this.pending.get(apiKeyId) ?? 0;
137
+ }
138
+
107
139
  /**
108
140
  * Writes everything accumulated so far, in bounded statements of at most
109
141
  * 500 keys each. One giant statement grows the SQL string with the
@@ -116,9 +148,14 @@ export class UsageFlushBuffer {
116
148
  */
117
149
  async flush(): Promise<void> {
118
150
  if (this.flushing || this.pending.size === 0) return;
151
+ // Backing off during a sustained outage: without this the writer probed a
152
+ // failing database on every tick, adding write load to the very overload
153
+ // that caused the failure.
154
+ if (Date.now() < this.backoffUntil) return;
119
155
  this.flushing = true;
120
156
  const batch = new Map(this.pending);
121
157
  this.pending.clear();
158
+ let failed = 0;
122
159
  try {
123
160
  const entries = [...batch];
124
161
  for (let offset = 0; offset < entries.length; offset += FLUSH_STATEMENT_MAX_KEYS) {
@@ -134,15 +171,38 @@ export class UsageFlushBuffer {
134
171
  try {
135
172
  await this.write(this.db, apiKeyId, delta);
136
173
  } catch {
137
- this.pending.set(apiKeyId, (this.pending.get(apiKeyId) ?? 0) + delta);
174
+ // A transient failure retries, but only while the buffer is not
175
+ // itself saturated: an outage that outlasts the cap must drop
176
+ // accounting rather than grow the pending map without bound.
177
+ if (this.pending.size < FLUSH_PENDING_MAX_KEYS) {
178
+ this.pending.set(apiKeyId, (this.pending.get(apiKeyId) ?? 0) + delta);
179
+ } else {
180
+ this.droppedDelta += delta;
181
+ }
182
+ failed += 1;
138
183
  }
139
184
  }
140
185
  }
141
186
  } finally {
187
+ if (failed > 0) {
188
+ this.failureStreak += 1;
189
+ if (this.failureStreak >= FLUSH_FAILURE_STREAK_LIMIT) {
190
+ this.backoffUntil = Date.now() + FLUSH_BACKOFF_MS;
191
+ this.failureStreak = 0;
192
+ }
193
+ } else {
194
+ this.failureStreak = 0;
195
+ this.backoffUntil = 0;
196
+ }
142
197
  this.flushing = false;
143
198
  }
144
199
  }
145
200
 
201
+ /** Total usage delta abandoned after sustained write failure. */
202
+ get droppedTotal(): number {
203
+ return this.droppedDelta;
204
+ }
205
+
146
206
  /** Stops the timer. Pass `{ flush: true }` to persist what is still pending. */
147
207
  async stop(options?: { flush?: boolean }): Promise<void> {
148
208
  this.stopped = true;
@@ -33,7 +33,16 @@ export type WireFamily = (typeof WIRE_FAMILIES)[number];
33
33
  * the console's Elysia body schema and its validator both project this list, so
34
34
  * a new kind cannot reach one and miss the other.
35
35
  */
36
- export const SERVICE_KINDS = ["llm", "systemone", "websearch", "imageGeneration", "videoGeneration"] as const;
36
+ export const SERVICE_KINDS = [
37
+ "llm",
38
+ "systemone",
39
+ "websearch",
40
+ "imageGeneration",
41
+ "videoGeneration",
42
+ "embedding",
43
+ "audioTranscription",
44
+ "audioSpeech",
45
+ ] as const;
37
46
 
38
47
  /** Provider service kind selected by a model's catalog row. */
39
48
  export type ServiceKind = (typeof SERVICE_KINDS)[number];
@@ -354,6 +363,19 @@ export interface CanonicalTerminalEvent {
354
363
  readonly response_id?: string;
355
364
  /** Event timestamp in milliseconds since epoch (optional for profiling). */
356
365
  readonly timestamp?: number;
366
+ /**
367
+ * Set by an adapter whose turn is not finished: the stream ended because the
368
+ * model asked for a tool the client must answer, and the provider's
369
+ * authoritative usage arrives with the *later* request that continues the
370
+ * same upstream turn.
371
+ *
372
+ * The settle path reads this to skip charging estimated tokens for the
373
+ * interim response — otherwise one logical turn bills an input/output
374
+ * estimate on the tool-call response and the real usage on the continuation,
375
+ * which double-counts the turn. Only a provider that genuinely spans client
376
+ * requests with one upstream turn sets it.
377
+ */
378
+ readonly usage_deferred?: true;
357
379
  }
358
380
 
359
381
  /** An ordered canonical stream event; sequence numbers must increase monotonically. */
@@ -11,6 +11,11 @@ import { usageFromProvider, readReasoningText } from "../../providers/provider-u
11
11
  import { createToolEmitLedger } from "../tool-identity";
12
12
  import { MimoThinkSplitter, stripThinkingTags } from "../../providers/integrations/xiaomi-mimo/think-stream";
13
13
  import { parseDsmlToolCalls } from "./dsml";
14
+ import {
15
+ OmpToolCallMarkupParser,
16
+ parseOmpToolCallMarkup,
17
+ type OmpTextToolCall,
18
+ } from "./omp-tool-call-markup";
14
19
 
15
20
 
16
21
  export function mapChatStopReason(
@@ -43,14 +48,23 @@ export function parseChatResponseToEvents(
43
48
  const choices = (json["choices"] as Array<Record<string, unknown>>) ?? [];
44
49
  const first = choices[0] as Record<string, unknown> | undefined;
45
50
  const message = first?.["message"] as Record<string, unknown> | undefined;
51
+ const declaredToolNames = declaredToolNamesForRequest(request);
52
+ const callableToolNames = callableToolNamesForRequest(request);
53
+ let recoveredToolCalls = 0;
46
54
  if (message) {
47
55
  const reasoningRaw = readReasoningText(message);
56
+ const reasoningMarkup =
57
+ typeof reasoningRaw === "string" && (request.tools?.length ?? 0) > 0
58
+ ? parseOmpToolCallMarkup(reasoningRaw, declaredToolNames)
59
+ : undefined;
48
60
  // hy4-preview-f (and other buddy reasoning models) sometimes wraps its own
49
61
  // reasoning in `<think>…</think>` inside `reasoning_content`, which then
50
62
  // reaches the client as literal tag bytes. The field itself is the stream
51
63
  // for reasoning here, so the tags cannot carry structure — strip them.
52
64
  const reasoning =
53
- typeof reasoningRaw === "string" ? stripThinkingTags(reasoningRaw) : reasoningRaw;
65
+ typeof reasoningRaw === "string"
66
+ ? stripThinkingTags(reasoningMarkup?.text ?? reasoningRaw)
67
+ : reasoningRaw;
54
68
  if (typeof reasoning === "string" && reasoning.length > 0) {
55
69
  events.push({
56
70
  type: "content_delta",
@@ -62,8 +76,16 @@ export function parseChatResponseToEvents(
62
76
  });
63
77
  }
64
78
  const content = message["content"] as string | null;
65
- const recovered = typeof content === "string" ? parseDsmlToolCalls(content) : undefined;
66
- const visible = recovered === undefined ? content : recovered.text;
79
+ const recoveredMarkup =
80
+ typeof content === "string" && (request.tools?.length ?? 0) > 0
81
+ ? parseOmpToolCallMarkup(content, declaredToolNames)
82
+ : undefined;
83
+ const contentWithoutMarkup = recoveredMarkup?.text ?? content;
84
+ const recovered =
85
+ typeof contentWithoutMarkup === "string"
86
+ ? parseDsmlToolCalls(contentWithoutMarkup)
87
+ : undefined;
88
+ const visible = recovered === undefined ? contentWithoutMarkup : recovered.text;
67
89
  if (typeof visible === "string" && visible.includes("<think")) {
68
90
  const think = new MimoThinkSplitter();
69
91
  const split = think.push(visible);
@@ -104,9 +126,39 @@ export function parseChatResponseToEvents(
104
126
  });
105
127
  }
106
128
  const structuredCalls = message["tool_calls"];
107
- const toolCalls = Array.isArray(structuredCalls) && structuredCalls.length > 0
129
+ const hasStructuredCalls =
130
+ Array.isArray(structuredCalls) &&
131
+ structuredCalls.some((candidate) => {
132
+ if (typeof candidate !== "object" || candidate === null) return false;
133
+ const record = candidate as Record<string, unknown>;
134
+ const fn =
135
+ typeof record["function"] === "object" && record["function"] !== null
136
+ ? (record["function"] as Record<string, unknown>)
137
+ : undefined;
138
+ const callId = typeof record["id"] === "string" && record["id"].length > 0;
139
+ const name =
140
+ typeof fn?.["name"] === "string" && fn["name"].length > 0
141
+ ? fn["name"]
142
+ : typeof record["name"] === "string"
143
+ ? record["name"]
144
+ : undefined;
145
+ return callId && name !== undefined && name.length > 0;
146
+ });
147
+ const toolCalls = hasStructuredCalls
108
148
  ? structuredCalls
109
- : recovered?.calls ?? [];
149
+ : recoveredMarkup?.calls.some((call) => callableToolNames.has(call.name))
150
+ ? recoveredMarkup.calls
151
+ .filter((call) => callableToolNames.has(call.name))
152
+ .map((call) => ({
153
+ id: call.id,
154
+ function: { name: call.name, arguments: call.arguments },
155
+ }))
156
+ : recovered?.calls ?? [];
157
+ if (!hasStructuredCalls && recoveredMarkup !== undefined) {
158
+ recoveredToolCalls = recoveredMarkup.calls.filter((call) =>
159
+ callableToolNames.has(call.name),
160
+ ).length;
161
+ }
110
162
  for (const tc of toolCalls) {
111
163
  if (typeof tc !== "object" || tc === null) continue;
112
164
  const record = tc as Record<string, unknown>;
@@ -166,7 +218,7 @@ export function parseChatResponseToEvents(
166
218
  sequenceNumber: events.length + 1,
167
219
  responseId: id,
168
220
  state: "complete",
169
- stopReason: chatJsonStopReason,
221
+ stopReason: recoveredToolCalls > 0 ? "tool_use" : chatJsonStopReason,
170
222
  providerStopReason: chatJsonProviderStopReason,
171
223
  usage: usageRecord,
172
224
  }),
@@ -194,9 +246,17 @@ export async function* decodeChatSseStream(
194
246
  const lastToolNameByIndex = new Map<number, string>();
195
247
  let lastSeenToolId: string | undefined;
196
248
  let lastSeenToolName: string | undefined;
249
+ let structuredToolCallEmitted = false;
250
+ const recoveredMarkupCalls: OmpTextToolCall[] = [];
197
251
  // Single emission ledger: one entry per logical call.
198
252
  const ledger = createToolEmitLedger();
199
253
  const think = new MimoThinkSplitter();
254
+ const declaredToolNames = declaredToolNamesForRequest(request);
255
+ const callableToolNames = callableToolNamesForRequest(request);
256
+ const contentMarkup =
257
+ (request.tools?.length ?? 0) > 0 ? new OmpToolCallMarkupParser(declaredToolNames) : undefined;
258
+ const reasoningMarkup =
259
+ (request.tools?.length ?? 0) > 0 ? new OmpToolCallMarkupParser(declaredToolNames) : undefined;
200
260
  yield {
201
261
  type: "response_start",
202
262
  sequence_number: seq++,
@@ -240,8 +300,10 @@ export async function* decodeChatSseStream(
240
300
  // text/thinking blocks.
241
301
  if (typeof delta?.["content"] === "string" && delta["content"].length > 0) {
242
302
  const raw = delta["content"] as string;
243
- const recovered = parseDsmlToolCalls(raw);
244
- const visible = recovered?.text ?? raw;
303
+ const markup = contentMarkup?.push(raw);
304
+ if (markup !== undefined) recoveredMarkupCalls.push(...markup.calls);
305
+ const recovered = parseDsmlToolCalls(markup?.text ?? raw);
306
+ const visible = recovered?.text ?? markup?.text ?? raw;
245
307
  const split = think.push(visible);
246
308
  if (split.reasoning.length > 0) {
247
309
  yield {
@@ -270,12 +332,16 @@ export async function* decodeChatSseStream(
270
332
  }
271
333
  }
272
334
  const reasoningRaw = delta ? readReasoningText(delta) : undefined;
335
+ const reasoningMarkupResult =
336
+ typeof reasoningRaw === "string" ? reasoningMarkup?.push(reasoningRaw) : undefined;
273
337
  // Strip stray `<think>` tags the upstream double-wrapped inside its own
274
338
  // reasoning stream (buddy's hy4-preview-f leaks them per-delta). Done
275
339
  // per delta rather than once at the end because an open-ended tag that
276
340
  // spans multiple deltas is a splitter job, not this one.
277
341
  const reasoningText =
278
- typeof reasoningRaw === "string" ? stripThinkingTags(reasoningRaw) : reasoningRaw;
342
+ typeof reasoningRaw === "string"
343
+ ? stripThinkingTags(reasoningMarkupResult?.text ?? reasoningRaw)
344
+ : reasoningRaw;
279
345
  if (typeof reasoningText === "string" && reasoningText.length > 0) {
280
346
  yield {
281
347
  type: "content_delta",
@@ -353,6 +419,7 @@ export async function* decodeChatSseStream(
353
419
  ledger.isDuplicateDefinition(id, fragName, args);
354
420
  if (!duplicate) {
355
421
  if (establishedNew) ledger.claimFirstEmission(id, fragName, args);
422
+ structuredToolCallEmitted = true;
356
423
  yield {
357
424
  type: "tool_call_delta",
358
425
  sequence_number: seq++,
@@ -390,6 +457,38 @@ export async function* decodeChatSseStream(
390
457
  );
391
458
  }
392
459
  }
460
+ const contentMarkupTail = contentMarkup?.flush();
461
+ if (contentMarkupTail !== undefined) {
462
+ recoveredMarkupCalls.push(...contentMarkupTail.calls);
463
+ if (contentMarkupTail.text.length > 0) {
464
+ const split = think.push(contentMarkupTail.text);
465
+ if (split.reasoning.length > 0) {
466
+ yield {
467
+ type: "content_delta",
468
+ sequence_number: seq++,
469
+ content: { kind: "reasoning", payload: null, summary: split.reasoning },
470
+ } as CanonicalEvent;
471
+ }
472
+ if (split.text.length > 0) {
473
+ yield {
474
+ type: "content_delta",
475
+ sequence_number: seq++,
476
+ content: { kind: "text", text: split.text },
477
+ } as CanonicalEvent;
478
+ }
479
+ }
480
+ }
481
+ const reasoningMarkupTail = reasoningMarkup?.flush();
482
+ if (reasoningMarkupTail?.text) {
483
+ const reasoningText = stripThinkingTags(reasoningMarkupTail.text);
484
+ if (reasoningText.length > 0) {
485
+ yield {
486
+ type: "content_delta",
487
+ sequence_number: seq++,
488
+ content: { kind: "reasoning", payload: null, summary: reasoningText },
489
+ } as CanonicalEvent;
490
+ }
491
+ }
393
492
  const rest = think.flush();
394
493
  if (rest.reasoning.length > 0) {
395
494
  yield {
@@ -405,6 +504,20 @@ export async function* decodeChatSseStream(
405
504
  content: { kind: "text", text: rest.text },
406
505
  } as CanonicalEvent;
407
506
  }
507
+ let recoveredMarkupToolCallsEmitted = 0;
508
+ if (!structuredToolCallEmitted) {
509
+ for (const call of recoveredMarkupCalls) {
510
+ if (!callableToolNames.has(call.name)) continue;
511
+ yield {
512
+ type: "tool_call_delta",
513
+ sequence_number: seq++,
514
+ call_id: call.id,
515
+ name: call.name,
516
+ arguments_delta: call.arguments,
517
+ } as CanonicalEvent;
518
+ recoveredMarkupToolCallsEmitted += 1;
519
+ }
520
+ }
408
521
  const chatStopReason = mapChatStopReason(finishReason);
409
522
  const chatProviderStopReason =
410
523
  typeof finishReason === "string" ? finishReason : undefined;
@@ -423,8 +536,28 @@ export async function* decodeChatSseStream(
423
536
  // `stop`. `chatFinishReason(undefined)` falls back to `stop`, so the client
424
537
  // saw a completed turn while telemetry recorded a 502 upstream transport
425
538
  // failure — the failure this terminal already carries was invisible to it.
426
- stopReason: truncated && signal?.aborted !== true ? "error" : chatStopReason,
539
+ stopReason:
540
+ truncated && signal?.aborted !== true
541
+ ? "error"
542
+ : !structuredToolCallEmitted && recoveredMarkupToolCallsEmitted > 0
543
+ ? "tool_use"
544
+ : chatStopReason,
427
545
  providerStopReason: chatProviderStopReason,
428
546
  usage: usageFromProvider(rawUsage),
429
547
  });
430
548
  }
549
+
550
+ function declaredToolNamesForRequest(request: CanonicalRequest): ReadonlySet<string> {
551
+ return new Set((request.tools ?? []).map((tool) => tool.name));
552
+ }
553
+
554
+ function callableToolNamesForRequest(request: CanonicalRequest): ReadonlySet<string> {
555
+ const declared = declaredToolNamesForRequest(request);
556
+ const choice = request.tool_choice;
557
+ if (choice === "none") return new Set();
558
+ if (choice === undefined || typeof choice === "string") return declared;
559
+ if (choice.type === "tool" || choice.type === "custom") {
560
+ return declared.has(choice.name) ? new Set([choice.name]) : new Set();
561
+ }
562
+ return new Set(choice.names.filter((name) => declared.has(name)));
563
+ }
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Recovers the tool-call envelope some models occasionally emit as assistant
3
+ * text instead of OpenAI `tool_calls`.
4
+ *
5
+ * The wire shape is the captured `<tool_calls:8hex>` / `<tool_call:8hex>` /
6
+ * `<arg_key:8hex>` / `<arg_value:8hex>` form. Only complete envelopes are
7
+ * interpreted. Calls are returned only when their names were declared on the
8
+ * request; an undeclared name is never promoted into executable tool input.
9
+ */
10
+ export interface OmpTextToolCall {
11
+ readonly id: string;
12
+ readonly name: string;
13
+ readonly arguments: string;
14
+ }
15
+
16
+ /** Text remaining after complete tool envelopes are removed. */
17
+ export interface OmpTextToolCallResult {
18
+ readonly text: string;
19
+ readonly calls: readonly OmpTextToolCall[];
20
+ }
21
+
22
+ const OPEN_PREFIX = "<tool_calls:";
23
+ const OPEN_PATTERN = /^<tool_calls:([0-9a-f]{8})>/;
24
+ const TOOL_NAME_PATTERN = /^[A-Za-z0-9_][A-Za-z0-9_.:-]{0,63}$/;
25
+
26
+ /**
27
+ * Incrementally removes complete captured tool envelopes from text.
28
+ *
29
+ * The parser holds a possible envelope across SSE deltas, so a split opening
30
+ * tag, argument tag, or closing tag is never emitted as visible prose. At
31
+ * end-of-stream an unterminated envelope is dropped, not executed or leaked.
32
+ */
33
+ export class OmpToolCallMarkupParser {
34
+ readonly #allowedNames: ReadonlySet<string>;
35
+ #pending = "";
36
+ #callSequence = 0;
37
+
38
+ constructor(allowedNames: ReadonlySet<string>) {
39
+ this.#allowedNames = allowedNames;
40
+ }
41
+
42
+ /** Feeds one content or reasoning delta and returns only settled text. */
43
+ push(chunk: string): OmpTextToolCallResult {
44
+ this.#pending += chunk;
45
+ let cursor = 0;
46
+ let text = "";
47
+ const calls: OmpTextToolCall[] = [];
48
+
49
+ for (;;) {
50
+ const openIndex = this.#pending.indexOf(OPEN_PREFIX, cursor);
51
+ if (openIndex < 0) {
52
+ const safeEnd = this.#settledEnd(cursor);
53
+ text += this.#pending.slice(cursor, safeEnd);
54
+ cursor = safeEnd;
55
+ break;
56
+ }
57
+
58
+ text += this.#pending.slice(cursor, openIndex);
59
+ const remainder = this.#pending.slice(openIndex);
60
+ const opening = OPEN_PATTERN.exec(remainder);
61
+ if (opening === null || opening[1] === undefined) {
62
+ if (this.#couldBeOpening(remainder)) {
63
+ cursor = openIndex;
64
+ break;
65
+ }
66
+ text += "<";
67
+ cursor = openIndex + 1;
68
+ continue;
69
+ }
70
+
71
+ const marker = opening[1];
72
+ const openTag = opening[0];
73
+ const closeTag = `</tool_calls:${marker}>`;
74
+ const closeIndex = this.#pending.indexOf(closeTag, openIndex + openTag.length);
75
+ if (closeIndex < 0) {
76
+ cursor = openIndex;
77
+ break;
78
+ }
79
+
80
+ const envelopeEnd = closeIndex + closeTag.length;
81
+ const block = this.#pending.slice(openIndex, envelopeEnd);
82
+ const parsed = parseEnvelope(block, marker);
83
+ if (parsed === undefined) {
84
+ text += block;
85
+ } else if (parsed.some((call) => !this.#allowedNames.has(call.name))) {
86
+ // A syntactically valid call to an undeclared tool is model text, not
87
+ // authority to invoke a tool the client did not offer.
88
+ text += block;
89
+ } else {
90
+ for (const call of parsed) {
91
+ calls.push({
92
+ id: `call_omp_${marker}_${this.#callSequence++}`,
93
+ name: call.name,
94
+ arguments: call.arguments,
95
+ });
96
+ }
97
+ }
98
+ cursor = envelopeEnd;
99
+ }
100
+
101
+ this.#pending = this.#pending.slice(cursor);
102
+ return { text, calls };
103
+ }
104
+
105
+ /**
106
+ * Settles trailing text. A complete internal envelope cannot remain here;
107
+ * an unfinished envelope is deliberately discarded rather than leaked.
108
+ */
109
+ flush(): OmpTextToolCallResult {
110
+ const pending = this.#pending;
111
+ this.#pending = "";
112
+ if (this.#couldBeOpening(pending) || OPEN_PATTERN.test(pending)) {
113
+ return { text: "", calls: [] };
114
+ }
115
+ return { text: pending, calls: [] };
116
+ }
117
+
118
+ #settledEnd(start: number): number {
119
+ const remaining = this.#pending.slice(start);
120
+ const maxPrefixLength = Math.min(OPEN_PREFIX.length - 1, remaining.length);
121
+ for (let length = maxPrefixLength; length > 0; length -= 1) {
122
+ if (OPEN_PREFIX.startsWith(remaining.slice(-length))) {
123
+ return this.#pending.length - length;
124
+ }
125
+ }
126
+ return this.#pending.length;
127
+ }
128
+
129
+ #couldBeOpening(value: string): boolean {
130
+ if (value.length < OPEN_PREFIX.length) return OPEN_PREFIX.startsWith(value);
131
+ if (!value.startsWith(OPEN_PREFIX)) return false;
132
+ const suffix = value.slice(OPEN_PREFIX.length);
133
+ return /^[0-9a-f]{0,8}$/.test(suffix) || /^[0-9a-f]{8}$/.test(suffix) && !value.endsWith(">");
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Parses complete envelopes from one settled response field.
139
+ *
140
+ * The same parser backs JSON responses and streaming responses so recovery
141
+ * semantics do not differ merely because the upstream selected `stream`.
142
+ */
143
+ export function parseOmpToolCallMarkup(
144
+ text: string,
145
+ allowedNames: ReadonlySet<string>,
146
+ ): OmpTextToolCallResult {
147
+ const parser = new OmpToolCallMarkupParser(allowedNames);
148
+ const first = parser.push(text);
149
+ const last = parser.flush();
150
+ return { text: first.text + last.text, calls: first.calls };
151
+ }
152
+
153
+ /**
154
+ * Parses a complete captured tool envelope, returning undefined for malformed
155
+ * structure. Argument values are JSON-decoded when they contain valid JSON;
156
+ * otherwise their exact text is preserved as a string.
157
+ */
158
+ function parseEnvelope(block: string, marker: string): readonly Omit<OmpTextToolCall, "id">[] | undefined {
159
+ const openTag = `<tool_calls:${marker}>`;
160
+ const closeTag = `</tool_calls:${marker}>`;
161
+ if (!block.startsWith(openTag) || !block.endsWith(closeTag)) return undefined;
162
+
163
+ const body = block.slice(openTag.length, -closeTag.length);
164
+ const calls: Omit<OmpTextToolCall, "id">[] = [];
165
+ const callPattern = new RegExp(`<tool_call:${marker}>([\\s\\S]*?)</tool_call:${marker}>`, "g");
166
+ let cursor = 0;
167
+
168
+ for (const match of body.matchAll(callPattern)) {
169
+ const index = match.index;
170
+ const full = match[0];
171
+ const callBody = match[1];
172
+ if (index === undefined || full === undefined || callBody === undefined) return undefined;
173
+ if (body.slice(cursor, index).trim().length > 0) return undefined;
174
+
175
+ const keyTag = `<arg_key:${marker}>`;
176
+ const firstKey = callBody.indexOf(keyTag);
177
+ const name = (firstKey < 0 ? callBody : callBody.slice(0, firstKey)).trim();
178
+ if (!TOOL_NAME_PATTERN.test(name)) return undefined;
179
+
180
+ const argumentBody = firstKey < 0 ? "" : callBody.slice(firstKey);
181
+ const pairPattern = new RegExp(
182
+ `<arg_key:${marker}>([\\s\\S]*?)</arg_key:${marker}>\\s*<arg_value:${marker}>([\\s\\S]*?)</arg_value:${marker}>`,
183
+ "g",
184
+ );
185
+ const argumentsObject: Record<string, unknown> = {};
186
+ let argumentCursor = 0;
187
+ for (const pair of argumentBody.matchAll(pairPattern)) {
188
+ const pairIndex = pair.index;
189
+ const pairFull = pair[0];
190
+ const key = pair[1]?.trim();
191
+ const value = pair[2];
192
+ if (
193
+ pairIndex === undefined ||
194
+ pairFull === undefined ||
195
+ key === undefined ||
196
+ key.length === 0 ||
197
+ value === undefined ||
198
+ argumentBody.slice(argumentCursor, pairIndex).trim().length > 0
199
+ ) {
200
+ return undefined;
201
+ }
202
+ argumentsObject[key] = parseArgumentValue(value);
203
+ argumentCursor = pairIndex + pairFull.length;
204
+ }
205
+ if (argumentBody.slice(argumentCursor).trim().length > 0) return undefined;
206
+
207
+ calls.push({ name, arguments: JSON.stringify(argumentsObject) });
208
+ cursor = index + full.length;
209
+ }
210
+
211
+ if (calls.length === 0 || body.slice(cursor).trim().length > 0) return undefined;
212
+ return calls;
213
+ }
214
+
215
+ function parseArgumentValue(value: string): unknown {
216
+ try {
217
+ return JSON.parse(value) as unknown;
218
+ } catch {
219
+ return value;
220
+ }
221
+ }