@kitn.ai/ui 0.28.0 → 0.29.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 (376) hide show
  1. package/README.md +1 -1
  2. package/dist/agent-tooling/construct/public.d.ts +15 -0
  3. package/dist/agent-tooling/construct/schema.d.ts +111 -0
  4. package/dist/construct-cli.es.js +3 -3
  5. package/dist/construct.d.ts +5 -0
  6. package/dist/construct.js +327 -0
  7. package/dist/core-C8fzo39E.js +1 -12
  8. package/dist/engine-javascript-C1x7zo1_.js +1 -141
  9. package/dist/mcp.es.js +1 -1
  10. package/package.json +16 -16
  11. package/bin/route.test.js +0 -58
  12. package/src/agent-tooling/README.md +0 -192
  13. package/src/agent-tooling/archetypes.ts +0 -205
  14. package/src/agent-tooling/catalog/README.md +0 -676
  15. package/src/agent-tooling/catalog/catalog-types.ts +0 -231
  16. package/src/agent-tooling/catalog/fabrications.ts +0 -96
  17. package/src/agent-tooling/catalog/invariants.ts +0 -284
  18. package/src/agent-tooling/catalog/labs-titles.ts +0 -114
  19. package/src/agent-tooling/catalog/scenarios.ts +0 -87
  20. package/src/agent-tooling/catalog/surfaces.ts +0 -347
  21. package/src/agent-tooling/construct/cli-entry.ts +0 -8
  22. package/src/agent-tooling/construct/cli.ts +0 -147
  23. package/src/agent-tooling/construct/codegen.ts +0 -1635
  24. package/src/agent-tooling/construct/construct.v1.schema.json +0 -319
  25. package/src/agent-tooling/construct/dev.ts +0 -148
  26. package/src/agent-tooling/construct/fixtures/demo-widget.construct.json +0 -7
  27. package/src/agent-tooling/construct/fixtures/ops-console.construct.json +0 -44
  28. package/src/agent-tooling/construct/fixtures/owner-widget.construct.json +0 -32
  29. package/src/agent-tooling/construct/schema.ts +0 -452
  30. package/src/agent-tooling/integrations/anthropic.ts +0 -442
  31. package/src/agent-tooling/integrations/cloudflare.ts +0 -218
  32. package/src/agent-tooling/integrations/langgraph.ts +0 -133
  33. package/src/agent-tooling/integrations/mastra.ts +0 -182
  34. package/src/agent-tooling/integrations/mock.ts +0 -148
  35. package/src/agent-tooling/integrations/ollama.ts +0 -97
  36. package/src/agent-tooling/integrations/openai.ts +0 -95
  37. package/src/agent-tooling/integrations/openrouter.ts +0 -82
  38. package/src/agent-tooling/integrations/pi.ts +0 -176
  39. package/src/agent-tooling/integrations/pydantic-ai.ts +0 -108
  40. package/src/agent-tooling/integrations/vercel-ai-sdk.ts +0 -398
  41. package/src/agent-tooling/mcp/css-raw.d.ts +0 -12
  42. package/src/agent-tooling/mcp/manifest.ts +0 -422
  43. package/src/agent-tooling/mcp/server.ts +0 -116
  44. package/src/agent-tooling/mcp/stdio.ts +0 -18
  45. package/src/agent-tooling/mcp/tools/construct.ts +0 -130
  46. package/src/agent-tooling/mcp/tools/debug.ts +0 -621
  47. package/src/agent-tooling/mcp/tools/reference.ts +0 -980
  48. package/src/agent-tooling/mcp/tools/scaffold.ts +0 -6708
  49. package/src/agent-tooling/mcp/tools/theme.ts +0 -456
  50. package/src/agent-tooling/mcp/tools/types.ts +0 -14
  51. package/src/agent-tooling/mcp/validate-args.ts +0 -141
  52. package/src/agent-tooling/recipes/composed-thread.ts +0 -714
  53. package/src/agent-tooling/recipes/index.ts +0 -22
  54. package/src/agent-tooling/recipes/types.ts +0 -32
  55. package/src/agent-tooling/registry.ts +0 -132
  56. package/src/agent-tooling/route-emit.ts +0 -305
  57. package/src/agent-tooling/types.ts +0 -390
  58. package/src/components/artifact-card.tsx +0 -120
  59. package/src/components/artifact.tsx +0 -914
  60. package/src/components/attachment-types.ts +0 -66
  61. package/src/components/attachments.tsx +0 -536
  62. package/src/components/audio-visualizer/audio-visualizer.voice-fixture.ts +0 -257
  63. package/src/components/audio-visualizer/aurora.glsl.ts +0 -330
  64. package/src/components/audio-visualizer/fit-scale.ts +0 -112
  65. package/src/components/audio-visualizer/index.tsx +0 -528
  66. package/src/components/audio-visualizer/labs/lab-choreography.ts +0 -187
  67. package/src/components/audio-visualizer/labs/lab-shaders.ts +0 -363
  68. package/src/components/audio-visualizer/labs/lab-visualizer.tsx +0 -102
  69. package/src/components/audio-visualizer/shader-canvas.tsx +0 -876
  70. package/src/components/audio-visualizer/sizes.ts +0 -54
  71. package/src/components/audio-visualizer/variant-aurora.tsx +0 -305
  72. package/src/components/audio-visualizer/variant-bar.tsx +0 -175
  73. package/src/components/audio-visualizer/variant-custom.tsx +0 -344
  74. package/src/components/audio-visualizer/variant-grid.tsx +0 -175
  75. package/src/components/audio-visualizer/variant-radial.tsx +0 -163
  76. package/src/components/audio-visualizer/variant-wave.tsx +0 -130
  77. package/src/components/audio-visualizer/wave.glsl.ts +0 -130
  78. package/src/components/card-fallback.tsx +0 -60
  79. package/src/components/card-renderer.tsx +0 -220
  80. package/src/components/card.tsx +0 -212
  81. package/src/components/chain-of-thought.tsx +0 -348
  82. package/src/components/chat-container.tsx +0 -89
  83. package/src/components/chat-scope-picker.tsx +0 -57
  84. package/src/components/chat-thread.tsx +0 -1014
  85. package/src/components/checkpoint.tsx +0 -88
  86. package/src/components/choice-card.tsx +0 -743
  87. package/src/components/coachmark.tsx +0 -249
  88. package/src/components/code-block.tsx +0 -181
  89. package/src/components/composer-dom.ts +0 -159
  90. package/src/components/composer-highlight.ts +0 -242
  91. package/src/components/composer-history.ts +0 -82
  92. package/src/components/composer.tsx +0 -1075
  93. package/src/components/confirm-card.tsx +0 -402
  94. package/src/components/context.tsx +0 -377
  95. package/src/components/conversation-item.tsx +0 -241
  96. package/src/components/conversation-list.tsx +0 -429
  97. package/src/components/conversation-panel.tsx +0 -118
  98. package/src/components/dismissed-stub.tsx +0 -80
  99. package/src/components/embed.tsx +0 -200
  100. package/src/components/empty.tsx +0 -177
  101. package/src/components/feedback-bar.tsx +0 -166
  102. package/src/components/file-tree.tsx +0 -530
  103. package/src/components/file-upload.tsx +0 -167
  104. package/src/components/form-widgets.tsx +0 -525
  105. package/src/components/form.tsx +0 -1296
  106. package/src/components/home-panel.tsx +0 -159
  107. package/src/components/image.tsx +0 -65
  108. package/src/components/link-preview.tsx +0 -195
  109. package/src/components/loader.tsx +0 -367
  110. package/src/components/markdown.tsx +0 -162
  111. package/src/components/message-skills.tsx +0 -40
  112. package/src/components/message.tsx +0 -765
  113. package/src/components/model-switcher.tsx +0 -132
  114. package/src/components/prompt-input.tsx +0 -188
  115. package/src/components/prompt-suggestion.tsx +0 -150
  116. package/src/components/reasoning.tsx +0 -293
  117. package/src/components/response-compare-types.ts +0 -152
  118. package/src/components/response-compare.tsx +0 -455
  119. package/src/components/response-stream.tsx +0 -109
  120. package/src/components/screen.tsx +0 -181
  121. package/src/components/scroll-button.tsx +0 -93
  122. package/src/components/source.tsx +0 -192
  123. package/src/components/tasks-card.tsx +0 -801
  124. package/src/components/text-shimmer.tsx +0 -37
  125. package/src/components/thinking-bar.tsx +0 -50
  126. package/src/components/thread.tsx +0 -228
  127. package/src/components/toast.tsx +0 -503
  128. package/src/components/tool-classify.ts +0 -26
  129. package/src/components/tool-types.ts +0 -49
  130. package/src/components/tool.tsx +0 -190
  131. package/src/components/use-card-resolution.ts +0 -44
  132. package/src/components/voice-input.tsx +0 -222
  133. package/src/components/voice-output.tsx +0 -242
  134. package/src/components/widget-tab-bar.tsx +0 -73
  135. package/src/components/workspace-shell.tsx +0 -285
  136. package/src/diagnostics/hook.ts +0 -352
  137. package/src/diagnostics/index.ts +0 -82
  138. package/src/diagnostics/report-request.ts +0 -296
  139. package/src/elements/agent-card.tsx +0 -67
  140. package/src/elements/artifact.tsx +0 -194
  141. package/src/elements/attachments.tsx +0 -140
  142. package/src/elements/audio-visualizer.tsx +0 -227
  143. package/src/elements/autoloader.ts +0 -89
  144. package/src/elements/avatar.tsx +0 -33
  145. package/src/elements/badge.tsx +0 -32
  146. package/src/elements/button.tsx +0 -149
  147. package/src/elements/card.tsx +0 -133
  148. package/src/elements/cards.tsx +0 -266
  149. package/src/elements/chain-of-thought.tsx +0 -141
  150. package/src/elements/chat-scope-picker.tsx +0 -65
  151. package/src/elements/chat-types.ts +0 -102
  152. package/src/elements/chat-workspace.tsx +0 -188
  153. package/src/elements/chat.tsx +0 -304
  154. package/src/elements/checkbox-group.tsx +0 -164
  155. package/src/elements/checkbox.tsx +0 -134
  156. package/src/elements/checkpoint.tsx +0 -47
  157. package/src/elements/choice.tsx +0 -100
  158. package/src/elements/coachmark.tsx +0 -90
  159. package/src/elements/code-block.tsx +0 -60
  160. package/src/elements/command.tsx +0 -197
  161. package/src/elements/compare.tsx +0 -111
  162. package/src/elements/compiled.css +0 -2
  163. package/src/elements/composer.tsx +0 -162
  164. package/src/elements/confirm-card.tsx +0 -76
  165. package/src/elements/context-meter.tsx +0 -107
  166. package/src/elements/conversation-item.tsx +0 -120
  167. package/src/elements/conversation-list.tsx +0 -203
  168. package/src/elements/css.ts +0 -5
  169. package/src/elements/default-input.tsx +0 -392
  170. package/src/elements/define-entry.ts +0 -12
  171. package/src/elements/define.tsx +0 -563
  172. package/src/elements/diagnostic-events.ts +0 -114
  173. package/src/elements/dialog.tsx +0 -145
  174. package/src/elements/disclosure.ts +0 -95
  175. package/src/elements/dock.tsx +0 -215
  176. package/src/elements/dropdown.tsx +0 -123
  177. package/src/elements/editable-label.tsx +0 -140
  178. package/src/elements/element-data-types.ts +0 -152
  179. package/src/elements/element-diagnostics.ts +0 -392
  180. package/src/elements/element-manifest.json +0 -354
  181. package/src/elements/element-nonscalar.json +0 -187
  182. package/src/elements/element-types.d.ts +0 -4353
  183. package/src/elements/embed.tsx +0 -35
  184. package/src/elements/empty.tsx +0 -29
  185. package/src/elements/feedback-bar.tsx +0 -66
  186. package/src/elements/file-tree.tsx +0 -62
  187. package/src/elements/file-upload.tsx +0 -44
  188. package/src/elements/form.tsx +0 -94
  189. package/src/elements/hover-card.tsx +0 -80
  190. package/src/elements/icon.tsx +0 -41
  191. package/src/elements/image.tsx +0 -32
  192. package/src/elements/input.tsx +0 -492
  193. package/src/elements/kbd.tsx +0 -45
  194. package/src/elements/link-preview.tsx +0 -34
  195. package/src/elements/loader.tsx +0 -25
  196. package/src/elements/markdown.tsx +0 -38
  197. package/src/elements/menu.tsx +0 -241
  198. package/src/elements/message-skills.tsx +0 -83
  199. package/src/elements/message.tsx +0 -361
  200. package/src/elements/model-switcher.tsx +0 -121
  201. package/src/elements/nav.tsx +0 -87
  202. package/src/elements/notice.tsx +0 -53
  203. package/src/elements/pane-grid.tsx +0 -111
  204. package/src/elements/pane-group.tsx +0 -119
  205. package/src/elements/pane.tsx +0 -129
  206. package/src/elements/popover.tsx +0 -81
  207. package/src/elements/progress-bar.tsx +0 -39
  208. package/src/elements/prompt-dock.tsx +0 -82
  209. package/src/elements/prompt-input.tsx +0 -240
  210. package/src/elements/prompt-suggestions.tsx +0 -113
  211. package/src/elements/radio-group.tsx +0 -129
  212. package/src/elements/reasoning.tsx +0 -71
  213. package/src/elements/register-impl.ts +0 -148
  214. package/src/elements/register.ts +0 -76
  215. package/src/elements/remote.tsx +0 -225
  216. package/src/elements/resizable.globals.d.ts +0 -36
  217. package/src/elements/resizable.tsx +0 -704
  218. package/src/elements/response-stream.tsx +0 -40
  219. package/src/elements/screen.tsx +0 -90
  220. package/src/elements/scroll-area.tsx +0 -33
  221. package/src/elements/scroll-button.tsx +0 -179
  222. package/src/elements/search.tsx +0 -185
  223. package/src/elements/segmented.tsx +0 -129
  224. package/src/elements/select.tsx +0 -165
  225. package/src/elements/separator.tsx +0 -36
  226. package/src/elements/setting-item.tsx +0 -50
  227. package/src/elements/settings-group.tsx +0 -37
  228. package/src/elements/skeleton.tsx +0 -45
  229. package/src/elements/slider.tsx +0 -171
  230. package/src/elements/slot-text.ts +0 -73
  231. package/src/elements/slots.ts +0 -787
  232. package/src/elements/source.tsx +0 -126
  233. package/src/elements/status.tsx +0 -42
  234. package/src/elements/styles.css +0 -328
  235. package/src/elements/switch.tsx +0 -118
  236. package/src/elements/tabs.tsx +0 -95
  237. package/src/elements/tasks.tsx +0 -103
  238. package/src/elements/text-shimmer.tsx +0 -28
  239. package/src/elements/thinking-bar.tsx +0 -43
  240. package/src/elements/thread.tsx +0 -139
  241. package/src/elements/toast.tsx +0 -106
  242. package/src/elements/tool.tsx +0 -78
  243. package/src/elements/tooltip.tsx +0 -85
  244. package/src/elements/validate-messages.ts +0 -76
  245. package/src/elements/voice-input.tsx +0 -100
  246. package/src/elements/voice-output.tsx +0 -82
  247. package/src/index.ts +0 -430
  248. package/src/primitives/audio-bands.ts +0 -174
  249. package/src/primitives/card-component-types.ts +0 -63
  250. package/src/primitives/card-contract.ts +0 -80
  251. package/src/primitives/card-data-types.ts +0 -361
  252. package/src/primitives/card-host.tsx +0 -37
  253. package/src/primitives/card-recovery.ts +0 -160
  254. package/src/primitives/card-registry.tsx +0 -78
  255. package/src/primitives/card-resolution.ts +0 -36
  256. package/src/primitives/card-routing.ts +0 -85
  257. package/src/primitives/card-schemas/artifact.schema.json +0 -92
  258. package/src/primitives/card-schemas/card-envelope.schema.json +0 -14
  259. package/src/primitives/card-schemas/card-event.schema.json +0 -12
  260. package/src/primitives/card-schemas/choice.schema.json +0 -75
  261. package/src/primitives/card-schemas/confirm.schema.json +0 -65
  262. package/src/primitives/card-schemas/embed.schema.json +0 -63
  263. package/src/primitives/card-schemas/form.result.schema.json +0 -7
  264. package/src/primitives/card-schemas/form.schema.json +0 -53
  265. package/src/primitives/card-schemas/link.schema.json +0 -54
  266. package/src/primitives/card-schemas/tasks.result.schema.json +0 -16
  267. package/src/primitives/card-schemas/tasks.schema.json +0 -83
  268. package/src/primitives/card-tags.ts +0 -61
  269. package/src/primitives/card-validate-cards.ts +0 -255
  270. package/src/primitives/card-validate-schemas.ts +0 -58
  271. package/src/primitives/card-validate.ts +0 -131
  272. package/src/primitives/chat-config.tsx +0 -103
  273. package/src/primitives/composer-model.ts +0 -49
  274. package/src/primitives/composer-triggers.ts +0 -35
  275. package/src/primitives/controllable.ts +0 -21
  276. package/src/primitives/conversation-store.ts +0 -248
  277. package/src/primitives/create-kai-chat.ts +0 -66
  278. package/src/primitives/create-tween.ts +0 -266
  279. package/src/primitives/embed-providers.ts +0 -254
  280. package/src/primitives/field-mask.ts +0 -256
  281. package/src/primitives/field-semantics.ts +0 -115
  282. package/src/primitives/highlighter.ts +0 -157
  283. package/src/primitives/input-mask.ts +0 -853
  284. package/src/primitives/link-preview.ts +0 -87
  285. package/src/primitives/message-feedback.ts +0 -124
  286. package/src/primitives/pdf-preview.ts +0 -121
  287. package/src/primitives/toast-store.ts +0 -338
  288. package/src/primitives/url-scheme-policy.ts +0 -70
  289. package/src/primitives/use-audio-analysis.ts +0 -340
  290. package/src/primitives/use-auto-resize.ts +0 -31
  291. package/src/primitives/use-resize-observer.ts +0 -45
  292. package/src/primitives/use-sequencer.ts +0 -72
  293. package/src/primitives/use-speech-recognition.ts +0 -146
  294. package/src/primitives/use-stick-to-bottom.ts +0 -75
  295. package/src/primitives/use-text-stream.ts +0 -143
  296. package/src/primitives/use-voice-recorder.ts +0 -62
  297. package/src/primitives/visualizer-sequences.ts +0 -350
  298. package/src/remote/host-embed.ts +0 -345
  299. package/src/remote/index.ts +0 -2
  300. package/src/remote/origin.ts +0 -30
  301. package/src/remote/provider-runtime.ts +0 -262
  302. package/src/remote/provider.ts +0 -2
  303. package/src/remote/validate.ts +0 -22
  304. package/src/remote/version.ts +0 -12
  305. package/src/remote/wire.ts +0 -48
  306. package/src/schemas/from-tool-call.ts +0 -151
  307. package/src/schemas/index.ts +0 -341
  308. package/src/schemas/provider-subsets.ts +0 -538
  309. package/src/schemas/registry.ts +0 -355
  310. package/src/schemas/tool-defs.ts +0 -793
  311. package/src/solid.ts +0 -173
  312. package/src/state/index.ts +0 -44
  313. package/src/state/messages.ts +0 -53
  314. package/src/state/mock.ts +0 -297
  315. package/src/state/parts.ts +0 -295
  316. package/src/state/persistence.ts +0 -180
  317. package/src/state/stream.ts +0 -246
  318. package/src/state/suggestions.ts +0 -9
  319. package/src/state/threads.ts +0 -142
  320. package/src/test-utils/fake-clock.ts +0 -88
  321. package/src/types.ts +0 -95
  322. package/src/ui/action-icons.ts +0 -53
  323. package/src/ui/agent-card.tsx +0 -218
  324. package/src/ui/avatar.tsx +0 -23
  325. package/src/ui/badge.tsx +0 -21
  326. package/src/ui/button.tsx +0 -49
  327. package/src/ui/card.tsx +0 -245
  328. package/src/ui/checkbox-group.tsx +0 -153
  329. package/src/ui/checkbox.tsx +0 -63
  330. package/src/ui/collapsible.tsx +0 -163
  331. package/src/ui/command.tsx +0 -103
  332. package/src/ui/dialog.tsx +0 -223
  333. package/src/ui/dock.tsx +0 -630
  334. package/src/ui/dropdown.tsx +0 -636
  335. package/src/ui/editable-label.tsx +0 -112
  336. package/src/ui/hover-card.tsx +0 -345
  337. package/src/ui/icon.tsx +0 -167
  338. package/src/ui/input.tsx +0 -391
  339. package/src/ui/kbd.tsx +0 -118
  340. package/src/ui/nav.tsx +0 -282
  341. package/src/ui/notice.tsx +0 -84
  342. package/src/ui/overlay.tsx +0 -277
  343. package/src/ui/pane-grid.tsx +0 -117
  344. package/src/ui/pane-group.tsx +0 -291
  345. package/src/ui/pane.tsx +0 -193
  346. package/src/ui/popover.tsx +0 -106
  347. package/src/ui/progress-bar.tsx +0 -83
  348. package/src/ui/prompt-dock.tsx +0 -145
  349. package/src/ui/radio.tsx +0 -150
  350. package/src/ui/resizable.tsx +0 -693
  351. package/src/ui/scroll-area.tsx +0 -26
  352. package/src/ui/segmented.tsx +0 -109
  353. package/src/ui/select.tsx +0 -168
  354. package/src/ui/separator.tsx +0 -10
  355. package/src/ui/settings-group.tsx +0 -67
  356. package/src/ui/skeleton.tsx +0 -73
  357. package/src/ui/slider.tsx +0 -178
  358. package/src/ui/status.tsx +0 -48
  359. package/src/ui/switch.tsx +0 -119
  360. package/src/ui/tabs.tsx +0 -168
  361. package/src/ui/textarea.tsx +0 -21
  362. package/src/ui/tooltip.tsx +0 -118
  363. package/src/utils/cn.ts +0 -30
  364. package/src/wire/chunk.ts +0 -248
  365. package/src/wire/consume.ts +0 -599
  366. package/src/wire/diagnostics.ts +0 -727
  367. package/src/wire/encode-probe.ts +0 -214
  368. package/src/wire/encode.ts +0 -947
  369. package/src/wire/files.ts +0 -342
  370. package/src/wire/formats/anthropic.ts +0 -274
  371. package/src/wire/formats/openai.ts +0 -228
  372. package/src/wire/index.ts +0 -95
  373. package/src/wire/media-types.ts +0 -344
  374. package/src/wire/read.ts +0 -396
  375. package/src/wire/sink-helpers.ts +0 -50
  376. package/src/wire/sse.ts +0 -153
package/src/wire/files.ts DELETED
@@ -1,342 +0,0 @@
1
- // Attachment -> wire classification, shared by both encoders.
2
- //
3
- // Split out of encode.ts because the two wires disagree about attachments in a
4
- // way that is worth stating once: WHAT a file is (an image, a PDF, or something
5
- // neither API takes as message content) is provider-neutral and decided here;
6
- // HOW it rides on the wire is provider-specific and decided in encode.ts. The
7
- // one gap that falls out of that split is OpenAI's, and it is real: a remote PDF
8
- // has no representation in chat-completions at all.
9
- //
10
- // No I/O, exactly like the rest of this layer. Nothing here fetches a URL,
11
- // reads a File, or base64s anything: an attachment either already carries bytes
12
- // (a `data:` URI) or already carries an address the provider can resolve
13
- // itself. That constraint is what makes a `blob:` URL unencodable rather than
14
- // merely inconvenient -- resolving one requires the browser tab that minted it.
15
- import type { AttachmentData } from '../components/attachment-types';
16
- import {
17
- DEFAULT_MEDIA_POLICY,
18
- UNNAMED_TEXT_MEDIA_TYPE,
19
- namesNothing,
20
- type MediaPolicy,
21
- } from './media-types';
22
-
23
- /** `data:<media type>;base64,<data>`. Base64 is required -- both APIs need it,
24
- * and a `data:` URI without it cannot become a content block without guessing.
25
- * The media type is NOT required, because a browser omits it for a file it
26
- * could not name: the File API says to emit a `data:` URL with no media type,
27
- * and Chrome and jsdom both write `application/octet-stream` there instead.
28
- * Either way there is no type to read, which `decide()` answers with
29
- * `undetermined` and this file settles by decoding. */
30
- const BASE64_DATA_URI = /^data:([^;,]*);base64,([\s\S]*)$/;
31
-
32
- const SCHEME = /^([a-z][a-z0-9+.-]*):/i;
33
-
34
- /** Where the bytes live. `base64` carries them inline; `remote` is an address
35
- * the PROVIDER dereferences, never us. */
36
- export type FileSource =
37
- | { type: 'base64'; mediaType: string; data: string; dataUri: string }
38
- | { type: 'remote'; url: string };
39
-
40
- /** Bytes already in hand, shaped for the provider. A union rather than an
41
- * optional `text` field so neither encoder can reach for text that is not
42
- * there, and so adding a kind is a compile error at both wires rather than a
43
- * silently unhandled branch. */
44
- export type ClassifiedFile =
45
- /** Named for the Anthropic blocks they become. `document` is PDF-only. */
46
- | { kind: 'image'; mediaType: string; source: FileSource; filename?: string }
47
- | { kind: 'document'; mediaType: string; source: FileSource; filename?: string }
48
- /** The bytes, decoded. A text file rides as TEXT CONTENT on both wires --
49
- * neither API has an arbitrary-file block, so this is the only representation
50
- * the wire can express, not a preference. `source` is narrowed to `base64`
51
- * because the text has to be in hand to inline it, and fetching a remote one
52
- * would put I/O in the encoder. */
53
- | {
54
- kind: 'text';
55
- mediaType: string;
56
- source: Extract<FileSource, { type: 'base64' }>;
57
- filename?: string;
58
- text: string;
59
- };
60
-
61
- export type FileClassification =
62
- /** Ready for either wire to shape. A wire may still refuse it -- see
63
- * OpenAI and remote PDFs -- but the attachment itself is sound. */
64
- | { status: 'encodable'; file: ClassifiedFile }
65
- /** Not an upload at all. A `source-document` is the citation chip an app
66
- * renders next to a RAG answer, not something the user staged to send, and
67
- * its content is already in the prompt that produced the answer. Encoding it
68
- * would send the same text twice. */
69
- | { status: 'kit-side' }
70
- /** Cannot reach a model. `reason` completes the sentence "Cannot encode file
71
- * part N of message X: ...", so it explains AND says what to do. */
72
- | { status: 'unencodable'; reason: string };
73
-
74
- const quotedList = (values: readonly string[]): string => values.map((v) => `"${v}"`).join(', ');
75
-
76
- /**
77
- * Base64 to the bytes it stands for.
78
- *
79
- * NOT a violation of this file's no-I/O rule. The bytes are already inline in
80
- * the `data:` URI the host staged; this decodes what is in hand and reaches for
81
- * nothing. `atob` yields a binary string (one char per byte), which is then read
82
- * as UTF-8 -- going straight from `atob` to text would mangle every non-ASCII
83
- * character in the file.
84
- *
85
- * ★ WHAT A CLEAN DECODE PROVES, AND WHAT IT DOES NOT. It proves the bytes are
86
- * well-formed UTF-8. It does not prove they are meaningful text, and the gap is
87
- * not hypothetical: every byte below 0x80 is valid UTF-8 on its own, so a small
88
- * binary made of low bytes decodes cleanly. A bare zip header -- `50 4b 03 04`
89
- * followed by NULs -- is exactly that. This matters most on the path where
90
- * nothing named the file, because there the decode is the ONLY evidence there is.
91
- *
92
- * A NUL-byte heuristic (git's rule for "is this binary") would catch most of
93
- * them, and it is deliberately NOT here. This same function serves the LABELLED
94
- * text path, where a host that said `text/plain` has asserted something the kit
95
- * has no business overruling, and a text file that legitimately contains a NUL
96
- * would start being refused on a rule nobody asked for. Weigh the two failures:
97
- * being wrong the current way sends a block of gibberish the model can see and
98
- * describe, and being wrong the other way silently refuses a real text file.
99
- * Only the first is recoverable by the person it happens to.
100
- */
101
- function decodeBase64Text(data: string): { ok: true; text: string; bytes: number } | { ok: false } {
102
- try {
103
- const binary = atob(data);
104
- const bytes = new Uint8Array(binary.length);
105
- for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
106
- // `fatal` so mislabelled binary (a .zip renamed .txt) is refused with a
107
- // reason instead of silently becoming a screenful of replacement characters.
108
- return { ok: true, text: new TextDecoder('utf-8', { fatal: true }).decode(bytes), bytes: binary.length };
109
- } catch {
110
- return { ok: false };
111
- }
112
- }
113
-
114
- /**
115
- * XML attribute escaping, for the two attributes the envelope carries.
116
- *
117
- * A filename is whatever the user called their file, and it lands between double
118
- * quotes: `notes" x="` would otherwise rewrite the header into attributes of its
119
- * own choosing. Newlines are escaped for the same reason and not for tidiness --
120
- * a raw one splits the header across lines, which is how a filename gets to look
121
- * like the start of the content.
122
- *
123
- * The media type goes through the same function, which is not symmetry for its
124
- * own sake: it comes from a `data:` URI, where it is anything up to the first
125
- * `;`, and `text/pl"ain` matches the `text/*` capability and arrives here whole.
126
- */
127
- const escapeAttribute = (value: string): string =>
128
- value
129
- .replace(/&/g, '&amp;')
130
- .replace(/</g, '&lt;')
131
- .replace(/>/g, '&gt;')
132
- .replace(/"/g, '&quot;')
133
- .replace(/\r/g, '&#13;')
134
- .replace(/\n/g, '&#10;');
135
-
136
- /** The only sequence in the body that could end the block early, and so the only
137
- * thing in the body that gets touched. Whitespace-tolerant and case-insensitive
138
- * because the reader here is a model, and `</file >` reads as the end to one
139
- * just as readily as `</file>` does. */
140
- const CLOSING_DELIMITER = /<(\/file\s*)>/gi;
141
-
142
- /**
143
- * The text content a text attachment contributes to the prompt.
144
- *
145
- * ★ THE SWAP POINT for how a text file appears in a message, and the shape is
146
- * SETTLED: `<file name="..." type="...">…</file>`. Two alternatives were weighed
147
- * and rejected. A bare `filename:\n` prefix marks where the file BEGINS and not
148
- * where it ends, so the next part of the turn reads as more file. An Anthropic
149
- * `document` block would buy citations, but it is Anthropic-specific -- the
150
- * OpenAI wire has no equivalent, so the two wires would disagree about one
151
- * attachment -- and it needs a Files API upload round trip, which is I/O, in a
152
- * layer that by design has none. Changing the envelope is still a change to this
153
- * function and nothing else; it just is not an open question.
154
- *
155
- * ★ WHY THE BODY IS BARELY TOUCHED. Both attributes are escaped in full, as
156
- * attributes always are. The body is not, and that is deliberate: these are
157
- * source files, a reader will diff what they attached against what arrived, and
158
- * XML-escaping every `<` in a TSX file would mangle the exact thing this feature
159
- * exists to send and pay tokens to do it. So the body transform is the smallest
160
- * one that makes the delimiter unforgeable -- the end tag, and nothing else.
161
- *
162
- * Left alone, that end tag is a way out of the block. A file containing `</file>`
163
- * closes it early, and everything after that point reads to the model as the turn
164
- * AROUND the file rather than as file content: user-supplied bytes in instruction
165
- * position, reachable by anyone who can get a file in front of the composer.
166
- * Escaped, it stays where it belongs. `&lt;/file&gt;` ends nothing, and it is
167
- * visible in the output and reversible by eye, which a deletion or a silent
168
- * substitution would not be -- a reader who spots it can say exactly what the
169
- * original byte was. The OPENING tag is left untouched on purpose: it cannot end
170
- * the block, and escaping it would corrupt every HTML file anyone ever attaches.
171
- */
172
- export function textFileContent(file: Extract<ClassifiedFile, { kind: 'text' }>): string {
173
- const name = escapeAttribute(file.filename ?? 'attachment');
174
- const type = escapeAttribute(file.mediaType);
175
- const text = file.text.replace(CLOSING_DELIMITER, '&lt;$1&gt;');
176
- return `<file name="${name}" type="${type}">\n${text}\n</file>`;
177
- }
178
-
179
- /**
180
- * One attachment to the provider-neutral facts both encoders need.
181
- *
182
- * A `data:` URI's OWN media type wins over `attachment.mediaType` when they
183
- * disagree: the URI describes the bytes actually present, while the field is a
184
- * label the host set, and it is the bytes the provider will decode. The one
185
- * exception is a URI whose media type names nothing, where there is no answer to
186
- * prefer -- see below.
187
- *
188
- * When NOTHING names the file, it is settled by DECODING it rather than by
189
- * reading its filename. That path can only ever produce text, and only when the
190
- * policy still allows text, so it cannot be used to get around `accept`.
191
- *
192
- * `policy` narrows what counts as encodable. It defaults to the kit's full
193
- * capability set, so an omitted policy behaves exactly as before for images and
194
- * PDFs. It is the SAME object the composer resolves for its picker -- see
195
- * `media-types.ts` for why that sharing is the point rather than a convenience.
196
- */
197
- export function classifyAttachment(
198
- attachment: AttachmentData,
199
- policy: MediaPolicy = DEFAULT_MEDIA_POLICY,
200
- ): FileClassification {
201
- if (attachment.type === 'source-document') return { status: 'kit-side' };
202
-
203
- const url = attachment.url;
204
- if (url === undefined || url === '') {
205
- return {
206
- status: 'unencodable',
207
- reason:
208
- 'it has no `url`, so there are no bytes and no address to send. Set `url` to a `data:` URI (read the File with FileReader.readAsDataURL before you stage it) or to an https URL the provider can fetch.',
209
- };
210
- }
211
-
212
- let source: FileSource;
213
- let mediaType: string;
214
-
215
- const asData = BASE64_DATA_URI.exec(url);
216
- if (asData) {
217
- const inUri = asData[1].toLowerCase();
218
- // The URI's own media type wins -- but only when it NAMES something. A
219
- // browser writes `application/octet-stream` (or nothing) for a file it could
220
- // not identify, and a non-answer describes the bytes no better than the
221
- // host's `mediaType` field does. In that one case the field is the only
222
- // label anybody actually wrote down, so it is read rather than overridden.
223
- mediaType = namesNothing(inUri) ? (attachment.mediaType?.toLowerCase() ?? inUri) : inUri;
224
- source = { type: 'base64', mediaType, data: asData[2], dataUri: url };
225
- } else if (/^https?:\/\//i.test(url)) {
226
- const declared = attachment.mediaType?.toLowerCase();
227
- if (declared === undefined || declared === '') {
228
- return {
229
- status: 'unencodable',
230
- reason: `its \`url\` is remote ("${url}") but it has no \`mediaType\`, so there is no way to tell an image from a document without fetching it -- and this layer does no I/O. Set \`mediaType\` when you stage the attachment.`,
231
- };
232
- }
233
- mediaType = declared;
234
- source = { type: 'remote', url };
235
- } else if (url.startsWith('data:')) {
236
- return {
237
- status: 'unencodable',
238
- reason:
239
- 'its `url` is a `data:` URI that is not base64-encoded with an explicit media type. Both APIs need `data:<media type>;base64,<data>`.',
240
- };
241
- } else {
242
- const scheme = SCHEME.exec(url)?.[1].toLowerCase();
243
- // blob: is the one people hit, because it is what URL.createObjectURL and
244
- // every drag-and-drop example hand you. It resolves only inside the tab that
245
- // created it, so it looks perfect in the thread and is meaningless to a model.
246
- const detail =
247
- scheme === 'blob'
248
- ? 'a `blob:` URL resolves only inside the browser tab that created it, so a model can never fetch it. Read the File with FileReader.readAsDataURL and stage the `data:` URI instead, or upload it and stage the resulting https URL'
249
- : `a "${scheme ?? url}" URL is not something a provider can fetch. Use a \`data:\` URI or an https URL`;
250
- return { status: 'unencodable', reason: `its \`url\` is "${url}": ${detail}.` };
251
- }
252
-
253
- const decision = policy.decide(mediaType);
254
-
255
- if (decision.status === 'undetermined') {
256
- // ★ NOBODY NAMED THIS FILE, SO READ IT. Seven of the file types a developer
257
- // is likeliest to attach to a coding chat arrive from Chrome with no media
258
- // type at all (the table in media-types.ts), and the tempting fix -- map
259
- // `.rs` to text -- is a filename talking, which is the one thing this design
260
- // refuses. The bytes are already in hand, so ask them instead: a clean UTF-8
261
- // decode IS the evidence that this is text, and it is evidence about the
262
- // actual file rather than about its name.
263
- //
264
- // The developer's filter is already enforced: `undetermined` comes back only
265
- // when the effective policy still admits plain text, so reaching this line at
266
- // all means a text file is something this policy would take. An
267
- // images-only `accept` gets `unsupported` and never arrives here.
268
- if (source.type !== 'base64') {
269
- return {
270
- status: 'unencodable',
271
- reason: `it is at a remote URL ("${source.url}") and its media type "${mediaType}" says nothing about the bytes -- that is the label for "arbitrary binary", so it separates an image from a document not at all. Settling it by reading the bytes would need a fetch, and this layer does no I/O. Set \`mediaType\` to what the file actually is when you stage it.`,
272
- };
273
- }
274
- const decoded = decodeBase64Text(source.data);
275
- if (!decoded.ok) {
276
- return {
277
- status: 'unencodable',
278
- reason: `nothing names it -- ${mediaType === '' ? 'it carries no media type' : `its media type is "${mediaType}"`}, so the only way to tell what it is was to read it -- and its bytes are not valid UTF-8, so it is binary of some unidentified kind. Neither API has an arbitrary-file block to carry that. Set \`mediaType\` if you know what it is, or hand the file to the model through a tool.`,
279
- };
280
- }
281
- // `text/plain` and not the media type it arrived with: a decode establishes
282
- // "this is text" and nothing more specific, and that is exactly what
283
- // `text/plain` means. See UNNAMED_TEXT_MEDIA_TYPE.
284
- return {
285
- status: 'encodable',
286
- file: {
287
- kind: 'text',
288
- mediaType: UNNAMED_TEXT_MEDIA_TYPE,
289
- source,
290
- filename: attachment.filename,
291
- text: decoded.text,
292
- },
293
- };
294
- }
295
-
296
- if (decision.status === 'filtered') {
297
- return {
298
- status: 'unencodable',
299
- reason: `its media type "${mediaType}" is one this kit can encode, but your \`accept\` filter excludes it. The types you allowed are: ${quotedList(policy.types)}. Widen \`accept\` to include it, or stop staging it.`,
300
- };
301
- }
302
-
303
- if (decision.status === 'unsupported') {
304
- return {
305
- status: 'unencodable',
306
- reason: `its media type "${mediaType}" is not one either API accepts as message content. Supported: ${quotedList(policy.types)}. Extract the content yourself and send it as text, or hand it to the model through a tool.`,
307
- };
308
- }
309
-
310
- if (decision.kind === 'text') {
311
- // A remote text file would have to be FETCHED to be inlined, and this layer
312
- // does no I/O. Refused with the fix rather than silently dropped.
313
- if (source.type !== 'base64') {
314
- return {
315
- status: 'unencodable',
316
- reason: `it is a text file at a remote URL ("${source.url}"), and text has to ride as text CONTENT -- neither API has an arbitrary-file block to point at a URL with. Reading it here would put I/O in the encoder. Fetch it yourself and stage the \`data:\` URI, or paste the contents as a text part.`,
317
- };
318
- }
319
- const decoded = decodeBase64Text(source.data);
320
- if (!decoded.ok) {
321
- return {
322
- status: 'unencodable',
323
- reason: `its media type "${mediaType}" says text, but its bytes are not valid UTF-8 -- so it is binary wearing a text label, and inlining it would send the model garbage. Send it under its real media type, or extract the text yourself.`,
324
- };
325
- }
326
- return {
327
- status: 'encodable',
328
- file: {
329
- kind: 'text',
330
- mediaType,
331
- source,
332
- filename: attachment.filename,
333
- text: decoded.text,
334
- },
335
- };
336
- }
337
-
338
- return {
339
- status: 'encodable',
340
- file: { kind: decision.kind, mediaType, source, filename: attachment.filename },
341
- };
342
- }
@@ -1,274 +0,0 @@
1
- // Anthropic Messages SSE.
2
- //
3
- // This format is STATEFUL. `content_block_delta` names the delta kind
4
- // (`thinking_delta`, `input_json_delta`, ...) but not which block kind the
5
- // fragment belongs to, so the block map is populated at `content_block_start`
6
- // and read on every later event for that index.
7
- //
8
- // It is also the only format in the kit carrying a VERBATIM requirement:
9
- // Anthropic returns 400 if a `thinking` block is modified, reordered or
10
- // reconstructed on the way back in. The block emitted at `content_block_stop`
11
- // is ASSEMBLED, not invented: `thinking` is the concatenation of the provider's
12
- // own thinking_delta payloads and `signature` is the provider's signature_delta,
13
- // so every byte came off the wire. Nothing is derived from the reasoning PART's
14
- // text, which a consumer can edit.
15
- //
16
- // Three of the chunks below carry NO reasoning text (the block start, every
17
- // signature_delta, the block stop). They depend on consume.ts gating reasoning
18
- // on `reasoning !== undefined || reasoningRaw || reasoningSignature` rather than
19
- // on truthiness. Under the old guard all three vanish.
20
- import type { MessageSource } from '../../elements/chat-types';
21
- import type { ModelStreamChunk, ModelUsage, WireFormat, WireFormatReader } from '../chunk';
22
-
23
- function isRecord(v: unknown): v is Record<string, unknown> {
24
- return typeof v === 'object' && v !== null && !Array.isArray(v);
25
- }
26
-
27
- function str(v: unknown): string | undefined {
28
- return typeof v === 'string' ? v : undefined;
29
- }
30
-
31
- function num(v: unknown): number | undefined {
32
- return typeof v === 'number' ? v : undefined;
33
- }
34
-
35
- type BlockKind = 'text' | 'thinking' | 'redacted_thinking' | 'tool_use' | 'other';
36
-
37
- interface BlockState {
38
- kind: BlockKind;
39
- /** Accumulated thinking_delta payloads, for the verbatim block at stop. */
40
- thinking: string;
41
- /** Accumulated signature_delta payloads. */
42
- signature?: string;
43
- }
44
-
45
- function usageOf(raw: unknown): ModelUsage | undefined {
46
- if (!isRecord(raw)) return undefined;
47
- const out: ModelUsage = {};
48
- const input = num(raw.input_tokens);
49
- if (input !== undefined) out.inputTokens = input;
50
- const output = num(raw.output_tokens);
51
- if (output !== undefined) out.outputTokens = output;
52
- const cached = num(raw.cache_read_input_tokens);
53
- if (cached !== undefined) out.cachedInputTokens = cached;
54
- return Object.keys(out).length > 0 ? out : undefined;
55
- }
56
-
57
- function blockStart(
58
- frame: Record<string, unknown>,
59
- blocks: Map<number, BlockState>,
60
- toolIndexById: Map<string, number>,
61
- ): ModelStreamChunk[] {
62
- const index = num(frame.index) ?? 0;
63
- const block = frame.content_block;
64
- if (!isRecord(block)) return [];
65
-
66
- switch (str(block.type)) {
67
- case 'text': {
68
- blocks.set(index, { kind: 'text', thinking: '' });
69
- const text = str(block.text);
70
- return text ? [{ text }] : [];
71
- }
72
- case 'thinking': {
73
- const seed = str(block.thinking) ?? '';
74
- blocks.set(index, { kind: 'thinking', thinking: seed });
75
- // An EMPTY reasoning delta on purpose: it opens the reasoning part at the
76
- // point in the stream where the block started, so block order in `parts`
77
- // matches block order on the wire. The encoder round-trips that order.
78
- return [{ reasoning: seed, reasoningIndex: index }];
79
- }
80
- case 'redacted_thinking': {
81
- blocks.set(index, { kind: 'redacted_thinking', thinking: '' });
82
- // The WHOLE block arrives here; there are no deltas and there is no
83
- // readable text. It must still round-trip: the docs require sending back
84
- // every block "including any blocks with empty thinking fields".
85
- return [
86
- {
87
- reasoning: '',
88
- reasoningIndex: index,
89
- reasoningRaw: { source: 'anthropic.content_block', payload: block },
90
- },
91
- ];
92
- }
93
- case 'tool_use':
94
- case 'server_tool_use': {
95
- const id = str(block.id);
96
- const name = str(block.name);
97
- blocks.set(index, { kind: 'tool_use', thinking: '' });
98
- if (id) toolIndexById.set(id, index);
99
- return [{ toolCalls: [{ index, ...(id ? { id } : {}), ...(name ? { name } : {}) }] }];
100
- }
101
- case 'web_search_tool_result': {
102
- // A result the PROVIDER executed. It correlates by `tool_use_id`, NOT by
103
- // its own block index, so it is routed back to the index of the
104
- // server_tool_use block that opened the call. With no such block the
105
- // result has nothing to complete and is dropped rather than inventing a
106
- // panel.
107
- blocks.set(index, { kind: 'other', thinking: '' });
108
- const toolUseId = str(block.tool_use_id);
109
- const target = toolUseId !== undefined ? toolIndexById.get(toolUseId) : undefined;
110
- if (target === undefined) return [];
111
- const content = block.content;
112
- if (isRecord(content) && (str(content.type) ?? '').endsWith('_error')) {
113
- return [
114
- {
115
- toolCalls: [
116
- {
117
- index: target,
118
- ...(toolUseId ? { id: toolUseId } : {}),
119
- outputError: str(content.error_code) ?? 'web_search_tool_result_error',
120
- },
121
- ],
122
- },
123
- ];
124
- }
125
- return [
126
- {
127
- toolCalls: [
128
- { index: target, ...(toolUseId ? { id: toolUseId } : {}), output: { content } },
129
- ],
130
- },
131
- ];
132
- }
133
- default:
134
- blocks.set(index, { kind: 'other', thinking: '' });
135
- return [];
136
- }
137
- }
138
-
139
- function blockDelta(
140
- frame: Record<string, unknown>,
141
- blocks: Map<number, BlockState>,
142
- ): ModelStreamChunk[] {
143
- const index = num(frame.index) ?? 0;
144
- const delta = frame.delta;
145
- if (!isRecord(delta)) return [];
146
-
147
- switch (str(delta.type)) {
148
- case 'text_delta': {
149
- const text = str(delta.text);
150
- return text ? [{ text }] : [];
151
- }
152
- case 'thinking_delta': {
153
- const text = str(delta.thinking) ?? '';
154
- const state = blocks.get(index);
155
- if (state) state.thinking += text;
156
- return [{ reasoning: text, reasoningIndex: index }];
157
- }
158
- case 'signature_delta': {
159
- const signature = str(delta.signature) ?? '';
160
- const state = blocks.get(index);
161
- // Concatenated rather than assigned: a split signature must reassemble.
162
- if (state) state.signature = (state.signature ?? '') + signature;
163
- // No text. Carried so the part's informational `signature` is populated;
164
- // the round-trip payload lands at content_block_stop.
165
- return [{ reasoning: '', reasoningIndex: index, reasoningSignature: signature }];
166
- }
167
- case 'input_json_delta': {
168
- return [{ toolCalls: [{ index, arguments: str(delta.partial_json) ?? '' }] }];
169
- }
170
- case 'citations_delta': {
171
- const citation = delta.citation;
172
- if (!isRecord(citation)) return [];
173
- const source: MessageSource = {};
174
- const url = str(citation.url);
175
- if (url) source.url = url;
176
- const title = str(citation.title);
177
- if (title) source.title = title;
178
- const snippet = str(citation.cited_text);
179
- if (snippet) source.snippet = snippet;
180
- return Object.keys(source).length > 0 ? [{ sources: [source] }] : [];
181
- }
182
- default:
183
- return [];
184
- }
185
- }
186
-
187
- function blockStop(
188
- frame: Record<string, unknown>,
189
- blocks: Map<number, BlockState>,
190
- ): ModelStreamChunk[] {
191
- const index = num(frame.index) ?? 0;
192
- const state = blocks.get(index);
193
- // Only a streamed `thinking` block needs assembling. `redacted_thinking`
194
- // already emitted its whole payload at start, and text and tool blocks have
195
- // no verbatim requirement.
196
- if (!state || state.kind !== 'thinking') return [];
197
- const payload: Record<string, unknown> = { type: 'thinking', thinking: state.thinking };
198
- if (state.signature !== undefined) payload.signature = state.signature;
199
- return [
200
- {
201
- reasoning: '',
202
- reasoningIndex: index,
203
- reasoningRaw: { source: 'anthropic.content_block', payload },
204
- },
205
- ];
206
- }
207
-
208
- function messageDelta(frame: Record<string, unknown>): ModelStreamChunk[] {
209
- const out: ModelStreamChunk = {};
210
- const delta = frame.delta;
211
- if (isRecord(delta)) {
212
- const stop = str(delta.stop_reason);
213
- if (stop) out.finishReason = stop;
214
- }
215
- const usage = usageOf(frame.usage);
216
- if (usage) out.usage = usage;
217
- return Object.keys(out).length > 0 ? [out] : [];
218
- }
219
-
220
- function errorFrame(frame: Record<string, unknown>): ModelStreamChunk[] {
221
- const err = frame.error;
222
- const message = isRecord(err) ? str(err.message) : undefined;
223
- const code = isRecord(err) ? str(err.type) : undefined;
224
- return [
225
- {
226
- error: {
227
- message: message ?? 'The provider sent an error frame with no message.',
228
- ...(code ? { code } : {}),
229
- },
230
- },
231
- ];
232
- }
233
-
234
- export const anthropicMessagesFormat: WireFormat = {
235
- id: 'anthropic.messages',
236
- open(): WireFormatReader {
237
- // Per-stream state, created HERE so two open() calls share nothing.
238
- const blocks = new Map<number, BlockState>();
239
- const toolIndexById = new Map<string, number>();
240
- return {
241
- push(frame: unknown): ModelStreamChunk[] {
242
- if (!isRecord(frame)) return [];
243
- switch (str(frame.type)) {
244
- case 'message_start': {
245
- const message = frame.message;
246
- if (!isRecord(message)) return [];
247
- // `message.model` sits right beside the `message.usage` this handler
248
- // already destructures. Anthropic states it once, here, and nowhere
249
- // else in the stream.
250
- const out: ModelStreamChunk = {};
251
- const usage = usageOf(message.usage);
252
- if (usage) out.usage = usage;
253
- const model = str(message.model);
254
- if (model) out.model = model;
255
- return Object.keys(out).length > 0 ? [out] : [];
256
- }
257
- case 'content_block_start':
258
- return blockStart(frame, blocks, toolIndexById);
259
- case 'content_block_delta':
260
- return blockDelta(frame, blocks);
261
- case 'content_block_stop':
262
- return blockStop(frame, blocks);
263
- case 'message_delta':
264
- return messageDelta(frame);
265
- case 'error':
266
- return errorFrame(frame);
267
- default:
268
- // ping, message_stop, and anything Anthropic adds later.
269
- return [];
270
- }
271
- },
272
- };
273
- },
274
- };