@kitn.ai/ui 0.28.0 → 0.30.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 (615) hide show
  1. package/README.md +11 -7
  2. package/dist/{Icon-DObhsRtm.js → Icon-BzBET3E-.js} +1 -1
  3. package/dist/{action-icons-CcgNN2oT.js → action-icons-DaW2pU6m.js} +1 -1
  4. package/dist/agent-tooling/construct/public.d.ts +15 -0
  5. package/dist/agent-tooling/construct/schema.d.ts +111 -0
  6. package/dist/arrow-down-D783iMJj.js +1 -0
  7. package/dist/arrow-left-0VLM5kyg.js +1 -0
  8. package/dist/artifact-DGjzHYUT.js +1 -0
  9. package/dist/{attachments-Qr4kg-2b.js → attachments-YvXHlg88.js} +1 -1
  10. package/dist/{audio-visualizer-DToYdfSc.js → audio-visualizer-9lz5IHsL.js} +1 -1
  11. package/dist/{badge-CdEu_jn2.js → badge-Dh_vp8A5.js} +1 -1
  12. package/dist/{bell-DQAAOpDz.js → bell-CT7_WUwS.js} +1 -1
  13. package/dist/{button-DkuOnGLy.js → button-C_3te6fm.js} +1 -1
  14. package/dist/{card-renderer-Jx70m6_l.js → card-renderer-DZVaGH2e.js} +1 -1
  15. package/dist/check-CiEOVnEN.js +1 -0
  16. package/dist/{checkbox-D7iRB3nL.js → checkbox-Bu2T2e0U.js} +1 -1
  17. package/dist/{checkbox-group-B2_rpoPM.js → checkbox-group-BiryEXPQ.js} +1 -1
  18. package/dist/chevron-down-CSobuZRG.js +1 -0
  19. package/dist/chevron-right-DR9ZAP-2.js +1 -0
  20. package/dist/{choice-card-m0BYqqOK.js → choice-card-CM6IjkmL.js} +1 -1
  21. package/dist/circle-BC66EYF7.js +1 -0
  22. package/dist/circle-check-BqmahW0O.js +1 -0
  23. package/dist/{circle-x-BPG0iPst.js → circle-x-Bk-dA5Wf.js} +1 -1
  24. package/dist/{code-block-ClMLKb7C.js → code-block-B26t-Q7q.js} +1 -1
  25. package/dist/{collapsible-B4DlWicL.js → collapsible-Bhc636Cf.js} +1 -1
  26. package/dist/components/builder-composer-triggers.d.ts +62 -0
  27. package/dist/components/builder-layout.d.ts +54 -0
  28. package/dist/components/builder-message-actions.d.ts +72 -0
  29. package/dist/components/builder-panel.d.ts +189 -0
  30. package/dist/components/builder-preview.d.ts +11 -0
  31. package/dist/components/builder-shell-controls.d.ts +69 -0
  32. package/dist/components/builder-skeleton.d.ts +58 -0
  33. package/dist/components/builder-start.d.ts +75 -0
  34. package/dist/components/builder-workspace-variants.d.ts +23 -0
  35. package/dist/components/captions.d.ts +66 -0
  36. package/dist/{composer-sQ0HcvyG.js → composer-DMI85B6j.js} +1 -1
  37. package/dist/{confirm-card-rQWDQsnC.js → confirm-card-B0YIAwXp.js} +1 -1
  38. package/dist/construct-cli.es.js +3 -3
  39. package/dist/construct.d.ts +5 -0
  40. package/dist/construct.js +327 -0
  41. package/dist/{context-0kg4ZX1J.js → context-DWgfhqWp.js} +1 -1
  42. package/dist/controllable-BagRpyHv.js +1 -0
  43. package/dist/{conversation-item-BxwQLaFJ.js → conversation-item-BsOsVGmT.js} +1 -1
  44. package/dist/{conversation-list-BCfGN1JU.js → conversation-list-aX3Vtc7L.js} +1 -1
  45. package/dist/{copy-n9u9rqpi.js → copy-BmEwsNhJ.js} +1 -1
  46. package/dist/core-C8fzo39E.js +1 -12
  47. package/dist/{create-tween-uPkArjIG.js → create-tween-CNOAU3Fx.js} +1 -1
  48. package/dist/{create-tween-C-q-JCM-.js → create-tween-WfqiENls.js} +1 -1
  49. package/dist/{create-tween-CRrJXxBd.js → create-tween-wlFtW9Kh.js} +1 -1
  50. package/dist/{create-tween-DZ3RRDHD.js → create-tween-zt79ba32.js} +1 -1
  51. package/dist/{default-input-CaMrJkgH.js → default-input-DMV5Eaze.js} +1 -1
  52. package/dist/define-CI0y3eE4.js +1 -0
  53. package/dist/define.js +40 -40
  54. package/dist/define.server.js +45 -45
  55. package/dist/{disclosure-WUImBbDv.js → disclosure-NnACBxsC.js} +1 -1
  56. package/dist/{dropdown-wZVeJOMd.js → dropdown-DT7G0RGR.js} +1 -1
  57. package/dist/elements/agent-card.js +1 -1
  58. package/dist/elements/artifact.js +1 -1
  59. package/dist/elements/attachments.js +1 -1
  60. package/dist/elements/audio-visualizer.js +1 -1
  61. package/dist/elements/avatar.js +1 -1
  62. package/dist/elements/badge.js +1 -1
  63. package/dist/elements/button.js +1 -1
  64. package/dist/elements/card.js +1 -1
  65. package/dist/elements/cards.js +1 -1
  66. package/dist/elements/chain-of-thought.js +1 -1
  67. package/dist/elements/chat-scope-picker.js +1 -1
  68. package/dist/elements/chat-workspace.js +1 -1
  69. package/dist/elements/chat.js +1 -1
  70. package/dist/elements/checkbox-group.js +1 -1
  71. package/dist/elements/checkbox.js +1 -1
  72. package/dist/elements/checkpoint.js +1 -1
  73. package/dist/elements/choice.js +1 -1
  74. package/dist/elements/coachmark.js +1 -1
  75. package/dist/elements/code-block.js +1 -1
  76. package/dist/elements/command.js +1 -1
  77. package/dist/elements/compare.js +1 -1
  78. package/dist/elements/composer.js +1 -1
  79. package/dist/elements/confirm-card.js +1 -1
  80. package/dist/elements/context-meter.js +1 -1
  81. package/dist/elements/conversation-item.js +1 -1
  82. package/dist/elements/conversation-list.js +1 -1
  83. package/dist/elements/dialog.js +1 -1
  84. package/dist/elements/dock.js +1 -1
  85. package/dist/elements/dropdown.js +1 -1
  86. package/dist/elements/editable-label.js +1 -1
  87. package/dist/elements/embed.js +1 -1
  88. package/dist/elements/empty.js +1 -1
  89. package/dist/elements/feedback-bar.js +1 -1
  90. package/dist/elements/file-tree.js +1 -1
  91. package/dist/elements/file-upload.js +1 -1
  92. package/dist/elements/form.js +1 -1
  93. package/dist/elements/hover-card.js +1 -1
  94. package/dist/elements/icon.js +1 -1
  95. package/dist/elements/image.js +1 -1
  96. package/dist/elements/input.js +1 -1
  97. package/dist/elements/kbd.js +1 -1
  98. package/dist/elements/link-preview.js +1 -1
  99. package/dist/elements/loader.js +1 -1
  100. package/dist/elements/markdown.js +1 -1
  101. package/dist/elements/menu.js +1 -1
  102. package/dist/elements/message-skills.js +1 -1
  103. package/dist/elements/message.js +1 -1
  104. package/dist/elements/model-switcher.js +1 -1
  105. package/dist/elements/nav.js +1 -1
  106. package/dist/elements/notice.js +1 -1
  107. package/dist/elements/pane-grid.js +1 -1
  108. package/dist/elements/pane-group.js +1 -1
  109. package/dist/elements/pane.js +1 -1
  110. package/dist/elements/popover.js +1 -1
  111. package/dist/elements/progress-bar.js +1 -1
  112. package/dist/elements/prompt-dock.js +1 -1
  113. package/dist/elements/prompt-input.js +1 -1
  114. package/dist/elements/prompt-suggestions.js +1 -1
  115. package/dist/elements/radio-group.js +1 -1
  116. package/dist/elements/reasoning.js +1 -1
  117. package/dist/elements/remote.js +1 -1
  118. package/dist/elements/resizable.js +1 -1
  119. package/dist/elements/response-stream.js +1 -1
  120. package/dist/elements/screen.js +1 -1
  121. package/dist/elements/scroll-area.js +1 -1
  122. package/dist/elements/scroll-button.js +1 -1
  123. package/dist/elements/search.js +1 -1
  124. package/dist/elements/segmented.js +1 -1
  125. package/dist/elements/select.js +1 -1
  126. package/dist/elements/separator.js +1 -1
  127. package/dist/elements/setting-item.js +1 -1
  128. package/dist/elements/settings-group.js +1 -1
  129. package/dist/elements/skeleton.js +1 -1
  130. package/dist/elements/slider.js +1 -1
  131. package/dist/elements/source.js +1 -1
  132. package/dist/elements/status.js +1 -1
  133. package/dist/elements/switch.js +1 -1
  134. package/dist/elements/tabs.js +1 -1
  135. package/dist/elements/tasks.js +1 -1
  136. package/dist/elements/text-shimmer.js +1 -1
  137. package/dist/elements/thinking-bar.js +1 -1
  138. package/dist/elements/thread.js +1 -1
  139. package/dist/elements/toast.js +1 -1
  140. package/dist/elements/tool.js +1 -1
  141. package/dist/elements/tooltip.js +1 -1
  142. package/dist/elements/voice-input.js +1 -1
  143. package/dist/elements/voice-output.js +1 -1
  144. package/dist/{ellipsis-CbOpJnU3.js → ellipsis-6pJ7SxYT.js} +1 -1
  145. package/dist/{embed-CrfjkbUG.js → embed-BMje-nxj.js} +1 -1
  146. package/dist/engine-javascript-C1x7zo1_.js +1 -141
  147. package/dist/{external-link-ByXAwBBQ.js → external-link-Beh9DKeR.js} +1 -1
  148. package/dist/{file-text-DMib2qwt.js → file-text-D1GPfZ8Q.js} +1 -1
  149. package/dist/{file-tree-CJqM2aP7.js → file-tree-Dwj2lqg-.js} +1 -1
  150. package/dist/{folder-_BAb929C.js → folder-b6D8x1DY.js} +1 -1
  151. package/dist/{form-DSZgZQZk.js → form-DWgdxDXu.js} +1 -1
  152. package/dist/{hover-card-BQbul9eq.js → hover-card-CWRkqm40.js} +1 -1
  153. package/dist/{icon-CdcfRhYI.js → icon-CL0jdhsf.js} +1 -1
  154. package/dist/index.js +1741 -1725
  155. package/dist/index.server.js +1582 -1566
  156. package/dist/{info-DsFjGuI_.js → info-0cOUherQ.js} +1 -1
  157. package/dist/{input-DKbmkMos.js → input-BH6Ru91Q.js} +1 -1
  158. package/dist/kai.es.js +1 -1
  159. package/dist/{kbd-PvdGhX4s.js → kbd-DRAnVtNX.js} +1 -1
  160. package/dist/{link-zF8PzWfF.js → link-Dlr0vssB.js} +1 -1
  161. package/dist/{link-preview-DbM1866N.js → link-preview-DODgRyV9.js} +1 -1
  162. package/dist/loader-KvJB6NrT.js +1 -0
  163. package/dist/{markdown-LMvmAyBk.js → markdown-DX9tuJVj.js} +1 -1
  164. package/dist/mcp.es.js +9 -5
  165. package/dist/{message-BYZECyek.js → message-BqQ7Ot5q.js} +1 -1
  166. package/dist/{message-BSsgvHvf.js → message-DkaGfBd8.js} +1 -1
  167. package/dist/message-circle-BPpp5QKv.js +1 -0
  168. package/dist/message-square-BLbsd9BJ.js +1 -0
  169. package/dist/{minimize-2-BQRoBV1V.js → minimize-2-CyOHJwcN.js} +1 -1
  170. package/dist/{model-switcher-CMpCOYcB.js → model-switcher-0hrKDiK2.js} +1 -1
  171. package/dist/{overlay-Cb-MkyDa.js → overlay-DBMFXie6.js} +1 -1
  172. package/dist/{panel-right-i8warFxT.js → panel-right-t8SWJL6e.js} +1 -1
  173. package/dist/{paperclip-ByY3UVO6.js → paperclip-CdzuOjbU.js} +1 -1
  174. package/dist/play-BXy1u7rW.js +1 -0
  175. package/dist/primitives/use-auto-resize.d.ts +11 -0
  176. package/dist/{progress-bar-jVAHvMUf.js → progress-bar-CiBtxMWT.js} +1 -1
  177. package/dist/{prompt-suggestion-CD-qtNgC.js → prompt-suggestion-I_DHP21i.js} +1 -1
  178. package/dist/{radio-BsoBMhkF.js → radio-C_p3n39l.js} +1 -1
  179. package/dist/{reasoning-4kBPtlTx.js → reasoning-BDwy_tIe.js} +1 -1
  180. package/dist/register-impl-DvEnhIxx.js +424 -0
  181. package/dist/{resizable-uPmY9VY-.js → resizable-SYC-fStM.js} +1 -1
  182. package/dist/{rotate-ccw-B2xn93py.js → rotate-ccw-B1nwmwuY.js} +1 -1
  183. package/dist/{rotate-cw-CMVLoCyH.js → rotate-cw-CLAnBcm1.js} +1 -1
  184. package/dist/{scroll-area-BqLUo8ZZ.js → scroll-area-zRyXoLhI.js} +1 -1
  185. package/dist/{scroll-button-DslXiJyL.js → scroll-button-jzrbYCG9.js} +1 -1
  186. package/dist/{select-2cLRtPGT.js → select-DM-0nEe0.js} +1 -1
  187. package/dist/{separator-BWQtNJcM.js → separator-BToOOoOt.js} +1 -1
  188. package/dist/{settings-BBR63jLg.js → settings-PaGo_S4U.js} +1 -1
  189. package/dist/{settings-group-B2ciy-Iw.js → settings-group-BFXGIfXq.js} +1 -1
  190. package/dist/{skeleton-CkHI-HgC.js → skeleton-DYpmFnnm.js} +1 -1
  191. package/dist/{slider-DDR9U-i0.js → slider-ebutAmkQ.js} +1 -1
  192. package/dist/{solid-DruVOXnf.js → solid-15v_3WVL.js} +3563 -3412
  193. package/dist/{solid-BzCFdL25.js → solid-5P-otgdi.js} +4502 -4296
  194. package/dist/solid.d.ts +6 -0
  195. package/dist/solid.js +259 -254
  196. package/dist/solid.server.js +259 -254
  197. package/dist/{source-GEnFF528.js → source-c1gpRl9h.js} +1 -1
  198. package/dist/{star-CilHq8GP.js → star-pcV_3LqF.js} +1 -1
  199. package/dist/{store-Ciyk6eZG.js → store-CSgbFFCg.js} +1 -1
  200. package/dist/{switch-KkeQIbuH.js → switch-Bh2OdLV2.js} +1 -1
  201. package/dist/{tasks-card-BrN4bggJ.js → tasks-card-CbqQAn2S.js} +1 -1
  202. package/dist/text-shimmer-BSzgEnDH.js +1 -0
  203. package/dist/textarea-DrYNt_0z.js +1 -0
  204. package/dist/theme.tokens.css +22 -21
  205. package/dist/{thumbs-up-CsT9Wzb9.js → thumbs-up-BL3-NenK.js} +1 -1
  206. package/dist/{toast-store-tgK6MuMv.js → toast-store-DkOlYczt.js} +1 -1
  207. package/dist/{tool-DzUjIBT8.js → tool-CeIWUVUe.js} +1 -1
  208. package/dist/{tooltip-Bkkg_4t0.js → tooltip-GA-nmg06.js} +1 -1
  209. package/dist/{trash-2-jBKxjGGQ.js → trash-2-CT66QWlm.js} +1 -1
  210. package/dist/{triangle-alert-DlQ68ppf.js → triangle-alert-CeHLWxhD.js} +1 -1
  211. package/dist/ui/color-field.d.ts +48 -0
  212. package/dist/ui/textarea.d.ts +4 -0
  213. package/dist/ui/toggle-chip.d.ts +40 -0
  214. package/dist/{upload-TcAHXv9W.js → upload-9Csb0F0J.js} +1 -1
  215. package/dist/{use-card-resolution-DaZqAdWf.js → use-card-resolution-Zg0a08Z-.js} +1 -1
  216. package/dist/{variant-aurora-DbW_OWL2.js → variant-aurora-4VBQHIKV.js} +1 -1
  217. package/dist/{variant-aurora-CNmfHhcx.js → variant-aurora-DBeCAQNk.js} +2 -2
  218. package/dist/{variant-aurora-D6GsdTPA.js → variant-aurora-FzHVXHby.js} +2 -2
  219. package/dist/{variant-aurora-BhLuYxJR.js → variant-aurora-LE3fqxgw.js} +1 -1
  220. package/dist/{variant-custom-BvYHR62b.js → variant-custom-AFRGcOBS.js} +1 -1
  221. package/dist/{variant-custom-DV2x254s.js → variant-custom-BQkNuPpE.js} +2 -2
  222. package/dist/{variant-custom-BRvaNHSi.js → variant-custom-DI5R0rln.js} +1 -1
  223. package/dist/{variant-custom-DMTrEO5w.js → variant-custom-WWnrrRHS.js} +2 -2
  224. package/dist/{variant-wave-CyBueXj9.js → variant-wave-DUCbH5oI.js} +2 -2
  225. package/dist/{variant-wave-BNMGrDfj.js → variant-wave-DuQV7DhQ.js} +2 -2
  226. package/dist/{variant-wave-BpDd8t3Z.js → variant-wave-nSI2sfII.js} +1 -1
  227. package/dist/{variant-wave-sp-qwsdI.js → variant-wave-wTP5v_jU.js} +1 -1
  228. package/dist/x-eiO9V6J6.js +1 -0
  229. package/{src/elements/styles.css → kit-base.css} +27 -44
  230. package/package.json +32 -17
  231. package/solid.css +33 -0
  232. package/theme.css +39 -23
  233. package/bin/route.test.js +0 -58
  234. package/dist/arrow-down-CjRU3JgV.js +0 -1
  235. package/dist/arrow-left-Bnpb1UEW.js +0 -1
  236. package/dist/artifact-Bwt7OgcR.js +0 -1
  237. package/dist/check-njkVdZu2.js +0 -1
  238. package/dist/chevron-down-B72s6PXR.js +0 -1
  239. package/dist/chevron-right-BatuHzUm.js +0 -1
  240. package/dist/circle-check-B62pODB3.js +0 -1
  241. package/dist/circle-zx0ByzY9.js +0 -1
  242. package/dist/controllable-CnGpMF6r.js +0 -1
  243. package/dist/define-By-uL588.js +0 -1
  244. package/dist/loader-BM4CEz4G.js +0 -1
  245. package/dist/message-circle-DruwObfX.js +0 -1
  246. package/dist/message-square-BG6q6mlm.js +0 -1
  247. package/dist/play-BvKKq3fX.js +0 -1
  248. package/dist/register-impl-B1hLkrI8.js +0 -424
  249. package/dist/text-shimmer-Dcj1MHuq.js +0 -1
  250. package/dist/textarea-Dpvbngbg.js +0 -1
  251. package/dist/x-Z5UntkIJ.js +0 -1
  252. package/src/agent-tooling/README.md +0 -192
  253. package/src/agent-tooling/archetypes.ts +0 -205
  254. package/src/agent-tooling/catalog/README.md +0 -676
  255. package/src/agent-tooling/catalog/catalog-types.ts +0 -231
  256. package/src/agent-tooling/catalog/fabrications.ts +0 -96
  257. package/src/agent-tooling/catalog/invariants.ts +0 -284
  258. package/src/agent-tooling/catalog/labs-titles.ts +0 -114
  259. package/src/agent-tooling/catalog/scenarios.ts +0 -87
  260. package/src/agent-tooling/catalog/surfaces.ts +0 -347
  261. package/src/agent-tooling/construct/cli-entry.ts +0 -8
  262. package/src/agent-tooling/construct/cli.ts +0 -147
  263. package/src/agent-tooling/construct/codegen.ts +0 -1635
  264. package/src/agent-tooling/construct/construct.v1.schema.json +0 -319
  265. package/src/agent-tooling/construct/dev.ts +0 -148
  266. package/src/agent-tooling/construct/fixtures/demo-widget.construct.json +0 -7
  267. package/src/agent-tooling/construct/fixtures/ops-console.construct.json +0 -44
  268. package/src/agent-tooling/construct/fixtures/owner-widget.construct.json +0 -32
  269. package/src/agent-tooling/construct/schema.ts +0 -452
  270. package/src/agent-tooling/integrations/anthropic.ts +0 -442
  271. package/src/agent-tooling/integrations/cloudflare.ts +0 -218
  272. package/src/agent-tooling/integrations/langgraph.ts +0 -133
  273. package/src/agent-tooling/integrations/mastra.ts +0 -182
  274. package/src/agent-tooling/integrations/mock.ts +0 -148
  275. package/src/agent-tooling/integrations/ollama.ts +0 -97
  276. package/src/agent-tooling/integrations/openai.ts +0 -95
  277. package/src/agent-tooling/integrations/openrouter.ts +0 -82
  278. package/src/agent-tooling/integrations/pi.ts +0 -176
  279. package/src/agent-tooling/integrations/pydantic-ai.ts +0 -108
  280. package/src/agent-tooling/integrations/vercel-ai-sdk.ts +0 -398
  281. package/src/agent-tooling/mcp/css-raw.d.ts +0 -12
  282. package/src/agent-tooling/mcp/manifest.ts +0 -422
  283. package/src/agent-tooling/mcp/server.ts +0 -116
  284. package/src/agent-tooling/mcp/stdio.ts +0 -18
  285. package/src/agent-tooling/mcp/tools/construct.ts +0 -130
  286. package/src/agent-tooling/mcp/tools/debug.ts +0 -621
  287. package/src/agent-tooling/mcp/tools/reference.ts +0 -980
  288. package/src/agent-tooling/mcp/tools/scaffold.ts +0 -6708
  289. package/src/agent-tooling/mcp/tools/theme.ts +0 -456
  290. package/src/agent-tooling/mcp/tools/types.ts +0 -14
  291. package/src/agent-tooling/mcp/validate-args.ts +0 -141
  292. package/src/agent-tooling/recipes/composed-thread.ts +0 -714
  293. package/src/agent-tooling/recipes/index.ts +0 -22
  294. package/src/agent-tooling/recipes/types.ts +0 -32
  295. package/src/agent-tooling/registry.ts +0 -132
  296. package/src/agent-tooling/route-emit.ts +0 -305
  297. package/src/agent-tooling/types.ts +0 -390
  298. package/src/components/artifact-card.tsx +0 -120
  299. package/src/components/artifact.tsx +0 -914
  300. package/src/components/attachment-types.ts +0 -66
  301. package/src/components/attachments.tsx +0 -536
  302. package/src/components/audio-visualizer/audio-visualizer.voice-fixture.ts +0 -257
  303. package/src/components/audio-visualizer/aurora.glsl.ts +0 -330
  304. package/src/components/audio-visualizer/fit-scale.ts +0 -112
  305. package/src/components/audio-visualizer/index.tsx +0 -528
  306. package/src/components/audio-visualizer/labs/lab-choreography.ts +0 -187
  307. package/src/components/audio-visualizer/labs/lab-shaders.ts +0 -363
  308. package/src/components/audio-visualizer/labs/lab-visualizer.tsx +0 -102
  309. package/src/components/audio-visualizer/shader-canvas.tsx +0 -876
  310. package/src/components/audio-visualizer/sizes.ts +0 -54
  311. package/src/components/audio-visualizer/variant-aurora.tsx +0 -305
  312. package/src/components/audio-visualizer/variant-bar.tsx +0 -175
  313. package/src/components/audio-visualizer/variant-custom.tsx +0 -344
  314. package/src/components/audio-visualizer/variant-grid.tsx +0 -175
  315. package/src/components/audio-visualizer/variant-radial.tsx +0 -163
  316. package/src/components/audio-visualizer/variant-wave.tsx +0 -130
  317. package/src/components/audio-visualizer/wave.glsl.ts +0 -130
  318. package/src/components/card-fallback.tsx +0 -60
  319. package/src/components/card-renderer.tsx +0 -220
  320. package/src/components/card.tsx +0 -212
  321. package/src/components/chain-of-thought.tsx +0 -348
  322. package/src/components/chat-container.tsx +0 -89
  323. package/src/components/chat-scope-picker.tsx +0 -57
  324. package/src/components/chat-thread.tsx +0 -1014
  325. package/src/components/checkpoint.tsx +0 -88
  326. package/src/components/choice-card.tsx +0 -743
  327. package/src/components/coachmark.tsx +0 -249
  328. package/src/components/code-block.tsx +0 -181
  329. package/src/components/composer-dom.ts +0 -159
  330. package/src/components/composer-highlight.ts +0 -242
  331. package/src/components/composer-history.ts +0 -82
  332. package/src/components/composer.tsx +0 -1075
  333. package/src/components/confirm-card.tsx +0 -402
  334. package/src/components/context.tsx +0 -377
  335. package/src/components/conversation-item.tsx +0 -241
  336. package/src/components/conversation-list.tsx +0 -429
  337. package/src/components/conversation-panel.tsx +0 -118
  338. package/src/components/dismissed-stub.tsx +0 -80
  339. package/src/components/embed.tsx +0 -200
  340. package/src/components/empty.tsx +0 -177
  341. package/src/components/feedback-bar.tsx +0 -166
  342. package/src/components/file-tree.tsx +0 -530
  343. package/src/components/file-upload.tsx +0 -167
  344. package/src/components/form-widgets.tsx +0 -525
  345. package/src/components/form.tsx +0 -1296
  346. package/src/components/home-panel.tsx +0 -159
  347. package/src/components/image.tsx +0 -65
  348. package/src/components/link-preview.tsx +0 -195
  349. package/src/components/loader.tsx +0 -367
  350. package/src/components/markdown.tsx +0 -162
  351. package/src/components/message-skills.tsx +0 -40
  352. package/src/components/message.tsx +0 -765
  353. package/src/components/model-switcher.tsx +0 -132
  354. package/src/components/prompt-input.tsx +0 -188
  355. package/src/components/prompt-suggestion.tsx +0 -150
  356. package/src/components/reasoning.tsx +0 -293
  357. package/src/components/response-compare-types.ts +0 -152
  358. package/src/components/response-compare.tsx +0 -455
  359. package/src/components/response-stream.tsx +0 -109
  360. package/src/components/screen.tsx +0 -181
  361. package/src/components/scroll-button.tsx +0 -93
  362. package/src/components/source.tsx +0 -192
  363. package/src/components/tasks-card.tsx +0 -801
  364. package/src/components/text-shimmer.tsx +0 -37
  365. package/src/components/thinking-bar.tsx +0 -50
  366. package/src/components/thread.tsx +0 -228
  367. package/src/components/toast.tsx +0 -503
  368. package/src/components/tool-classify.ts +0 -26
  369. package/src/components/tool-types.ts +0 -49
  370. package/src/components/tool.tsx +0 -190
  371. package/src/components/use-card-resolution.ts +0 -44
  372. package/src/components/voice-input.tsx +0 -222
  373. package/src/components/voice-output.tsx +0 -242
  374. package/src/components/widget-tab-bar.tsx +0 -73
  375. package/src/components/workspace-shell.tsx +0 -285
  376. package/src/diagnostics/hook.ts +0 -352
  377. package/src/diagnostics/index.ts +0 -82
  378. package/src/diagnostics/report-request.ts +0 -296
  379. package/src/elements/agent-card.tsx +0 -67
  380. package/src/elements/artifact.tsx +0 -194
  381. package/src/elements/attachments.tsx +0 -140
  382. package/src/elements/audio-visualizer.tsx +0 -227
  383. package/src/elements/autoloader.ts +0 -89
  384. package/src/elements/avatar.tsx +0 -33
  385. package/src/elements/badge.tsx +0 -32
  386. package/src/elements/button.tsx +0 -149
  387. package/src/elements/card.tsx +0 -133
  388. package/src/elements/cards.tsx +0 -266
  389. package/src/elements/chain-of-thought.tsx +0 -141
  390. package/src/elements/chat-scope-picker.tsx +0 -65
  391. package/src/elements/chat-types.ts +0 -102
  392. package/src/elements/chat-workspace.tsx +0 -188
  393. package/src/elements/chat.tsx +0 -304
  394. package/src/elements/checkbox-group.tsx +0 -164
  395. package/src/elements/checkbox.tsx +0 -134
  396. package/src/elements/checkpoint.tsx +0 -47
  397. package/src/elements/choice.tsx +0 -100
  398. package/src/elements/coachmark.tsx +0 -90
  399. package/src/elements/code-block.tsx +0 -60
  400. package/src/elements/command.tsx +0 -197
  401. package/src/elements/compare.tsx +0 -111
  402. package/src/elements/compiled.css +0 -2
  403. package/src/elements/composer.tsx +0 -162
  404. package/src/elements/confirm-card.tsx +0 -76
  405. package/src/elements/context-meter.tsx +0 -107
  406. package/src/elements/conversation-item.tsx +0 -120
  407. package/src/elements/conversation-list.tsx +0 -203
  408. package/src/elements/css.ts +0 -5
  409. package/src/elements/default-input.tsx +0 -392
  410. package/src/elements/define-entry.ts +0 -12
  411. package/src/elements/define.tsx +0 -563
  412. package/src/elements/diagnostic-events.ts +0 -114
  413. package/src/elements/dialog.tsx +0 -145
  414. package/src/elements/disclosure.ts +0 -95
  415. package/src/elements/dock.tsx +0 -215
  416. package/src/elements/dropdown.tsx +0 -123
  417. package/src/elements/editable-label.tsx +0 -140
  418. package/src/elements/element-data-types.ts +0 -152
  419. package/src/elements/element-diagnostics.ts +0 -392
  420. package/src/elements/element-manifest.json +0 -354
  421. package/src/elements/element-nonscalar.json +0 -187
  422. package/src/elements/element-types.d.ts +0 -4353
  423. package/src/elements/embed.tsx +0 -35
  424. package/src/elements/empty.tsx +0 -29
  425. package/src/elements/feedback-bar.tsx +0 -66
  426. package/src/elements/file-tree.tsx +0 -62
  427. package/src/elements/file-upload.tsx +0 -44
  428. package/src/elements/form.tsx +0 -94
  429. package/src/elements/hover-card.tsx +0 -80
  430. package/src/elements/icon.tsx +0 -41
  431. package/src/elements/image.tsx +0 -32
  432. package/src/elements/input.tsx +0 -492
  433. package/src/elements/kbd.tsx +0 -45
  434. package/src/elements/link-preview.tsx +0 -34
  435. package/src/elements/loader.tsx +0 -25
  436. package/src/elements/markdown.tsx +0 -38
  437. package/src/elements/menu.tsx +0 -241
  438. package/src/elements/message-skills.tsx +0 -83
  439. package/src/elements/message.tsx +0 -361
  440. package/src/elements/model-switcher.tsx +0 -121
  441. package/src/elements/nav.tsx +0 -87
  442. package/src/elements/notice.tsx +0 -53
  443. package/src/elements/pane-grid.tsx +0 -111
  444. package/src/elements/pane-group.tsx +0 -119
  445. package/src/elements/pane.tsx +0 -129
  446. package/src/elements/popover.tsx +0 -81
  447. package/src/elements/progress-bar.tsx +0 -39
  448. package/src/elements/prompt-dock.tsx +0 -82
  449. package/src/elements/prompt-input.tsx +0 -240
  450. package/src/elements/prompt-suggestions.tsx +0 -113
  451. package/src/elements/radio-group.tsx +0 -129
  452. package/src/elements/reasoning.tsx +0 -71
  453. package/src/elements/register-impl.ts +0 -148
  454. package/src/elements/register.ts +0 -76
  455. package/src/elements/remote.tsx +0 -225
  456. package/src/elements/resizable.globals.d.ts +0 -36
  457. package/src/elements/resizable.tsx +0 -704
  458. package/src/elements/response-stream.tsx +0 -40
  459. package/src/elements/screen.tsx +0 -90
  460. package/src/elements/scroll-area.tsx +0 -33
  461. package/src/elements/scroll-button.tsx +0 -179
  462. package/src/elements/search.tsx +0 -185
  463. package/src/elements/segmented.tsx +0 -129
  464. package/src/elements/select.tsx +0 -165
  465. package/src/elements/separator.tsx +0 -36
  466. package/src/elements/setting-item.tsx +0 -50
  467. package/src/elements/settings-group.tsx +0 -37
  468. package/src/elements/skeleton.tsx +0 -45
  469. package/src/elements/slider.tsx +0 -171
  470. package/src/elements/slot-text.ts +0 -73
  471. package/src/elements/slots.ts +0 -787
  472. package/src/elements/source.tsx +0 -126
  473. package/src/elements/status.tsx +0 -42
  474. package/src/elements/switch.tsx +0 -118
  475. package/src/elements/tabs.tsx +0 -95
  476. package/src/elements/tasks.tsx +0 -103
  477. package/src/elements/text-shimmer.tsx +0 -28
  478. package/src/elements/thinking-bar.tsx +0 -43
  479. package/src/elements/thread.tsx +0 -139
  480. package/src/elements/toast.tsx +0 -106
  481. package/src/elements/tool.tsx +0 -78
  482. package/src/elements/tooltip.tsx +0 -85
  483. package/src/elements/validate-messages.ts +0 -76
  484. package/src/elements/voice-input.tsx +0 -100
  485. package/src/elements/voice-output.tsx +0 -82
  486. package/src/index.ts +0 -430
  487. package/src/primitives/audio-bands.ts +0 -174
  488. package/src/primitives/card-component-types.ts +0 -63
  489. package/src/primitives/card-contract.ts +0 -80
  490. package/src/primitives/card-data-types.ts +0 -361
  491. package/src/primitives/card-host.tsx +0 -37
  492. package/src/primitives/card-recovery.ts +0 -160
  493. package/src/primitives/card-registry.tsx +0 -78
  494. package/src/primitives/card-resolution.ts +0 -36
  495. package/src/primitives/card-routing.ts +0 -85
  496. package/src/primitives/card-schemas/artifact.schema.json +0 -92
  497. package/src/primitives/card-schemas/card-envelope.schema.json +0 -14
  498. package/src/primitives/card-schemas/card-event.schema.json +0 -12
  499. package/src/primitives/card-schemas/choice.schema.json +0 -75
  500. package/src/primitives/card-schemas/confirm.schema.json +0 -65
  501. package/src/primitives/card-schemas/embed.schema.json +0 -63
  502. package/src/primitives/card-schemas/form.result.schema.json +0 -7
  503. package/src/primitives/card-schemas/form.schema.json +0 -53
  504. package/src/primitives/card-schemas/link.schema.json +0 -54
  505. package/src/primitives/card-schemas/tasks.result.schema.json +0 -16
  506. package/src/primitives/card-schemas/tasks.schema.json +0 -83
  507. package/src/primitives/card-tags.ts +0 -61
  508. package/src/primitives/card-validate-cards.ts +0 -255
  509. package/src/primitives/card-validate-schemas.ts +0 -58
  510. package/src/primitives/card-validate.ts +0 -131
  511. package/src/primitives/chat-config.tsx +0 -103
  512. package/src/primitives/composer-model.ts +0 -49
  513. package/src/primitives/composer-triggers.ts +0 -35
  514. package/src/primitives/controllable.ts +0 -21
  515. package/src/primitives/conversation-store.ts +0 -248
  516. package/src/primitives/create-kai-chat.ts +0 -66
  517. package/src/primitives/create-tween.ts +0 -266
  518. package/src/primitives/embed-providers.ts +0 -254
  519. package/src/primitives/field-mask.ts +0 -256
  520. package/src/primitives/field-semantics.ts +0 -115
  521. package/src/primitives/highlighter.ts +0 -157
  522. package/src/primitives/input-mask.ts +0 -853
  523. package/src/primitives/link-preview.ts +0 -87
  524. package/src/primitives/message-feedback.ts +0 -124
  525. package/src/primitives/pdf-preview.ts +0 -121
  526. package/src/primitives/toast-store.ts +0 -338
  527. package/src/primitives/url-scheme-policy.ts +0 -70
  528. package/src/primitives/use-audio-analysis.ts +0 -340
  529. package/src/primitives/use-auto-resize.ts +0 -31
  530. package/src/primitives/use-resize-observer.ts +0 -45
  531. package/src/primitives/use-sequencer.ts +0 -72
  532. package/src/primitives/use-speech-recognition.ts +0 -146
  533. package/src/primitives/use-stick-to-bottom.ts +0 -75
  534. package/src/primitives/use-text-stream.ts +0 -143
  535. package/src/primitives/use-voice-recorder.ts +0 -62
  536. package/src/primitives/visualizer-sequences.ts +0 -350
  537. package/src/remote/host-embed.ts +0 -345
  538. package/src/remote/index.ts +0 -2
  539. package/src/remote/origin.ts +0 -30
  540. package/src/remote/provider-runtime.ts +0 -262
  541. package/src/remote/provider.ts +0 -2
  542. package/src/remote/validate.ts +0 -22
  543. package/src/remote/version.ts +0 -12
  544. package/src/remote/wire.ts +0 -48
  545. package/src/schemas/from-tool-call.ts +0 -151
  546. package/src/schemas/index.ts +0 -341
  547. package/src/schemas/provider-subsets.ts +0 -538
  548. package/src/schemas/registry.ts +0 -355
  549. package/src/schemas/tool-defs.ts +0 -793
  550. package/src/solid.ts +0 -173
  551. package/src/state/index.ts +0 -44
  552. package/src/state/messages.ts +0 -53
  553. package/src/state/mock.ts +0 -297
  554. package/src/state/parts.ts +0 -295
  555. package/src/state/persistence.ts +0 -180
  556. package/src/state/stream.ts +0 -246
  557. package/src/state/suggestions.ts +0 -9
  558. package/src/state/threads.ts +0 -142
  559. package/src/test-utils/fake-clock.ts +0 -88
  560. package/src/types.ts +0 -95
  561. package/src/ui/action-icons.ts +0 -53
  562. package/src/ui/agent-card.tsx +0 -218
  563. package/src/ui/avatar.tsx +0 -23
  564. package/src/ui/badge.tsx +0 -21
  565. package/src/ui/button.tsx +0 -49
  566. package/src/ui/card.tsx +0 -245
  567. package/src/ui/checkbox-group.tsx +0 -153
  568. package/src/ui/checkbox.tsx +0 -63
  569. package/src/ui/collapsible.tsx +0 -163
  570. package/src/ui/command.tsx +0 -103
  571. package/src/ui/dialog.tsx +0 -223
  572. package/src/ui/dock.tsx +0 -630
  573. package/src/ui/dropdown.tsx +0 -636
  574. package/src/ui/editable-label.tsx +0 -112
  575. package/src/ui/hover-card.tsx +0 -345
  576. package/src/ui/icon.tsx +0 -167
  577. package/src/ui/input.tsx +0 -391
  578. package/src/ui/kbd.tsx +0 -118
  579. package/src/ui/nav.tsx +0 -282
  580. package/src/ui/notice.tsx +0 -84
  581. package/src/ui/overlay.tsx +0 -277
  582. package/src/ui/pane-grid.tsx +0 -117
  583. package/src/ui/pane-group.tsx +0 -291
  584. package/src/ui/pane.tsx +0 -193
  585. package/src/ui/popover.tsx +0 -106
  586. package/src/ui/progress-bar.tsx +0 -83
  587. package/src/ui/prompt-dock.tsx +0 -145
  588. package/src/ui/radio.tsx +0 -150
  589. package/src/ui/resizable.tsx +0 -693
  590. package/src/ui/scroll-area.tsx +0 -26
  591. package/src/ui/segmented.tsx +0 -109
  592. package/src/ui/select.tsx +0 -168
  593. package/src/ui/separator.tsx +0 -10
  594. package/src/ui/settings-group.tsx +0 -67
  595. package/src/ui/skeleton.tsx +0 -73
  596. package/src/ui/slider.tsx +0 -178
  597. package/src/ui/status.tsx +0 -48
  598. package/src/ui/switch.tsx +0 -119
  599. package/src/ui/tabs.tsx +0 -168
  600. package/src/ui/textarea.tsx +0 -21
  601. package/src/ui/tooltip.tsx +0 -118
  602. package/src/utils/cn.ts +0 -30
  603. package/src/wire/chunk.ts +0 -248
  604. package/src/wire/consume.ts +0 -599
  605. package/src/wire/diagnostics.ts +0 -727
  606. package/src/wire/encode-probe.ts +0 -214
  607. package/src/wire/encode.ts +0 -947
  608. package/src/wire/files.ts +0 -342
  609. package/src/wire/formats/anthropic.ts +0 -274
  610. package/src/wire/formats/openai.ts +0 -228
  611. package/src/wire/index.ts +0 -95
  612. package/src/wire/media-types.ts +0 -344
  613. package/src/wire/read.ts +0 -396
  614. package/src/wire/sink-helpers.ts +0 -50
  615. package/src/wire/sse.ts +0 -153
@@ -1,947 +0,0 @@
1
- // ChatMessage[] back onto the wire, so a host can run a multi-round tool loop in
2
- // about fifteen lines of its own code. The kit never calls a consumer's function
3
- // and never drives the loop; these two functions are the whole contribution.
4
- //
5
- // No provider SDK, no fetch, no DOM. Pure functions over the content model.
6
- import type { ChatMessage, MessagePart } from '../elements/chat-types';
7
- import type { ToolPart } from '../components/tool-types';
8
- import { classifyAttachment, textFileContent, type ClassifiedFile } from './files';
9
- import { resolveMediaPolicy, type MediaPolicy, type MediaTypeFilter } from './media-types';
10
- import { wireDiagnosticsActive, wirePayloadActive } from './diagnostics';
11
- import { base64Bytes, createEncodeProbe, type EncodeProbe } from './encode-probe';
12
-
13
- export interface OpenAIToolCall {
14
- id: string;
15
- type: 'function';
16
- function: { name: string; arguments: string };
17
- }
18
-
19
- /** One `reasoning_details` entry. Provider-owned open shape, kept as a record
20
- * for the same reason `AnthropicContentBlock` is: an opaque entry has to pass
21
- * through UNTOUCHED, and a closed type would be a list of the fields we happen
22
- * to have seen. */
23
- export type OpenAIReasoningDetail = Record<string, unknown>;
24
-
25
- /** A multimodal user message's content entries. `image_url` takes an https URL
26
- * or a `data:` URI in the same field; `file` takes `file_data`, which is a DATA
27
- * URI on this wire (`data:application/pdf;base64,...`) and not bare base64. */
28
- export type OpenAIContentPart =
29
- | { type: 'text'; text: string }
30
- | { type: 'image_url'; image_url: { url: string } }
31
- | { type: 'file'; file: { filename?: string; file_data: string } };
32
-
33
- export interface OpenAIWireMessage {
34
- role: 'system' | 'user' | 'assistant' | 'tool';
35
- /** An ARRAY only when the turn carries an encodable `file` part. A text-only
36
- * turn stays a plain string, so adding attachment support changed nothing
37
- * about what an existing thread puts on the wire. */
38
- content: string | OpenAIContentPart[] | null;
39
- tool_calls?: OpenAIToolCall[];
40
- tool_call_id?: string;
41
- name?: string;
42
- /** Only ever present when `toOpenAIMessages` was asked for it. See
43
- * `OpenAIEncodeOptions.reasoning`. */
44
- reasoning_details?: OpenAIReasoningDetail[];
45
- }
46
-
47
- /** EXTENDS rather than restates the file options: `onUnencodableFile` and
48
- * `accept` mean the same thing on both wires, and a second declaration of them
49
- * here is a second place to forget to update. */
50
- export interface OpenAIEncodeOptions extends FileEncodeOptions {
51
- /**
52
- * Whether to send the assistant's own reasoning back with the thread.
53
- *
54
- * DEFAULT `'omit'`, and that default is a measurement, not caution. Omitting
55
- * reasoning is accepted by every configuration tested -- five live omission
56
- * trials plus 28 recorded live requests per configuration across the spike's
57
- * conformance sweep, zero 400s -- so the path that ships today demonstrably
58
- * works, while including reasoning cost about 25% more prompt tokens per round
59
- * when measured (665 -> 834 on a two-round loop). A library does not get to
60
- * raise every consumer's bill and add a new provider-validation surface as a
61
- * side effect of a bug fix.
62
- *
63
- * `'include'` is for a multi-round TOOL loop, which is where OpenRouter says it
64
- * pays: "when you post tool results, including the original reasoning ensures
65
- * the model can continue its reasoning from where it left off". Measured
66
- * accepted (HTTP 200) for a signed Anthropic block and for an OpenAI encrypted
67
- * block, over the OpenAI-compatible wire.
68
- *
69
- * The Anthropic wire has no such knob because it has no such choice: a filtered
70
- * or rebuilt thinking block there is a hard 400.
71
- */
72
- reasoning?: 'omit' | 'include';
73
- }
74
-
75
- /**
76
- * What to do with a `file` part this wire cannot carry.
77
- *
78
- * DEFAULT `'throw'`, and the default is the whole point. Skipping is how
79
- * attachments came to render perfectly in the thread and reach the model as
80
- * nothing: the developer wires up upload, watches it work, and ships a model
81
- * that cannot see the file. A throw here names the message, the part and the
82
- * reason, which is strictly more than a 400 at request time would tell you.
83
- *
84
- * `'skip'` restores the lenient behaviour for a host that would rather send a
85
- * degraded turn than fail one. It is silent, but it is silence the developer
86
- * asked for by name, which is the difference that matters.
87
- */
88
- export type UnencodableFilePolicy = 'throw' | 'skip';
89
-
90
- export interface FileEncodeOptions {
91
- onUnencodableFile?: UnencodableFilePolicy;
92
- /**
93
- * Narrow which attachment media types reach the wire, as HTML `accept` syntax
94
- * (`'image/*,application/pdf'`) or an array of the same.
95
- *
96
- * THE SAME STRING the composer takes as `<kai-chat accept="...">`, resolved by
97
- * the same function against the same declaration -- so a developer writes the
98
- * set once as a constant and hands it to both ends. Omitted means the kit's
99
- * full capability set, which is `encodableMediaTypes()`.
100
- *
101
- * It can only NARROW. Naming a type the encoders cannot represent does not
102
- * enable it; that would just move the failure to a provider 400.
103
- */
104
- accept?: MediaTypeFilter;
105
- /**
106
- * The app's own id for the logical turn this encode belongs to, carried onto
107
- * every diagnostic event the encode emits. Purely diagnostic: nothing here
108
- * branches on it and it never reaches a provider.
109
- *
110
- * THE SAME FIELD, THE SAME MEANING, as `ConsumeOptions.traceId` -- and that
111
- * symmetry is the whole payoff. Encoding happens BEFORE a read opens, so
112
- * there is no stream to attach an encode to and the kit will not invent one.
113
- * Pass the same id to both halves:
114
- *
115
- * const body = toOpenAIMessages(messages, { traceId: 'turn-42' });
116
- * readOpenAIStream(res, sink, { traceId: 'turn-42' });
117
- *
118
- * and the request and the response it produced sit together, with a tool loop
119
- * or a sub-agent fan-out grouping into one trace. Without it you still see
120
- * both halves; they are simply unlinked, which is the honest rendering --
121
- * pinning an encode to "the next stream that opens" would be a guess, and an
122
- * encode may be followed by no stream at all.
123
- */
124
- traceId?: string;
125
- /** The app's name for this call inside its trace (`'planner'`, `'retry-2'`).
126
- * Same field and same meaning as `ConsumeOptions.label`. Absent when not
127
- * supplied. */
128
- label?: string;
129
- }
130
-
131
- export type AnthropicEncodeOptions = FileEncodeOptions;
132
-
133
- /** Anthropic content blocks are an open, provider-owned union. Keeping them as
134
- * records is what lets a verbatim `thinking` payload pass through UNTOUCHED,
135
- * which is the entire point of this encoder. */
136
- export type AnthropicContentBlock = Record<string, unknown>;
137
-
138
- export interface AnthropicWireMessage {
139
- role: 'user' | 'assistant';
140
- content: AnthropicContentBlock[];
141
- }
142
-
143
- /** A message cannot be encoded without losing something the provider will reject.
144
- * Thrown at encode time, on purpose: a throw here beats a 400 at request time,
145
- * because here you still know which message and which part caused it. */
146
- export class WireEncodeError extends Error {
147
- readonly messageId: string;
148
- readonly partIndex: number;
149
-
150
- constructor(message: string, messageId: string, partIndex: number) {
151
- super(message);
152
- // Restores the prototype chain when this class is DOWNLEVELLED to ES5 by a
153
- // consumer's build, exactly as WireError does. Without it
154
- // `err instanceof WireEncodeError` is false there and the documented way to
155
- // catch an unencodable history stops working.
156
- Object.setPrototypeOf(this, WireEncodeError.prototype);
157
- this.name = 'WireEncodeError';
158
- this.messageId = messageId;
159
- this.partIndex = partIndex;
160
- }
161
- }
162
-
163
- type TextPart = Extract<MessagePart, { type: 'text' }>;
164
-
165
- /** A tool that can be echoed back: it has the provider's own call id AND a
166
- * result. Narrowing `toolCallId` to `string` here is what removes every
167
- * non-null assertion downstream. */
168
- type SettledTool = ToolPart & { toolCallId: string };
169
-
170
- const isTextPart = (p: MessagePart): p is TextPart => p.type === 'text';
171
-
172
- // lint-silent-drops: drops reasoning,tool,card,source,file -- text projection by name and contract, used for the plain-string form of a turn; every caller that needs the other variants encodes them itself before falling back here.
173
- const textOf = (parts: MessagePart[]): string =>
174
- parts
175
- .filter(isTextPart)
176
- .map((p) => p.text)
177
- .join('');
178
-
179
- type FilePart = Extract<MessagePart, { type: 'file' }>;
180
-
181
- /** A wire either shapes a classified file into a block, or explains why it
182
- * cannot. `ok: false` is NOT the same as an unencodable attachment: the
183
- * attachment can be perfectly sound and still have no form on this particular
184
- * wire, which is exactly the remote-PDF case on OpenAI. */
185
- type WireFileResult<T> = { ok: true; block: T } | { ok: false; reason: string };
186
-
187
- /**
188
- * One `file` part to one wire block, or `null` when it is skipped.
189
- *
190
- * Skipped means one of two things and never a third: the attachment is kit-side
191
- * (a citation chip), or the host explicitly opted into `onUnencodableFile:
192
- * 'skip'`. Everything else throws, because a dropped attachment is invisible in
193
- * exactly the way the developer needs it not to be.
194
- */
195
- function encodeFilePart<T>(
196
- part: FilePart,
197
- messageId: string,
198
- partIndex: number,
199
- policy: UnencodableFilePolicy,
200
- media: MediaPolicy,
201
- toBlock: (file: ClassifiedFile) => WireFileResult<T>,
202
- probe: EncodeProbe | undefined,
203
- messageIndex: number,
204
- ): T | null {
205
- const classified = classifyAttachment(part.attachment, media);
206
- // What the HOST declared, which is the only label available on a path where
207
- // classification never got as far as settling one.
208
- const declared = part.attachment.mediaType;
209
- const filename = part.attachment.filename;
210
-
211
- if (classified.status === 'kit-side') {
212
- // Reported in the attachment ledger, and NOT as a drop: a `source-document`
213
- // is the citation chip beside a RAG answer, and its content is already in
214
- // the prompt that produced that answer. Nothing was lost.
215
- probe?.attachment(
216
- {
217
- ...(declared !== undefined ? { mediaType: declared } : {}),
218
- encoded: false,
219
- disposition: 'skipped',
220
- reason:
221
- 'it is a kit-side attachment (a source-document citation chip), not something the user staged to send.',
222
- },
223
- filename,
224
- );
225
- return null;
226
- }
227
-
228
- const refuse = (reason: string, file?: ClassifiedFile): null => {
229
- const mediaType = file?.mediaType ?? declared;
230
- if (policy === 'skip') {
231
- // THE SKIP THE DEVELOPER ASKED FOR BY NAME, now visible. It is still
232
- // silent on the wire, exactly as documented; what changed is that the
233
- // panel can say "this attachment rendered in the thread and was never
234
- // sent", which is the sentence nobody could get out of the kit before.
235
- probe?.attachment(
236
- {
237
- ...(mediaType !== undefined ? { mediaType } : {}),
238
- ...bytesOf(file),
239
- encoded: false,
240
- disposition: 'skipped',
241
- reason,
242
- },
243
- filename,
244
- );
245
- probe?.dropped('file', reason, messageIndex, partIndex, part);
246
- return null;
247
- }
248
- throw new WireEncodeError(
249
- `Cannot encode file part ${partIndex} of message "${messageId}": ${reason} Pass { onUnencodableFile: 'skip' } to drop it instead of failing.`,
250
- messageId,
251
- partIndex,
252
- );
253
- };
254
-
255
- if (classified.status === 'unencodable') return refuse(classified.reason);
256
-
257
- const shaped = toBlock(classified.file);
258
- if (!shaped.ok) return refuse(shaped.reason, classified.file);
259
-
260
- probe?.attachment(
261
- {
262
- mediaType: classified.file.mediaType,
263
- ...bytesOf(classified.file),
264
- encoded: true,
265
- // A text file rides as text CONTENT on both wires -- worth naming
266
- // separately, because it is how an attachment can be "sent" and still not
267
- // be a file as far as the model is concerned.
268
- disposition: classified.file.kind === 'text' ? 'as-text' : 'encoded',
269
- },
270
- filename,
271
- );
272
- return shaped.block;
273
- }
274
-
275
- /**
276
- * The size of what went on the wire, when it went on the wire at all.
277
- *
278
- * ★ GATED BEHIND PAYLOAD CAPTURE, for the same reason `encode.request.bytes`
279
- * is, and the deciding argument is that THE COST RECURS. The whole thread is
280
- * re-encoded on every turn, so a 10 MB attachment sitting in history is not a
281
- * one-time 4.3 ms -- it is 4.3 ms per turn for as long as a subscriber is
282
- * attached. Unbounded-per-turn is what fails "cheap enough to leave on", not
283
- * the absolute number.
284
- *
285
- * The diagnosis survives the omission: "your image/png attachment was skipped,
286
- * so the model never saw it" is the finding, and the size is a refinement of
287
- * it. `mediaType`, `encoded`, `disposition` and `reason` cost nothing and are
288
- * reported unconditionally. Arming payload gives exact sizes, which is
289
- * precisely the moment a developer has already accepted paying for content.
290
- *
291
- * ABSENT rather than estimated, always. For a remote attachment the provider
292
- * dereferences that URL itself and the bytes never enter this process; with
293
- * payload off the scan is simply not run. Both read as "not reported" under the
294
- * absence rule, and neither is ever backfilled with a guess -- see the rejected
295
- * candidates at `base64Bytes`.
296
- */
297
- function bytesOf(file?: ClassifiedFile): { bytes?: number } {
298
- if (!wirePayloadActive()) return {};
299
- if (file?.source.type !== 'base64') return {};
300
- return { bytes: base64Bytes(file.source.data) };
301
- }
302
-
303
- /** `image_url` takes an https URL and a `data:` URI in the same field, so an
304
- * image is one shape here. A PDF is not: `file_data` is base64-only. A text
305
- * file is neither: it becomes ordinary text content, because that is the only
306
- * thing either API's block set can express it as. */
307
- const openAIFileBlock = (file: ClassifiedFile): WireFileResult<OpenAIContentPart> => {
308
- if (file.kind === 'text') {
309
- return { ok: true, block: { type: 'text', text: textFileContent(file) } };
310
- }
311
-
312
- if (file.kind === 'image') {
313
- const url = file.source.type === 'base64' ? file.source.dataUri : file.source.url;
314
- return { ok: true, block: { type: 'image_url', image_url: { url } } };
315
- }
316
-
317
- if (file.source.type === 'remote') {
318
- return {
319
- ok: false,
320
- reason: `it is a PDF at a remote URL ("${file.source.url}"), and a chat-completions \`file\` content part carries only \`file_id\` or \`file_data\` -- this wire has no URL form for a document. Fetch it and stage it as a \`data:\` URI, or upload it through the Files API and send \`file_id\` yourself; fetching it HERE would put I/O in the encoder. The Anthropic wire does take a remote PDF, so toAnthropicMessages accepts this same message.`,
321
- };
322
- }
323
-
324
- return {
325
- ok: true,
326
- block: {
327
- type: 'file',
328
- file: {
329
- ...(file.filename !== undefined ? { filename: file.filename } : {}),
330
- file_data: file.source.dataUri,
331
- },
332
- },
333
- };
334
- };
335
-
336
- /** The image and document blocks take the same two source shapes, which is why
337
- * this wire has no gap where OpenAI has one. Text is the third case and is not
338
- * a `document` block: this API's content-block set is text / image / document
339
- * with no arbitrary-file member, so an inlined text file IS a text block.
340
- * (A `document` block CAN carry text via the Files API, which would buy
341
- * citations -- see the design note; it needs an upload step this layer cannot
342
- * make, because it does no I/O.) */
343
- const anthropicFileBlock = (file: ClassifiedFile): WireFileResult<AnthropicContentBlock> => {
344
- if (file.kind === 'text') {
345
- return { ok: true, block: { type: 'text', text: textFileContent(file) } };
346
- }
347
-
348
- return {
349
- ok: true,
350
- block: {
351
- type: file.kind === 'image' ? 'image' : 'document',
352
- source:
353
- file.source.type === 'base64'
354
- ? { type: 'base64', media_type: file.source.mediaType, data: file.source.data }
355
- : { type: 'url', url: file.source.url },
356
- },
357
- };
358
- };
359
-
360
- /** A tool is encodable only once it has a RESULT. Both APIs require every echoed
361
- * tool call to have exactly one matching result, so a call with no answer yet is
362
- * skipped entirely rather than sent half-formed. In a real loop the host
363
- * executes tools before re-encoding, so this only drops genuinely pending work.
364
- *
365
- * A tool with no `toolCallId` is skipped for the same reason: there is no id to
366
- * correlate a result with, and synthesising one (`call_0`) produces a request
367
- * the provider rejects. */
368
- const isSettled = (tool: ToolPart): tool is SettledTool =>
369
- typeof tool.toolCallId === 'string' &&
370
- tool.toolCallId !== '' &&
371
- (tool.output !== undefined || tool.errorText !== undefined);
372
-
373
- /** The argument text to echo. `rawInput` is the raw accumulated fragments and is
374
- * preferred: providers validate an echoed tool block against what they emitted,
375
- * and re-stringifying a parse changes key order and whitespace. An EMPTY
376
- * `rawInput` is not valid JSON, so it falls through to the parsed snapshot the
377
- * same way an absent one does. */
378
- function argumentsOf(tool: ToolPart): string {
379
- if (tool.rawInput !== undefined && tool.rawInput !== '') return tool.rawInput;
380
- return tool.input !== undefined ? JSON.stringify(tool.input) : '{}';
381
- }
382
-
383
- const toolCallOf = (tool: SettledTool): OpenAIToolCall => ({
384
- id: tool.toolCallId,
385
- type: 'function',
386
- function: { name: tool.type, arguments: argumentsOf(tool) },
387
- });
388
-
389
- /** `errorText` WINS over `output` when a part somehow carries both, on both
390
- * wires. A host that reported a failure after a partial result meant the
391
- * failure, and echoing a half-written success back to the model is the version
392
- * of this that produces confidently wrong follow-up work. */
393
- const toolResultOf = (tool: SettledTool): OpenAIWireMessage => ({
394
- role: 'tool',
395
- tool_call_id: tool.toolCallId,
396
- name: tool.type,
397
- content: tool.errorText ?? JSON.stringify(tool.output),
398
- });
399
-
400
- type ReasoningPart = Extract<MessagePart, { type: 'reasoning' }>;
401
-
402
- const isRecord = (v: unknown): v is Record<string, unknown> =>
403
- typeof v === 'object' && v !== null && !Array.isArray(v);
404
-
405
- /**
406
- * The provider's own `reasoning_details` entry for this block, if the part came
407
- * from the OpenAI wire at all.
408
- *
409
- * WHAT THIS IS NOT: the block. `part.raw` holds the LAST delta that touched this
410
- * part, because `appendReasoningPart` resolves `raw` last-write-wins, and every
411
- * reasoning-carrying provider streams its blocks in fragments. So this entry is
412
- * read for its SHAPE -- type, format, id, index, and an opaque payload if it has
413
- * one -- and never for its text.
414
- *
415
- * A delta carrying several entries at once is rare but legal, so the entry is
416
- * matched on the part's own block index first; that is the only correlator there
417
- * is. The last one wins otherwise, which is the entry `raw` was captured from.
418
- */
419
- function detailOf(part: ReasoningPart): Record<string, unknown> | undefined {
420
- if (part.raw?.source !== 'openai.reasoning_details') return undefined;
421
- const payload = part.raw.payload;
422
- if (!Array.isArray(payload)) return undefined;
423
- const records = payload.filter(isRecord);
424
- const matched = records.filter((d) => d.index === part.index);
425
- const pool = matched.length > 0 ? matched : records;
426
- return pool[pool.length - 1];
427
- }
428
-
429
- /**
430
- * ONE reasoning part to ONE `reasoning_details` entry, or nothing.
431
- *
432
- * Three cases, and every one of them is in a recorded fixture:
433
- *
434
- * 1. OPAQUE (`reasoning.encrypted`, `openai/gpt-5.4-mini`). The `data` blob is
435
- * the block; there is no text and no signature to rebuild it from, and it
436
- * arrives whole in a single frame, so `raw` really is the block here. It goes
437
- * back BY REFERENCE, untouched.
438
- *
439
- * 2. SIGNED (`reasoning.text` with a signature, an Anthropic model over this
440
- * wire). REASSEMBLED from `part.text` and `part.signature`, never echoed.
441
- * Echoing `raw` sends the final frame, which carries the signature and NO
442
- * text -- measured: of 85 reasoning frames in one turn, only the last has the
443
- * signature and it has no text. The signature is the load-bearing field:
444
- * stripping it is a hard 400 (`Invalid signature in thinking block`) from all
445
- * four providers OpenRouter tried, while mutating the text was accepted.
446
- *
447
- * 3. NEITHER (`deepseek` `format: "unknown"`, and the `reasoning.summary`
448
- * entries that ride alongside gpt-5.4-mini's encrypted block). SKIPPED.
449
- * Nothing measured supports sending these: the two accepted round-trips were
450
- * a signed block and an encrypted one, and every measured 400 was a provider
451
- * failing to verify a block it was handed. Omission, by contrast, is proven
452
- * accepted everywhere. `format: "unknown"` is OpenRouter saying it could not
453
- * attribute the block to a round-trippable format, and re-sending a summary
454
- * OpenAI itself treats as a display artifact spends tokens on nothing.
455
- *
456
- * On the sequence rule -- "the entire sequence of consecutive reasoning blocks
457
- * must match the outputs generated by the model" -- case 3 is the one place this
458
- * is READ rather than proven. It holds up because verifiability is a
459
- * per-configuration property in all 40 recorded reasoning turns, never a
460
- * per-block one: Haiku signs every block, deepseek signs none, gpt-5.4-mini
461
- * signs none but carries `data`. So the skip never filters one block out of a
462
- * run of comparable ones. For gpt-5.4-mini it drops the summary at block index 0
463
- * and keeps the encrypted carrier at index 1, which is exactly the single-entry
464
- * payload the live probe sent and got a 200 for.
465
- */
466
- function reasoningDetailOf(part: ReasoningPart): OpenAIReasoningDetail | undefined {
467
- const detail = detailOf(part);
468
- if (!detail) return undefined;
469
-
470
- if (detail.type === 'reasoning.encrypted' || typeof detail.data === 'string') return detail;
471
-
472
- if (typeof part.signature !== 'string' || part.signature === '') return undefined;
473
-
474
- const type = typeof detail.type === 'string' ? detail.type : 'reasoning.text';
475
- // The readable field is named for the entry type: `summary` entries carry
476
- // `summary`, everything else carries `text`.
477
- const textKey = type === 'reasoning.summary' ? 'summary' : 'text';
478
- const out: OpenAIReasoningDetail = { type, [textKey]: part.text, signature: part.signature };
479
- if (typeof detail.format === 'string') out.format = detail.format;
480
- if (typeof detail.id === 'string') out.id = detail.id;
481
- if (detail.index !== undefined) out.index = detail.index;
482
- return out;
483
- }
484
-
485
- /**
486
- * ChatMessage[] to an OpenAI chat-completions `messages` array.
487
- *
488
- * ONE ChatMessage CAN BECOME SEVERAL WIRE MESSAGES. The kit streams a whole
489
- * assistant turn into a single message, so text, a tool call and the model's
490
- * answer to that call all live in one `parts` array. The OpenAI wire has no such
491
- * shape: a `role:'tool'` result must sit between the assistant message that
492
- * announced the call and whatever the model said afterwards. So the turn is
493
- * SPLIT at each tool boundary, into
494
- *
495
- * assistant(pre-tool text + tool_calls) -> tool(result)... -> assistant(answer)
496
- *
497
- * Flattening instead would put the model's answer BEFORE the result it was based
498
- * on. No endpoint rejects that, which is exactly why it is worth spelling out:
499
- * it quietly degrades every later round of a tool loop.
500
- *
501
- * Consecutive tool parts stay in ONE assistant message, because parallel calls
502
- * are announced together and their results follow together.
503
- *
504
- * A turn that encodes to nothing is SKIPPED, never sent as `{ content: null }`
505
- * with no `tool_calls`: OpenAI treats `content` as required unless `tool_calls`
506
- * is present, and strict-compatible endpoints reject it.
507
- *
508
- * REASONING IS OPT-IN, and off by default. OpenRouter's OpenAI-compatible
509
- * endpoint does have a channel on the way back in -- `reasoning_details` on the
510
- * assistant message -- and `{ reasoning: 'include' }` uses it, one entry per
511
- * reasoning part, in part order, reassembled by `reasoningDetailOf` rather than
512
- * echoed out of `part.raw`. Read that function for which blocks make it and why.
513
- * The default omits, because omitting is measured-accepted everywhere and costs
514
- * about 25% fewer prompt tokens per round; see `OpenAIEncodeOptions.reasoning`.
515
- *
516
- * Reasoning alone still encodes to NOTHING. A block is content the model already
517
- * produced, not a reason to send a turn, so a message carrying reasoning and no
518
- * text and no settled tool is skipped exactly as before, rather than becoming
519
- * `{ content: null }` with no `tool_calls`.
520
- *
521
- * `card` and `source` parts are never encoded; they are kit-side.
522
- *
523
- * `file` parts ARE encoded, on a USER turn, and a turn carrying nothing but an
524
- * attachment is now a real message rather than nothing. Images become
525
- * `image_url` (https URL or `data:` URI alike); a base64 PDF becomes a `file`
526
- * part whose `file_data` is the data URI. Two cases have no form here and THROW
527
- * by default: a remote PDF, because this wire's `file` part has no URL variant,
528
- * and anything that is neither -- see `UnencodableFilePolicy` for why the
529
- * default is a throw and not a skip.
530
- *
531
- * A `file` part on an ASSISTANT turn is still dropped. Neither API accepts image
532
- * or document content in an assistant message, so there is nothing to encode it
533
- * to; attachments belong to the user turn that sent them.
534
- */
535
- // lint-silent-drops: drops card,source -- kit-side parts with no OpenAI wire representation; cards come from tool calls, which ARE encoded, and sources are UI citations.
536
- export function toOpenAIMessages(
537
- messages: ChatMessage[],
538
- options: OpenAIEncodeOptions = {},
539
- ): OpenAIWireMessage[] {
540
- const includeReasoning = options.reasoning === 'include';
541
- const filePolicy = options.onUnencodableFile ?? 'throw';
542
- const media = resolveMediaPolicy({ accept: options.accept });
543
- const out: OpenAIWireMessage[] = [];
544
-
545
- // Gated BEFORE anything is allocated, so with nobody listening this whole
546
- // ledger costs one symbol read for the entire encode.
547
- const probe = wireDiagnosticsActive() ? createEncodeProbe(options) : undefined;
548
- if (probe) {
549
- for (const message of messages) for (const part of message.parts) probe.seen(part.type);
550
- }
551
-
552
- let messageIndex = -1;
553
- for (const message of messages) {
554
- messageIndex++;
555
- if (message.role === 'user') {
556
- // Built in PART ORDER, with runs of adjacent text merged into one entry.
557
- // A file authored after the text stays after it: `parts` is an ordered
558
- // content model and reordering it here would be the encoder second-guessing
559
- // the author. Anthropic's docs do suggest putting images before text for
560
- // best results, which is guidance for whoever builds the parts array.
561
- const content: OpenAIContentPart[] = [];
562
- let buffered = '';
563
- let carriesFile = false;
564
-
565
- const flushText = (): void => {
566
- if (buffered === '') return;
567
- content.push({ type: 'text', text: buffered });
568
- buffered = '';
569
- };
570
-
571
- // lint-silent-drops: drops reasoning,tool,card,source -- a USER turn carries authored content only; reasoning and tool parts are assistant-side, cards and sources are kit-side UI with no OpenAI wire form.
572
- message.parts.forEach((part, partIndex) => {
573
- if (part.type === 'text') {
574
- buffered += part.text;
575
- if (part.text !== '') probe?.encoded('text');
576
- return;
577
- }
578
- if (part.type !== 'file') {
579
- // The drop the waiver above declares, now also reported. `part.type`
580
- // is read off a part this branch has ALREADY discriminated -- no new
581
- // switch, which is what keeps `lint:silent-drops` looking at exactly
582
- // the sites it looked at before.
583
- probe?.dropped(
584
- part.type,
585
- 'a USER turn carries authored content only: reasoning and tool parts are assistant-side, and cards and sources are kit-side UI with no OpenAI wire form.',
586
- messageIndex,
587
- partIndex,
588
- part,
589
- );
590
- return;
591
- }
592
- const block = encodeFilePart(
593
- part,
594
- message.id,
595
- partIndex,
596
- filePolicy,
597
- media,
598
- openAIFileBlock,
599
- probe,
600
- messageIndex,
601
- );
602
- if (!block) return;
603
- flushText();
604
- content.push(block);
605
- probe?.encoded('file');
606
- carriesFile = true;
607
- });
608
- flushText();
609
-
610
- // No file survived, so this is an ordinary turn and stays a plain string.
611
- if (!carriesFile) {
612
- // `textOf`'s own waiver names the same drops the callback above just
613
- // reported, over the same parts array in the same branch. Reporting them
614
- // again HERE would double-count every card and source in a user turn, so
615
- // the callback is the single site and this one deliberately stays quiet.
616
- const text = textOf(message.parts);
617
- if (text !== '') out.push({ role: 'user', content: text });
618
- continue;
619
- }
620
- out.push({ role: 'user', content });
621
- continue;
622
- }
623
-
624
- let text = '';
625
- let pending: SettledTool[] = [];
626
- let reasoning: OpenAIReasoningDetail[] = [];
627
-
628
- /** Emit the assistant message for everything buffered so far, then the
629
- * result message for each call it announced, in the same order.
630
- *
631
- * The early return leaves any buffered reasoning where it is on purpose:
632
- * nothing was emitted, so those blocks still belong to a message that has
633
- * not been written yet. In practice it is unreachable with reasoning
634
- * buffered, because the only mid-loop `flush()` runs with `pending`
635
- * non-empty. */
636
- const flush = (): void => {
637
- if (text === '' && pending.length === 0) return;
638
- out.push({
639
- role: 'assistant',
640
- content: text === '' ? null : text,
641
- ...(reasoning.length > 0 ? { reasoning_details: reasoning } : {}),
642
- ...(pending.length > 0 ? { tool_calls: pending.map(toolCallOf) } : {}),
643
- });
644
- for (const tool of pending) out.push(toolResultOf(tool));
645
- text = '';
646
- pending = [];
647
- reasoning = [];
648
- };
649
-
650
- let partIndex = -1;
651
- for (const part of message.parts) {
652
- partIndex++;
653
- if (part.type === 'text') {
654
- // Text that arrives AFTER a call is the model's answer to it, so the
655
- // call and its result have to be on the wire before this text is.
656
- if (pending.length > 0) flush();
657
- text += part.text;
658
- if (part.text !== '') probe?.encoded('text');
659
- continue;
660
- }
661
- if (part.type === 'reasoning') {
662
- if (!includeReasoning) {
663
- probe?.dropped(
664
- 'reasoning',
665
- "reasoning is omitted by default, because omitting is measured-accepted everywhere and costs about 25% fewer prompt tokens per round. Pass { reasoning: 'include' } to send the model its own prior thinking back.",
666
- messageIndex,
667
- partIndex,
668
- part,
669
- );
670
- continue;
671
- }
672
- // A block after a call belongs to the round that READ the result, for the
673
- // same reason text does. Same rule as `toAnthropicMessages`.
674
- if (pending.length > 0) flush();
675
- const detail = reasoningDetailOf(part);
676
- if (detail) {
677
- reasoning.push(detail);
678
- probe?.encoded('reasoning');
679
- continue;
680
- }
681
- // ★ THE REASONING ROUND TRIP, reported rather than changed. `detailOf`
682
- // only produces an entry for a part whose `raw` came from
683
- // `openai.reasoning_details`, so a part read off `reasoning_content`
684
- // (DeepSeek-direct) has no round-trippable carrier and is not echoed --
685
- // and the model loses its own prior thinking on the next turn. The
686
- // ruling stands and is deliberate; what changes here is that it is
687
- // visible. See `reasoningDetailOf` for which blocks make it and why.
688
- probe?.dropped(
689
- 'reasoning',
690
- 'it has no round-trippable `reasoning_details` carrier -- an opaque or signed block can be echoed back, but a part read from `reasoning_content` has neither, and re-sending an unverifiable block is the documented 400. The model does not see its own prior thinking on the next turn.',
691
- messageIndex,
692
- partIndex,
693
- part,
694
- );
695
- continue;
696
- }
697
- if (part.type === 'tool') {
698
- if (isSettled(part.tool)) {
699
- pending.push(part.tool);
700
- probe?.encoded('tool');
701
- } else {
702
- probe?.dropped(
703
- 'tool',
704
- 'the call has no result yet (or no provider tool call id). Both APIs require every echoed tool call to have exactly one matching result, so a half-formed one is left off rather than sent.',
705
- messageIndex,
706
- partIndex,
707
- part,
708
- );
709
- }
710
- continue;
711
- }
712
- // card, source, and a `file` part on an ASSISTANT turn. Same parts the
713
- // function's waiver names, read off a part already discriminated above.
714
- probe?.dropped(
715
- part.type,
716
- 'cards and sources are kit-side UI with no OpenAI wire form, and a `file` part on an ASSISTANT turn has none either -- the API takes image and document content on user turns only.',
717
- messageIndex,
718
- partIndex,
719
- part,
720
- );
721
- }
722
- flush();
723
- }
724
-
725
- probe?.finish('openai', messages.length, out);
726
- return out;
727
- }
728
-
729
- /**
730
- * ChatMessage[] to an Anthropic Messages `messages` array. THE ROUND-TRIP
731
- * ENCODER.
732
- *
733
- * A reasoning block is emitted as `part.raw.payload` verbatim and is NEVER
734
- * rebuilt from `text` plus `signature`: Anthropic returns 400 if a thinking
735
- * block in the most recent assistant message is modified, reordered, filtered or
736
- * reconstructed. A reasoning part with no `raw`, or with a `raw` captured from
737
- * some other format, therefore THROWS rather than silently producing a request
738
- * that will fail.
739
- *
740
- * Block order follows part order, which follows stream order, with no filtering,
741
- * because the API validates order too. An empty-text reasoning part (an omitted
742
- * or redacted block) is still emitted: the docs require sending back every block
743
- * "including any blocks with empty thinking fields".
744
- *
745
- * ONE ChatMessage CAN BECOME SEVERAL WIRE MESSAGES, for the same reason as
746
- * `toOpenAIMessages`: the kit streams a whole assistant turn into one message, so
747
- * the tool call and the model's answer to it share a `parts` array, but Anthropic
748
- * carries the result in a SEPARATE user message that has to sit between them. So
749
- * the turn is SPLIT at each tool boundary, into
750
- *
751
- * assistant(pre-tool blocks + tool_use) -> user(tool_result)... -> assistant(answer)
752
- *
753
- * Flattening instead puts the model's answer BEFORE the result it was based on,
754
- * and strands every later round's thinking block in the first assistant message.
755
- * Consecutive tool parts stay in ONE assistant message, because parallel calls are
756
- * announced together and their results come back together.
757
- *
758
- * Adjacent user messages are MERGED. The API combines consecutive same-role turns
759
- * itself rather than rejecting them, so this is not what stands between you and a
760
- * 400; it is emitted anyway because the tool-result turn and a following user turn
761
- * are one turn, several OpenAI-compatible Anthropic proxies do enforce strict
762
- * alternation, and the merged form is what the models are trained on. Ordering is
763
- * safe by construction: `results` is only non-empty when `blocks` is, so a
764
- * tool_result message always follows its assistant message and can never be
765
- * appended after a plain user turn.
766
- *
767
- * `file` parts on a USER turn become `image` and `document` blocks, in part
768
- * order. Both take `source: {type:'base64'}` and `source: {type:'url'}`, so this
769
- * wire can carry a remote PDF that `toOpenAIMessages` has to refuse. Anything
770
- * neither API accepts as message content THROWS by default; see
771
- * `UnencodableFilePolicy`. A `file` part on an ASSISTANT turn is dropped, because
772
- * an assistant message here carries only text, thinking and tool_use.
773
- *
774
- * Asymmetry worth knowing: `tool_use.input` is a parsed OBJECT on this wire, not
775
- * a string, so it uses `input` and not `rawInput`. Only thinking blocks carry a
776
- * verbatim requirement.
777
- */
778
- export function toAnthropicMessages(
779
- messages: ChatMessage[],
780
- options: AnthropicEncodeOptions = {},
781
- ): AnthropicWireMessage[] {
782
- const filePolicy = options.onUnencodableFile ?? 'throw';
783
- const media = resolveMediaPolicy({ accept: options.accept });
784
- const out: AnthropicWireMessage[] = [];
785
-
786
- // Gated before anything is allocated, exactly as in `toOpenAIMessages`.
787
- const probe = wireDiagnosticsActive() ? createEncodeProbe(options) : undefined;
788
- if (probe) {
789
- for (const message of messages) for (const part of message.parts) probe.seen(part.type);
790
- }
791
-
792
- /** Append to the trailing user message when there is one, so the wire never
793
- * carries two user turns in a row. */
794
- const pushUser = (content: AnthropicContentBlock[]): void => {
795
- const last = out[out.length - 1];
796
- if (last?.role === 'user') last.content = [...last.content, ...content];
797
- else out.push({ role: 'user', content });
798
- };
799
-
800
- let messageIndex = -1;
801
- for (const message of messages) {
802
- messageIndex++;
803
- if (message.role === 'user') {
804
- // Part order, for the same reason as `toOpenAIMessages`.
805
- const userBlocks: AnthropicContentBlock[] = [];
806
- // lint-silent-drops: drops reasoning,tool,card,source -- a USER turn carries authored content only; reasoning and tool parts are assistant-side, cards and sources are kit-side UI with no Anthropic wire form.
807
- message.parts.forEach((part, partIndex) => {
808
- if (part.type === 'text') {
809
- if (part.text !== '') {
810
- userBlocks.push({ type: 'text', text: part.text });
811
- probe?.encoded('text');
812
- }
813
- return;
814
- }
815
- if (part.type !== 'file') {
816
- // The drop this callback's waiver declares, reported. `part.type` is
817
- // read off a part the branch already discriminated.
818
- probe?.dropped(
819
- part.type,
820
- 'a USER turn carries authored content only: reasoning and tool parts are assistant-side, and cards and sources are kit-side UI with no Anthropic wire form.',
821
- messageIndex,
822
- partIndex,
823
- part,
824
- );
825
- return;
826
- }
827
- const block = encodeFilePart(
828
- part,
829
- message.id,
830
- partIndex,
831
- filePolicy,
832
- media,
833
- anthropicFileBlock,
834
- probe,
835
- messageIndex,
836
- );
837
- if (block) {
838
- userBlocks.push(block);
839
- probe?.encoded('file');
840
- }
841
- });
842
- if (userBlocks.length > 0) pushUser(userBlocks);
843
- continue;
844
- }
845
-
846
- let blocks: AnthropicContentBlock[] = [];
847
- let results: AnthropicContentBlock[] = [];
848
-
849
- /** Emit the assistant message built so far, then the user message carrying the
850
- * results of the calls it announced. */
851
- const flush = (): void => {
852
- if (blocks.length > 0) out.push({ role: 'assistant', content: blocks });
853
- // Anthropic carries tool results in the FOLLOWING user message.
854
- if (results.length > 0) pushUser(results);
855
- blocks = [];
856
- results = [];
857
- };
858
-
859
- // lint-silent-drops: drops card,source,file -- cards and sources are kit-side UI; a file part on an ASSISTANT turn has no Anthropic representation, since the API takes image and document content on user turns only.
860
- message.parts.forEach((part, partIndex) => {
861
- switch (part.type) {
862
- case 'reasoning': {
863
- if (!part.raw) {
864
- throw new WireEncodeError(
865
- `Cannot encode reasoning part ${partIndex} of message "${message.id}": it has no \`raw\` payload, and Anthropic requires a thinking block to be echoed back verbatim. Rebuilding one from text plus signature is the documented 400. Produce reasoning parts with readAnthropicStream, which attaches the provider's own block, and keep \`raw\` when you persist a message.`,
866
- message.id,
867
- partIndex,
868
- );
869
- }
870
- if (!part.raw.source.startsWith('anthropic.')) {
871
- throw new WireEncodeError(
872
- `Cannot encode reasoning part ${partIndex} of message "${message.id}": its \`raw\` came from "${part.raw.source}", not from the Anthropic Messages format. Only a payload tagged \`anthropic.\` can be echoed back verbatim. If you are talking to an Anthropic model through an OpenAI-compatible endpoint, use toOpenAIMessages.`,
873
- message.id,
874
- partIndex,
875
- );
876
- }
877
- // A thinking block after a call belongs to the round that READ the
878
- // result, so the result has to be on the wire before it is.
879
- if (results.length > 0) flush();
880
- blocks.push(part.raw.payload as AnthropicContentBlock);
881
- probe?.encoded('reasoning');
882
- break;
883
- }
884
- case 'text': {
885
- if (part.text === '') break;
886
- // Text after a call is the model's answer to it. Same reason.
887
- if (results.length > 0) flush();
888
- blocks.push({ type: 'text', text: part.text });
889
- probe?.encoded('text');
890
- break;
891
- }
892
- case 'tool': {
893
- const tool = part.tool;
894
- if (!isSettled(tool)) {
895
- probe?.dropped(
896
- 'tool',
897
- 'the call has no result yet (or no provider tool call id). Both APIs require every echoed tool call to have exactly one matching result, so a half-formed one is left off rather than sent.',
898
- messageIndex,
899
- partIndex,
900
- part,
901
- );
902
- break;
903
- }
904
- probe?.encoded('tool');
905
- blocks.push({
906
- type: 'tool_use',
907
- id: tool.toolCallId,
908
- name: tool.type,
909
- input: tool.input ?? {},
910
- });
911
- results.push(
912
- tool.errorText !== undefined
913
- ? {
914
- type: 'tool_result',
915
- tool_use_id: tool.toolCallId,
916
- is_error: true,
917
- content: tool.errorText,
918
- }
919
- : {
920
- type: 'tool_result',
921
- tool_use_id: tool.toolCallId,
922
- content: JSON.stringify(tool.output),
923
- },
924
- );
925
- break;
926
- }
927
- default:
928
- // card and source are kit-side. A `file` part reaches this arm only on
929
- // an ASSISTANT turn, where the API accepts no image or document
930
- // content at all -- user attachments are handled in the user branch.
931
- probe?.dropped(
932
- part.type,
933
- 'cards and sources are kit-side UI, and a `file` part on an ASSISTANT turn has no Anthropic representation -- the API takes image and document content on user turns only.',
934
- messageIndex,
935
- partIndex,
936
- part,
937
- );
938
- break;
939
- }
940
- });
941
-
942
- flush();
943
- }
944
-
945
- probe?.finish('anthropic', messages.length, out);
946
- return out;
947
- }