@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
@@ -1,442 +0,0 @@
1
- import type { Integration } from '../types';
2
-
3
- /**
4
- * Anthropic Messages, and NOT a copy of the OpenRouter route.
5
- *
6
- * Four constraints established by live testing and recorded in
7
- * docs/superpowers/HANDOFF-model-driven-components.md §4. Every one of them is a
8
- * 400 (or worse, a silent 200) if you port an OpenAI-shaped body over verbatim:
9
- *
10
- * 1. `system` is a TOP-LEVEL field. A `{ role: 'system' }` entry in `messages`
11
- * is rejected.
12
- * 2. `tool_choice` is an OBJECT, never the bare string OpenAI accepts.
13
- * 3. `max_tokens` is REQUIRED. OpenAI defaults it; this API does not.
14
- * 4. Content blocks stream strictly sequentially and are indexed by CONTENT
15
- * BLOCK, so a thinking block pushes a tool call from block 0 to block 1.
16
- * OpenAI's `tool_calls[].index` counts TOOL CALLS, so the two indices are
17
- * not the same number and passing one through as the other is the original
18
- * Critical.
19
- *
20
- * The thinking constraint in that document has since MOVED, and the route says
21
- * so rather than repeating it: `budget_tokens` is not a tunable with a floor of
22
- * 1024 any more, it is REMOVED from the current models and returns a 400. See
23
- * the block comment beside `thinking` in the emitted body.
24
- */
25
- const anthropic: Integration = {
26
- id: 'anthropic',
27
- title: 'Anthropic',
28
- category: 'provider',
29
- language: 'ts',
30
- // 'native': the Messages API streams its OWN SSE dialect (message_start,
31
- // content_block_delta, message_stop), not OpenAI's. The route re-frames it —
32
- // see `reframeToOpenAISse` and the note in streamMapping for why.
33
- streamFormat: 'native',
34
- envVars: ['ANTHROPIC_API_KEY'],
35
- // No per-framework templates: the handler below is web-standard, so the
36
- // scaffolder wraps it in whatever the target framework routes with.
37
- routeTemplates: {},
38
- webRoute: `/** Where an image or document's bytes come from. Anthropic takes both forms
39
- * for both block types, which is why this route has no gap where the
40
- * OpenAI-shaped one does: a chat-completions \`file\` part is base64-only. */
41
- type AnthropicSource =
42
- | { type: 'base64'; media_type: string; data: string }
43
- | { type: 'url'; url: string };
44
-
45
- /** One Anthropic content block. Open union on the wire; these are the ones the
46
- * scaffold's own thread can produce. */
47
- type AnthropicBlock =
48
- | { type: 'text'; text: string }
49
- | { type: 'image'; source: AnthropicSource }
50
- | { type: 'document'; source: AnthropicSource }
51
- | { type: 'tool_use'; id: string; name: string; input: unknown }
52
- | { type: 'tool_result'; tool_use_id: string; content: string };
53
-
54
- /** Anthropic messages carry ONLY 'user' and 'assistant'. There is no 'system'
55
- * role here and no 'tool' role: a system prompt is a top-level field, and a
56
- * tool RESULT rides on a user message. Both are handled in toAnthropicBody. */
57
- type AnthropicMessage = { role: 'user' | 'assistant'; content: AnthropicBlock[] };
58
-
59
- /** Anthropic calls the JSON Schema \`input_schema\`; OpenAI calls it
60
- * \`parameters\`, and nests everything under \`function\`. Handing the OpenAI
61
- * shape over unconverted is a 400. */
62
- type AnthropicTool = { name: string; description?: string; input_schema: unknown };
63
-
64
- /** The OpenAI tool shape the front end sends, narrowed from \`unknown[]\`. */
65
- type OpenAIFunctionTool = {
66
- function?: { name?: string; description?: string; parameters?: unknown };
67
- };
68
-
69
- /** One decoded Messages-API SSE frame. Every field is optional because this is
70
- * a provider-owned union and an unrecognised frame must be skipped, not throw. */
71
- type AnthropicEvent = {
72
- type?: string;
73
- index?: number;
74
- content_block?: { type?: string; id?: string; name?: string };
75
- delta?: {
76
- type?: string;
77
- text?: string;
78
- thinking?: string;
79
- partial_json?: string;
80
- stop_reason?: string;
81
- };
82
- error?: { message?: string };
83
- };
84
-
85
- /** Anthropic stop reasons -> OpenAI finish_reason. 'refusal' has no OpenAI
86
- * equivalent and is reported in band instead (see the message_delta case). */
87
- const FINISH_REASONS: Record<string, string> = {
88
- end_turn: 'stop',
89
- stop_sequence: 'stop',
90
- max_tokens: 'length',
91
- tool_use: 'tool_calls',
92
- };
93
-
94
- function toAnthropicTools(tools: unknown[] | undefined): AnthropicTool[] | undefined {
95
- if (!tools || tools.length === 0) return undefined;
96
- return tools.map((raw) => {
97
- const fn = (raw as OpenAIFunctionTool).function ?? {};
98
- return {
99
- name: fn.name ?? '',
100
- description: fn.description,
101
- input_schema: fn.parameters ?? { type: 'object', properties: {} },
102
- };
103
- });
104
- }
105
-
106
- /**
107
- * The OpenAI-shaped thread the front end posts -> an Anthropic Messages body.
108
- *
109
- * \`system\` comes back SEPARATELY because it is a top-level request field here.
110
- * A \`{ role: 'system' }\` entry left in \`messages\` is a 400, and it is the most
111
- * common thing to get wrong when porting a route across.
112
- */
113
- function toAnthropicBody(messages: ChatRequestBody['messages']): {
114
- system?: string;
115
- messages: AnthropicMessage[];
116
- } {
117
- let system: string | undefined;
118
- const out: AnthropicMessage[] = [];
119
-
120
- // Append to the trailing user message when there is one. A tool result and the
121
- // user's next turn are one turn on this wire, and several Anthropic-compatible
122
- // proxies enforce strict alternation.
123
- const pushUser = (content: AnthropicBlock[]): void => {
124
- const last = out[out.length - 1];
125
- if (last?.role === 'user') last.content.push(...content);
126
- else out.push({ role: 'user', content });
127
- };
128
-
129
- for (const message of messages) {
130
- switch (message.role) {
131
- case 'system': {
132
- // TOP-LEVEL, not a message. Several system turns concatenate.
133
- const text = wireText(message.content);
134
- system = system === undefined ? text : \`\${system}\\n\\n\${text}\`;
135
- break;
136
- }
137
- case 'user': {
138
- // Attachments ride HERE, as image and document blocks beside the text,
139
- // in the order the thread authored them.
140
- const blocks = wireParts(message.content).map((part): AnthropicBlock => {
141
- if (part.kind === 'text') return { type: 'text', text: part.text };
142
- const source: AnthropicSource =
143
- part.source.type === 'data'
144
- ? { type: 'base64', media_type: part.mediaType, data: part.source.data }
145
- : { type: 'url', url: part.source.url };
146
- // \`image/png\` and the bare top-level \`image\` both mean an image; a URL
147
- // source reports only the segment because the wire carries no more.
148
- return part.mediaType.startsWith('image') ? { type: 'image', source } : { type: 'document', source };
149
- });
150
- if (blocks.length > 0) pushUser(blocks);
151
- break;
152
- }
153
- case 'tool': {
154
- // A tool RESULT is a block on a USER message here, not a role of its own.
155
- pushUser([
156
- {
157
- type: 'tool_result',
158
- tool_use_id: message.tool_call_id ?? '',
159
- content: wireText(message.content),
160
- },
161
- ]);
162
- break;
163
- }
164
- case 'assistant': {
165
- const content: AnthropicBlock[] = [];
166
- const text = wireText(message.content);
167
- if (text) content.push({ type: 'text', text });
168
- for (const call of message.tool_calls ?? []) {
169
- content.push({
170
- type: 'tool_use',
171
- id: call.id,
172
- name: call.function.name,
173
- // \`input\` is a parsed OBJECT here. OpenAI carries the same value as a
174
- // JSON STRING in \`function.arguments\`; forwarding the string is a 400.
175
- input: call.function.arguments ? JSON.parse(call.function.arguments) : {},
176
- });
177
- }
178
- // An assistant turn with nothing in it is not a legal message.
179
- if (content.length > 0) out.push({ role: 'assistant', content });
180
- break;
181
- }
182
- }
183
- }
184
-
185
- return { system, messages: out };
186
- }
187
-
188
- /**
189
- * Anthropic Messages SSE -> OpenAI-format SSE.
190
- *
191
- * WHY THIS EXISTS AT ALL: the front end reads with readOpenAIStream. Handing it
192
- * Anthropic frames does not throw — it parses to NOTHING, so the turn ends empty
193
- * with no error anywhere, which is the worst failure this kit has. Re-framing
194
- * here keeps the browser half of an anthropic scaffold identical to every other
195
- * integration's, the same way the cloudflare Worker route re-frames env.AI.
196
- *
197
- * The alternative, if you would rather keep the native frames: return
198
- * \`upstream.body\` untouched and read it with readAnthropicStream from
199
- * '@kitn.ai/ui/wire'. That is also the ONLY way to round-trip thinking blocks
200
- * verbatim (toAnthropicMessages echoes \`part.raw\` — a rebuilt block is a hard
201
- * 400), so it is the path to take the moment you enable thinking below.
202
- */
203
- function reframeToOpenAISse(body: ReadableStream<Uint8Array>): ReadableStream<Uint8Array> {
204
- const encoder = new TextEncoder();
205
- const decoder = new TextDecoder();
206
- // THE INDEX TRAP. Anthropic's \`index\` counts CONTENT BLOCKS; OpenAI's
207
- // \`tool_calls[].index\` counts TOOL CALLS. A thinking or text block ahead of a
208
- // tool call shifts the block index, but the call is still tool call 0 — so the
209
- // block index is translated through this map rather than passed through.
210
- const toolIndexOfBlock = new Map<number, number>();
211
- let toolCount = 0;
212
-
213
- return new ReadableStream<Uint8Array>({
214
- async start(controller) {
215
- const send = (chunk: unknown): void => {
216
- controller.enqueue(encoder.encode(\`data: \${JSON.stringify(chunk)}\\n\\n\`));
217
- };
218
- const reader = body.getReader();
219
- let buffer = '';
220
- try {
221
- for (;;) {
222
- const { value, done } = await reader.read();
223
- if (done) break;
224
- buffer += decoder.decode(value, { stream: true });
225
- // Split on \\n rather than readline: readline also breaks on the
226
- // Unicode separators U+2028/U+2029, which appear inside model output.
227
- const lines = buffer.split('\\n');
228
- buffer = lines.pop() ?? ''; // hold the partial line for the next chunk
229
- for (const line of lines) {
230
- const trimmed = line.trim();
231
- // \`event:\` lines are redundant — every frame's data carries \`type\`.
232
- if (!trimmed.startsWith('data:')) continue;
233
- let event: AnthropicEvent;
234
- try {
235
- event = JSON.parse(trimmed.slice(5)) as AnthropicEvent;
236
- } catch {
237
- continue; // skip a malformed frame rather than kill the turn
238
- }
239
-
240
- if (event.type === 'content_block_start') {
241
- const block = event.content_block;
242
- if (block?.type !== 'tool_use') continue;
243
- const index = toolCount;
244
- toolCount += 1;
245
- toolIndexOfBlock.set(event.index ?? 0, index);
246
- send({
247
- choices: [{
248
- delta: {
249
- tool_calls: [{
250
- index,
251
- id: block.id,
252
- type: 'function',
253
- function: { name: block.name, arguments: '' },
254
- }],
255
- },
256
- }],
257
- });
258
- continue;
259
- }
260
-
261
- if (event.type === 'content_block_delta') {
262
- const delta = event.delta;
263
- if (delta?.type === 'text_delta') {
264
- send({ choices: [{ delta: { content: delta.text } }] });
265
- } else if (delta?.type === 'thinking_delta') {
266
- send({ choices: [{ delta: { reasoning: delta.thinking } }] });
267
- } else if (delta?.type === 'input_json_delta') {
268
- const index = toolIndexOfBlock.get(event.index ?? 0);
269
- // No entry means these fragments belong to a block that is not a
270
- // tool_use. Dropping them beats inventing a tool call.
271
- if (index !== undefined) {
272
- send({
273
- choices: [{
274
- delta: {
275
- tool_calls: [{ index, function: { arguments: delta.partial_json } }],
276
- },
277
- }],
278
- });
279
- }
280
- }
281
- continue;
282
- }
283
-
284
- if (event.type === 'message_delta') {
285
- const reason = event.delta?.stop_reason;
286
- if (!reason) continue;
287
- if (reason === 'refusal') {
288
- // A safety classifier declined. HTTP 200, no content, so without
289
- // this the turn just ends blank. Report it IN BAND:
290
- // readOpenAIStream lands it on turn.error.
291
- send({ error: { message: 'The model declined this request (stop_reason: refusal).' } });
292
- continue;
293
- }
294
- send({ choices: [{ delta: {}, finish_reason: FINISH_REASONS[reason] ?? 'stop' }] });
295
- continue;
296
- }
297
-
298
- if (event.type === 'error') {
299
- send({ error: { message: event.error?.message ?? 'Anthropic returned an error frame.' } });
300
- }
301
- }
302
- }
303
- } catch (err) {
304
- // The headers went out with the first byte, so the status is spent:
305
- // report IN BAND. readOpenAIStream lands this on turn.error and keeps
306
- // whatever already streamed.
307
- const message = err instanceof Error ? err.message : 'Anthropic stream failed';
308
- send({ error: { message } });
309
- }
310
- // Always: an unclosed stream leaves the browser waiting forever.
311
- controller.enqueue(encoder.encode('data: [DONE]\\n\\n'));
312
- controller.close();
313
- },
314
- });
315
- }
316
-
317
- async function chatHandler(request: Request): Promise<Response> {
318
- // Both model and tools come from the browser. \`tools\` arrives in OpenAI
319
- // function form and is converted to this API's shape below.
320
- let chatBody: ChatRequestBody;
321
- try {
322
- chatBody = await readChatRequest(request);
323
- } catch (error) {
324
- return toChatErrorResponse(error);
325
- }
326
- const { model, messages, tools } = chatBody;
327
- const { system, messages: anthropicMessages } = toAnthropicBody(messages);
328
- const anthropicTools = toAnthropicTools(tools);
329
-
330
- const upstream = await fetch('https://api.anthropic.com/v1/messages', {
331
- method: 'POST',
332
- headers: {
333
- 'x-api-key': \`\${process.env.ANTHROPIC_API_KEY}\`,
334
- 'anthropic-version': '2023-06-01',
335
- 'Content-Type': 'application/json',
336
- },
337
- body: JSON.stringify({
338
- // Model ids here are Anthropic's own — 'claude-opus-5', 'claude-sonnet-5',
339
- // 'claude-haiku-4-5'. An OpenAI id or an OpenRouter 'vendor/model' slug is
340
- // a 404. The front end's \`model\` const is the one to edit; it is seeded
341
- // from CLIENT_MODEL_IDS.anthropic.
342
- model,
343
- // REQUIRED. OpenAI defaults it; this API 400s without it. It caps thinking
344
- // AND response text together, so raise it if you turn thinking on.
345
- max_tokens: 4096,
346
- // TOP-LEVEL, never a { role: 'system' } entry in \`messages\`.
347
- ...(system ? { system } : {}),
348
- messages: anthropicMessages,
349
- ...(anthropicTools ? { tools: anthropicTools } : {}),
350
- // tool_choice, if you set one, is an OBJECT: { type: 'auto' } |
351
- // { type: 'any' } | { type: 'tool', name: 'search' }. OpenAI's bare string
352
- // ('auto') is a 400 here.
353
- // tool_choice: { type: 'auto' },
354
- //
355
- // THINKING. The rules changed with the model generation, so check yours:
356
- // · claude-opus-5 / sonnet-5 / opus-4.8 / opus-4.7 / fable-5 —
357
- // \`budget_tokens\` is REMOVED and returns a 400. Use
358
- // \`thinking: { type: 'adaptive', display: 'summarized' }\` plus
359
- // \`output_config: { effort: 'low' | 'medium' | 'high' | 'xhigh' | 'max' }\`.
360
- // On claude-opus-5 thinking is already ON by default; what is NOT on is
361
- // \`display\`, which defaults to 'omitted' and streams thinking blocks
362
- // with EMPTY text. That reads exactly like "this model has no reasoning
363
- // mode" — a silent 200 with nothing in the panel — and it is the same
364
- // shape of mistake as the budget bug below. Set display explicitly.
365
- // · older models (claude-haiku-4-5, claude-sonnet-4-5) — still
366
- // \`thinking: { type: 'enabled', budget_tokens: N }\`, with N >= 1024 and
367
- // max_tokens STRICTLY GREATER than N. Deriving N as a fraction of a
368
- // small max_tokens returns HTTP 200 with no thinking and no error.
369
- // Either way, once thinking is on a tool loop must echo every thinking
370
- // block back VERBATIM, which this route cannot do: it re-frames to OpenAI
371
- // SSE and the thread is re-encoded with toOpenAIMessages each round. Switch
372
- // to readAnthropicStream + toAnthropicMessages (both from '@kitn.ai/ui/wire')
373
- // and pass upstream.body through untouched before you enable it.
374
- stream: true,
375
- }),
376
- });
377
-
378
- // FORWARD THE STATUS. Returning 200 here is how a missing key turns into
379
- // silence: the 401 body is JSON, it goes out labelled text/event-stream, the
380
- // SSE reader finds no frame, and the turn resolves empty with nothing logged
381
- // and no bubble. With the status intact readOpenAIStream throws a WireError
382
- // carrying Anthropic's own message — which for this API is usually the exact
383
- // field you got wrong.
384
- if (!upstream.ok) {
385
- return new Response(await upstream.text(), {
386
- status: upstream.status,
387
- headers: {
388
- 'Content-Type': upstream.headers.get('content-type') ?? 'application/json',
389
- },
390
- });
391
- }
392
- if (!upstream.body) {
393
- return new Response(JSON.stringify({ error: { message: 'Anthropic returned no body to stream.' } }), {
394
- status: 502,
395
- headers: { 'Content-Type': 'application/json' },
396
- });
397
- }
398
-
399
- return new Response(reframeToOpenAISse(upstream.body), {
400
- status: 200,
401
- headers: {
402
- 'Content-Type': 'text/event-stream; charset=utf-8',
403
- // no-transform stops a proxy buffering the stream into one blob.
404
- 'Cache-Control': 'no-cache, no-transform',
405
- Connection: 'keep-alive',
406
- },
407
- });
408
- }`,
409
- streamMapping:
410
- "The Anthropic Messages API streams its OWN SSE dialect: message_start, content_block_start / content_block_delta / content_block_stop, message_delta, message_stop. It is NOT OpenAI-format, and feeding it to readOpenAIStream does not throw — it parses to nothing and the turn ends silently empty. The route therefore re-frames each frame to OpenAI form before it reaches the browser: text_delta -> delta.content, thinking_delta -> delta.reasoning, a tool_use content_block_start plus its input_json_delta fragments -> delta.tool_calls, message_delta.stop_reason -> finish_reason, an error frame -> an in-band {error:{message}}. readOpenAIStream from @kitn.ai/ui/wire then parses it exactly as it does every other integration. Anthropic indexes by CONTENT BLOCK while OpenAI's tool_calls[].index counts TOOL CALLS, so the route keeps a block-index -> tool-index map: a thinking block ahead of a tool call shifts the block index but not the tool index. Usage is the one thing not re-framed (input_tokens arrives on message_start and output_tokens on message_delta, so joining them needs state) — map both onto a {usage:{prompt_tokens,completion_tokens}} frame if you want token counts. To keep the native frames instead, return upstream.body untouched and read it with readAnthropicStream; that is also the only way to round-trip thinking blocks verbatim.",
411
- runNote:
412
- "Set ANTHROPIC_API_KEY (console.anthropic.com). The route pins claude-opus-5; claude-sonnet-5 and claude-haiku-4-5 are the cheaper swaps. Four things this API does NOT share with the OpenAI one, all of them 400s: system is a top-level field and not a message, max_tokens is required, tool_choice is an object ({ type: 'auto' }) and not a string, and tool schemas use input_schema rather than function.parameters. Extended thinking: on claude-opus-5 and the other current models budget_tokens is REMOVED (400) — use thinking: { type: 'adaptive', display: 'summarized' } with output_config.effort, and note that display defaults to 'omitted', which streams empty thinking blocks that look exactly like no reasoning at all. On older models (claude-haiku-4-5, claude-sonnet-4-5) budget_tokens still applies and must be >= 1024 with max_tokens strictly greater.",
413
- docsSlug: 'integrations/connect-any-model',
414
- // 'model' was withheld until scaffold.ts could seed it correctly: the shared
415
- // fallback is 'openai/gpt-4o-mini', which api.anthropic.com rejects outright,
416
- // and shipping an editable const with a broken default is the dead-const defect
417
- // forwardsFromClient exists to prevent. CLIENT_MODEL_IDS now carries a real
418
- // Anthropic id, so the const is live and the route reads it.
419
- forwardsFromClient: ['model', 'tools'],
420
- // 'openai', NOT 'anthropic' — and this is the counter-intuitive one, so read
421
- // `toAnthropicTools` above before changing it. This route CONVERTS the array
422
- // server-side: it reads `raw.function.name` and `raw.function.parameters` off
423
- // each entry, i.e. the OpenAI envelope, and rebuilds it as
424
- // `{ name, description, input_schema }`. Sending Anthropic's own shape from the
425
- // client would leave `.function` undefined, so every tool would go upstream
426
- // with a blank name and an empty schema. The envelope belongs to the ROUTE's
427
- // request contract, not to the provider it happens to POST to — which is
428
- // exactly why this is declared here rather than derived from `streamFormat`.
429
- clientToolFormat: 'openai',
430
- // Nothing to install. The route hand-rolls the Messages API over global
431
- // `fetch` and its own re-framer, so it imports no module — not even
432
- // @anthropic-ai/sdk.
433
- deps: { npm: [], pip: [] },
434
- // The route reads ANTHROPIC_API_KEY into an `x-api-key` header. Note the
435
- // header is NOT `Authorization`, which is why the schema's detector matches
436
- // both spellings.
437
- keyExposure: 'needs-proxy',
438
- // A remote HTTPS endpoint and a key. Nothing to install, nothing to start.
439
- outOfBand: 'none',
440
- };
441
-
442
- export default anthropic;
@@ -1,218 +0,0 @@
1
- import type { Integration } from '../types';
2
-
3
- const cloudflare: Integration = {
4
- id: 'cloudflare',
5
- title: 'Cloudflare AI',
6
- category: 'provider',
7
- language: 'ts',
8
- streamFormat: 'openai-sse',
9
- envVars: ['CF_ACCOUNT_ID', 'CF_API_TOKEN'],
10
- routeTemplates: {
11
- // Kept as a framework-specific template because it cannot be expressed as a
12
- // portable handler: `env.AI` is a Worker binding, and only a Worker has one.
13
- // Everything else uses `webRoute` below.
14
- worker: `// Worker handler: env.AI is bound in wrangler.toml
15
- // env.AI.run emits Cloudflare-native SSE (data: {"response":"<token>"}).
16
- // The TransformStream below re-frames each chunk to OpenAI-format SSE so
17
- // readOpenAIStream reads it without any client-side changes.
18
- import type { OpenAIWireMessage } from '@kitn.ai/ui/wire';
19
-
20
- /**
21
- * What the front end POSTs. \`req.json()\` is \`unknown\` (it is whatever the
22
- * client sent), so the body is narrowed once here instead of at every use —
23
- * without it this Worker does not compile. Widen it as you add fields.
24
- */
25
- type ChatRequestBody = { messages: OpenAIWireMessage[] };
26
-
27
- class ChatRequestError extends Error {
28
- constructor(readonly status: number, message: string) { super(message); }
29
- }
30
-
31
- /** Narrow the JSON body once, at the edge. A bare GET, a malformed body, or a
32
- * missing messages array is a ChatRequestError with a status — NEVER an
33
- * unhandled SyntaxError (findings F-10). */
34
- async function readChatRequest(request: Request): Promise<ChatRequestBody> {
35
- if (request.method !== 'POST') {
36
- throw new ChatRequestError(405, \`Method \${request.method} not allowed — POST /api/chat.\`);
37
- }
38
- let parsed: unknown;
39
- try { parsed = await request.json(); } catch {
40
- throw new ChatRequestError(400, 'Request body is not valid JSON.');
41
- }
42
- const body = parsed as ChatRequestBody;
43
- if (!Array.isArray(body?.messages)) {
44
- throw new ChatRequestError(400, 'Request body must carry a messages array.');
45
- }
46
- return body;
47
- }
48
-
49
- /** Map a guard rejection to the Response its status demands; rethrow anything
50
- * else — an unexpected error should be loud, not laundered into a 400. */
51
- function toChatErrorResponse(error: unknown): Response {
52
- if (error instanceof ChatRequestError) return Response.json({ error: error.message }, { status: error.status });
53
- throw error;
54
- }
55
-
56
- export default {
57
- async fetch(req: Request, env: Env): Promise<Response> {
58
- let chatBody: ChatRequestBody;
59
- try {
60
- chatBody = await readChatRequest(req);
61
- } catch (error) {
62
- return toChatErrorResponse(error);
63
- }
64
- const { messages } = chatBody;
65
-
66
- let nativeStream: ReadableStream<Uint8Array>;
67
- try {
68
- // env.AI.run is typed to return the non-streaming shape; \`stream: true\`
69
- // makes it a ReadableStream at runtime, which the types cannot express, so
70
- // the hop through \`unknown\` is required rather than cosmetic.
71
- nativeStream = (await env.AI.run('@cf/meta/llama-3.1-8b-instruct', {
72
- messages,
73
- stream: true,
74
- })) as unknown as ReadableStream<Uint8Array>;
75
- } catch (err) {
76
- // A REAL status, before a byte is streamed. Returning 200 here would send
77
- // a JSON error labelled text/event-stream: the reader finds no frame and
78
- // the turn resolves empty, with nothing logged and no bubble.
79
- return new Response(
80
- JSON.stringify({ error: { message: err instanceof Error ? err.message : 'Workers AI call failed' } }),
81
- { status: 502, headers: { 'Content-Type': 'application/json' } },
82
- );
83
- }
84
-
85
- // Re-frame Cloudflare-native SSE → OpenAI-format SSE
86
- const { readable, writable } = new TransformStream();
87
- const writer = writable.getWriter();
88
- const encoder = new TextEncoder();
89
- const decoder = new TextDecoder();
90
-
91
- (async () => {
92
- const reader = nativeStream.getReader();
93
- let buffer = '';
94
- try {
95
- while (true) {
96
- const { value, done } = await reader.read();
97
- if (done) break;
98
- buffer += decoder.decode(value, { stream: true });
99
- const lines = buffer.split('\\n');
100
- buffer = lines.pop() ?? '';
101
- for (const line of lines) {
102
- const s = line.trim();
103
- if (!s.startsWith('data:')) continue;
104
- const payload = s.slice(5).trim();
105
- if (payload === '[DONE]') continue;
106
- try {
107
- const { response } = JSON.parse(payload) as { response?: string };
108
- if (response == null) continue;
109
- const openaiChunk = JSON.stringify({ choices: [{ delta: { content: response } }] });
110
- await writer.write(encoder.encode(\`data: \${openaiChunk}\\n\\n\`));
111
- } catch { /* skip malformed lines */ }
112
- }
113
- }
114
- } catch (err) {
115
- // The response is already streaming, so the status is spent: report the
116
- // failure IN BAND. readOpenAIStream lands this on turn.error and keeps
117
- // whatever already streamed.
118
- const message = err instanceof Error ? err.message : 'Workers AI stream failed';
119
- await writer.write(encoder.encode(\`data: \${JSON.stringify({ error: { message } })}\\n\\n\`));
120
- } finally {
121
- // Always: an unclosed writer leaves the browser waiting forever.
122
- await writer.write(encoder.encode('data: [DONE]\\n\\n'));
123
- await writer.close();
124
- }
125
- })();
126
-
127
- return new Response(readable, {
128
- status: 200,
129
- headers: {
130
- 'Content-Type': 'text/event-stream; charset=utf-8',
131
- 'Cache-Control': 'no-cache, no-transform',
132
- },
133
- });
134
- },
135
- };`,
136
- },
137
- webRoute: `async function chatHandler(request: Request): Promise<Response> {
138
- // Proxy Workers AI over its OpenAI-compatible HTTP endpoint, token server-side.
139
- let chatBody: ChatRequestBody;
140
- try {
141
- chatBody = await readChatRequest(request);
142
- } catch (error) {
143
- return toChatErrorResponse(error);
144
- }
145
- const { messages } = chatBody;
146
-
147
- const upstream = await fetch(
148
- \`https://api.cloudflare.com/client/v4/accounts/\${process.env.CF_ACCOUNT_ID}/ai/v1/chat/completions\`,
149
- {
150
- method: 'POST',
151
- headers: {
152
- Authorization: \`Bearer \${process.env.CF_API_TOKEN}\`,
153
- 'Content-Type': 'application/json',
154
- },
155
- body: JSON.stringify({
156
- model: '@cf/meta/llama-3.1-8b-instruct',
157
- messages,
158
- stream: true,
159
- }),
160
- },
161
- );
162
-
163
- // FORWARD THE STATUS. A wrong CF_API_TOKEN is a 401/403: returning 200 sends
164
- // its JSON body out labelled text/event-stream, the SSE reader finds no frame,
165
- // and the turn resolves empty with nothing logged and no bubble.
166
- if (!upstream.ok) {
167
- return new Response(await upstream.text(), {
168
- status: upstream.status,
169
- headers: {
170
- 'Content-Type': upstream.headers.get('content-type') ?? 'application/json',
171
- },
172
- });
173
- }
174
- if (!upstream.body) {
175
- return new Response(JSON.stringify({ error: { message: 'Workers AI returned no body to stream.' } }), {
176
- status: 502,
177
- headers: { 'Content-Type': 'application/json' },
178
- });
179
- }
180
-
181
- // Workers AI returns OpenAI-format SSE. Pass it straight through.
182
- return new Response(upstream.body, {
183
- status: 200,
184
- headers: {
185
- 'Content-Type': 'text/event-stream; charset=utf-8',
186
- 'Cache-Control': 'no-cache, no-transform',
187
- Connection: 'keep-alive',
188
- },
189
- });
190
- }`,
191
- streamMapping:
192
- "Workers AI via the OpenAI-compatible HTTP endpoint returns OpenAI-format SSE. Pipe upstream.body straight to the browser; readOpenAIStream from @kitn.ai/ui/wire parses it, including tool calls and reasoning. The native env.AI binding streams Cloudflare's own format (data: {\"response\":\"...token...\"}); the worker route template re-frames these chunks to OpenAI-format SSE via a TransformStream before returning.",
193
- runNote: 'Set CF_ACCOUNT_ID and CF_API_TOKEN. Model ids are prefixed with @cf/, e.g. @cf/meta/llama-3.1-8b-instruct. For the AI binding (worker key), add an [ai] block with binding = "AI" in wrangler.toml.',
194
- docsSlug: 'integrations/cloudflare-ai',
195
- // Nothing. Both routes pin '@cf/meta/llama-3.1-8b-instruct' (the generic
196
- // 'openai/gpt-4o-mini' a scaffold used to default to is not even a valid
197
- // Workers AI id), and the worker route's re-framing reader forwards only the
198
- // text `response` field, so a tool call could not reach the browser anyway.
199
- forwardsFromClient: [],
200
- // Nothing. The webRoute is global `fetch`, and the worker template's only
201
- // import is a type from '@kitn.ai/ui/wire' — the kit itself, which every
202
- // scaffold already depends on. Its `Env` is an ambient wrangler type, not an
203
- // import, and the Worker skeleton owns wrangler/@cloudflare/workers-types.
204
- deps: { npm: [], pip: [] },
205
- // The webRoute reads CF_API_TOKEN into an `Authorization: Bearer` header.
206
- //
207
- // The two routes differ and the stricter one governs: the worker template uses
208
- // the `env.AI` binding and holds no token in code at all, but a binding is a
209
- // Worker capability, not a browser one. Either way a server is required.
210
- keyExposure: 'needs-proxy',
211
- // Workers AI is a remote HTTPS endpoint (the REST route) or a platform binding
212
- // (the worker route) — either way there is nothing on the developer's machine
213
- // to install or run. `wrangler` is a devDependency of a Worker app, not an
214
- // out-of-band prerequisite of this integration.
215
- outOfBand: 'none',
216
- };
217
-
218
- export default cloudflare;