cartethyia 2.4.5 → 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 (415) hide show
  1. package/.env.example +4 -1
  2. package/README.md +63 -13
  3. package/dist/dashboard/assets/AccountCooldown-ChpdzxRH.js.gz +0 -0
  4. package/dist/dashboard/assets/BannedPage-9igfctfZ.js +1 -0
  5. package/dist/dashboard/assets/BannedPage-9igfctfZ.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolDetailPage-BzGevuql.js → CliToolDetailPage-ZRr2K2sF.js} +1 -1
  7. package/dist/dashboard/assets/CliToolDetailPage-ZRr2K2sF.js.gz +0 -0
  8. package/dist/dashboard/assets/{CliToolsPage-BaRM_7We.js → CliToolsPage-D3xqZbN-.js} +1 -1
  9. package/dist/dashboard/assets/CliToolsPage-D3xqZbN-.js.gz +0 -0
  10. package/dist/dashboard/assets/CombosPage-CVnQPBBz.js +1 -0
  11. package/dist/dashboard/assets/CombosPage-CVnQPBBz.js.gz +0 -0
  12. package/dist/dashboard/assets/ConsoleLogPage-u6W1UXSc.js +1 -0
  13. package/dist/dashboard/assets/ConsoleLogPage-u6W1UXSc.js.gz +0 -0
  14. package/dist/dashboard/assets/{CustomizationPage-DqUGcoH0.js → CustomizationPage-CTTpCb25.js} +1 -1
  15. package/dist/dashboard/assets/CustomizationPage-CTTpCb25.js.gz +0 -0
  16. package/dist/dashboard/assets/HealthEventsModal-C5XpWlvy.js +1 -0
  17. package/dist/dashboard/assets/HealthEventsModal-C5XpWlvy.js.gz +0 -0
  18. package/dist/dashboard/assets/{LoginPage-DbhTGfhb.js → LoginPage-DHoB3Agn.js} +1 -1
  19. package/dist/dashboard/assets/LoginPage-DHoB3Agn.js.gz +0 -0
  20. package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js +1 -0
  21. package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js.gz +0 -0
  22. package/dist/dashboard/assets/OverviewPage-BrY4SgIt.js +1 -0
  23. package/dist/dashboard/assets/OverviewPage-BrY4SgIt.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-D9VSt7T_.js +3 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-D9VSt7T_.js.gz +0 -0
  26. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js +1 -0
  27. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js.gz +0 -0
  28. package/dist/dashboard/assets/ProvidersPage-Df77V5I5.js +1 -0
  29. package/dist/dashboard/assets/ProvidersPage-Df77V5I5.js.gz +0 -0
  30. package/dist/dashboard/assets/ProxyPage-CDfxP7AC.js +7 -0
  31. package/dist/dashboard/assets/ProxyPage-CDfxP7AC.js.gz +0 -0
  32. package/dist/dashboard/assets/QuotaPage-CY4o2t4p.js +1 -0
  33. package/dist/dashboard/assets/QuotaPage-CY4o2t4p.js.gz +0 -0
  34. package/dist/dashboard/assets/{SettingsPage-YxRml8AT.js → SettingsPage-CvjZxHE4.js} +1 -1
  35. package/dist/dashboard/assets/SettingsPage-CvjZxHE4.js.gz +0 -0
  36. package/dist/dashboard/assets/SetupPage-D3nza7LH.js +1 -0
  37. package/dist/dashboard/assets/SetupPage-D3nza7LH.js.gz +0 -0
  38. package/dist/dashboard/assets/{SortableList-Bol1ASmI.js → SortableList-Dtt12WT1.js} +1 -1
  39. package/dist/dashboard/assets/SortableList-Dtt12WT1.js.gz +0 -0
  40. package/dist/dashboard/assets/StudioPage-CbK3hXvU.js +31 -0
  41. package/dist/dashboard/assets/StudioPage-CbK3hXvU.js.gz +0 -0
  42. package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js.gz +0 -0
  43. package/dist/dashboard/assets/UsagePage-BQlWbWg2.js +56 -0
  44. package/dist/dashboard/assets/UsagePage-BQlWbWg2.js.gz +0 -0
  45. package/dist/dashboard/assets/api-keys-BmDbsior.js +1 -0
  46. package/dist/dashboard/assets/api-keys-BmDbsior.js.gz +0 -0
  47. package/dist/dashboard/assets/app-BbpSarVV.css +2 -0
  48. package/dist/dashboard/assets/app-BbpSarVV.css.gz +0 -0
  49. package/dist/dashboard/assets/{app-BDJWMThu.js → app-C-Y4oZT0.js} +3 -3
  50. package/dist/dashboard/assets/app-C-Y4oZT0.js.gz +0 -0
  51. package/dist/dashboard/assets/arrow-left-CpBxkoxI.js.gz +0 -0
  52. package/dist/dashboard/assets/arrow-right-Dl5yKnp8.js.gz +0 -0
  53. package/dist/dashboard/assets/assert-BHfaei-X.js +1 -0
  54. package/dist/dashboard/assets/assert-BHfaei-X.js.gz +0 -0
  55. package/dist/dashboard/assets/backup-file-gdKIlg8U.js.gz +0 -0
  56. package/dist/dashboard/assets/{badge-BwmwEOgA.js → badge-DpXT4Ixt.js} +1 -1
  57. package/dist/dashboard/assets/badge-DpXT4Ixt.js.gz +0 -0
  58. package/dist/dashboard/assets/{banner-BKBfKTqn.js → banner-CSAAWLJv.js} +1 -1
  59. package/dist/dashboard/assets/banner-CSAAWLJv.js.gz +0 -0
  60. package/dist/dashboard/assets/brain-Cwed_72z.js.gz +0 -0
  61. package/dist/dashboard/assets/{card-BTdbRFFg.js → card-DEhRXXw4.js} +1 -1
  62. package/dist/dashboard/assets/card-DEhRXXw4.js.gz +0 -0
  63. package/dist/dashboard/assets/check-D69FzdvC.js.gz +0 -0
  64. package/dist/dashboard/assets/chevron-right-BXIy5l3h.js.gz +0 -0
  65. package/dist/dashboard/assets/circle-alert-CntYHV5v.js.gz +0 -0
  66. package/dist/dashboard/assets/circle-check-BU3v_mFW.js.gz +0 -0
  67. package/dist/dashboard/assets/cli-tools-B0wBq-RL.js +1 -0
  68. package/dist/dashboard/assets/cli-tools-B0wBq-RL.js.gz +0 -0
  69. package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js +1 -0
  70. package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js.gz +0 -0
  71. package/dist/dashboard/assets/cog-CdWhsajs.js.gz +0 -0
  72. package/dist/dashboard/assets/copy-ngmQ9zcZ.js.gz +0 -0
  73. package/dist/dashboard/assets/{dialog-iocKA3jr.js → dialog-BvQeBMfP.js} +1 -1
  74. package/dist/dashboard/assets/dialog-BvQeBMfP.js.gz +0 -0
  75. package/dist/dashboard/assets/dialog-actions-B1axpEg7.js.gz +0 -0
  76. package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
  77. package/dist/dashboard/assets/download-dRFo5_Wu.js.gz +0 -0
  78. package/dist/dashboard/assets/{drawer-CDKOTZdY.js → drawer-CMwPXwQB.js} +1 -1
  79. package/dist/dashboard/assets/drawer-CMwPXwQB.js.gz +0 -0
  80. package/dist/dashboard/assets/entry-D4O2229X.js +1 -0
  81. package/dist/dashboard/assets/entry-D4O2229X.js.gz +0 -0
  82. package/dist/dashboard/assets/entry-DKvr5Q9K.js +1 -0
  83. package/dist/dashboard/assets/entry-DKvr5Q9K.js.gz +0 -0
  84. package/dist/dashboard/assets/entry-FQCejoDY.js +4 -0
  85. package/dist/dashboard/assets/entry-FQCejoDY.js.gz +0 -0
  86. package/dist/dashboard/assets/eye-C_hawfH4.js.gz +0 -0
  87. package/dist/dashboard/assets/eye-off-Bk5g2luF.js.gz +0 -0
  88. package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
  89. package/dist/dashboard/assets/gauge-BbIMy7MB.js.gz +0 -0
  90. package/dist/dashboard/assets/github-badge-zliLvBwr.js +1 -0
  91. package/dist/dashboard/assets/github-badge-zliLvBwr.js.gz +0 -0
  92. package/dist/dashboard/assets/image-BPaouViG.js.gz +0 -0
  93. package/dist/dashboard/assets/{index-C4hXfAMs.js → index-CXm3FIyt.js} +2 -2
  94. package/dist/dashboard/assets/index-CXm3FIyt.js.gz +0 -0
  95. package/dist/dashboard/assets/{inline-DCejSLlA.js → inline-D1IlZZ8H.js} +1 -1
  96. package/dist/dashboard/assets/inline-D1IlZZ8H.js.gz +0 -0
  97. package/dist/dashboard/assets/{input-Bn4_kNav.js → input-BpVZdrbc.js} +1 -1
  98. package/dist/dashboard/assets/input-BpVZdrbc.js.gz +0 -0
  99. package/dist/dashboard/assets/jsx-runtime-CXUMngI_.js.gz +0 -0
  100. package/dist/dashboard/assets/key-round-UkavsGxp.js.gz +0 -0
  101. package/dist/dashboard/assets/layers-2-glBbjl.js.gz +0 -0
  102. package/dist/dashboard/assets/{layout-Bv-NxqZ5.js → layout-BioLW7gN.js} +1 -1
  103. package/dist/dashboard/assets/layout-BioLW7gN.js.gz +0 -0
  104. package/dist/dashboard/assets/{list-row-BLA0sldD.js → list-row-CwVcGAIc.js} +1 -1
  105. package/dist/dashboard/assets/list-row-CwVcGAIc.js.gz +0 -0
  106. package/dist/dashboard/assets/{live-YlifPYox.js → live-yfEJFF9T.js} +1 -1
  107. package/dist/dashboard/assets/live-yfEJFF9T.js.gz +0 -0
  108. package/dist/dashboard/assets/log-in-BmRjy60c.js.gz +0 -0
  109. package/dist/dashboard/assets/message-circle-CCxGwRvv.js.gz +0 -0
  110. package/dist/dashboard/assets/model-bans-CT-TxSNX.js +1 -0
  111. package/dist/dashboard/assets/model-bans-CT-TxSNX.js.gz +0 -0
  112. package/dist/dashboard/assets/mutation-BkoLM5Ba.js +1 -0
  113. package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
  114. package/dist/dashboard/assets/numbers-CFL4_Dqc.js.gz +0 -0
  115. package/dist/dashboard/assets/pencil-768ysRAr.js.gz +0 -0
  116. package/dist/dashboard/assets/plus-DIdX_g-x.js.gz +0 -0
  117. package/dist/dashboard/assets/power-off-Yjw9GMJh.js +1 -0
  118. package/dist/dashboard/assets/power-off-Yjw9GMJh.js.gz +0 -0
  119. package/dist/dashboard/assets/privacy-mode-GM8bej85.js +1 -0
  120. package/dist/dashboard/assets/privacy-mode-GM8bej85.js.gz +0 -0
  121. package/dist/dashboard/assets/providers/agentrouter.svg.gz +0 -0
  122. package/dist/dashboard/assets/providers/aihubmix.svg.gz +0 -0
  123. package/dist/dashboard/assets/providers/alibaba-coding-plan.svg.gz +0 -0
  124. package/dist/dashboard/assets/providers/alibaba.svg.gz +0 -0
  125. package/dist/dashboard/assets/providers/antigravity.svg.gz +0 -0
  126. package/dist/dashboard/assets/providers/bai.svg.gz +0 -0
  127. package/dist/dashboard/assets/providers/blackbox.svg.gz +0 -0
  128. package/dist/dashboard/assets/providers/claude-code.svg.gz +0 -0
  129. package/dist/dashboard/assets/providers/cursor.webp +0 -0
  130. package/dist/dashboard/assets/providers/exa.svg.gz +0 -0
  131. package/dist/dashboard/assets/providers/gmi-cloud.svg.gz +0 -0
  132. package/dist/dashboard/assets/providers/inferhub.svg.gz +0 -0
  133. package/dist/dashboard/assets/providers/muse.svg.gz +0 -0
  134. package/dist/dashboard/assets/providers/nous-research.svg.gz +0 -0
  135. package/dist/dashboard/assets/providers/openai-light.svg.gz +0 -0
  136. package/dist/dashboard/assets/providers/parallel.svg.gz +0 -0
  137. package/dist/dashboard/assets/providers/tokenharbor.svg.gz +0 -0
  138. package/dist/dashboard/assets/providers/trae.webp +0 -0
  139. package/dist/dashboard/assets/providers/zai-coding-plan.svg.gz +0 -0
  140. package/dist/dashboard/assets/{query-keys-ROf-g1Wi.js → query-keys-D7wX2vM5.js} +1 -1
  141. package/dist/dashboard/assets/query-keys-D7wX2vM5.js.gz +0 -0
  142. package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
  143. package/dist/dashboard/assets/refresh-cw-BqJFk4sU.js.gz +0 -0
  144. package/dist/dashboard/assets/rotate-ccw-CufcqITi.js.gz +0 -0
  145. package/dist/dashboard/assets/route-CNSNj9Zm.js.gz +0 -0
  146. package/dist/dashboard/assets/routing-Cs6jl49L.js +1 -0
  147. package/dist/dashboard/assets/routing-Cs6jl49L.js.gz +0 -0
  148. package/dist/dashboard/assets/scaling-nWE4Cx8f.js.gz +0 -0
  149. package/dist/dashboard/assets/search-JOKus9rb.js.gz +0 -0
  150. package/dist/dashboard/assets/select-C0Rer3gF.js +41 -0
  151. package/dist/dashboard/assets/select-C0Rer3gF.js.gz +0 -0
  152. package/dist/dashboard/assets/settings-CfcTcOyU.js +1 -0
  153. package/dist/dashboard/assets/settings-CfcTcOyU.js.gz +0 -0
  154. package/dist/dashboard/assets/shield-check-DVS0Foqh.js.gz +0 -0
  155. package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
  156. package/dist/dashboard/assets/{stack-C1kTcOmP.js → stack-C9_uUjaA.js} +1 -1
  157. package/dist/dashboard/assets/stack-C9_uUjaA.js.gz +0 -0
  158. package/dist/dashboard/assets/{state-DHKdg-pm.js → state-DA0B95UQ.js} +1 -1
  159. package/dist/dashboard/assets/state-DA0B95UQ.js.gz +0 -0
  160. package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
  161. package/dist/dashboard/assets/{switch-DIDeAiz9.js → switch-Cittxy-x.js} +1 -1
  162. package/dist/dashboard/assets/switch-Cittxy-x.js.gz +0 -0
  163. package/dist/dashboard/assets/terminal-Bd4ee8TA.js +1 -0
  164. package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
  165. package/dist/dashboard/assets/toast-CNt_jzO4.js +2 -0
  166. package/dist/dashboard/assets/toast-CNt_jzO4.js.gz +0 -0
  167. package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
  168. package/dist/dashboard/assets/trash-Cppn5c3R.js.gz +0 -0
  169. package/dist/dashboard/assets/use-clipboard-B0Sy03fE.js.gz +0 -0
  170. package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
  171. package/dist/dashboard/assets/use-reduced-motion-C4B0R6Sz.js.gz +0 -0
  172. package/dist/dashboard/assets/use-timeout-BwcBBRrT.js.gz +0 -0
  173. package/dist/dashboard/assets/{useMutation-DVmwOKU-.js → useMutation-BQKMcJ1H.js} +1 -1
  174. package/dist/dashboard/assets/useMutation-BQKMcJ1H.js.gz +0 -0
  175. package/dist/dashboard/assets/wrench-B6E1oEuY.js.gz +0 -0
  176. package/dist/dashboard/index.html +1 -1
  177. package/dist/dashboard/index.html.gz +0 -0
  178. package/dist/dashboard/precompressed-assets.json +122 -0
  179. package/migrations/full-postgres/0013_add_providers_tenant_id_index.sql +22 -0
  180. package/migrations/full-postgres/0014_add_provider_accounts_tenant_id_index.sql +21 -0
  181. package/migrations/full-postgres/0015_add_payload_stages.sql +18 -0
  182. package/migrations/full-postgres/0016_add_share_link_slug.sql +20 -0
  183. package/migrations/full-postgres/0017_add_models_pro_only.sql +18 -0
  184. package/migrations/full-postgres/0018_add_accounts_quota_plan.sql +18 -0
  185. package/migrations/full-postgres/0019_add_models_free_tier.sql +15 -0
  186. package/migrations/lite-sqlite3/0000_baseline.sql +4 -2
  187. package/migrations/lite-sqlite3/0013_add_providers_tenant_id_index.sql +4 -0
  188. package/migrations/lite-sqlite3/0014_add_provider_accounts_tenant_id_index.sql +4 -0
  189. package/migrations/lite-sqlite3/0015_add_payload_stages.sql +18 -0
  190. package/migrations/lite-sqlite3/0016_add_share_link_slug.sql +20 -0
  191. package/migrations/lite-sqlite3/0017_add_models_pro_only.sql +18 -0
  192. package/migrations/lite-sqlite3/0018_add_accounts_quota_plan.sql +18 -0
  193. package/migrations/lite-sqlite3/0019_add_models_free_tier.sql +15 -0
  194. package/package.json +26 -30
  195. package/scripts/cli/{carte-launcher.cjs → bin/carte.cjs} +8 -8
  196. package/scripts/cli/{cli-launcher.cjs → bin/cartethyia.cjs} +167 -134
  197. package/scripts/cli/command/admin.ts +65 -0
  198. package/scripts/cli/command/console-menu.ts +246 -0
  199. package/scripts/cli/{doctor.ts → command/doctor.ts} +257 -258
  200. package/scripts/cli/{setup.ts → command/setup.ts} +8 -12
  201. package/scripts/cli/command/start.ts +77 -0
  202. package/scripts/cli/command/tray.ts +165 -0
  203. package/scripts/cli/command/update.ts +23 -0
  204. package/scripts/cli/{runtime-state.ts → gateway/drain-token.ts} +78 -78
  205. package/scripts/cli/gateway/instance-lock.ts +105 -0
  206. package/scripts/cli/gateway/spawn.ts +173 -0
  207. package/scripts/cli/gateway/stop.ts +51 -0
  208. package/scripts/cli/main.ts +94 -0
  209. package/scripts/cli/support/bun.ts +27 -0
  210. package/scripts/cli/support/env.ts +99 -0
  211. package/scripts/cli/support/platform.ts +64 -0
  212. package/scripts/cli/support/services.ts +88 -0
  213. package/scripts/cli/support/startup-signal.ts +23 -0
  214. package/scripts/cli/{version.ts → support/version.ts} +1 -1
  215. package/scripts/cli/tray/hidden-window.vbs +28 -0
  216. package/scripts/cli/{tray.ts → tray/index.ts} +3 -3
  217. package/scripts/cli/{tray.ps1 → tray/windows.ps1} +107 -98
  218. package/scripts/cli/ui/background.ts +110 -0
  219. package/src/boot/app.ts +23 -7
  220. package/src/boot/dependencies.ts +55 -13
  221. package/src/boot/elysia-typebox-bridge.ts +37 -0
  222. package/src/boot/lifecycle.ts +2 -0
  223. package/src/config/cooldowns.ts +138 -138
  224. package/src/config/providers.ts +31 -5
  225. package/src/config/runtime.ts +19 -3
  226. package/src/config/spec.ts +20 -0
  227. package/src/console/api-keys/contracts.ts +414 -391
  228. package/src/console/api-keys/routes.ts +880 -870
  229. package/src/console/auth/account-reset-store.ts +14 -0
  230. package/src/console/auth/account-reset.ts +3 -2
  231. package/src/console/auth/setup-service.ts +6 -3
  232. package/src/console/backup/routes.ts +8 -0
  233. package/src/console/cli-tools/contracts.ts +6 -6
  234. package/src/console/console-router.ts +5 -3
  235. package/src/console/dashboard-assets.ts +81 -11
  236. package/src/console/domain-registration.ts +9 -2
  237. package/src/console/observability/contracts.ts +475 -423
  238. package/src/console/observability/routes.ts +31 -6
  239. package/src/console/observability/store.ts +1015 -912
  240. package/src/console/providers/catalog/contracts.ts +31 -0
  241. package/src/console/providers/catalog/provider-operations.ts +8 -0
  242. package/src/console/providers/catalog/public-model-store.ts +91 -60
  243. package/src/console/providers/catalog/store.ts +1532 -1521
  244. package/src/console/providers/oauth/callback-listener.ts +18 -3
  245. package/src/console/providers/oauth/routes.ts +38 -15
  246. package/src/console/quota/account-quota-tenant.ts +9 -6
  247. package/src/console/quota/refresh.ts +19 -0
  248. package/src/console/response-compression.ts +69 -2
  249. package/src/console/routing/pools/probe-verdict.ts +72 -0
  250. package/src/console/share/share-router.ts +7 -4
  251. package/src/console/share/share-stats.ts +12 -5
  252. package/src/console/shared/read-through-cache.ts +133 -7
  253. package/src/console/updates/repository-updates.ts +39 -34
  254. package/src/console/updates/routes.ts +162 -4
  255. package/src/main.ts +3 -0
  256. package/src/network/http2-bidi.ts +85 -0
  257. package/src/network/outbound-fetch.ts +87 -24
  258. package/src/network/pool/agent.ts +478 -460
  259. package/src/network/pool/resolver.ts +1104 -544
  260. package/src/network/pool-health-machine.ts +12 -5
  261. package/src/observability/event-log.ts +29 -8
  262. package/src/observability/metrics.ts +40 -13
  263. package/src/observability/payload-capture.ts +344 -324
  264. package/src/observability/runtime-metrics.ts +7 -0
  265. package/src/persistence/db-pg.ts +9 -3
  266. package/src/persistence/db-sqlite.ts +497 -495
  267. package/src/persistence/health-event-flush.ts +137 -0
  268. package/src/persistence/migrate.ts +181 -129
  269. package/src/persistence/schema.ts +1360 -1257
  270. package/src/persistence/settings-revision.ts +25 -0
  271. package/src/persistence/shared-cache.ts +217 -0
  272. package/src/persistence/stores/share-store.ts +556 -528
  273. package/src/persistence/usage-flush.ts +61 -1
  274. package/src/protocol/canonical-model.ts +23 -1
  275. package/src/protocol/response/chat.ts +143 -10
  276. package/src/protocol/response/codex.ts +9 -75
  277. package/src/protocol/response/omp-tool-call-markup.ts +221 -0
  278. package/src/protocol/tool-identity.ts +43 -9
  279. package/src/providers/authentication/jwt-validator.ts +130 -187
  280. package/src/providers/authentication/oauth-flow-store.ts +16 -9
  281. package/src/providers/default-registry.ts +177 -1
  282. package/src/providers/discovery/catalog/catalog-format.ts +20 -0
  283. package/src/providers/discovery/models-dev-catalog.ts +39 -22
  284. package/src/providers/discovery/openai-model-discovery.ts +39 -3
  285. package/src/providers/discovery/openrouter-free-roster.ts +98 -0
  286. package/src/providers/discovery/probe-wire.ts +29 -5
  287. package/src/providers/discovery/probing-service.ts +1022 -1001
  288. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +42 -2
  289. package/src/providers/integrations/buddy/buddy-chat-shared.ts +913 -914
  290. package/src/providers/integrations/buddy/buddy-cli-headers.ts +88 -0
  291. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +22 -5
  292. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +33 -1
  293. package/src/providers/integrations/buddy/buddy-identity.ts +46 -0
  294. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +37 -1
  295. package/src/providers/integrations/buddy/buddy-usage-store.ts +40 -5
  296. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +4 -0
  297. package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -175
  298. package/src/providers/integrations/buddy/codebuddy/shared.ts +115 -73
  299. package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +5 -2
  300. package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -204
  301. package/src/providers/integrations/buddy/workbuddy/quota.ts +172 -159
  302. package/src/providers/integrations/buddy/workbuddy/shared.ts +200 -171
  303. package/src/providers/integrations/claude/claude.ts +606 -582
  304. package/src/providers/integrations/cline/cline-quota.ts +18 -16
  305. package/src/providers/integrations/cline/cline.ts +539 -468
  306. package/src/providers/integrations/codex/codex.ts +5 -8
  307. package/src/providers/integrations/commandcode/commandcode.ts +675 -541
  308. package/src/providers/integrations/commandcode/discovery.ts +116 -0
  309. package/src/providers/integrations/commandcode/quota.ts +160 -0
  310. package/src/providers/integrations/cursor/cursor-auth.ts +418 -0
  311. package/src/providers/integrations/cursor/cursor-catalog.ts +368 -0
  312. package/src/providers/integrations/cursor/cursor-http2.ts +300 -0
  313. package/src/providers/integrations/cursor/cursor-identity.ts +92 -0
  314. package/src/providers/integrations/cursor/cursor-request.ts +471 -0
  315. package/src/providers/integrations/cursor/cursor-session-manager.ts +281 -0
  316. package/src/providers/integrations/cursor/cursor-shared.ts +108 -0
  317. package/src/providers/integrations/cursor/cursor.ts +960 -0
  318. package/src/providers/integrations/cursor/protocol/framing.ts +129 -0
  319. package/src/providers/integrations/cursor/protocol/messages.ts +2355 -0
  320. package/src/providers/integrations/cursor/protocol/struct.ts +305 -0
  321. package/src/providers/integrations/factory/factory-oauth.ts +7 -11
  322. package/src/providers/integrations/github/github.ts +4 -4
  323. package/src/providers/integrations/hermes.ts +79 -8
  324. package/src/providers/integrations/kilo/kilo-discovery.ts +10 -1
  325. package/src/providers/integrations/kilo/kilo.ts +153 -146
  326. package/src/providers/integrations/opencode/opencode-go-quota.ts +130 -0
  327. package/src/providers/integrations/opencode/provider.ts +532 -499
  328. package/src/providers/integrations/openrouter/provider.ts +139 -281
  329. package/src/providers/integrations/qoder/qoder-catalog.ts +450 -0
  330. package/src/providers/integrations/qoder/qoder-oauth.ts +310 -263
  331. package/src/providers/integrations/qoder/qoder-realm.ts +106 -0
  332. package/src/providers/integrations/qoder/qoder-virtual.ts +77 -0
  333. package/src/providers/integrations/qoder/qoder.ts +865 -730
  334. package/src/providers/integrations/shared/install-id.ts +29 -2
  335. package/src/providers/integrations/trae/trae-catalog.ts +190 -0
  336. package/src/providers/integrations/trae/trae-headers.ts +149 -0
  337. package/src/providers/integrations/trae/trae-oauth.ts +773 -0
  338. package/src/providers/integrations/trae/trae-protocol.ts +400 -0
  339. package/src/providers/integrations/trae/trae-realm.ts +220 -0
  340. package/src/providers/integrations/trae/trae.ts +458 -0
  341. package/src/providers/operations/account-health-service.ts +107 -10
  342. package/src/providers/operations/client-versions.ts +143 -6
  343. package/src/providers/operations/model-catalog-cache.ts +18 -3
  344. package/src/providers/operations/provider-catalog-seeder.ts +95 -86
  345. package/src/providers/operations/provider-catalog-service.ts +76 -0
  346. package/src/providers/operations/provider-credential-service.ts +46 -7
  347. package/src/providers/provider-contracts.ts +76 -0
  348. package/src/providers/provider-metadata.ts +78 -6
  349. package/src/providers/provider-model-definition.ts +373 -349
  350. package/src/security/api-key-auth.ts +126 -8
  351. package/src/transport/contracts.ts +3 -0
  352. package/src/transport/dispatch/attempt-finalize.ts +39 -2
  353. package/src/transport/dispatch/attempt-loop.ts +530 -513
  354. package/src/transport/dispatch/empty-stream.ts +17 -26
  355. package/src/transport/dispatch/generation.ts +88 -21
  356. package/src/transport/dispatch/leases.ts +99 -2
  357. package/src/transport/dispatch/proxy-request.ts +60 -3
  358. package/src/transport/dispatch/streaming/finalize.ts +16 -1
  359. package/src/transport/dispatch/streaming/stream.ts +22 -2
  360. package/src/transport/dispatch/upstream.ts +19 -1
  361. package/src/transport/failure-policy.ts +20 -0
  362. package/src/transport/guards/auth-guard.ts +26 -1
  363. package/src/transport/guards/body-policy.ts +5 -4
  364. package/src/transport/request/preparer.ts +103 -0
  365. package/src/transport/request/state.ts +14 -0
  366. package/src/transport/routing/plan-gate.ts +45 -0
  367. package/src/transport/routing/route-catalog.ts +799 -738
  368. package/src/transport/routing/route-model.ts +52 -1
  369. package/src/transport/routing/router.ts +51 -0
  370. package/src/transport/surface/chat/encode.ts +18 -31
  371. package/src/transport/surface/messages/stream.ts +3 -2
  372. package/src/transport/translation/tool-repair.ts +37 -16
  373. package/src/workers/account-identity-sweep.ts +14 -12
  374. package/src/workers/daily-checkin.ts +62 -0
  375. package/src/workers/quota-refresh-sweep.ts +27 -2
  376. package/dist/dashboard/assets/BannedPage-DUrXpp-X.js +0 -1
  377. package/dist/dashboard/assets/CombosPage-B7ETkfVH.js +0 -1
  378. package/dist/dashboard/assets/ConsoleLogPage-AisQzCI1.js +0 -1
  379. package/dist/dashboard/assets/HealthEventsModal-BNflDm_f.js +0 -1
  380. package/dist/dashboard/assets/ModelPicker-CAiPR5Vd.js +0 -1
  381. package/dist/dashboard/assets/OverviewPage-C3NIzsDx.js +0 -1
  382. package/dist/dashboard/assets/ProviderDetailPage-BK8i5B5X.js +0 -3
  383. package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js +0 -1
  384. package/dist/dashboard/assets/ProvidersPage-CvCVgm1e.js +0 -1
  385. package/dist/dashboard/assets/ProxyPage-DjabkuzP.js +0 -7
  386. package/dist/dashboard/assets/QuotaPage-Dxe0B0iu.js +0 -1
  387. package/dist/dashboard/assets/SetupPage-BaD4P_jW.js +0 -1
  388. package/dist/dashboard/assets/StudioPage-CLP4le3U.js +0 -31
  389. package/dist/dashboard/assets/UsagePage-Djes-Epy.js +0 -56
  390. package/dist/dashboard/assets/api-keys-CjIGQhe7.js +0 -1
  391. package/dist/dashboard/assets/app-Cjgo4-Lf.css +0 -2
  392. package/dist/dashboard/assets/assert-Bwh6DJTK.js +0 -1
  393. package/dist/dashboard/assets/cli-tools-DccaOfhX.js +0 -1
  394. package/dist/dashboard/assets/clipboard-button-DQmMikNQ.js +0 -1
  395. package/dist/dashboard/assets/entry-Cpi0rph1.js +0 -4
  396. package/dist/dashboard/assets/entry-Dm3PpDCO.js +0 -1
  397. package/dist/dashboard/assets/entry-keNDZ_Wz.js +0 -1
  398. package/dist/dashboard/assets/github-badge-BuAJr8oU.js +0 -1
  399. package/dist/dashboard/assets/model-bans-CVvHZ-qz.js +0 -1
  400. package/dist/dashboard/assets/mutation-BIizFZav.js +0 -1
  401. package/dist/dashboard/assets/privacy-mode-CQjbZ3xk.js +0 -1
  402. package/dist/dashboard/assets/routing-jntw9-Ui.js +0 -1
  403. package/dist/dashboard/assets/select-CGUuTQmj.js +0 -41
  404. package/dist/dashboard/assets/settings-BAS6QbmJ.js +0 -1
  405. package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js +0 -1
  406. package/dist/dashboard/assets/terminal-C9mZ4Ugm.js +0 -1
  407. package/dist/dashboard/assets/toast-DKAdP2mo.js +0 -2
  408. package/scripts/cli/cli.ts +0 -771
  409. package/scripts/operations/probe-egress.ts +0 -335
  410. package/scripts/operations/restart.ts +0 -120
  411. package/scripts/shared/env.ts +0 -269
  412. /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.ico +0 -0
  413. /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.png +0 -0
  414. /package/scripts/cli/{menu.ts → ui/prompt.ts} +0 -0
  415. /package/scripts/cli/{wordmark.ts → ui/wordmark.ts} +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
+ }
@@ -281,9 +281,7 @@ export class CodexStreamFrameProcessor {
281
281
  // output_index routing + `output_item.added` registration.
282
282
  readonly #openItems = new Map<string, OpenItemEntry>();
283
283
  readonly #openItemsByOutputIndex = new Map<number, OpenItemEntry>();
284
- // Track tool call argument accumulation, retained for diagnostics and
285
- // future reconciliation of streamed arguments against authoritative ones.
286
- readonly #toolCallArgs = new Map<string, string[]>();
284
+
287
285
  // Whitespace-loop guard: a model stuck emitting only whitespace tool-call
288
286
  // argument deltas would otherwise hang the dispatch loop forever.
289
287
  readonly #whitespaceDeltaTracking = new Map<
@@ -427,9 +425,6 @@ export class CodexStreamFrameProcessor {
427
425
  : undefined;
428
426
  const key = itemId ?? callId;
429
427
  this.#checkWhitespaceLoopGuard(key, json["delta"] as string);
430
- const chunks = this.#toolCallArgs.get(key) ?? [];
431
- chunks.push(json["delta"] as string);
432
- this.#toolCallArgs.set(key, chunks);
433
428
  events.push({
434
429
  type: "tool_call_delta",
435
430
  sequence_number: this.#seq++,
@@ -443,14 +438,7 @@ export class CodexStreamFrameProcessor {
443
438
  typeof json["delta"] === "string"
444
439
  ) {
445
440
  const { callId, name } = this.#resolveToolIdentity(json);
446
- const itemId =
447
- typeof json["item_id"] === "string"
448
- ? (json["item_id"] as string)
449
- : undefined;
450
- const key = itemId ?? callId;
451
- const chunks = this.#toolCallArgs.get(key) ?? [];
452
- chunks.push(json["delta"] as string);
453
- this.#toolCallArgs.set(key, chunks);
441
+
454
442
  events.push({
455
443
  type: "tool_call_delta",
456
444
  sequence_number: this.#seq++,
@@ -505,37 +493,13 @@ export class CodexStreamFrameProcessor {
505
493
  },
506
494
  });
507
495
  } else if (t === "response.function_call_arguments.done") {
508
- const rawCallId =
509
- (json["call_id"] as string | undefined) ??
510
- (json["item_id"] as string | undefined);
511
- const itemId =
512
- typeof json["item_id"] === "string"
513
- ? (json["item_id"] as string)
514
- : undefined;
515
- let key: string | undefined;
516
- if (itemId !== undefined) key = itemId;
517
- else if (rawCallId !== undefined) key = decodeCodexToolCallId(rawCallId);
518
- else {
519
- const outputIndex = readOutputIndex(json);
520
- if (outputIndex !== undefined) key = `idx_${outputIndex}`;
521
- }
522
- const finalArgs = (json as Record<string, unknown>)["arguments"];
523
- if (typeof finalArgs === "string" && key !== undefined) {
524
- this.#toolCallArgs.set(key, [finalArgs]);
525
- }
496
+ // The arguments themselves are emitted by the delta events above; the
497
+ // `.done` event only marks the call complete.
498
+
526
499
  this.hasToolCall = true;
527
500
  } else if (t === "response.custom_tool_call_input.done") {
528
- const itemId =
529
- typeof json["item_id"] === "string"
530
- ? (json["item_id"] as string)
531
- : undefined;
532
- const rawCallId = (json["call_id"] as string | undefined) ?? itemId;
533
- let key: string | undefined;
534
- if (itemId !== undefined) key = itemId;
535
- else if (rawCallId !== undefined) key = decodeCodexToolCallId(rawCallId);
536
- const finalInput = (json as Record<string, unknown>)["input"];
537
- if (typeof finalInput === "string" && key !== undefined)
538
- this.#toolCallArgs.set(key, [finalInput]);
501
+ // As above: the input arrives as deltas, so `.done` only completes it.
502
+
539
503
  this.hasToolCall = true;
540
504
  } else if (t === "response.output_item.done") {
541
505
  const item = json["item"];
@@ -598,43 +562,13 @@ export class CodexStreamFrameProcessor {
598
562
  }
599
563
  if (outputItem["type"] === "function_call") {
600
564
  this.hasToolCall = true;
601
- const callIdRaw =
602
- typeof outputItem["call_id"] === "string"
603
- ? (outputItem["call_id"] as string)
604
- : undefined;
605
- const decodedCallId =
606
- callIdRaw !== undefined
607
- ? decodeCodexToolCallId(callIdRaw)
608
- : undefined;
609
- const authoritative =
610
- typeof outputItem["arguments"] === "string"
611
- ? (outputItem["arguments"] as string)
612
- : undefined;
613
- if (authoritative !== undefined && decodedCallId !== undefined) {
614
- const key = outputItemId ?? decodedCallId;
615
- this.#toolCallArgs.set(key, [authoritative]);
616
- }
565
+
617
566
  if (outputItemId !== undefined) this.#openItems.delete(outputItemId);
618
567
  if (outputIndex !== undefined)
619
568
  this.#openItemsByOutputIndex.delete(outputIndex);
620
569
  } else if (outputItem["type"] === "custom_tool_call") {
621
570
  this.hasToolCall = true;
622
- const callIdRaw =
623
- typeof outputItem["call_id"] === "string"
624
- ? (outputItem["call_id"] as string)
625
- : undefined;
626
- const decodedCallId =
627
- callIdRaw !== undefined
628
- ? decodeCodexToolCallId(callIdRaw)
629
- : undefined;
630
- const authoritative =
631
- typeof outputItem["input"] === "string"
632
- ? (outputItem["input"] as string)
633
- : undefined;
634
- if (authoritative !== undefined && decodedCallId !== undefined) {
635
- const key = outputItemId ?? decodedCallId;
636
- this.#toolCallArgs.set(key, [authoritative]);
637
- }
571
+
638
572
  if (outputItemId !== undefined) this.#openItems.delete(outputItemId);
639
573
  if (outputIndex !== undefined)
640
574
  this.#openItemsByOutputIndex.delete(outputIndex);