cartethyia 2.5.4 → 2.5.7

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 (395) hide show
  1. package/.env.example +13 -4
  2. package/README.md +2 -2
  3. package/dist/dashboard/assets/{BannedPage-C2orBsI2.js → BannedPage-DE2607Tb.js} +1 -1
  4. package/dist/dashboard/assets/BannedPage-DE2607Tb.js.gz +0 -0
  5. package/dist/dashboard/assets/CliToolDetailPage-Duv6mfrl.js +1 -0
  6. package/dist/dashboard/assets/CliToolDetailPage-Duv6mfrl.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-BV6pxVQo.js → CliToolsPage-C2PNYWzk.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-C2PNYWzk.js.gz +0 -0
  9. package/dist/dashboard/assets/CombosPage-TXP9OfAM.js +1 -0
  10. package/dist/dashboard/assets/CombosPage-TXP9OfAM.js.gz +0 -0
  11. package/dist/dashboard/assets/{ConsoleLogPage-CW-wPmJ6.js → ConsoleLogPage-B-9xnAQN.js} +1 -1
  12. package/dist/dashboard/assets/ConsoleLogPage-B-9xnAQN.js.gz +0 -0
  13. package/dist/dashboard/assets/{CustomizationPage-0p8uX7vB.js → CustomizationPage-xeax9nJt.js} +1 -1
  14. package/dist/dashboard/assets/CustomizationPage-xeax9nJt.js.gz +0 -0
  15. package/dist/dashboard/assets/{HealthEventsModal-BjqSMfko.js → HealthEventsModal-Df4jjaHJ.js} +1 -1
  16. package/dist/dashboard/assets/HealthEventsModal-Df4jjaHJ.js.gz +0 -0
  17. package/dist/dashboard/assets/{LoginPage-BvzCx8Kr.js → LoginPage-Dyojsm84.js} +1 -1
  18. package/dist/dashboard/assets/LoginPage-Dyojsm84.js.gz +0 -0
  19. package/dist/dashboard/assets/ModelPicker-C7pDb41R.js +1 -0
  20. package/dist/dashboard/assets/ModelPicker-C7pDb41R.js.gz +0 -0
  21. package/dist/dashboard/assets/OverviewPage-BJfg4ow6.js +1 -0
  22. package/dist/dashboard/assets/OverviewPage-BJfg4ow6.js.gz +0 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-ChCX1gmT.js +3 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-ChCX1gmT.js.gz +0 -0
  25. package/dist/dashboard/assets/{ProvidersPage-Js9jUPSb.js → ProvidersPage-DeWSOEUO.js} +1 -1
  26. package/dist/dashboard/assets/ProvidersPage-DeWSOEUO.js.gz +0 -0
  27. package/dist/dashboard/assets/ProxyPage-CkpWln2S.js +7 -0
  28. package/dist/dashboard/assets/ProxyPage-CkpWln2S.js.gz +0 -0
  29. package/dist/dashboard/assets/QuotaPage-B3VOgQmt.js +1 -0
  30. package/dist/dashboard/assets/QuotaPage-B3VOgQmt.js.gz +0 -0
  31. package/dist/dashboard/assets/SettingsPage-C_klIZom.js +1 -0
  32. package/dist/dashboard/assets/SettingsPage-C_klIZom.js.gz +0 -0
  33. package/dist/dashboard/assets/{SetupPage-DDbMQsoo.js → SetupPage-BYph-InK.js} +1 -1
  34. package/dist/dashboard/assets/SetupPage-BYph-InK.js.gz +0 -0
  35. package/dist/dashboard/assets/{SortableList-CB3Z7VTf.js → SortableList-CvyQCYWJ.js} +1 -1
  36. package/dist/dashboard/assets/SortableList-CvyQCYWJ.js.gz +0 -0
  37. package/dist/dashboard/assets/{StudioPage-D95UCPHI.js → StudioPage-Cm1JYilI.js} +3 -3
  38. package/dist/dashboard/assets/StudioPage-Cm1JYilI.js.gz +0 -0
  39. package/dist/dashboard/assets/UsagePage-DlFnc3_S.js +56 -0
  40. package/dist/dashboard/assets/UsagePage-DlFnc3_S.js.gz +0 -0
  41. package/dist/dashboard/assets/api-keys-DQYLQxpW.js +1 -0
  42. package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
  43. package/dist/dashboard/assets/app-CX_Z8cMs.css +2 -0
  44. package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
  45. package/dist/dashboard/assets/{assert-Chi7pCnt.js → assert-C9FD_JwJ.js} +1 -1
  46. package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
  47. package/dist/dashboard/assets/{badge-D4pQapKj.js → badge-CR72dasT.js} +1 -1
  48. package/dist/dashboard/assets/badge-CR72dasT.js.gz +0 -0
  49. package/dist/dashboard/assets/{banner-DH6cg-_B.js → banner-CI9_eSfO.js} +1 -1
  50. package/dist/dashboard/assets/banner-CI9_eSfO.js.gz +0 -0
  51. package/dist/dashboard/assets/{card-BKqlUOe4.js → card-DS7CQEsO.js} +1 -1
  52. package/dist/dashboard/assets/card-DS7CQEsO.js.gz +0 -0
  53. package/dist/dashboard/assets/{cli-tools-fTgljTGA.js → cli-tools-CNqzrfdb.js} +1 -1
  54. package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
  55. package/dist/dashboard/assets/{clipboard-button-D5Ys_sM0.js → clipboard-button-611pSsaX.js} +1 -1
  56. package/dist/dashboard/assets/clipboard-button-611pSsaX.js.gz +0 -0
  57. package/dist/dashboard/assets/{fingerprint-pattern-BWuHtbW4.js → concurrency-BDF0ok7y.js} +1 -1
  58. package/dist/dashboard/assets/concurrency-BDF0ok7y.js.gz +0 -0
  59. package/dist/dashboard/assets/dialog-k37exjNt.js +1 -0
  60. package/dist/dashboard/assets/dialog-k37exjNt.js.gz +0 -0
  61. package/dist/dashboard/assets/drawer-CX7N8ucR.js +1 -0
  62. package/dist/dashboard/assets/drawer-CX7N8ucR.js.gz +0 -0
  63. package/dist/dashboard/assets/{entry-CQBr6U9c.js → entry-B9Xvtnfj.js} +1 -1
  64. package/dist/dashboard/assets/entry-B9Xvtnfj.js.gz +0 -0
  65. package/dist/dashboard/assets/entry-Bog0p925.js +4 -0
  66. package/dist/dashboard/assets/entry-Bog0p925.js.gz +0 -0
  67. package/dist/dashboard/assets/entry-CTP8AyiS.js +1 -0
  68. package/dist/dashboard/assets/entry-CTP8AyiS.js.gz +0 -0
  69. package/dist/dashboard/assets/{github-badge-BlptL8Uf.js → github-badge-sltk1qUi.js} +1 -1
  70. package/dist/dashboard/assets/github-badge-sltk1qUi.js.gz +0 -0
  71. package/dist/dashboard/assets/{index-js6TJvdr.js → index-CZQIrdoX.js} +2 -2
  72. package/dist/dashboard/assets/index-CZQIrdoX.js.gz +0 -0
  73. package/dist/dashboard/assets/{inline-DUWDSmDV.js → inline-BhY2sSnO.js} +1 -1
  74. package/dist/dashboard/assets/inline-BhY2sSnO.js.gz +0 -0
  75. package/dist/dashboard/assets/{input-LVjgZq4f.js → input-V1umFiBk.js} +1 -1
  76. package/dist/dashboard/assets/input-V1umFiBk.js.gz +0 -0
  77. package/dist/dashboard/assets/layout-BBOYycyS.js +1 -0
  78. package/dist/dashboard/assets/layout-BBOYycyS.js.gz +0 -0
  79. package/dist/dashboard/assets/{list-row-DupvLfDx.js → list-row-1zzZYM0B.js} +1 -1
  80. package/dist/dashboard/assets/list-row-1zzZYM0B.js.gz +0 -0
  81. package/dist/dashboard/assets/{live-C4pEJUqI.js → live-BivGfuUs.js} +1 -1
  82. package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
  83. package/dist/dashboard/assets/{model-bans-BRiuufkB.js → model-bans-CmffjLuG.js} +1 -1
  84. package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
  85. package/dist/dashboard/assets/{mutation-B4ROwk8L.js → mutation-46P0oWMe.js} +1 -1
  86. package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
  87. package/dist/dashboard/assets/numbers-DkfMR5Qs.js +2 -0
  88. package/dist/dashboard/assets/numbers-DkfMR5Qs.js.gz +0 -0
  89. package/dist/dashboard/assets/{query-keys-gmjzC3RS.js → query-keys-BFhGpwR1.js} +1 -1
  90. package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
  91. package/dist/dashboard/assets/{routing-C7tBT5o-.js → routing-CAb4F37R.js} +1 -1
  92. package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
  93. package/dist/dashboard/assets/{select-WUCXorXw.js → select-BPpSsBgL.js} +1 -1
  94. package/dist/dashboard/assets/select-BPpSsBgL.js.gz +0 -0
  95. package/dist/dashboard/assets/{settings-C_zKl1GS.js → settings-DNX9sydi.js} +1 -1
  96. package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
  97. package/dist/dashboard/assets/{stack-_3O0F8Xn.js → stack-BHTfXdz_.js} +1 -1
  98. package/dist/dashboard/assets/stack-BHTfXdz_.js.gz +0 -0
  99. package/dist/dashboard/assets/{state-BgTxHcJS.js → state-mPDRlQkl.js} +1 -1
  100. package/dist/dashboard/assets/state-mPDRlQkl.js.gz +0 -0
  101. package/dist/dashboard/assets/{switch-DEjTtOWf.js → switch-DOmJxg7H.js} +1 -1
  102. package/dist/dashboard/assets/switch-DOmJxg7H.js.gz +0 -0
  103. package/dist/dashboard/assets/tokens-COzario9.js +1 -0
  104. package/dist/dashboard/assets/tokens-COzario9.js.gz +0 -0
  105. package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js +1 -0
  106. package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js.gz +0 -0
  107. package/dist/dashboard/assets/{useMutation-BWSC0UAS.js → useMutation-Dem0lXQj.js} +1 -1
  108. package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
  109. package/dist/dashboard/index.html +1 -1
  110. package/dist/dashboard/index.html.gz +0 -0
  111. package/dist/dashboard/precompressed-assets.json +54 -54
  112. package/migrations/README.md +52 -52
  113. package/migrations/lite-sqlite3/0000_baseline.sql +2 -2
  114. package/package.json +95 -95
  115. package/scripts/cli/admin.ts +125 -0
  116. package/scripts/cli/{ui/background.ts → background.ts} +1 -1
  117. package/scripts/cli/{command/console-menu.ts → console-menu.ts} +275 -244
  118. package/scripts/cli/doctor.ts +37 -0
  119. package/scripts/cli/{support/env.ts → env.ts} +99 -99
  120. package/scripts/cli/{gateway/spawn.ts → gateway.ts} +112 -24
  121. package/scripts/cli/instance.ts +224 -0
  122. package/scripts/cli/main.ts +61 -18
  123. package/scripts/cli/{support/platform.ts → platform.ts} +64 -64
  124. package/scripts/cli/port.ts +100 -0
  125. package/scripts/cli/{support/services.ts → services.ts} +88 -88
  126. package/scripts/cli/{command/setup.ts → setup.ts} +35 -34
  127. package/scripts/cli/{command/start.ts → start.ts} +10 -5
  128. package/scripts/cli/{support/startup-signal.ts → startup-signal.ts} +23 -23
  129. package/scripts/cli/tray/index.ts +62 -12
  130. package/scripts/cli/{command/tray.ts → tray.ts} +208 -208
  131. package/scripts/cli/{command/update.ts → update.ts} +39 -39
  132. package/scripts/cli/version.ts +67 -0
  133. package/scripts/cli/{ui/wordmark.ts → wordmark.ts} +58 -58
  134. package/src/boot/app.ts +697 -693
  135. package/src/boot/dependencies.ts +647 -646
  136. package/src/boot/timeout.ts +2 -13
  137. package/src/config/cooldowns.ts +186 -138
  138. package/src/config/network.ts +8 -14
  139. package/src/config/providers.ts +7 -10
  140. package/src/config/runtime.ts +76 -76
  141. package/src/config/security.ts +15 -0
  142. package/src/config/spec.ts +20 -30
  143. package/src/config/telemetry.ts +0 -20
  144. package/src/config/timeouts.ts +0 -6
  145. package/src/console/admin/model-abuse/contracts.ts +28 -6
  146. package/src/console/admin/model-abuse/routes.ts +56 -3
  147. package/src/console/api-keys/contracts.ts +410 -414
  148. package/src/console/api-keys/routes.ts +881 -880
  149. package/src/console/auth/credentials.ts +3 -3
  150. package/src/console/auth/setup-service.ts +2 -2
  151. package/src/console/backup/export-endpoint.ts +6 -2
  152. package/src/console/backup/nine-router.ts +2 -5
  153. package/src/console/backup/service.ts +4 -0
  154. package/src/console/backup/store.ts +192 -29
  155. package/src/console/backup/validate.ts +3 -45
  156. package/src/console/cli-tools/fs-ops.ts +487 -487
  157. package/src/console/console-router-guards.ts +1 -1
  158. package/src/console/data-management/contracts.ts +13 -6
  159. package/src/console/data-management/operations.ts +23 -18
  160. package/src/console/data-management/routes.ts +0 -1
  161. package/src/console/domain-registration.ts +10 -5
  162. package/src/console/observability/live.ts +22 -0
  163. package/src/console/observability/store.ts +39 -16
  164. package/src/console/observability/usage-dimensions.ts +4 -4
  165. package/src/console/providers/catalog/catalog-projections.ts +6 -7
  166. package/src/console/providers/catalog/model-operations.ts +5 -2
  167. package/src/console/providers/catalog/provider-operations.ts +4 -4
  168. package/src/console/providers/catalog/public-model-store.ts +8 -11
  169. package/src/console/providers/catalog/store.ts +1553 -1553
  170. package/src/console/providers/oauth/routes.ts +8 -12
  171. package/src/console/quota/account-quota-tenant.ts +6 -6
  172. package/src/console/quota/quota-view.ts +1 -1
  173. package/src/console/remote-support/contracts.ts +61 -0
  174. package/src/console/remote-support/routes.ts +65 -0
  175. package/src/console/remote-support/service.ts +207 -0
  176. package/src/console/response-compression.ts +0 -3
  177. package/src/console/routing/pools/probe-result.ts +3 -2
  178. package/src/console/routing/pools/probe-verdict.ts +132 -132
  179. package/src/console/routing/pools/store.ts +8 -8
  180. package/src/console/settings/contracts.ts +58 -2
  181. package/src/console/settings/store.ts +30 -3
  182. package/src/console/shared/cursor.ts +8 -8
  183. package/src/console/shared/errors.ts +1 -3
  184. package/src/network/http2-bidi.ts +2 -4
  185. package/src/network/pool/agent.ts +478 -478
  186. package/src/network/pool/selector.ts +5 -4
  187. package/src/network/retry.ts +1 -2
  188. package/src/network/ssrf.ts +5 -4
  189. package/src/observability/event-compaction.ts +80 -78
  190. package/src/observability/event-day-index.ts +13 -15
  191. package/src/observability/event-log.ts +4 -4
  192. package/src/observability/logger.ts +6 -6
  193. package/src/observability/payload-capture.ts +151 -33
  194. package/src/observability/payload-store.ts +397 -440
  195. package/src/observability/redaction.ts +5 -6
  196. package/src/observability/request-notify.ts +43 -0
  197. package/src/observability/telemetry-status.ts +8 -9
  198. package/src/observability/token-speed.ts +5 -4
  199. package/src/persistence/db-mode.ts +7 -7
  200. package/src/persistence/readiness.ts +1 -1
  201. package/src/persistence/schema.ts +1406 -1402
  202. package/src/persistence/shared-cache.ts +1 -2
  203. package/src/persistence/stores/api-key-store.ts +18 -6
  204. package/src/persistence/stores/share-store.ts +4 -3
  205. package/src/persistence/stores/telemetry-store.ts +236 -237
  206. package/src/protocol/canonical-model.ts +11 -3
  207. package/src/protocol/codecs.ts +138 -4
  208. package/src/protocol/gateway-error.ts +6 -4
  209. package/src/protocol/request/chat.ts +21 -1
  210. package/src/protocol/request/codex.ts +10 -1
  211. package/src/protocol/request/messages.ts +4 -5
  212. package/src/protocol/request/responses.ts +9 -1
  213. package/src/protocol/response/chat.ts +34 -4
  214. package/src/protocol/response/codex.ts +7 -4
  215. package/src/protocol/response/dsml.ts +140 -28
  216. package/src/protocol/response/responses.ts +11 -5
  217. package/src/protocol/stream-error-frames.ts +4 -5
  218. package/src/providers/authentication/jwt-validator.ts +9 -10
  219. package/src/providers/authentication/oauth-refresh-service.ts +9 -11
  220. package/src/providers/configured-openai-providers.ts +134 -136
  221. package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
  222. package/src/providers/discovery/catalog/catalog-format.ts +3 -6
  223. package/src/providers/discovery/models-dev-catalog.ts +354 -354
  224. package/src/providers/discovery/probe-phases.ts +4 -4
  225. package/src/providers/discovery/probe-wire.ts +6 -13
  226. package/src/providers/discovery/probing-service.ts +1117 -1045
  227. package/src/providers/integrations/alibabacp/alibabacp.ts +1 -1
  228. package/src/providers/integrations/antigravity/antigravity-quota.ts +5 -7
  229. package/src/providers/integrations/buddy/buddy-chat-shared.ts +23 -2
  230. package/src/providers/integrations/buddy/buddy-checkin.ts +1 -4
  231. package/src/providers/integrations/buddy/buddy-cli-headers.ts +23 -6
  232. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -1
  233. package/src/providers/integrations/buddy/buddy-identity.ts +52 -0
  234. package/src/providers/integrations/buddy/buddy-media.ts +2 -2
  235. package/src/providers/integrations/buddy/buddy-usage-store.ts +3 -4
  236. package/src/providers/integrations/buddy/codebuddy/quota.ts +2 -13
  237. package/src/providers/integrations/buddy/codebuddy/shared.ts +118 -115
  238. package/src/providers/integrations/buddy/workbuddy/quota.ts +0 -11
  239. package/src/providers/integrations/buddy/workbuddy/shared.ts +5 -2
  240. package/src/providers/integrations/claude/claude.ts +608 -608
  241. package/src/providers/integrations/cline/cline-quota.ts +132 -132
  242. package/src/providers/integrations/cline/cline.ts +537 -539
  243. package/src/providers/integrations/codex/codex-oauth.ts +1 -1
  244. package/src/providers/integrations/commandcode/commandcode.ts +674 -675
  245. package/src/providers/integrations/commandcode/discovery.ts +117 -116
  246. package/src/providers/integrations/cursor/cursor-auth.ts +418 -418
  247. package/src/providers/integrations/cursor/cursor-quota.ts +309 -310
  248. package/src/providers/integrations/cursor/cursor.ts +11 -163
  249. package/src/providers/integrations/factory/factory-models.ts +3 -5
  250. package/src/providers/integrations/grok/grok-install-id.ts +2 -3
  251. package/src/providers/integrations/grok/grok.ts +6 -7
  252. package/src/providers/integrations/hermes.ts +14 -18
  253. package/src/providers/integrations/kiro/kiro-discovery.ts +1 -1
  254. package/src/providers/integrations/kiro/kiro.ts +1 -1
  255. package/src/providers/integrations/mistral.ts +6 -6
  256. package/src/providers/integrations/opencode/fingerprint.ts +3 -3
  257. package/src/providers/integrations/opencode/provider.ts +8 -8
  258. package/src/providers/integrations/qoder/qoder-catalog.ts +450 -450
  259. package/src/providers/integrations/qoder/qoder-oauth.ts +388 -310
  260. package/src/providers/integrations/qoder/qoder.ts +884 -865
  261. package/src/providers/integrations/shared/connect.ts +6 -6
  262. package/src/providers/integrations/trae/trae-catalog.ts +1 -2
  263. package/src/providers/integrations/trae/trae.ts +2 -2
  264. package/src/providers/integrations/zcode/oauth.ts +6 -1
  265. package/src/providers/operations/account-health-service.ts +1528 -1523
  266. package/src/providers/operations/account-reset-service.ts +1 -1
  267. package/src/providers/operations/client-versions.ts +6 -6
  268. package/src/providers/operations/custom-cli-headers.ts +3 -4
  269. package/src/providers/operations/provider-catalog-seeder.ts +95 -95
  270. package/src/providers/operations/provider-catalog-service.ts +3 -4
  271. package/src/providers/provider-metadata.ts +4 -8
  272. package/src/providers/provider-registry.ts +6 -7
  273. package/src/providers/provider-usage.ts +1 -1
  274. package/src/security/abuse.ts +15 -10
  275. package/src/security/access-control.ts +144 -146
  276. package/src/security/admission/contracts.ts +11 -0
  277. package/src/security/admission/in-memory-store.ts +4 -3
  278. package/src/security/admission/lease-sweep.ts +6 -6
  279. package/src/security/admission/reasons.ts +5 -1
  280. package/src/security/admission/redis-store.ts +211 -210
  281. package/src/security/admission/service.ts +4 -0
  282. package/src/security/api-key-auth.ts +504 -504
  283. package/src/security/ip-boundary.ts +155 -15
  284. package/src/security/model-abuse.ts +366 -94
  285. package/src/security/outbound-headers.ts +3 -4
  286. package/src/shared/sleep.ts +4 -0
  287. package/src/transport/dispatch/attempt-finalize.ts +47 -5
  288. package/src/transport/dispatch/attempt-loop.ts +531 -530
  289. package/src/transport/dispatch/stream-bridge.ts +1 -3
  290. package/src/transport/dispatch/streaming/finalize.ts +1 -1
  291. package/src/transport/dispatch/streaming/stream.ts +5 -6
  292. package/src/transport/failure-policy.ts +162 -8
  293. package/src/transport/guards/auth-guard.ts +7 -4
  294. package/src/transport/guards/concurrency-guard.ts +0 -5
  295. package/src/transport/guards/error-lifecycle.ts +13 -6
  296. package/src/transport/guards/pipeline.ts +15 -3
  297. package/src/transport/guards/request-context.ts +8 -2
  298. package/src/transport/request/inflight.ts +5 -6
  299. package/src/transport/request/preparer.ts +2 -2
  300. package/src/transport/request/state.ts +12 -8
  301. package/src/transport/routing/admission.ts +4 -0
  302. package/src/transport/routing/route-catalog.ts +8 -1
  303. package/src/transport/routing/route-model.ts +46 -14
  304. package/src/transport/routing/router.ts +21 -14
  305. package/src/transport/surface/completion.ts +2 -3
  306. package/src/transport/surface/dialects.ts +5 -6
  307. package/src/transport/surface/responses/encode.ts +38 -0
  308. package/src/transport/surface/responses/parse.ts +4 -0
  309. package/src/transport/translation/capabilities.ts +2 -3
  310. package/src/transport/translation/quirks.ts +3 -3
  311. package/src/workers/buddy-growth-tasks.ts +5 -10
  312. package/src/workers/daily-checkin.ts +17 -23
  313. package/src/workers/quota-refresh-sweep.ts +26 -26
  314. package/dist/dashboard/assets/BannedPage-C2orBsI2.js.gz +0 -0
  315. package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js +0 -1
  316. package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js.gz +0 -0
  317. package/dist/dashboard/assets/CliToolsPage-BV6pxVQo.js.gz +0 -0
  318. package/dist/dashboard/assets/CombosPage-Bsan4679.js +0 -1
  319. package/dist/dashboard/assets/CombosPage-Bsan4679.js.gz +0 -0
  320. package/dist/dashboard/assets/ConsoleLogPage-CW-wPmJ6.js.gz +0 -0
  321. package/dist/dashboard/assets/CustomizationPage-0p8uX7vB.js.gz +0 -0
  322. package/dist/dashboard/assets/HealthEventsModal-BjqSMfko.js.gz +0 -0
  323. package/dist/dashboard/assets/LoginPage-BvzCx8Kr.js.gz +0 -0
  324. package/dist/dashboard/assets/ModelPicker-BY1aKEkP.js +0 -1
  325. package/dist/dashboard/assets/ModelPicker-BY1aKEkP.js.gz +0 -0
  326. package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js +0 -1
  327. package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js.gz +0 -0
  328. package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js +0 -3
  329. package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js.gz +0 -0
  330. package/dist/dashboard/assets/ProvidersPage-Js9jUPSb.js.gz +0 -0
  331. package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js +0 -7
  332. package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js.gz +0 -0
  333. package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js +0 -1
  334. package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js.gz +0 -0
  335. package/dist/dashboard/assets/SettingsPage-CS86ho5X.js +0 -1
  336. package/dist/dashboard/assets/SettingsPage-CS86ho5X.js.gz +0 -0
  337. package/dist/dashboard/assets/SetupPage-DDbMQsoo.js.gz +0 -0
  338. package/dist/dashboard/assets/SortableList-CB3Z7VTf.js.gz +0 -0
  339. package/dist/dashboard/assets/StudioPage-D95UCPHI.js.gz +0 -0
  340. package/dist/dashboard/assets/UsagePage-Cnae9SWA.js +0 -56
  341. package/dist/dashboard/assets/UsagePage-Cnae9SWA.js.gz +0 -0
  342. package/dist/dashboard/assets/api-keys-VY68Mhd1.js +0 -1
  343. package/dist/dashboard/assets/api-keys-VY68Mhd1.js.gz +0 -0
  344. package/dist/dashboard/assets/app-COlK95oi.css +0 -2
  345. package/dist/dashboard/assets/app-COlK95oi.css.gz +0 -0
  346. package/dist/dashboard/assets/assert-Chi7pCnt.js.gz +0 -0
  347. package/dist/dashboard/assets/badge-D4pQapKj.js.gz +0 -0
  348. package/dist/dashboard/assets/banner-DH6cg-_B.js.gz +0 -0
  349. package/dist/dashboard/assets/card-BKqlUOe4.js.gz +0 -0
  350. package/dist/dashboard/assets/cli-tools-fTgljTGA.js.gz +0 -0
  351. package/dist/dashboard/assets/clipboard-button-D5Ys_sM0.js.gz +0 -0
  352. package/dist/dashboard/assets/dialog-BYj-8WuK.js +0 -1
  353. package/dist/dashboard/assets/dialog-BYj-8WuK.js.gz +0 -0
  354. package/dist/dashboard/assets/drawer-CmXIZ6H1.js +0 -1
  355. package/dist/dashboard/assets/drawer-CmXIZ6H1.js.gz +0 -0
  356. package/dist/dashboard/assets/entry-CBg5zeu8.js +0 -1
  357. package/dist/dashboard/assets/entry-CBg5zeu8.js.gz +0 -0
  358. package/dist/dashboard/assets/entry-CQBr6U9c.js.gz +0 -0
  359. package/dist/dashboard/assets/entry-CgdSAzEV.js +0 -4
  360. package/dist/dashboard/assets/entry-CgdSAzEV.js.gz +0 -0
  361. package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
  362. package/dist/dashboard/assets/github-badge-BlptL8Uf.js.gz +0 -0
  363. package/dist/dashboard/assets/index-js6TJvdr.js.gz +0 -0
  364. package/dist/dashboard/assets/inline-DUWDSmDV.js.gz +0 -0
  365. package/dist/dashboard/assets/input-LVjgZq4f.js.gz +0 -0
  366. package/dist/dashboard/assets/layout-CPfxpsfy.js +0 -1
  367. package/dist/dashboard/assets/layout-CPfxpsfy.js.gz +0 -0
  368. package/dist/dashboard/assets/list-row-DupvLfDx.js.gz +0 -0
  369. package/dist/dashboard/assets/live-C4pEJUqI.js.gz +0 -0
  370. package/dist/dashboard/assets/model-bans-BRiuufkB.js.gz +0 -0
  371. package/dist/dashboard/assets/mutation-B4ROwk8L.js.gz +0 -0
  372. package/dist/dashboard/assets/numbers-YNcHvRY7.js +0 -2
  373. package/dist/dashboard/assets/numbers-YNcHvRY7.js.gz +0 -0
  374. package/dist/dashboard/assets/query-keys-gmjzC3RS.js.gz +0 -0
  375. package/dist/dashboard/assets/routing-C7tBT5o-.js.gz +0 -0
  376. package/dist/dashboard/assets/select-WUCXorXw.js.gz +0 -0
  377. package/dist/dashboard/assets/settings-C_zKl1GS.js.gz +0 -0
  378. package/dist/dashboard/assets/stack-_3O0F8Xn.js.gz +0 -0
  379. package/dist/dashboard/assets/state-BgTxHcJS.js.gz +0 -0
  380. package/dist/dashboard/assets/switch-DEjTtOWf.js.gz +0 -0
  381. package/dist/dashboard/assets/tokens-DDxMbSRL.js +0 -1
  382. package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
  383. package/dist/dashboard/assets/use-presence-BSZEBx7U.js +0 -1
  384. package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
  385. package/dist/dashboard/assets/useMutation-BWSC0UAS.js.gz +0 -0
  386. package/scripts/cli/command/admin.ts +0 -65
  387. package/scripts/cli/command/doctor.ts +0 -257
  388. package/scripts/cli/gateway/drain-token.ts +0 -78
  389. package/scripts/cli/gateway/instance-lock.ts +0 -105
  390. package/scripts/cli/gateway/stop.ts +0 -51
  391. package/scripts/cli/support/bun.ts +0 -27
  392. package/scripts/cli/support/version.ts +0 -37
  393. /package/dist/dashboard/assets/{app-DCQZRmHY.js → app-BIt446yE.js} +0 -0
  394. /package/dist/dashboard/assets/{app-DCQZRmHY.js.gz → app-BIt446yE.js.gz} +0 -0
  395. /package/scripts/cli/{ui/prompt.ts → prompt.ts} +0 -0
@@ -3,8 +3,7 @@
3
3
  * `src/protocol/`. This is the single home for the generic guards, UTF-16
4
4
  * well-formedness coercion, JSON/argument helpers, hash helpers, endpoint
5
5
  * URL construction, image-source resolution, and custom-header validation
6
- * that the per-wire request/response codecs build on — previously duplicated
7
- * across the Claude/Codex protocol modules and `providers/compatible-adapter`.
6
+ * that the per-wire request/response codecs build on.
8
7
  *
9
8
  * Provider-specific primitives that depend on provider identity (e.g. Codex
10
9
  * identity headers) deliberately live with their provider, not here.
@@ -273,6 +272,46 @@ const SUPPORTED_SCHEMA_KEYS: Readonly<Record<string, true>> = Object.freeze({
273
272
  maxProperties: true,
274
273
  });
275
274
 
275
+ /**
276
+ * True when a schema string carries a backslash before an underscore.
277
+ *
278
+ * Some clients emit a property or pattern key containing a literal `\_` (an
279
+ * escaped underscore). Upstreams reject it, so the escape has to be stripped —
280
+ * and stripping it can collapse two previously distinct keys onto the same
281
+ * name, which is why {@link mergeCollidingKeys} exists below.
282
+ */
283
+ function hasUnderscoreEscape(value: string): boolean {
284
+ return /\\+_/.test(value);
285
+ }
286
+
287
+ /**
288
+ * Merges map entries whose normalized keys collide, rather than overwriting.
289
+ *
290
+ * `allOf` accepts boolean schemas, so neither constraint is lost — a plain
291
+ * overwrite would silently drop one of two rules the caller actually sent.
292
+ */
293
+ function mergeCollidingKeys(entries: ReadonlyArray<readonly [string, unknown]>): Record<string, unknown> {
294
+ const merged: Record<string, unknown> = {};
295
+ for (const [key, value] of entries) {
296
+ const existing = merged[key];
297
+ if (existing === undefined) {
298
+ merged[key] = value;
299
+ continue;
300
+ }
301
+ merged[key] = { allOf: [existing, value] };
302
+ }
303
+ return merged;
304
+ }
305
+
306
+ /** One schema map's keys, with `\_` escapes stripped and collisions merged. */
307
+ function normalizeSchemaMapKeys(map: Record<string, unknown>): Record<string, unknown> {
308
+ const entries = Object.entries(map);
309
+ if (!entries.some(([key]) => hasUnderscoreEscape(key))) return map;
310
+ return mergeCollidingKeys(
311
+ entries.map(([key, value]) => [key.replace(/\\+_/g, "_"), value] as const),
312
+ );
313
+ }
314
+
276
315
  export function sanitizeSchemaForAnthropic(value: unknown): unknown {
277
316
  return sanitizeSchemaWalk(value, ROOT_WALK);
278
317
  }
@@ -292,10 +331,21 @@ function sanitizeSchemaWalk(value: unknown, walk: RewriteWalk): unknown {
292
331
  // preserve them while sanitizing each property's schema recursively.
293
332
  if (key === "properties" && isRecord(child)) {
294
333
  out.properties = Object.fromEntries(
295
- Object.entries(child).map(([name, schema]) => [name, sanitizeSchemaWalk(schema, inner)]),
334
+ // Escaped-underscore keys are normalized first: two names differing only
335
+ // by an escape would otherwise survive as two properties and be rebuilt
336
+ // into a `required` list the upstream rejects.
337
+ Object.entries(normalizeSchemaMapKeys(child)).map(([name, schema]) => [
338
+ name,
339
+ sanitizeSchemaWalk(schema, inner),
340
+ ]),
296
341
  );
297
342
  continue;
298
343
  }
344
+ // `patternProperties` is deliberately NOT re-emitted: it is absent from
345
+ // `SUPPORTED_SCHEMA_KEYS` because the Anthropic wire does not accept it, and
346
+ // writing it here would be undone by the keyword filter a few lines down
347
+ // anyway. Its keys are still walked below, so nested schemas are sanitized
348
+ // and an escaped-underscore key inside one is still normalized.
299
349
  // Drop wire-unsupported keywords so a strict upstream never 400s on
300
350
  // them — but never invent caller text to say so. The dropped names are
301
351
  // debug-logged by the caller-facing builder when that matters.
@@ -676,12 +726,96 @@ export function canonicalTerminal(input: {
676
726
  };
677
727
  }
678
728
 
729
+ /**
730
+ * JSON Schema keywords. A parameter root containing any of these is already a
731
+ * schema object, not a legacy property-name → schema map.
732
+ */
733
+ const SCHEMA_KEYWORDS: ReadonlySet<string> = new Set([
734
+ "$schema", "$id", "$ref", "$anchor", "$dynamicRef", "$dynamicAnchor", "$defs",
735
+ "$vocabulary", "$comment", "definitions", "type", "properties", "patternProperties",
736
+ "additionalProperties", "unevaluatedProperties", "propertyNames", "required",
737
+ "dependentRequired", "dependentSchemas", "dependencies", "minProperties", "maxProperties",
738
+ "allOf", "anyOf", "oneOf", "not", "if", "then", "else", "items", "prefixItems",
739
+ "additionalItems", "contains", "unevaluatedItems", "minItems", "maxItems", "uniqueItems",
740
+ "minContains", "maxContains", "minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum",
741
+ "multipleOf", "minLength", "maxLength", "pattern", "format", "contentEncoding",
742
+ "contentMediaType", "contentSchema", "enum", "const", "title", "description", "default",
743
+ "examples", "deprecated", "readOnly", "writeOnly",
744
+ ]);
745
+
746
+ /** The JSON Schema primitive type names. */
747
+ const SCHEMA_TYPES: ReadonlySet<string> = new Set([
748
+ "string", "number", "integer", "boolean", "object", "array", "null",
749
+ ]);
750
+
751
+ /**
752
+ * Rewrites one legacy tool parameter root into an explicit object schema.
753
+ *
754
+ * Two shapes reach here and are both rejected upstream as-is:
755
+ *
756
+ * - **An empty root.** `{}` carries no `type`, and a schema with no type is not
757
+ * a schema the upstream will accept — it answers `invalid_function_parameters`
758
+ * (CodeBuddy `11129`).
759
+ * - **A legacy property-name → schema map.** Every field names a parameter and
760
+ * carries its own boolean `required` flag, with no `{"type":"object",
761
+ * "properties":…}` wrapper around them. Old clients in the wild still emit
762
+ * this; the upstream wants the wrapper and the `required` array.
763
+ *
764
+ * Anything ambiguous is returned **untouched**. A root that already holds schema
765
+ * keywords, or one whose fields do not all look like `{type: <primitive>}`
766
+ * entries, is left alone: guessing there would rewrite a valid schema into a
767
+ * wrong one, which is worse than the refusal it was trying to avoid.
768
+ */
769
+ export function normalizeToolParameterRoot(value: unknown): unknown {
770
+ if (!isRecord(value)) return value;
771
+ if (Object.keys(value).length === 0) return { type: "object", properties: {} };
772
+ // Schema keywords (or an extension/annotation key) mean this is a real
773
+ // schema object already, not a property map.
774
+ if (SCHEMA_KEYWORDS.has(Object.keys(value)[0] ?? "")) return value;
775
+ if (Object.keys(value).some((name) => SCHEMA_KEYWORDS.has(name))) return value;
776
+ if (Object.keys(value).some((name) => name.startsWith("$") || name.startsWith("x-")))
777
+ return value;
778
+ const entries = Object.entries(value);
779
+ // A legacy map is identified by its per-field boolean `required`. Without at
780
+ // least one, this is not recognisably the legacy shape.
781
+ if (!entries.some(([, field]) => isRecord(field) && typeof field["required"] === "boolean"))
782
+ return value;
783
+ if (
784
+ !entries.every(
785
+ ([, field]) =>
786
+ isRecord(field) &&
787
+ typeof field["type"] === "string" &&
788
+ SCHEMA_TYPES.has(field["type"]) &&
789
+ (!("required" in field) || typeof field["required"] === "boolean"),
790
+ )
791
+ )
792
+ return value;
793
+
794
+ const properties: Record<string, unknown> = {};
795
+ const required: string[] = [];
796
+ for (const [name, field] of entries) {
797
+ const copy = { ...(field as Record<string, unknown>) };
798
+ if (copy["required"] === true) required.push(name);
799
+ delete copy["required"];
800
+ properties[name] = copy;
801
+ }
802
+ return {
803
+ type: "object",
804
+ properties,
805
+ ...(required.length > 0 ? { required } : {}),
806
+ };
807
+ }
808
+
679
809
  /**
680
810
  * Defaults a tool schema's `required` list to all declared properties while
681
811
  * preserving an explicit list, including optional properties.
812
+ *
813
+ * Also normalizes a legacy parameter root first (see
814
+ * {@link normalizeToolParameterRoot}), so a caller sending an empty root or a
815
+ * property-name → schema map still ships a schema the upstream accepts.
682
816
  */
683
817
  export function completeRequiredSchema(value: unknown): unknown {
684
- return completeRequiredWalk(value, ROOT_WALK);
818
+ return completeRequiredWalk(normalizeToolParameterRoot(value), ROOT_WALK);
685
819
  }
686
820
 
687
821
  function completeRequiredWalk(value: unknown, walk: RewriteWalk): unknown {
@@ -12,6 +12,8 @@ export type GatewayErrorCode =
12
12
  | "accounts_unavailable"
13
13
  | "accounts_rate_limited"
14
14
  | "capacity_exhausted"
15
+ | "provider_capacity_exhausted"
16
+ | "upstream_queued"
15
17
  | "proxy_pool_capacity_exceeded"
16
18
  | "proxy_pool_provider_capacity_exceeded"
17
19
  | "proxy_pool_blocked"
@@ -110,11 +112,11 @@ function stripLegacyOriginLabel(message: string): string {
110
112
  * stamped into it.
111
113
  *
112
114
  * The code is not lost — it travels in the envelope's `error.code` field, and
113
- * the HTTP status carries the class. It used to be prefixed into the text
115
+ * the HTTP status carries the class. Stamping it into the text
114
116
  * (`accounts_unavailable: Model 'x' has no available account (52 candidate(s)
115
- * unusable: credit_floor_reached, model_cooldown)`), which put internal enum
116
- * names in front of the operator's sentence and made the message unreadable
117
- * while adding nothing the field did not already carry.
117
+ * unusable: credit_floor_reached, model_cooldown)`) would put internal enum
118
+ * names in front of the operator's sentence and make the message unreadable
119
+ * while adding nothing the field does not already carry.
118
120
  *
119
121
  * Stripping is idempotent, and that is the point: a message that already
120
122
  * carries a `code:` prefix — stamped by an older builder, or by a code path
@@ -353,7 +353,24 @@ export function canonicalToChatPayload(
353
353
  // `display: "omitted"`, and dropping it stripped the replay the
354
354
  // provider demands on the next request. `reasoning_content` is an
355
355
  // assistant-only field, so it is emitted here and nowhere else.
356
- if (m.role === "assistant" && hasReasoning) {
356
+ //
357
+ // Load-bearing requires *content* in the replay: a non-empty summary
358
+ // or a non-empty serialised payload. Pushing `{content:"",
359
+ // reasoning_content:""}` is the opposite of load-bearing — the
360
+ // upstream reads it as "thinking mode with the reasoning stripped"
361
+ // and rejects the whole request (`WorkBuddy/CodeBuddy 11151 "history
362
+ // not found"`). The canonical form that reaches this branch with no
363
+ // trace is an input `reasoning` item whose `summary` was absent and
364
+ // whose `payload` was `null` — a signature-less thinking block
365
+ // emitted under `display: "omitted"` and already stripped by an
366
+ // earlier hop. There is nothing to replay, so the turn is dropped
367
+ // instead of being forwarded as an invalid stub.
368
+ // This non-empty echo is load-bearing for non-buddy
369
+ // OpenAI-compatible providers. Buddy providers deliberately drop
370
+ // the resulting bare stub later in `dropEmptyBuddyMessages`
371
+ // (`providers/integrations/buddy/buddy-chat-shared.ts`) because
372
+ // their upstream rejects that shape with 11151.
373
+ if (m.role === "assistant" && hasReasoning && reasoningContent.length > 0) {
357
374
  messages.push({
358
375
  role: "assistant",
359
376
  content: "",
@@ -361,6 +378,9 @@ export function canonicalToChatPayload(
361
378
  });
362
379
  continue;
363
380
  }
381
+ // No trace to carry: skip the turn entirely rather than emit an
382
+ // empty assistant message the upstream will reject.
383
+ if (m.role === "assistant" && hasReasoning) continue;
364
384
  if (m.role !== "assistant" && hasReasoning && reasoningContent.length > 0) {
365
385
  const last = messages[messages.length - 1];
366
386
  if (last !== undefined && last["role"] === "tool") {
@@ -374,7 +374,16 @@ export function canonicalToCodexResponsesPayload(
374
374
  summary: summaryArray,
375
375
  encrypted_content: r.encrypted_content,
376
376
  };
377
- if (r.summary !== undefined || r.encrypted_content !== undefined) {
377
+ // The upstream binds encrypted reasoning to the item it was issued
378
+ // for and rejects a replay whose id does not match, so the id that
379
+ // came back with the payload is authoritative. A synthesized
380
+ // `rs_<hash>` never matched the real item and failed the whole
381
+ // request with "Encrypted content item_id did not match the target
382
+ // item id"; only fall back to it when no id survived, where a
383
+ // mismatch is unavoidable anyway.
384
+ if (typeof r.item_id === "string" && r.item_id.length > 0) {
385
+ item["id"] = r.item_id;
386
+ } else if (r.summary !== undefined || r.encrypted_content !== undefined) {
378
387
  const idSource = `${r.summary ?? ""}|${r.encrypted_content ?? ""}`;
379
388
  if (idSource.length > 1) {
380
389
  item["id"] = `rs_${hashToBase36(idSource)}`;
@@ -157,12 +157,11 @@ function applyImageClamp(
157
157
  * the upstream rejects anything else. A canonical image part is an opaque
158
158
  * origin payload, so it can arrive in a vocabulary that is *not* Anthropic's:
159
159
  * a Responses part carries `type: "input_image"` with a top-level `image_url`
160
- * string, and a Chat part carries a nested `image_url` object. Both used to be
161
- * forwarded with their own `type` intact (or with no `source` at all), so a
162
- * Responses- or Chat-origin image sent to a Claude model produced
163
- * `source.type: "input_image"` — an invalid block the provider refused.
160
+ * string, and a Chat part carries a nested `image_url` object. Forwarding either
161
+ * with its own `type` intact (or with no `source` at all) sends a Claude model
162
+ * `source.type: "input_image"` — an invalid block the provider refuses.
164
163
  *
165
- * Every shape now collapses to a valid source, preferring the cheapest
164
+ * Every shape collapses to a valid source, preferring the cheapest
166
165
  * transport: a Files API reference, then a URL, then base64 bytes (a `data:`
167
166
  * URL is split rather than embedded, because Anthropic has no data-URL field).
168
167
  */
@@ -163,7 +163,15 @@ export function canonicalToResponsesPayload(
163
163
  flushMessage();
164
164
  // Replays prior encrypted reasoning so the model retains continuity
165
165
  // across turns; never decrypted or reconstructed from summary text.
166
- input.push({ type: "reasoning", encrypted_content: p.encrypted_content });
166
+ // The upstream binds encrypted reasoning to the item it was issued
167
+ // for and rejects a replay whose id does not match ("Encrypted content
168
+ // item_id did not match the target item id"), so the item carries the
169
+ // id it was parsed with. Without one there is nothing to match and the
170
+ // payload would be rejected anyway — it is dropped rather than sent
171
+ // under an id the upstream cannot verify.
172
+ if (p.item_id !== undefined) {
173
+ input.push({ type: "reasoning", id: p.item_id, encrypted_content: p.encrypted_content });
174
+ }
167
175
  } else if (p.kind === "reasoning") {
168
176
  // Plain reasoning replayed from another surface (a Messages `thinking`
169
177
  // block, or Chat `reasoning_content`). It has no encrypted artifact, so
@@ -10,7 +10,7 @@ import { mergeResponsesUsage } from "./responses";
10
10
  import { usageFromProvider, readReasoningText } from "../../providers/provider-usage";
11
11
  import { createToolEmitLedger } from "../tool-identity";
12
12
  import { MimoThinkSplitter, stripThinkingTags } from "../../providers/integrations/xiaomi-mimo/think-stream";
13
- import { parseDsmlToolCalls } from "./dsml";
13
+ import { DsmlToolCallParser, parseDsmlToolCalls } from "./dsml";
14
14
  import {
15
15
  OmpToolCallMarkupParser,
16
16
  parseOmpToolCallMarkup,
@@ -83,7 +83,7 @@ export function parseChatResponseToEvents(
83
83
  const contentWithoutMarkup = recoveredMarkup?.text ?? content;
84
84
  const recovered =
85
85
  typeof contentWithoutMarkup === "string"
86
- ? parseDsmlToolCalls(contentWithoutMarkup)
86
+ ? parseDsmlToolCalls(contentWithoutMarkup, declaredToolNames)
87
87
  : undefined;
88
88
  const visible = recovered === undefined ? contentWithoutMarkup : recovered.text;
89
89
  if (typeof visible === "string" && visible.includes("<think")) {
@@ -144,6 +144,13 @@ export function parseChatResponseToEvents(
144
144
  : undefined;
145
145
  return callId && name !== undefined && name.length > 0;
146
146
  });
147
+ // A request that declared no tools carries no allowlist to enforce, so a
148
+ // recovered block is honoured as-is exactly as before; a request that did
149
+ // declare them must never execute a name it did not offer.
150
+ const recoveredCalls =
151
+ declaredToolNames.size > 0
152
+ ? (recovered?.calls ?? []).filter((call) => callableToolNames.has(call.name))
153
+ : (recovered?.calls ?? []);
147
154
  const toolCalls = hasStructuredCalls
148
155
  ? structuredCalls
149
156
  : recoveredMarkup?.calls.some((call) => callableToolNames.has(call.name))
@@ -153,7 +160,7 @@ export function parseChatResponseToEvents(
153
160
  id: call.id,
154
161
  function: { name: call.name, arguments: call.arguments },
155
162
  }))
156
- : recovered?.calls ?? [];
163
+ : recoveredCalls;
157
164
  if (!hasStructuredCalls && recoveredMarkup !== undefined) {
158
165
  recoveredToolCalls = recoveredMarkup.calls.filter((call) =>
159
166
  callableToolNames.has(call.name),
@@ -257,6 +264,8 @@ export async function* decodeChatSseStream(
257
264
  (request.tools?.length ?? 0) > 0 ? new OmpToolCallMarkupParser(declaredToolNames) : undefined;
258
265
  const reasoningMarkup =
259
266
  (request.tools?.length ?? 0) > 0 ? new OmpToolCallMarkupParser(declaredToolNames) : undefined;
267
+ const dsmlMarkup =
268
+ (request.tools?.length ?? 0) > 0 ? new DsmlToolCallParser(declaredToolNames) : undefined;
260
269
  yield {
261
270
  type: "response_start",
262
271
  sequence_number: seq++,
@@ -302,7 +311,7 @@ export async function* decodeChatSseStream(
302
311
  const raw = delta["content"] as string;
303
312
  const markup = contentMarkup?.push(raw);
304
313
  if (markup !== undefined) recoveredMarkupCalls.push(...markup.calls);
305
- const recovered = parseDsmlToolCalls(markup?.text ?? raw);
314
+ const recovered = dsmlMarkup?.push(markup?.text ?? raw);
306
315
  const visible = recovered?.text ?? markup?.text ?? raw;
307
316
  const split = think.push(visible);
308
317
  if (split.reasoning.length > 0) {
@@ -478,6 +487,27 @@ export async function* decodeChatSseStream(
478
487
  }
479
488
  }
480
489
  }
490
+ const dsmlMarkupTail = dsmlMarkup?.flush();
491
+ if (dsmlMarkupTail !== undefined) {
492
+ if (dsmlMarkupTail.text.length > 0) {
493
+ const split = think.push(dsmlMarkupTail.text);
494
+ if (split.reasoning.length > 0) {
495
+ yield {
496
+ type: "content_delta",
497
+ sequence_number: seq++,
498
+ content: { kind: "reasoning", payload: null, summary: split.reasoning },
499
+ } as CanonicalEvent;
500
+ }
501
+ if (split.text.length > 0) {
502
+ yield {
503
+ type: "content_delta",
504
+ sequence_number: seq++,
505
+ content: { kind: "text", text: split.text },
506
+ } as CanonicalEvent;
507
+ }
508
+ }
509
+ for (const call of dsmlMarkupTail.calls) recoveredMarkupCalls.push(call);
510
+ }
481
511
  const reasoningMarkupTail = reasoningMarkup?.flush();
482
512
  if (reasoningMarkupTail?.text) {
483
513
  const reasoningText = stripThinkingTags(reasoningMarkupTail.text);
@@ -84,7 +84,10 @@ export function reasoningEvent(
84
84
  payload: null,
85
85
  ...(summary === undefined ? {} : { summary }),
86
86
  ...(typeof encrypted === "string"
87
- ? { encrypted_content: encrypted }
87
+ ? {
88
+ encrypted_content: encrypted,
89
+ ...(typeof item["id"] === "string" ? { item_id: item["id"] } : {}),
90
+ }
88
91
  : {}),
89
92
  ...(summary === undefined && typeof encrypted === "string"
90
93
  ? { opaque: true as const }
@@ -235,9 +238,9 @@ export function parseCodexResponsesJsonToEvents(
235
238
  | undefined);
236
239
  // An incomplete response that produced a tool call is always a tool-use
237
240
  // stop; every other incomplete response is a length stop. There is no
238
- // separate executability gate — the branch that used to scan tool arguments
239
- // for parseable JSON returned `tool_use` on both outcomes, so the scan and
240
- // the `incomplete_details.reason` read could not change the result.
241
+ // separate executability gate — scanning tool arguments for parseable JSON
242
+ // returns `tool_use` on both outcomes, so the scan and the
243
+ // `incomplete_details.reason` read could not change the result.
241
244
  const stopReason = mapCodexStopReason(status, hasToolCall, endTurn);
242
245
  let finalStopReason = stopReason;
243
246
  if (endTurn === false && stopReason === "stop") {
@@ -1,15 +1,17 @@
1
1
  /**
2
- * DeepSeek sometimes emits its native tool markup as assistant text when the
3
- * structured tool channel was dropped in transit. A complete, well-formed block
4
- * becomes ordinary tool calls; anything incomplete stays text.
2
+ * CodeBuddy-family DeepSeek sometimes emits native tool markup as assistant text.
3
+ * Complete declared calls are recovered; incomplete and undeclared blocks stay text.
5
4
  */
6
-
7
- const PIPE = "\uFF5C\uFF5C";
8
- const CALLS = new RegExp(`<${PIPE}DSML${PIPE}\\s+calls>([\\s\\S]*?)</${PIPE}DSML${PIPE}\\s+calls>`, "i");
9
- const INVOKE = new RegExp(`<${PIPE}DSML${PIPE}\\s+invoke\\s+name=["']([^"']+)["']>([\\s\\S]*?)</${PIPE}DSML${PIPE}\\s+invoke>`, "gi");
10
- const PARAMETER = new RegExp(`<${PIPE}DSML${PIPE}\\s+parameter\\s+name=["']([^"']+)["'][^>]*>([\\s\\S]*?)</${PIPE}DSML${PIPE}\\s+parameter>`, "gi");
5
+ const SEPARATORS = ["||", "|", "|"] as const;
6
+ const BLOCK_NAMES = ["calls", "function_calls"] as const;
7
+ const OPENERS = SEPARATORS.flatMap((separator) => BLOCK_NAMES.map((name) => `<${separator}DSML${separator} ${name}>`));
11
8
  const TOOL_NAME = /^[A-Za-z0-9_][A-Za-z0-9_.:-]{0,63}$/;
12
9
 
10
+ /** Escapes regex metacharacters so a `|` separator is literal, not alternation. */
11
+ function escapeRegExp(value: string): string {
12
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13
+ }
14
+
13
15
  export interface DsmlToolCall {
14
16
  readonly id: string;
15
17
  readonly name: string;
@@ -21,27 +23,137 @@ export interface DsmlRecovery {
21
23
  readonly calls: readonly DsmlToolCall[];
22
24
  }
23
25
 
24
- export function parseDsmlToolCalls(text: string): DsmlRecovery | undefined {
26
+ export class DsmlToolCallParser {
27
+ readonly #allowedNames: ReadonlySet<string>;
28
+ #pending = "";
29
+ #callSequence = 0;
30
+
31
+ constructor(allowedNames: ReadonlySet<string>) {
32
+ this.#allowedNames = allowedNames;
33
+ }
34
+
35
+ push(chunk: string): DsmlRecovery {
36
+ this.#pending += chunk;
37
+ let cursor = 0;
38
+ let visible = "";
39
+ const calls: DsmlToolCall[] = [];
40
+ for (;;) {
41
+ let openIndex = -1;
42
+ let opener = "";
43
+ for (const candidate of OPENERS) {
44
+ const index = this.#pending.indexOf(candidate, cursor);
45
+ if (index >= 0 && (openIndex < 0 || index < openIndex)) {
46
+ openIndex = index;
47
+ opener = candidate;
48
+ }
49
+ }
50
+ if (openIndex < 0) {
51
+ const safeEnd = this.#settledEnd(cursor);
52
+ visible += this.#pending.slice(cursor, safeEnd);
53
+ cursor = safeEnd;
54
+ break;
55
+ }
56
+ visible += this.#pending.slice(cursor, openIndex);
57
+ const separator = SEPARATORS.find((value) => opener.startsWith(`<${value}DSML`));
58
+ const blockName = BLOCK_NAMES.find((value) => opener.endsWith(` ${value}>`));
59
+ if (separator === undefined || blockName === undefined) {
60
+ visible += "<";
61
+ cursor = openIndex + 1;
62
+ continue;
63
+ }
64
+ const close = `</${separator}DSML${separator} ${blockName}>`;
65
+ const closeIndex = this.#pending.indexOf(close, openIndex + opener.length);
66
+ if (closeIndex < 0) {
67
+ cursor = openIndex;
68
+ break;
69
+ }
70
+ const end = closeIndex + close.length;
71
+ const block = this.#pending.slice(openIndex, end);
72
+ const parsed = parseBlock(block, opener, close, separator, this.#allowedNames);
73
+ if (parsed === undefined) visible += block;
74
+ else {
75
+ for (const call of parsed) calls.push({ ...call, id: `call_dsml_${this.#callSequence++}_${call.name}` });
76
+ }
77
+ cursor = end;
78
+ }
79
+ this.#pending = this.#pending.slice(cursor);
80
+ return { text: visible, calls };
81
+ }
82
+
83
+ flush(): DsmlRecovery {
84
+ const text = this.#pending;
85
+ this.#pending = "";
86
+ return { text, calls: [] };
87
+ }
88
+
89
+ #settledEnd(start: number): number {
90
+ const remaining = this.#pending.slice(start);
91
+ for (let length = Math.min(OPENERS.reduce((max, opener) => Math.max(max, opener.length - 1), 0), remaining.length); length > 0; length -= 1) {
92
+ const suffix = remaining.slice(-length);
93
+ if (OPENERS.some((opener) => opener.startsWith(suffix))) return this.#pending.length - length;
94
+ }
95
+ return this.#pending.length;
96
+ }
97
+ }
98
+
99
+ export function parseDsmlToolCalls(
100
+ text: string,
101
+ allowedNames: ReadonlySet<string> = new Set(),
102
+ ): DsmlRecovery | undefined {
25
103
  if (!text.includes("DSML")) return undefined;
26
- const block = CALLS.exec(text);
27
- if (block === null || block[1] === undefined || block.index === undefined) return undefined;
28
- const calls: DsmlToolCall[] = [];
29
- for (const invoke of block[1].matchAll(INVOKE)) {
30
- const name = invoke[1]?.trim() ?? "";
31
- if (!TOOL_NAME.test(name)) return undefined;
32
- const args: Record<string, string> = {};
33
- for (const parameter of (invoke[2] ?? "").matchAll(PARAMETER)) {
34
- const key = parameter[1]?.trim();
35
- if (key === undefined || key.length === 0) return undefined;
36
- args[key] = (parameter[2] ?? "").trim();
104
+ const parser = new DsmlToolCallParser(allowedNames);
105
+ const first = parser.push(text);
106
+ const last = parser.flush();
107
+ const recovered = [...first.calls, ...last.calls];
108
+ return recovered.length === 0 ? undefined : { text: `${first.text}${last.text}`.trim(), calls: recovered };
109
+ }
110
+
111
+ function parseBlock(
112
+ block: string,
113
+ opener: string,
114
+ close: string,
115
+ separator: string,
116
+ allowedNames: ReadonlySet<string>,
117
+ ): Omit<DsmlToolCall, "id">[] | undefined {
118
+ const inner = block.slice(opener.length, -close.length);
119
+ const pipe = escapeRegExp(separator);
120
+ const invokePattern = new RegExp(`<${pipe}DSML${pipe}\\s+invoke\\s+name="([^"]*)"\\s*>`, "g");
121
+ const calls: Omit<DsmlToolCall, "id">[] = [];
122
+ // The allowlist is strict only when the caller knows the declared tools: a
123
+ // caller without one (a direct parse) keeps the generic name rule, while a
124
+ // dispatch that declared its tools must never execute a name it did not offer.
125
+ const strict = allowedNames.size > 0;
126
+ let cursor = 0;
127
+ for (const invoke of inner.matchAll(invokePattern)) {
128
+ const name = invoke[1] ?? "";
129
+ if (!TOOL_NAME.test(name) || (strict && !allowedNames.has(name)) || invoke.index === undefined) return undefined;
130
+ if (inner.slice(cursor, invoke.index).trim().length > 0) return undefined;
131
+ const bodyStart = invoke.index + invoke[0].length;
132
+ const invokeClose = `</${separator}DSML${separator} invoke>`;
133
+ const invokeEnd = inner.indexOf(invokeClose, bodyStart);
134
+ if (invokeEnd < 0) return undefined;
135
+ const parameterPattern = new RegExp(`<${pipe}DSML${pipe}\\s+parameter\\s+name="([^"]*)"(?:\\s+string="(true|false)")?\\s*>`, "g");
136
+ const body = inner.slice(bodyStart, invokeEnd);
137
+ const args: Record<string, unknown> = {};
138
+ let parameterCursor = 0;
139
+ for (const parameter of body.matchAll(parameterPattern)) {
140
+ const key = parameter[1] ?? "";
141
+ if (key.length === 0 || parameter.index === undefined || body.slice(parameterCursor, parameter.index).trim().length > 0) return undefined;
142
+ const valueStart = parameter.index + parameter[0].length;
143
+ const parameterClose = `</${separator}DSML${separator} parameter>`;
144
+ const valueEnd = body.indexOf(parameterClose, valueStart);
145
+ if (valueEnd < 0) return undefined;
146
+ const raw = body.slice(valueStart, valueEnd);
147
+ if (parameter[2] === "true") args[key] = raw;
148
+ else {
149
+ try { args[key] = JSON.parse(raw); }
150
+ catch { args[key] = raw; }
151
+ }
152
+ parameterCursor = valueEnd + parameterClose.length;
37
153
  }
38
- calls.push({
39
- id: `call_dsml_${calls.length}_${name}`,
40
- name,
41
- arguments: JSON.stringify(args),
42
- });
154
+ if (body.slice(parameterCursor).trim().length > 0) return undefined;
155
+ calls.push({ name, arguments: JSON.stringify(args) });
156
+ cursor = invokeEnd + invokeClose.length;
43
157
  }
44
- if (calls.length === 0) return undefined;
45
- const visible = `${text.slice(0, block.index)}${text.slice(block.index + block[0].length)}`.trim();
46
- return { text: visible, calls };
158
+ return calls.length > 0 && inner.slice(cursor).trim().length === 0 ? calls : undefined;
47
159
  }
@@ -182,7 +182,10 @@ export function parseResponsesResponseToEvents(
182
182
  }
183
183
  : { payload: text }),
184
184
  ...(position === 0 && encryptedContent !== undefined
185
- ? { encrypted_content: encryptedContent }
185
+ ? {
186
+ encrypted_content: encryptedContent,
187
+ ...(itemId === undefined ? {} : { item_id: itemId }),
188
+ }
186
189
  : {}),
187
190
  },
188
191
  });
@@ -342,9 +345,9 @@ export async function* decodeResponsesSseStream(
342
345
  const eventType = event["type"];
343
346
  // An explicit failure inside a 200 OK. Two shapes carry one: a top-level
344
347
  // `type: "error"` frame (the error object itself), and a `response.failed`
345
- // terminal whose `response.error` explains why. Both used to be recorded
346
- // only as a `failed` terminal, so the client got no code and telemetry got
347
- // `unknown_error`; the Claude and Gemini decoders already raised.
348
+ // terminal whose `response.error` explains why. Recording either only as a
349
+ // `failed` terminal would leave the client with no code and telemetry with
350
+ // `unknown_error`; the Claude and Gemini decoders already raise.
348
351
  const failedResponse = eventType === "response.failed" && isRecord(event["response"]) ? event["response"] : undefined;
349
352
  const streamError = gatewayErrorFromStreamError(
350
353
  eventType === "error" ? event : failedResponse?.["error"],
@@ -566,7 +569,10 @@ export async function* decodeResponsesSseStream(
566
569
  ? { payload: null, summary: text, summary_index: reasoningPart.index }
567
570
  : { payload: text }),
568
571
  ...(position === 0 && encryptedContent !== undefined
569
- ? { encrypted_content: encryptedContent }
572
+ ? {
573
+ encrypted_content: encryptedContent,
574
+ ...(itemId === undefined ? {} : { item_id: itemId }),
575
+ }
570
576
  : {}),
571
577
  },
572
578
  } as CanonicalEvent;
@@ -2,11 +2,10 @@
2
2
  * OpenAI-family in-stream error frames → typed `GatewayError`.
3
3
  *
4
4
  * A `200 OK` whose SSE body carries an explicit error envelope is an upstream
5
- * failure that arrived *after* the status line. The chat, responses, and codex
6
- * decoders used to record it only as a terminal `state: "failed"`, which left
7
- * the client with no `error.code`, no message, and telemetry with the
8
- * `unknown_error` category — while the Claude (`messages.ts`) and Gemini
9
- * decoders already threw for the identical event. This module is the one
5
+ * failure that arrived *after* the status line. Recording it only as a terminal
6
+ * `state: "failed"` leaves the client with no `error.code`, no message, and
7
+ * telemetry with the `unknown_error` category — while the Claude (`messages.ts`)
8
+ * and Gemini decoders throw for the identical event. This module is the one
10
9
  * classifier those surfaces share.
11
10
  *
12
11
  * Classification reads **structured identifiers only** (`error.type`,