@kitn.ai/ui 0.21.0 → 0.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (243) hide show
  1. package/dist/components/chat-thread.d.ts +9 -0
  2. package/dist/{create-tween-D3Y5qiij.js → create-tween-COnPwAQN.js} +1 -1
  3. package/dist/{create-tween-DGKY1FDY.js → create-tween-DmTUaXWE.js} +1 -1
  4. package/dist/{create-tween-paho5Rh-.js → create-tween-qs-jjvNA.js} +1 -1
  5. package/dist/custom-elements.json +43 -7
  6. package/dist/elements/agent-card.js +1 -1
  7. package/dist/elements/artifact.js +1 -1
  8. package/dist/elements/attachments.js +1 -1
  9. package/dist/elements/audio-visualizer.js +1 -1
  10. package/dist/elements/avatar.js +1 -1
  11. package/dist/elements/badge.js +1 -1
  12. package/dist/elements/button.js +1 -1
  13. package/dist/elements/card.js +1 -1
  14. package/dist/elements/cards.js +1 -1
  15. package/dist/elements/chain-of-thought.js +1 -1
  16. package/dist/elements/chat-scope-picker.js +1 -1
  17. package/dist/elements/chat-workspace.js +1 -1
  18. package/dist/elements/chat.js +1 -1
  19. package/dist/elements/checkpoint.js +1 -1
  20. package/dist/elements/choice.js +1 -1
  21. package/dist/elements/chunks/{Icon-C-2LvGDG.js → Icon-CHG5z5L7.js} +1 -1
  22. package/dist/elements/chunks/{action-icons-CaYoImEk.js → action-icons-BAtCEEgg.js} +1 -1
  23. package/dist/elements/chunks/arrow-left-CFPTeL1Y.js +1 -0
  24. package/dist/elements/chunks/{artifact-BBIscOxJ.js → artifact-fBZOE4SO.js} +1 -1
  25. package/dist/elements/chunks/{attachments-BB56zoji.js → attachments-DpYMe5VQ.js} +1 -1
  26. package/dist/elements/chunks/{audio-visualizer-BxOx4THz.js → audio-visualizer-DFwzAcuL.js} +1 -1
  27. package/dist/elements/chunks/{badge-SxLSZ19R.js → badge-D6zodm6E.js} +1 -1
  28. package/dist/elements/chunks/{button-CmkUkz6c.js → button-C42vrGvO.js} +1 -1
  29. package/dist/elements/chunks/{card-renderer-Db4ffkWv.js → card-renderer-DMZsSuvU.js} +1 -1
  30. package/dist/elements/chunks/{chat-thread-B3Ks-cKr.js → chat-thread-CNNEhol-.js} +1 -1
  31. package/dist/elements/chunks/check-7Q3K-iXE.js +1 -0
  32. package/dist/elements/chunks/chevron-down-CpNYLn7E.js +1 -0
  33. package/dist/elements/chunks/chevron-right-DrH85Fha.js +1 -0
  34. package/dist/elements/chunks/{choice-card-Q9cYYDyM.js → choice-card-YoWjpsFV.js} +1 -1
  35. package/dist/elements/chunks/circle-C9KRjKYv.js +1 -0
  36. package/dist/elements/chunks/circle-check-Dmi7Y4zw.js +1 -0
  37. package/dist/elements/chunks/{circle-x-D0MEfxOM.js → circle-x-QIxXda6k.js} +1 -1
  38. package/dist/elements/chunks/{code-block-C8VzAraE.js → code-block-CqeugReY.js} +1 -1
  39. package/dist/elements/chunks/{collapsible-Cvj61obE.js → collapsible-sTv-_5lF.js} +1 -1
  40. package/dist/elements/chunks/{composer-D86ztyL0.js → composer-ldQyeVEB.js} +1 -1
  41. package/dist/elements/chunks/{confirm-card-BdyDAGLw.js → confirm-card-C0RNOwfn.js} +1 -1
  42. package/dist/elements/chunks/context-5qgypNHC.js +1 -0
  43. package/dist/elements/chunks/conversation-list-DLzOacXz.js +1 -0
  44. package/dist/elements/chunks/{create-tween-C_A_B638.js → create-tween-V4TXZ93X.js} +1 -1
  45. package/dist/elements/chunks/default-input-DeeRSDHc.js +1 -0
  46. package/dist/elements/chunks/{define-BzPnNsXg.js → define-lE-QvQFn.js} +1 -1
  47. package/dist/elements/chunks/{disclosure-DVKsoc1F.js → disclosure-DZSa9jog.js} +1 -1
  48. package/dist/elements/chunks/{download-BjA54IMJ.js → download-B2ZmnGqW.js} +1 -1
  49. package/dist/elements/chunks/{dropdown-BjVVHWXH.js → dropdown-R3B7cSRE.js} +1 -1
  50. package/dist/elements/chunks/{ellipsis-BNedmqfi.js → ellipsis-BmxceY4l.js} +1 -1
  51. package/dist/elements/chunks/{embed-DQsl4XEq.js → embed-BVrtTZPo.js} +1 -1
  52. package/dist/elements/chunks/{external-link-Di-dw2CU.js → external-link-BbdR7YO7.js} +1 -1
  53. package/dist/elements/chunks/{file-text-B3sAODCV.js → file-text-D7dijo1A.js} +1 -1
  54. package/dist/elements/chunks/{file-tree-Dgto-bMn.js → file-tree-0upJ5jeb.js} +1 -1
  55. package/dist/elements/chunks/{folder-D6rMadFV.js → folder-D8z58t7B.js} +1 -1
  56. package/dist/elements/chunks/{form-CILxvhQ7.js → form-p5BKJNw5.js} +1 -1
  57. package/dist/elements/chunks/{hover-card-BtiF4XiU.js → hover-card-CN9s2md5.js} +1 -1
  58. package/dist/elements/chunks/{icon-DKkmze3y.js → icon-CNiKP37E.js} +1 -1
  59. package/dist/elements/chunks/{info-CWRjZkuy.js → info-BSnuJiHI.js} +1 -1
  60. package/dist/elements/chunks/{input--QJBK6BA.js → input-BauDmzxX.js} +1 -1
  61. package/dist/elements/chunks/{kbd-C3zuKh8R.js → kbd-BlHJaP9b.js} +1 -1
  62. package/dist/elements/chunks/{link-BL5JA7Q0.js → link-DEi5iUWC.js} +1 -1
  63. package/dist/elements/chunks/{link-preview-CU37bbXh.js → link-preview-BaHyM6W4.js} +1 -1
  64. package/dist/elements/chunks/{loader-XmpyxyRH.js → loader-CmJG_Fkq.js} +1 -1
  65. package/dist/elements/chunks/{markdown-3Gv5UZ_o.js → markdown-D2jaMpCN.js} +1 -1
  66. package/dist/elements/chunks/{message-X5NZbq-W.js → message-C5UfXZ2Z.js} +1 -1
  67. package/dist/elements/chunks/{message-CnBA2OUt.js → message-CXWLmmLn.js} +1 -1
  68. package/dist/elements/chunks/message-square-hXylwsy0.js +1 -0
  69. package/dist/elements/chunks/{minimize-2-DwyrUTcM.js → minimize-2-C9ytZFMU.js} +1 -1
  70. package/dist/elements/chunks/model-switcher-CivGfD7-.js +1 -0
  71. package/dist/elements/chunks/{overlay-Cepd-KcS.js → overlay-DJL_sbOm.js} +1 -1
  72. package/dist/elements/chunks/{paperclip-CdwKjrlu.js → paperclip-CFaB8naM.js} +1 -1
  73. package/dist/elements/chunks/{progress-bar-DMVXBD3b.js → progress-bar-Br5SksfR.js} +1 -1
  74. package/dist/elements/chunks/{prompt-suggestion-Di_WZE_5.js → prompt-suggestion-CMIvtAhU.js} +1 -1
  75. package/dist/elements/chunks/{reasoning-D0jMch5u.js → reasoning-DpZcNzLq.js} +1 -1
  76. package/dist/elements/chunks/{resizable-1znbL1Gv.js → resizable-Q8i3T9a_.js} +1 -1
  77. package/dist/elements/chunks/{rotate-cw-DVhhgx8n.js → rotate-cw-B5IHCBFV.js} +1 -1
  78. package/dist/elements/chunks/{scroll-area-Bs4bFS1P.js → scroll-area-eLeydBje.js} +1 -1
  79. package/dist/elements/chunks/{scroll-button-Bet5NLc4.js → scroll-button-DWbz0yA1.js} +1 -1
  80. package/dist/elements/chunks/{separator-C5Gh8qGC.js → separator-Xic78AD_.js} +1 -1
  81. package/dist/elements/chunks/{settings-5vmEFg0K.js → settings-B8H51pAx.js} +1 -1
  82. package/dist/elements/chunks/{settings-group-BWLB38kz.js → settings-group-CsG9eYLe.js} +1 -1
  83. package/dist/elements/chunks/{share-BZ3G_DaB.js → share-CNTx0z__.js} +1 -1
  84. package/dist/elements/chunks/{skeleton-DafPJQTp.js → skeleton-CLlHTcMI.js} +1 -1
  85. package/dist/elements/chunks/{source-D_qj_6uJ.js → source-BoVcU5ok.js} +1 -1
  86. package/dist/elements/chunks/{star-Cuv7j2IE.js → star-CxQVTVhI.js} +1 -1
  87. package/dist/elements/chunks/{store-C1StPHfW.js → store-CLtS5CUG.js} +1 -1
  88. package/dist/elements/chunks/{tasks-card-EyoBZfpA.js → tasks-card-CzlkLmUt.js} +1 -1
  89. package/dist/elements/chunks/{text-shimmer-D9gRSh34.js → text-shimmer-D5NhpIQ1.js} +1 -1
  90. package/dist/elements/chunks/{textarea-DuhtHfcm.js → textarea-x5Uxsmtm.js} +1 -1
  91. package/dist/elements/chunks/{thumbs-up-B9hwv43w.js → thumbs-up-UobcJnx9.js} +1 -1
  92. package/dist/elements/chunks/{toast-store-Bm3DUEDe.js → toast-store-BOfs9eXq.js} +1 -1
  93. package/dist/elements/chunks/{tool-CDtW7L0z.js → tool-Be6Dh_dt.js} +1 -1
  94. package/dist/elements/chunks/{tooltip-eB226mGK.js → tooltip-RxMxyCLF.js} +1 -1
  95. package/dist/elements/chunks/{triangle-alert-C1SNzTqK.js → triangle-alert-C4yxH2XN.js} +1 -1
  96. package/dist/elements/chunks/{use-card-resolution-YZh2MvI5.js → use-card-resolution-jva9ehH6.js} +1 -1
  97. package/dist/elements/chunks/{variant-aurora-Cj1F7D0l.js → variant-aurora-Dx4VRcjS.js} +1 -1
  98. package/dist/elements/chunks/{variant-custom-T9gpt7QE.js → variant-custom-Y1ZTWgEa.js} +1 -1
  99. package/dist/elements/chunks/{variant-wave-CxU9z-TE.js → variant-wave-B9gml9Cg.js} +1 -1
  100. package/dist/elements/chunks/{video-CK4R7Rus.js → video-CrPHsytT.js} +1 -1
  101. package/dist/elements/chunks/x-_-OPHept.js +1 -0
  102. package/dist/elements/coachmark.js +1 -1
  103. package/dist/elements/code-block.js +1 -1
  104. package/dist/elements/command.js +1 -1
  105. package/dist/elements/compare.js +1 -1
  106. package/dist/elements/composer.js +1 -1
  107. package/dist/elements/confirm-card.js +1 -1
  108. package/dist/elements/context-meter.js +1 -1
  109. package/dist/elements/conversation-list.js +1 -1
  110. package/dist/elements/default-input.d.ts +27 -0
  111. package/dist/elements/dialog.js +1 -1
  112. package/dist/elements/editable-label.js +1 -1
  113. package/dist/elements/embed.js +1 -1
  114. package/dist/elements/empty.js +1 -1
  115. package/dist/elements/feedback-bar.js +1 -1
  116. package/dist/elements/file-tree.js +1 -1
  117. package/dist/elements/file-upload.js +1 -1
  118. package/dist/elements/form.js +1 -1
  119. package/dist/elements/hover-card.js +1 -1
  120. package/dist/elements/icon.js +1 -1
  121. package/dist/elements/image.js +1 -1
  122. package/dist/elements/input.js +1 -1
  123. package/dist/elements/kbd.js +1 -1
  124. package/dist/elements/link-preview.js +1 -1
  125. package/dist/elements/loader.js +1 -1
  126. package/dist/elements/markdown.js +1 -1
  127. package/dist/elements/menu.js +1 -1
  128. package/dist/elements/message-skills.js +1 -1
  129. package/dist/elements/message.js +1 -1
  130. package/dist/elements/model-switcher.js +1 -1
  131. package/dist/elements/nav.js +1 -1
  132. package/dist/elements/notice.js +1 -1
  133. package/dist/elements/pane-group.js +1 -1
  134. package/dist/elements/pane.js +1 -1
  135. package/dist/elements/popover.js +1 -1
  136. package/dist/elements/progress-bar.js +1 -1
  137. package/dist/elements/prompt-dock.js +1 -1
  138. package/dist/elements/prompt-input.js +1 -1
  139. package/dist/elements/prompt-suggestions.js +1 -1
  140. package/dist/elements/reasoning.js +1 -1
  141. package/dist/elements/remote.js +1 -1
  142. package/dist/elements/resizable.js +1 -1
  143. package/dist/elements/response-stream.js +2 -2
  144. package/dist/elements/screen.js +1 -1
  145. package/dist/elements/scroll-area.js +1 -1
  146. package/dist/elements/scroll-button.js +1 -1
  147. package/dist/elements/search.js +1 -1
  148. package/dist/elements/segmented.js +1 -1
  149. package/dist/elements/separator.js +1 -1
  150. package/dist/elements/setting-item.js +1 -1
  151. package/dist/elements/settings-group.js +1 -1
  152. package/dist/elements/skeleton.js +1 -1
  153. package/dist/elements/slot-text.d.ts +51 -0
  154. package/dist/elements/slots.d.ts +5 -0
  155. package/dist/elements/source.js +1 -1
  156. package/dist/elements/status.js +1 -1
  157. package/dist/elements/switch.js +1 -1
  158. package/dist/elements/tabs.js +1 -1
  159. package/dist/elements/tasks.js +1 -1
  160. package/dist/elements/text-shimmer.js +1 -1
  161. package/dist/elements/thinking-bar.js +1 -1
  162. package/dist/elements/thread.js +1 -1
  163. package/dist/elements/toast.js +1 -1
  164. package/dist/elements/tool.js +1 -1
  165. package/dist/elements/tooltip.js +1 -1
  166. package/dist/elements/voice-input.js +1 -1
  167. package/dist/elements/voice-output.js +1 -1
  168. package/dist/elements.d.ts +12 -6
  169. package/dist/index.js +1535 -1487
  170. package/dist/index.server.js +2330 -2296
  171. package/dist/kai.es.js +1 -1
  172. package/dist/llms/llms-full.txt +12 -3
  173. package/dist/mcp.es.js +890 -78
  174. package/dist/react/index.d.ts +13 -3
  175. package/dist/react.js +14 -14
  176. package/dist/register-impl-AxIlqjo7.js +145 -0
  177. package/dist/{solid-BCJUQqwS.js → solid-BGYSJyHG.js} +4901 -4691
  178. package/dist/{solid-BdENBcEq.js → solid-Dt2nEwfl.js} +4406 -4320
  179. package/dist/solid.js +1 -1
  180. package/dist/solid.server.js +1 -1
  181. package/dist/{variant-aurora-DOb_o4pC.js → variant-aurora-B44X69AG.js} +1 -1
  182. package/dist/{variant-aurora-BKHHiOgL.js → variant-aurora-BRhV8Flk.js} +2 -2
  183. package/dist/{variant-aurora-1wMcXRUS.js → variant-aurora-Ds5ou9BH.js} +2 -2
  184. package/dist/{variant-custom-DWSSgmd2.js → variant-custom-B20FhtYj.js} +2 -2
  185. package/dist/{variant-custom-CJGB0Kbw.js → variant-custom-BvA1Schl.js} +1 -1
  186. package/dist/{variant-custom-Co0CkXJF.js → variant-custom-D6Rtvneb.js} +2 -2
  187. package/dist/{variant-wave-CEODq-9q.js → variant-wave-BUQtkryq.js} +1 -1
  188. package/dist/{variant-wave-C3-45cL6.js → variant-wave-BVYnqy9L.js} +2 -2
  189. package/dist/{variant-wave-jdXBnCUZ.js → variant-wave-DXC6_LcH.js} +2 -2
  190. package/dist/wire/encode.d.ts +79 -6
  191. package/dist/wire/files.d.ts +116 -0
  192. package/dist/wire/index.d.ts +3 -1
  193. package/dist/wire/media-types.d.ts +106 -0
  194. package/dist/wire.js +596 -368
  195. package/frameworks/react/index.tsx +9 -5
  196. package/llms-full.txt +12 -3
  197. package/package.json +4 -1
  198. package/src/agent-tooling/archetypes.ts +43 -12
  199. package/src/agent-tooling/integrations/anthropic.ts +29 -5
  200. package/src/agent-tooling/integrations/cloudflare.ts +5 -0
  201. package/src/agent-tooling/integrations/langgraph.ts +13 -0
  202. package/src/agent-tooling/integrations/mastra.ts +46 -5
  203. package/src/agent-tooling/integrations/mock.ts +4 -0
  204. package/src/agent-tooling/integrations/ollama.ts +7 -0
  205. package/src/agent-tooling/integrations/openai.ts +2 -0
  206. package/src/agent-tooling/integrations/openrouter.ts +2 -0
  207. package/src/agent-tooling/integrations/pi.ts +26 -1
  208. package/src/agent-tooling/integrations/pydantic-ai.ts +37 -3
  209. package/src/agent-tooling/integrations/vercel-ai-sdk.ts +42 -6
  210. package/src/agent-tooling/mcp/tools/scaffold.ts +913 -32
  211. package/src/agent-tooling/registry.ts +57 -0
  212. package/src/agent-tooling/types.ts +132 -0
  213. package/src/components/chat-thread.tsx +10 -1
  214. package/src/components/context.tsx +18 -12
  215. package/src/components/conversation-item.tsx +6 -0
  216. package/src/components/model-switcher.tsx +38 -8
  217. package/src/components/toast.tsx +6 -1
  218. package/src/elements/button.tsx +26 -3
  219. package/src/elements/chat.tsx +28 -2
  220. package/src/elements/default-input.tsx +128 -5
  221. package/src/elements/element-meta.json +30 -4
  222. package/src/elements/element-types.d.ts +12 -6
  223. package/src/elements/menu.tsx +30 -3
  224. package/src/elements/slot-text.ts +73 -0
  225. package/src/elements/slots.ts +10 -0
  226. package/src/index.ts +9 -5
  227. package/src/wire/encode.ts +234 -14
  228. package/src/wire/files.ts +342 -0
  229. package/src/wire/index.ts +21 -0
  230. package/src/wire/media-types.ts +344 -0
  231. package/dist/elements/chunks/arrow-left-Bb46H7w9.js +0 -1
  232. package/dist/elements/chunks/check-C8desc_h.js +0 -1
  233. package/dist/elements/chunks/chevron-down-DKtc2HNB.js +0 -1
  234. package/dist/elements/chunks/chevron-right-Dw4GPhwL.js +0 -1
  235. package/dist/elements/chunks/circle-BeokBsvB.js +0 -1
  236. package/dist/elements/chunks/circle-check-wJSqt859.js +0 -1
  237. package/dist/elements/chunks/context-WHDl-xSx.js +0 -1
  238. package/dist/elements/chunks/conversation-list-CdR14_Y4.js +0 -1
  239. package/dist/elements/chunks/default-input-CSZqUM81.js +0 -1
  240. package/dist/elements/chunks/message-square-CnpAm_P0.js +0 -1
  241. package/dist/elements/chunks/model-switcher-CclaJEHt.js +0 -1
  242. package/dist/elements/chunks/x-D1qx0aLd.js +0 -1
  243. package/dist/register-impl-bzrZ44ft.js +0 -145
@@ -5,6 +5,8 @@
5
5
  // No provider SDK, no fetch, no DOM. Pure functions over the content model.
6
6
  import type { ChatMessage, MessagePart } from '../elements/chat-types';
7
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';
8
10
 
9
11
  export interface OpenAIToolCall {
10
12
  id: string;
@@ -18,9 +20,20 @@ export interface OpenAIToolCall {
18
20
  * to have seen. */
19
21
  export type OpenAIReasoningDetail = Record<string, unknown>;
20
22
 
23
+ /** A multimodal user message's content entries. `image_url` takes an https URL
24
+ * or a `data:` URI in the same field; `file` takes `file_data`, which is a DATA
25
+ * URI on this wire (`data:application/pdf;base64,...`) and not bare base64. */
26
+ export type OpenAIContentPart =
27
+ | { type: 'text'; text: string }
28
+ | { type: 'image_url'; image_url: { url: string } }
29
+ | { type: 'file'; file: { filename?: string; file_data: string } };
30
+
21
31
  export interface OpenAIWireMessage {
22
32
  role: 'system' | 'user' | 'assistant' | 'tool';
23
- content: string | null;
33
+ /** An ARRAY only when the turn carries an encodable `file` part. A text-only
34
+ * turn stays a plain string, so adding attachment support changed nothing
35
+ * about what an existing thread puts on the wire. */
36
+ content: string | OpenAIContentPart[] | null;
24
37
  tool_calls?: OpenAIToolCall[];
25
38
  tool_call_id?: string;
26
39
  name?: string;
@@ -29,7 +42,10 @@ export interface OpenAIWireMessage {
29
42
  reasoning_details?: OpenAIReasoningDetail[];
30
43
  }
31
44
 
32
- export interface OpenAIEncodeOptions {
45
+ /** EXTENDS rather than restates the file options: `onUnencodableFile` and
46
+ * `accept` mean the same thing on both wires, and a second declaration of them
47
+ * here is a second place to forget to update. */
48
+ export interface OpenAIEncodeOptions extends FileEncodeOptions {
33
49
  /**
34
50
  * Whether to send the assistant's own reasoning back with the thread.
35
51
  *
@@ -54,6 +70,40 @@ export interface OpenAIEncodeOptions {
54
70
  reasoning?: 'omit' | 'include';
55
71
  }
56
72
 
73
+ /**
74
+ * What to do with a `file` part this wire cannot carry.
75
+ *
76
+ * DEFAULT `'throw'`, and the default is the whole point. Skipping is how
77
+ * attachments came to render perfectly in the thread and reach the model as
78
+ * nothing: the developer wires up upload, watches it work, and ships a model
79
+ * that cannot see the file. A throw here names the message, the part and the
80
+ * reason, which is strictly more than a 400 at request time would tell you.
81
+ *
82
+ * `'skip'` restores the lenient behaviour for a host that would rather send a
83
+ * degraded turn than fail one. It is silent, but it is silence the developer
84
+ * asked for by name, which is the difference that matters.
85
+ */
86
+ export type UnencodableFilePolicy = 'throw' | 'skip';
87
+
88
+ export interface FileEncodeOptions {
89
+ onUnencodableFile?: UnencodableFilePolicy;
90
+ /**
91
+ * Narrow which attachment media types reach the wire, as HTML `accept` syntax
92
+ * (`'image/*,application/pdf'`) or an array of the same.
93
+ *
94
+ * THE SAME STRING the composer takes as `<kai-chat accept="...">`, resolved by
95
+ * the same function against the same declaration -- so a developer writes the
96
+ * set once as a constant and hands it to both ends. Omitted means the kit's
97
+ * full capability set, which is `encodableMediaTypes()`.
98
+ *
99
+ * It can only NARROW. Naming a type the encoders cannot represent does not
100
+ * enable it; that would just move the failure to a provider 400.
101
+ */
102
+ accept?: MediaTypeFilter;
103
+ }
104
+
105
+ export type AnthropicEncodeOptions = FileEncodeOptions;
106
+
57
107
  /** Anthropic content blocks are an open, provider-owned union. Keeping them as
58
108
  * records is what lets a verbatim `thinking` payload pass through UNTOUCHED,
59
109
  * which is the entire point of this encoder. */
@@ -93,12 +143,112 @@ type SettledTool = ToolPart & { toolCallId: string };
93
143
 
94
144
  const isTextPart = (p: MessagePart): p is TextPart => p.type === 'text';
95
145
 
146
+ // 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.
96
147
  const textOf = (parts: MessagePart[]): string =>
97
148
  parts
98
149
  .filter(isTextPart)
99
150
  .map((p) => p.text)
100
151
  .join('');
101
152
 
153
+ type FilePart = Extract<MessagePart, { type: 'file' }>;
154
+
155
+ /** A wire either shapes a classified file into a block, or explains why it
156
+ * cannot. `ok: false` is NOT the same as an unencodable attachment: the
157
+ * attachment can be perfectly sound and still have no form on this particular
158
+ * wire, which is exactly the remote-PDF case on OpenAI. */
159
+ type WireFileResult<T> = { ok: true; block: T } | { ok: false; reason: string };
160
+
161
+ /**
162
+ * One `file` part to one wire block, or `null` when it is skipped.
163
+ *
164
+ * Skipped means one of two things and never a third: the attachment is kit-side
165
+ * (a citation chip), or the host explicitly opted into `onUnencodableFile:
166
+ * 'skip'`. Everything else throws, because a dropped attachment is invisible in
167
+ * exactly the way the developer needs it not to be.
168
+ */
169
+ function encodeFilePart<T>(
170
+ part: FilePart,
171
+ messageId: string,
172
+ partIndex: number,
173
+ policy: UnencodableFilePolicy,
174
+ media: MediaPolicy,
175
+ toBlock: (file: ClassifiedFile) => WireFileResult<T>,
176
+ ): T | null {
177
+ const classified = classifyAttachment(part.attachment, media);
178
+ if (classified.status === 'kit-side') return null;
179
+
180
+ const refuse = (reason: string): null => {
181
+ if (policy === 'skip') return null;
182
+ throw new WireEncodeError(
183
+ `Cannot encode file part ${partIndex} of message "${messageId}": ${reason} Pass { onUnencodableFile: 'skip' } to drop it instead of failing.`,
184
+ messageId,
185
+ partIndex,
186
+ );
187
+ };
188
+
189
+ if (classified.status === 'unencodable') return refuse(classified.reason);
190
+
191
+ const shaped = toBlock(classified.file);
192
+ return shaped.ok ? shaped.block : refuse(shaped.reason);
193
+ }
194
+
195
+ /** `image_url` takes an https URL and a `data:` URI in the same field, so an
196
+ * image is one shape here. A PDF is not: `file_data` is base64-only. A text
197
+ * file is neither: it becomes ordinary text content, because that is the only
198
+ * thing either API's block set can express it as. */
199
+ const openAIFileBlock = (file: ClassifiedFile): WireFileResult<OpenAIContentPart> => {
200
+ if (file.kind === 'text') {
201
+ return { ok: true, block: { type: 'text', text: textFileContent(file) } };
202
+ }
203
+
204
+ if (file.kind === 'image') {
205
+ const url = file.source.type === 'base64' ? file.source.dataUri : file.source.url;
206
+ return { ok: true, block: { type: 'image_url', image_url: { url } } };
207
+ }
208
+
209
+ if (file.source.type === 'remote') {
210
+ return {
211
+ ok: false,
212
+ 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.`,
213
+ };
214
+ }
215
+
216
+ return {
217
+ ok: true,
218
+ block: {
219
+ type: 'file',
220
+ file: {
221
+ ...(file.filename !== undefined ? { filename: file.filename } : {}),
222
+ file_data: file.source.dataUri,
223
+ },
224
+ },
225
+ };
226
+ };
227
+
228
+ /** The image and document blocks take the same two source shapes, which is why
229
+ * this wire has no gap where OpenAI has one. Text is the third case and is not
230
+ * a `document` block: this API's content-block set is text / image / document
231
+ * with no arbitrary-file member, so an inlined text file IS a text block.
232
+ * (A `document` block CAN carry text via the Files API, which would buy
233
+ * citations -- see the design note; it needs an upload step this layer cannot
234
+ * make, because it does no I/O.) */
235
+ const anthropicFileBlock = (file: ClassifiedFile): WireFileResult<AnthropicContentBlock> => {
236
+ if (file.kind === 'text') {
237
+ return { ok: true, block: { type: 'text', text: textFileContent(file) } };
238
+ }
239
+
240
+ return {
241
+ ok: true,
242
+ block: {
243
+ type: file.kind === 'image' ? 'image' : 'document',
244
+ source:
245
+ file.source.type === 'base64'
246
+ ? { type: 'base64', media_type: file.source.mediaType, data: file.source.data }
247
+ : { type: 'url', url: file.source.url },
248
+ },
249
+ };
250
+ };
251
+
102
252
  /** A tool is encodable only once it has a RESULT. Both APIs require every echoed
103
253
  * tool call to have exactly one matching result, so a call with no answer yet is
104
254
  * skipped entirely rather than sent half-formed. In a real loop the host
@@ -260,21 +410,69 @@ function reasoningDetailOf(part: ReasoningPart): OpenAIReasoningDetail | undefin
260
410
  * text and no settled tool is skipped exactly as before, rather than becoming
261
411
  * `{ content: null }` with no `tool_calls`.
262
412
  *
263
- * `card`, `source` and `file` parts are never encoded; they are kit-side. File
264
- * attachments are a documented v1 limitation, which is why a user turn carrying
265
- * only an attachment encodes to nothing and is skipped.
413
+ * `card` and `source` parts are never encoded; they are kit-side.
414
+ *
415
+ * `file` parts ARE encoded, on a USER turn, and a turn carrying nothing but an
416
+ * attachment is now a real message rather than nothing. Images become
417
+ * `image_url` (https URL or `data:` URI alike); a base64 PDF becomes a `file`
418
+ * part whose `file_data` is the data URI. Two cases have no form here and THROW
419
+ * by default: a remote PDF, because this wire's `file` part has no URL variant,
420
+ * and anything that is neither -- see `UnencodableFilePolicy` for why the
421
+ * default is a throw and not a skip.
422
+ *
423
+ * A `file` part on an ASSISTANT turn is still dropped. Neither API accepts image
424
+ * or document content in an assistant message, so there is nothing to encode it
425
+ * to; attachments belong to the user turn that sent them.
266
426
  */
427
+ // 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.
267
428
  export function toOpenAIMessages(
268
429
  messages: ChatMessage[],
269
430
  options: OpenAIEncodeOptions = {},
270
431
  ): OpenAIWireMessage[] {
271
432
  const includeReasoning = options.reasoning === 'include';
433
+ const filePolicy = options.onUnencodableFile ?? 'throw';
434
+ const media = resolveMediaPolicy({ accept: options.accept });
272
435
  const out: OpenAIWireMessage[] = [];
273
436
 
274
437
  for (const message of messages) {
275
438
  if (message.role === 'user') {
276
- const content = textOf(message.parts);
277
- if (content !== '') out.push({ role: 'user', content });
439
+ // Built in PART ORDER, with runs of adjacent text merged into one entry.
440
+ // A file authored after the text stays after it: `parts` is an ordered
441
+ // content model and reordering it here would be the encoder second-guessing
442
+ // the author. Anthropic's docs do suggest putting images before text for
443
+ // best results, which is guidance for whoever builds the parts array.
444
+ const content: OpenAIContentPart[] = [];
445
+ let buffered = '';
446
+ let carriesFile = false;
447
+
448
+ const flushText = (): void => {
449
+ if (buffered === '') return;
450
+ content.push({ type: 'text', text: buffered });
451
+ buffered = '';
452
+ };
453
+
454
+ // 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.
455
+ message.parts.forEach((part, partIndex) => {
456
+ if (part.type === 'text') {
457
+ buffered += part.text;
458
+ return;
459
+ }
460
+ if (part.type !== 'file') return;
461
+ const block = encodeFilePart(part, message.id, partIndex, filePolicy, media, openAIFileBlock);
462
+ if (!block) return;
463
+ flushText();
464
+ content.push(block);
465
+ carriesFile = true;
466
+ });
467
+ flushText();
468
+
469
+ // No file survived, so this is an ordinary turn and stays a plain string.
470
+ if (!carriesFile) {
471
+ const text = textOf(message.parts);
472
+ if (text !== '') out.push({ role: 'user', content: text });
473
+ continue;
474
+ }
475
+ out.push({ role: 'user', content });
278
476
  continue;
279
477
  }
280
478
 
@@ -367,11 +565,23 @@ export function toOpenAIMessages(
367
565
  * tool_result message always follows its assistant message and can never be
368
566
  * appended after a plain user turn.
369
567
  *
568
+ * `file` parts on a USER turn become `image` and `document` blocks, in part
569
+ * order. Both take `source: {type:'base64'}` and `source: {type:'url'}`, so this
570
+ * wire can carry a remote PDF that `toOpenAIMessages` has to refuse. Anything
571
+ * neither API accepts as message content THROWS by default; see
572
+ * `UnencodableFilePolicy`. A `file` part on an ASSISTANT turn is dropped, because
573
+ * an assistant message here carries only text, thinking and tool_use.
574
+ *
370
575
  * Asymmetry worth knowing: `tool_use.input` is a parsed OBJECT on this wire, not
371
576
  * a string, so it uses `input` and not `rawInput`. Only thinking blocks carry a
372
577
  * verbatim requirement.
373
578
  */
374
- export function toAnthropicMessages(messages: ChatMessage[]): AnthropicWireMessage[] {
579
+ export function toAnthropicMessages(
580
+ messages: ChatMessage[],
581
+ options: AnthropicEncodeOptions = {},
582
+ ): AnthropicWireMessage[] {
583
+ const filePolicy = options.onUnencodableFile ?? 'throw';
584
+ const media = resolveMediaPolicy({ accept: options.accept });
375
585
  const out: AnthropicWireMessage[] = [];
376
586
 
377
587
  /** Append to the trailing user message when there is one, so the wire never
@@ -384,10 +594,18 @@ export function toAnthropicMessages(messages: ChatMessage[]): AnthropicWireMessa
384
594
 
385
595
  for (const message of messages) {
386
596
  if (message.role === 'user') {
387
- const userBlocks = message.parts
388
- .filter(isTextPart)
389
- .filter((p) => p.text !== '')
390
- .map<AnthropicContentBlock>((p) => ({ type: 'text', text: p.text }));
597
+ // Part order, for the same reason as `toOpenAIMessages`.
598
+ const userBlocks: AnthropicContentBlock[] = [];
599
+ // 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.
600
+ message.parts.forEach((part, partIndex) => {
601
+ if (part.type === 'text') {
602
+ if (part.text !== '') userBlocks.push({ type: 'text', text: part.text });
603
+ return;
604
+ }
605
+ if (part.type !== 'file') return;
606
+ const block = encodeFilePart(part, message.id, partIndex, filePolicy, media, anthropicFileBlock);
607
+ if (block) userBlocks.push(block);
608
+ });
391
609
  if (userBlocks.length > 0) pushUser(userBlocks);
392
610
  continue;
393
611
  }
@@ -405,6 +623,7 @@ export function toAnthropicMessages(messages: ChatMessage[]): AnthropicWireMessa
405
623
  results = [];
406
624
  };
407
625
 
626
+ // 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.
408
627
  message.parts.forEach((part, partIndex) => {
409
628
  switch (part.type) {
410
629
  case 'reasoning': {
@@ -461,8 +680,9 @@ export function toAnthropicMessages(messages: ChatMessage[]): AnthropicWireMessa
461
680
  break;
462
681
  }
463
682
  default:
464
- // card, source and file are kit-side and have no wire representation
465
- // in v1. File attachments in particular are a known limitation.
683
+ // card and source are kit-side. A `file` part reaches this arm only on
684
+ // an ASSISTANT turn, where the API accepts no image or document
685
+ // content at all -- user attachments are handled in the user branch.
466
686
  break;
467
687
  }
468
688
  });