sygnal 5.4.0 → 6.1.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 (360) hide show
  1. package/CHANGELOG.md +716 -0
  2. package/README.md +24 -9
  3. package/dist/ai.cjs.js +115 -0
  4. package/dist/ai.cjs.js.map +1 -0
  5. package/dist/ai.esm.js +2 -0
  6. package/dist/ai.esm.js.map +1 -0
  7. package/dist/astro/client.cjs.js +16 -9
  8. package/dist/astro/client.cjs.js.map +1 -1
  9. package/dist/astro/client.mjs +16 -9
  10. package/dist/astro/client.mjs.map +1 -1
  11. package/dist/astro/index.cjs.js +958 -92
  12. package/dist/astro/index.cjs.js.map +1 -1
  13. package/dist/astro/index.mjs +958 -93
  14. package/dist/astro/index.mjs.map +1 -1
  15. package/dist/astro/server.cjs.js +3396 -165
  16. package/dist/astro/server.cjs.js.map +1 -1
  17. package/dist/astro/server.mjs +3396 -165
  18. package/dist/astro/server.mjs.map +1 -1
  19. package/dist/devtools.cjs.js +1840 -0
  20. package/dist/devtools.cjs.js.map +1 -0
  21. package/dist/devtools.esm.js +1827 -0
  22. package/dist/devtools.esm.js.map +1 -0
  23. package/dist/diagnostics.cjs.js +3190 -296
  24. package/dist/diagnostics.cjs.js.map +1 -1
  25. package/dist/diagnostics.esm.js +3190 -296
  26. package/dist/diagnostics.esm.js.map +1 -1
  27. package/dist/element.cjs.js +230 -0
  28. package/dist/element.cjs.js.map +1 -0
  29. package/dist/element.esm.js +228 -0
  30. package/dist/element.esm.js.map +1 -0
  31. package/dist/guide/accessibility.md +310 -0
  32. package/dist/guide/adapters.md +221 -0
  33. package/dist/guide/agent.md +588 -0
  34. package/dist/guide/ai-chat.md +693 -0
  35. package/dist/guide/ai-decisions.md +330 -0
  36. package/dist/guide/behaviors.md +312 -0
  37. package/dist/guide/browser-sources.md +203 -0
  38. package/dist/guide/drag-and-drop.md +342 -0
  39. package/dist/guide/element-commands.md +306 -0
  40. package/dist/guide/error-boundaries.md +169 -0
  41. package/dist/guide/forms-reference.md +409 -0
  42. package/dist/guide/forms.md +251 -0
  43. package/dist/guide/http.md +301 -0
  44. package/dist/guide/inputs.md +173 -0
  45. package/dist/guide/mcp-apps.md +272 -0
  46. package/dist/guide/persistence.md +245 -0
  47. package/dist/guide/recipes/ai-form-fill.md +163 -0
  48. package/dist/guide/recipes/ai-summarize.md +155 -0
  49. package/dist/guide/recipes/ai-support-inbox.md +190 -0
  50. package/dist/guide/recipes/carousel.md +155 -0
  51. package/dist/guide/recipes/charts.md +177 -0
  52. package/dist/guide/recipes/code-editor.md +135 -0
  53. package/dist/guide/recipes/data-grid.md +155 -0
  54. package/dist/guide/recipes/data-table.md +196 -0
  55. package/dist/guide/recipes/i18n.md +222 -0
  56. package/dist/guide/recipes/icons.md +115 -0
  57. package/dist/guide/recipes/overview.md +43 -0
  58. package/dist/guide/recipes/rich-text.md +154 -0
  59. package/dist/guide/resources.md +471 -0
  60. package/dist/guide/ssr.md +255 -0
  61. package/dist/guide/timers.md +178 -0
  62. package/dist/guide/ui/accordion.md +93 -0
  63. package/dist/guide/ui/combobox.md +144 -0
  64. package/dist/guide/ui/dialog.md +124 -0
  65. package/dist/guide/ui/disclosure.md +60 -0
  66. package/dist/guide/ui/menu.md +125 -0
  67. package/dist/guide/ui/overview.md +166 -0
  68. package/dist/guide/ui/popover.md +101 -0
  69. package/dist/guide/ui/select.md +114 -0
  70. package/dist/guide/ui/tabs.md +161 -0
  71. package/dist/guide/ui/toaster.md +152 -0
  72. package/dist/guide/ui/tooltip.md +103 -0
  73. package/dist/guide/undo.md +178 -0
  74. package/dist/guide/virtual-collections.md +183 -0
  75. package/dist/guide/web-components.md +179 -0
  76. package/dist/guide/webmcp.md +226 -0
  77. package/dist/guide/widgets.md +217 -0
  78. package/dist/index.cjs.js +19288 -7408
  79. package/dist/index.cjs.js.map +1 -1
  80. package/dist/index.d.ts +4466 -552
  81. package/dist/index.esm.js +19274 -7461
  82. package/dist/index.esm.js.map +1 -1
  83. package/dist/jsx-dev-runtime.cjs.js +26 -334
  84. package/dist/jsx-dev-runtime.cjs.js.map +1 -1
  85. package/dist/jsx-dev-runtime.esm.js +19 -327
  86. package/dist/jsx-dev-runtime.esm.js.map +1 -1
  87. package/dist/jsx-runtime.cjs.js +26 -334
  88. package/dist/jsx-runtime.cjs.js.map +1 -1
  89. package/dist/jsx-runtime.esm.js +19 -327
  90. package/dist/jsx-runtime.esm.js.map +1 -1
  91. package/dist/jsx.cjs.js +10 -314
  92. package/dist/jsx.cjs.js.map +1 -1
  93. package/dist/jsx.esm.js +8 -315
  94. package/dist/jsx.esm.js.map +1 -1
  95. package/dist/react.cjs.js +117 -0
  96. package/dist/react.cjs.js.map +1 -0
  97. package/dist/react.esm.js +115 -0
  98. package/dist/react.esm.js.map +1 -0
  99. package/dist/shims/globalthis.cjs +20 -0
  100. package/dist/sygnal.min.js +1 -1
  101. package/dist/sygnal.min.js.map +1 -1
  102. package/dist/ui-combobox.cjs.js +169 -0
  103. package/dist/ui-combobox.cjs.js.map +1 -0
  104. package/dist/ui-combobox.esm.js +148 -0
  105. package/dist/ui-combobox.esm.js.map +1 -0
  106. package/dist/ui-menu.cjs.js +87 -0
  107. package/dist/ui-menu.cjs.js.map +1 -0
  108. package/dist/ui-menu.esm.js +66 -0
  109. package/dist/ui-menu.esm.js.map +1 -0
  110. package/dist/ui-select.cjs.js +104 -0
  111. package/dist/ui-select.cjs.js.map +1 -0
  112. package/dist/ui-select.esm.js +83 -0
  113. package/dist/ui-select.esm.js.map +1 -0
  114. package/dist/ui.cjs.js +738 -0
  115. package/dist/ui.cjs.js.map +1 -0
  116. package/dist/ui.esm.js +727 -0
  117. package/dist/ui.esm.js.map +1 -0
  118. package/dist/vike/ClientOnly.cjs.js +4 -14
  119. package/dist/vike/ClientOnly.cjs.js.map +1 -1
  120. package/dist/vike/ClientOnly.mjs +4 -14
  121. package/dist/vike/ClientOnly.mjs.map +1 -1
  122. package/dist/vike/{+config.js → config/+config.js} +9 -1
  123. package/dist/vike/config/+config.js.map +1 -0
  124. package/dist/vike/config/package.json +6 -0
  125. package/dist/vike/onRenderClient.cjs.js +165 -95
  126. package/dist/vike/onRenderClient.cjs.js.map +1 -1
  127. package/dist/vike/onRenderClient.mjs +165 -95
  128. package/dist/vike/onRenderClient.mjs.map +1 -1
  129. package/dist/vike/onRenderHtml.cjs.js +59 -24
  130. package/dist/vike/onRenderHtml.cjs.js.map +1 -1
  131. package/dist/vike/onRenderHtml.mjs +60 -25
  132. package/dist/vike/onRenderHtml.mjs.map +1 -1
  133. package/dist/vite/plugin.cjs.js +919 -89
  134. package/dist/vite/plugin.cjs.js.map +1 -1
  135. package/dist/vite/plugin.mjs +919 -90
  136. package/dist/vite/plugin.mjs.map +1 -1
  137. package/dist/zag.cjs.js +408 -0
  138. package/dist/zag.cjs.js.map +1 -0
  139. package/dist/zag.esm.js +405 -0
  140. package/dist/zag.esm.js.map +1 -0
  141. package/llms.txt +172 -103
  142. package/package.json +112 -14
  143. package/src/ai.d.ts +1186 -0
  144. package/src/ai.ts +23 -0
  145. package/src/astro/client.ts +27 -16
  146. package/src/astro/index.d.ts +24 -0
  147. package/src/astro/index.ts +69 -3
  148. package/src/astro/server.ts +8 -2
  149. package/src/collection.ts +6 -93
  150. package/src/core/actions.ts +134 -0
  151. package/src/core/cell.ts +202 -0
  152. package/src/core/debug.ts +23 -0
  153. package/src/core/define.ts +242 -0
  154. package/src/core/hooks.ts +314 -0
  155. package/src/core/hosts/collection.ts +325 -0
  156. package/src/core/hosts/switchable.ts +146 -0
  157. package/src/core/instance.ts +592 -0
  158. package/src/core/markers/clientonly.ts +13 -0
  159. package/src/core/markers/lazy.ts +40 -0
  160. package/src/core/markers/portal.ts +96 -0
  161. package/src/core/markers/suspense.ts +41 -0
  162. package/src/core/markers/transition.ts +70 -0
  163. package/src/core/registry.ts +25 -0
  164. package/src/core/runtime.ts +573 -0
  165. package/src/core/statics.ts +104 -0
  166. package/src/core/teardown.ts +60 -0
  167. package/src/core/view.ts +37 -0
  168. package/src/cycle/dom/DocumentDOMSource.ts +13 -8
  169. package/src/cycle/dom/ElementFinder.ts +7 -9
  170. package/src/cycle/dom/EventDelegator.ts +174 -223
  171. package/src/cycle/dom/IsolateModule.ts +65 -35
  172. package/src/cycle/dom/MainDOMSource.ts +9 -2
  173. package/src/cycle/dom/PriorityQueue.ts +4 -2
  174. package/src/cycle/dom/SymbolTree.ts +18 -47
  175. package/src/cycle/dom/classNameModule.ts +9 -7
  176. package/src/cycle/dom/controlledInputModule.ts +23 -6
  177. package/src/cycle/dom/enrichEventStream.ts +25 -47
  178. package/src/cycle/dom/fragment.ts +7 -0
  179. package/src/cycle/dom/isolate.ts +30 -27
  180. package/src/cycle/dom/makeDOMDriver.ts +115 -56
  181. package/src/cycle/dom/mockDOMSource.ts +21 -1
  182. package/src/cycle/dom/modules.ts +16 -3
  183. package/src/cycle/dom/propsModule.ts +65 -0
  184. package/src/cycle/dom/selectModule.ts +22 -17
  185. package/src/cycle/dom/snabbdom.ts +1 -6
  186. package/src/cycle/dom/thunk.ts +3 -0
  187. package/src/cycle/dom/utils.ts +98 -0
  188. package/src/cycle/dom/viewTransition.ts +43 -0
  189. package/src/cycle/state/StateSource.ts +19 -3
  190. package/src/cycle/state/objIsEqual.ts +50 -0
  191. package/src/cycle/state/types.ts +0 -10
  192. package/src/defineComponent.ts +34 -0
  193. package/src/devtools.d.ts +170 -0
  194. package/src/devtools.ts +37 -0
  195. package/src/element.d.ts +59 -0
  196. package/src/element.ts +235 -0
  197. package/src/extra/ai/agent/index.ts +484 -0
  198. package/src/extra/ai/answers.ts +115 -0
  199. package/src/extra/ai/chat/behavior.ts +336 -0
  200. package/src/extra/ai/chat/driver.ts +271 -0
  201. package/src/extra/ai/chat/memoryTransport.ts +61 -0
  202. package/src/extra/ai/chat/output.ts +37 -0
  203. package/src/extra/ai/commandBar.ts +281 -0
  204. package/src/extra/ai/decide.ts +104 -0
  205. package/src/extra/ai/index.ts +42 -0
  206. package/src/extra/ai/link.ts +90 -0
  207. package/src/extra/ai/mcpApp.ts +314 -0
  208. package/src/extra/ai/messages.ts +51 -0
  209. package/src/extra/ai/schema/index.ts +166 -0
  210. package/src/extra/ai/schema/jsonSchema.ts +72 -0
  211. package/src/extra/ai/schema/strict.ts +152 -0
  212. package/src/extra/ai/transports/agui.ts +177 -0
  213. package/src/extra/ai/transports/anthropicMessages.ts +175 -0
  214. package/src/extra/ai/transports/chatCompletions.ts +105 -0
  215. package/src/extra/ai/transports/chromePrompt.ts +77 -0
  216. package/src/extra/ai/transports/encodeOpenResponses.ts +87 -0
  217. package/src/extra/ai/transports/fromAISDK.ts +126 -0
  218. package/src/extra/ai/transports/openResponses.ts +105 -0
  219. package/src/extra/ai/transports/shared.ts +125 -0
  220. package/src/extra/ai/transports/tools.ts +59 -0
  221. package/src/extra/ai/transports/uiMessageStream.ts +107 -0
  222. package/src/extra/ai/webmcp.ts +260 -0
  223. package/src/extra/backoff.ts +9 -0
  224. package/src/extra/behaviors.ts +169 -0
  225. package/src/extra/browserSignals.ts +23 -0
  226. package/src/extra/browserSources.ts +326 -0
  227. package/src/extra/command.ts +3 -3
  228. package/src/extra/controls.ts +47 -0
  229. package/src/extra/copyAsTest.ts +286 -0
  230. package/src/extra/devMcp.ts +386 -0
  231. package/src/extra/devtools.ts +101 -31
  232. package/src/extra/devtoolsActions.ts +379 -0
  233. package/src/extra/devtoolsHook.ts +9 -0
  234. package/src/extra/devtoolsNext.ts +83 -0
  235. package/src/extra/diagnostics/checks/actionLog.ts +108 -0
  236. package/src/extra/diagnostics/checks/behaviors.ts +49 -0
  237. package/src/extra/diagnostics/checks/browserSources.ts +96 -0
  238. package/src/extra/diagnostics/checks/chat.ts +71 -0
  239. package/src/extra/diagnostics/checks/collections.ts +1 -1
  240. package/src/extra/diagnostics/checks/controls.ts +148 -0
  241. package/src/extra/diagnostics/checks/dataset.ts +57 -0
  242. package/src/extra/diagnostics/checks/dom.ts +20 -11
  243. package/src/extra/diagnostics/checks/elementCommands.ts +175 -0
  244. package/src/extra/diagnostics/checks/events.ts +17 -2
  245. package/src/extra/diagnostics/checks/fetch.ts +135 -0
  246. package/src/extra/diagnostics/checks/forms.ts +171 -0
  247. package/src/extra/diagnostics/checks/index.ts +103 -4
  248. package/src/extra/diagnostics/checks/inspect.ts +140 -32
  249. package/src/extra/diagnostics/checks/next.ts +417 -0
  250. package/src/extra/diagnostics/checks/persist.ts +54 -0
  251. package/src/extra/diagnostics/checks/props.ts +10 -5
  252. package/src/extra/diagnostics/checks/public.d.ts +115 -7
  253. package/src/extra/diagnostics/checks/replies.ts +128 -0
  254. package/src/extra/diagnostics/checks/router.ts +100 -0
  255. package/src/extra/diagnostics/checks/rxjsHints.ts +1 -1
  256. package/src/extra/diagnostics/checks/shared.ts +86 -2
  257. package/src/extra/diagnostics/checks/shorthand.ts +115 -0
  258. package/src/extra/diagnostics/checks/sortable.ts +129 -0
  259. package/src/extra/diagnostics/checks/state.ts +100 -7
  260. package/src/extra/diagnostics/checks/statics.ts +48 -0
  261. package/src/extra/diagnostics/checks/strict.ts +10 -67
  262. package/src/extra/diagnostics/checks/timers.ts +80 -0
  263. package/src/extra/diagnostics/checks/viewTransitions.ts +47 -0
  264. package/src/extra/diagnostics/checks/virtual.ts +73 -0
  265. package/src/extra/diagnostics/checks/widgets.ts +109 -0
  266. package/src/extra/diagnostics/checks/wiring.ts +69 -10
  267. package/src/extra/diagnostics/codes.ts +335 -23
  268. package/src/extra/diagnostics/index.ts +34 -9
  269. package/src/extra/diagnostics/legacy.ts +17 -0
  270. package/src/extra/driverFactories.ts +83 -19
  271. package/src/extra/elementCommands.ts +53 -0
  272. package/src/extra/fetchDriver.ts +519 -0
  273. package/src/extra/focusWithin.ts +27 -0
  274. package/src/extra/form.ts +284 -0
  275. package/src/extra/formHelpers.ts +139 -0
  276. package/src/extra/formTool.ts +150 -0
  277. package/src/extra/head.ts +106 -0
  278. package/src/extra/hmr.ts +2 -3
  279. package/src/extra/owned.ts +24 -0
  280. package/src/extra/pager.ts +50 -0
  281. package/src/extra/persist.ts +133 -0
  282. package/src/extra/pwa.ts +1 -1
  283. package/src/extra/queryCache.ts +107 -0
  284. package/src/extra/reducers.ts +1 -1
  285. package/src/extra/reduxDevtools.ts +92 -0
  286. package/src/extra/replies.ts +54 -0
  287. package/src/extra/router.ts +323 -0
  288. package/src/extra/run.ts +90 -217
  289. package/src/extra/selection.ts +76 -0
  290. package/src/extra/socketDriver.ts +265 -0
  291. package/src/extra/sortable.ts +377 -0
  292. package/src/extra/ssr.ts +427 -160
  293. package/src/extra/standardSchema.ts +27 -0
  294. package/src/extra/testing.ts +2550 -175
  295. package/src/extra/timers.ts +95 -0
  296. package/src/extra/undo.ts +261 -0
  297. package/src/extra/viewTransitions.ts +38 -0
  298. package/src/extra/virtual.ts +513 -0
  299. package/src/extra/widget.ts +201 -0
  300. package/src/index.d.ts +2754 -127
  301. package/src/index.ts +45 -4
  302. package/src/jsx-runtime.ts +15 -1
  303. package/src/jsx.ts +2 -1
  304. package/src/lazy.ts +50 -7
  305. package/src/portal.ts +3 -1
  306. package/src/pragma/index.ts +256 -133
  307. package/src/react-peers.d.ts +5 -0
  308. package/src/react.d.ts +43 -0
  309. package/src/react.ts +110 -0
  310. package/src/shared.ts +57 -0
  311. package/src/slot.ts +1 -1
  312. package/src/suspense.ts +3 -1
  313. package/src/switchable.ts +6 -121
  314. package/src/transition.ts +3 -1
  315. package/src/ui/accordion.ts +64 -0
  316. package/src/ui/dialog.ts +129 -0
  317. package/src/ui/disclosure.ts +35 -0
  318. package/src/ui/popover.ts +45 -0
  319. package/src/ui/shared.ts +94 -0
  320. package/src/ui/tabs.ts +82 -0
  321. package/src/ui/toaster.ts +198 -0
  322. package/src/ui/tooltip.ts +70 -0
  323. package/src/ui/zag/combobox.ts +115 -0
  324. package/src/ui/zag/menu.ts +40 -0
  325. package/src/ui/zag/select.ts +54 -0
  326. package/src/ui/zag/shared.ts +43 -0
  327. package/src/ui-combobox.d.ts +20 -0
  328. package/src/ui-combobox.ts +6 -0
  329. package/src/ui-menu.d.ts +30 -0
  330. package/src/ui-menu.ts +6 -0
  331. package/src/ui-select.d.ts +17 -0
  332. package/src/ui-select.ts +6 -0
  333. package/src/ui-zag-types.d.ts +42 -0
  334. package/src/ui.d.ts +208 -0
  335. package/src/ui.ts +15 -0
  336. package/src/vike/+config.ts +9 -1
  337. package/src/vike/ClientOnly.ts +1 -1
  338. package/src/vike/onRenderClient.ts +144 -96
  339. package/src/vike/onRenderHtml.ts +69 -26
  340. package/src/vike/types.ts +7 -1
  341. package/src/vite/globalthis-shim.ts +18 -0
  342. package/src/vite/globalthis.ts +56 -0
  343. package/src/vite/mcp.ts +568 -0
  344. package/src/vite/plugin.d.ts +47 -3
  345. package/src/vite/plugin.ts +262 -25
  346. package/src/zag.d.ts +64 -0
  347. package/src/zag.ts +238 -0
  348. package/dist/vike/+config.cjs.js +0 -66
  349. package/dist/vike/+config.cjs.js.map +0 -1
  350. package/dist/vike/+config.js.map +0 -1
  351. package/src/component.ts +0 -2301
  352. package/src/cycle/isolate/index.ts +0 -196
  353. package/src/cycle/run/index.ts +0 -151
  354. package/src/cycle/run/internals.ts +0 -143
  355. package/src/cycle/state/Collection.ts +0 -184
  356. package/src/cycle/state/index.ts +0 -5
  357. package/src/cycle/state/pickCombine.ts +0 -201
  358. package/src/cycle/state/pickMerge.ts +0 -122
  359. package/src/cycle/state/withState.ts +0 -47
  360. package/src/pragma/fn.ts +0 -55
@@ -0,0 +1,152 @@
1
+ import {resolve, walk, isObj} from './index';
2
+
3
+ /*
4
+ * PLAN-6 L-2 (0-S4 layer 2, D266, D285): the opt-in strict schema layer of the OpenAI- and
5
+ * Anthropic-backed transports. Apps opt in with an import (`openResponses({ strict:
6
+ * strictSchemas })`), so the transports don't carry it otherwise (≈ 1.1 KB, D285); unstrict is
7
+ * also used by the chat driver's structured output (./chat/output.ts), where it is always safe.
8
+ *
9
+ * strictSchemas(schema, dialect) turns a portable tool / output schema (./index.ts normalize)
10
+ * into a provider's strict subset; both walk with A-1's map-aware `walk` (G-629):
11
+ * - 'openai' (developers.openai.com structured outputs, "Supported schemas", 2026-10):
12
+ * `additionalProperties: false` on every object, every property required, the optional ones
13
+ * made nullable (a type array + null in the enum, or `anyOf [..., { type: 'null' }]`);
14
+ * unsupported keywords and formats moved into the description as JSON (the model still reads
15
+ * them; validation still enforces them), a tuple's `prefixItems` turned into `items: { anyOf }`.
16
+ * - 'anthropic' (platform.claude.com structured outputs; the subset @anthropic-ai/sdk's
17
+ * transformJSONSchema keeps): `additionalProperties: false` on every object, `required` as is
18
+ * (optional keys stay optional), and only type, anyOf (oneOf), allOf, $ref/$defs, enum, const,
19
+ * description, title, properties, required, items, the supported formats and `minItems` 0/1
20
+ * kept; the rest (numeric and string bounds, pattern, other array constraints) moved into the
21
+ * description. A recursive schema or an untyped (any) value has no strict form.
22
+ * Both return `errors` when the schema has no strict form (a record: `additionalProperties` with a
23
+ * schema; a non-object root): the transport then sends that tool non-strict (SYG675, dev info),
24
+ * and `optional` / `unions` (the counts Anthropic limits per request, G-609).
25
+ *
26
+ * unstrict(value, schema) drops the nulls a strict schema forced the model to send for keys the
27
+ * original schema leaves optional and doesn't allow null for, so validation sees the original
28
+ * shape. The transports run it on every tool call they sent strict.
29
+ */
30
+
31
+ export type StrictDialect = 'openai' | 'anthropic';
32
+ export interface Strictified {schema: any; errors: string[]; optional: number; unions: number}
33
+ /** what a request has left of a provider's per-request strict limits (Anthropic, G-609) */
34
+ export interface StrictLimits {tools: number; optional: number; unions: number}
35
+
36
+ const DROP = ['allOf', 'not', 'if', 'then', 'else', 'dependentRequired', 'dependentSchemas', 'patternProperties',
37
+ 'unevaluatedProperties', 'propertyNames', 'minProperties', 'maxProperties', 'unevaluatedItems', 'contains',
38
+ 'minContains', 'maxContains', 'uniqueItems', 'default', 'examples', 'title'];
39
+ const FORMATS = ['date-time', 'time', 'date', 'duration', 'email', 'hostname', 'ipv4', 'ipv6', 'uuid'];
40
+ // Anthropic's subset (the keywords @anthropic-ai/sdk's transformJSONSchema keeps)
41
+ const KEEP = /^(type|anyOf|allOf|\$ref|\$defs|enum|const|description|title|properties|required|additionalProperties|items)$/;
42
+ const ANT_FORMATS = /^(date-time|time|date|duration|email|hostname|uri|ipv4|ipv6|uuid)$/;
43
+
44
+ /** one schema object into Anthropic's subset, in place (oneOf -> anyOf): the keywords it moved out */
45
+ export function anthropicKeywords(o: any): any {
46
+ const moved: any = {};
47
+ if (o.oneOf && !o.anyOf) { o.anyOf = o.oneOf; delete o.oneOf; }
48
+ for (const k in o) if (k == 'format' ? !ANT_FORMATS.test(o[k]) : k == 'minItems' ? o[k] > 1 : !KEEP.test(k)) { moved[k] = o[k]; delete o[k]; }
49
+ return moved;
50
+ }
51
+
52
+ // the moved keywords as JSON at the end of the description: the model still reads them, validation still enforces them
53
+ const describe = (o: any, moved: any) => {
54
+ if (Object.keys(moved).length) {
55
+ const hint = JSON.stringify(moved);
56
+ o.description = o.description ? o.description + ' ' + hint : hint;
57
+ }
58
+ return o;
59
+ };
60
+
61
+ /**
62
+ * G-635: the light, always-on pass anthropicMessages() runs on a non-strict `output` schema
63
+ * (`output_config.format` is always constrained decoding, and a keyword outside Anthropic's subset
64
+ * is a 400): Anthropic's keywords only (the rest moved into the description), and
65
+ * `additionalProperties: false` on every object that isn't a record. No limits, no errors.
66
+ */
67
+ export const anthropicCompatible = (schema: any): any => walk(schema, o => {
68
+ const moved = anthropicKeywords(o);
69
+ if ((o.type == 'object' || o.properties) && !isObj(o.additionalProperties)) o.additionalProperties = false;
70
+ return describe(o, moved);
71
+ });
72
+
73
+ const nullable = (p: any): any => {
74
+ const t = p.type, withEnum = (o: any) => p.enum ? {...o, enum: [...p.enum, null]} : o;
75
+ if (Array.isArray(t)) return t.includes('null') ? p : withEnum({...p, type: [...t, 'null']});
76
+ if (typeof t == 'string' && !p.$ref) return withEnum({...p, type: [t, 'null']});
77
+ if (p.anyOf) return p.anyOf.some((b: any) => b.type == 'null') ? p : {...p, anyOf: [...p.anyOf, {type: 'null'}]};
78
+ const {description, ...rest} = p;
79
+ return {anyOf: [rest, {type: 'null'}], ...(description && {description})};
80
+ };
81
+
82
+ // a `$defs` entry that reaches itself through `$ref`s (or a `#` root reference)
83
+ const recursive = (s: any): boolean => {
84
+ const defs = s.$defs || {}, refs = (x: any): string[] => [...JSON.stringify(x ?? null).matchAll(/"\$ref":"#(?:\/\$defs\/([^"]*))?"/g)].map(m => m[1] ?? '#');
85
+ const to = (a: string, b: string, seen: Set<string>): boolean => !seen.has(a) && (seen.add(a), refs(defs[a]).some(n => n == b || (n in defs && to(n, b, seen))));
86
+ return refs(s).includes('#') || Object.keys(defs).some(k => to(k, k, new Set()));
87
+ };
88
+
89
+ /**
90
+ * `{ schema, errors, optional, unions }`: a provider's strict form of a portable schema, or why
91
+ * there is none. `left` (the transports'): what the request has left of the provider's
92
+ * per-request limits; a schema that fits is counted against it (`tool`: it is a tool), one that
93
+ * doesn't gets an error
94
+ */
95
+ export function strictSchemas(schema: any, dialect: StrictDialect = 'openai', left?: StrictLimits, tool?: boolean): Strictified {
96
+ const errors: string[] = [], ant = dialect == 'anthropic';
97
+ let optional = 0, unions = 0;
98
+ if (schema?.type != 'object') errors.push('the root is not an object');
99
+ if (ant && schema && recursive(schema)) errors.push('a recursive schema has no strict form');
100
+ const out = walk(schema, o => {
101
+ let moved: any = {};
102
+ if (ant) {
103
+ moved = anthropicKeywords(o);
104
+ if (!o.type && !o.anyOf && !o.allOf && !o.$ref && !o.enum && !('const' in o) && !o.properties) errors.push('a value of any type has no strict form');
105
+ } else {
106
+ for (const k of DROP) if (k in o) { moved[k] = o[k]; delete o[k]; }
107
+ if (o.format && !FORMATS.includes(o.format)) { moved.format = o.format; delete o.format; }
108
+ if (o.prefixItems) { moved.prefixItems = 'tuple'; o.items = {anyOf: o.prefixItems}; delete o.prefixItems; }
109
+ }
110
+ if (o.items === false) delete o.items;
111
+ if (o.type == 'object' || o.properties) {
112
+ if (o.additionalProperties && o.additionalProperties !== false) errors.push('a record (additionalProperties with a schema) has no strict form');
113
+ const props = o.properties ||= {}, req = new Set(o.required || []);
114
+ for (const k in props) {
115
+ const p = props[k];
116
+ if (p?.anyOf || Array.isArray(p?.type)) unions++;
117
+ if (!req.has(k)) { optional++; if (!ant) props[k] = nullable(p); }
118
+ }
119
+ if (!ant) o.required = Object.keys(props);
120
+ o.additionalProperties = false;
121
+ }
122
+ return describe(o, moved);
123
+ });
124
+ if (left && !errors.length) {
125
+ if ((tool && left.tools < 1) || left.optional < optional || left.unions < unions) errors.push('over the per-request strict limits (20 tools, 24 optional and 16 union-typed parameters)');
126
+ else { if (tool) left.tools--; left.optional -= optional; left.unions -= unions; }
127
+ }
128
+ return {schema: out, errors, optional, unions};
129
+ }
130
+
131
+ const acceptsNull = (p: any, root: any): boolean => {
132
+ p = p?.$ref ? resolve(p, root) : p;
133
+ return !!p && (p.type == 'null' || (Array.isArray(p.type) && p.type.includes('null')) || (p.enum || []).includes(null) ||
134
+ p.const === null || (p.anyOf || p.oneOf || []).some((b: any) => acceptsNull(b, root)));
135
+ };
136
+
137
+ /** strict-mode JSON -> what the original schema accepts: nulls for optional, non-nullable keys dropped */
138
+ export function unstrict(value: any, schema: any, root: any = schema): any {
139
+ if (schema?.$ref) schema = resolve(schema, root);
140
+ if (!schema || value == null || typeof value != 'object') return value;
141
+ if (Array.isArray(value)) return schema.items ? value.map(x => unstrict(x, schema.items, root)) : value;
142
+ const out = {...value};
143
+ for (const b0 of schema.anyOf || schema.oneOf || [schema]) {
144
+ const b = b0?.$ref ? resolve(b0, root) : b0, req = new Set(b?.required || []);
145
+ for (const k in b?.properties) {
146
+ if (!(k in out)) continue;
147
+ if (out[k] === null && !req.has(k) && !acceptsNull(b.properties[k], root)) delete out[k];
148
+ else out[k] = unstrict(out[k], b.properties[k], root);
149
+ }
150
+ }
151
+ return out;
152
+ }
@@ -0,0 +1,177 @@
1
+ import {post, sse, json, partsOf, toolName, toolOutput} from './shared';
2
+ import type {HttpOptions} from './shared';
3
+ import {outputJsonSchema} from '../chat/output';
4
+
5
+ /*
6
+ * PLAN-6 L-2 (wave 2): agui(url, { threadId?, state?, context?, headers?, fetch?, body?,
7
+ * dangerouslyAllowBrowser? }), a chat transport for an AG-UI agent endpoint (the Agent-User
8
+ * Interaction protocol: TanStack AI, CopilotKit runtimes, LangGraph / Mastra / Pydantic AI
9
+ * servers), checked against @ag-ui/core 1.0.2's types. POST `url` with a RunAgentInput, SSE back
10
+ * (`data: <event JSON>`).
11
+ *
12
+ * Request (RunAgentInput): `threadId` (the request's `chatId`, the option, or one per transport),
13
+ * a new `runId`, `state` (the request's `state`, else the latest state this transport saw, else
14
+ * the option, else {}), `messages` (the instructions as a system message; user / assistant / tool
15
+ * messages: an assistant message per step with its `toolCalls`, a `tool` message per result),
16
+ * `tools` (`{ name, description, parameters }`: client tools the agent may call), `context` (the
17
+ * option), `forwardedProps` (`{ model?, output? }`: the requested model and the `output` JSON
18
+ * Schema, for servers that use them); `body` merged in.
19
+ *
20
+ * Events -> ChatEvents:
21
+ * - TEXT_MESSAGE_START / CONTENT / CHUNK -> start (the first message id) and text (messages with a
22
+ * role other than assistant are skipped); REASONING_MESSAGE_CONTENT / CHUNK -> reasoning;
23
+ * - TOOL_CALL_START / ARGS / END (or CHUNK) -> a tool call, held until the step ends: a call the
24
+ * agent answers (TOOL_CALL_RESULT) is yielded `executed` + its result (the app doesn't run it), a
25
+ * call still open at STEP_FINISHED / RUN_FINISHED / the end of the stream (or when a new message
26
+ * starts) is a client tool call (the `tool` reply);
27
+ * - STATE_SNAPSHOT / STATE_DELTA (RFC 6902, applied here) -> a `data-agui-state` part (id
28
+ * 'state', so it replaces the previous one) holding the agent's whole current state;
29
+ * MESSAGES_SNAPSHOT -> a `data-agui-messages` part (id 'messages') with the agent's messages as
30
+ * sent (the app decides whether to adopt them); ACTIVITY_SNAPSHOT -> a `data-agui-activity` part
31
+ * (id: its message id);
32
+ * - RUN_FINISHED -> finish ('tool-calls' after client calls, 'other' for an interrupt outcome,
33
+ * else 'stop'; usage summed); RUN_ERROR -> a thrown Error. Everything else is ignored.
34
+ */
35
+
36
+ export interface AguiOptions extends HttpOptions {
37
+ threadId?: string;
38
+ state?: unknown;
39
+ context?: Array<{description: string; value: string}>;
40
+ }
41
+
42
+ let n = 0;
43
+ const uid = (p: string) => `${p}_${Date.now().toString(36)}_${++n}`;
44
+ const clone = (v: any) => v === undefined ? v : JSON.parse(JSON.stringify(v));
45
+
46
+ /** RFC 6902 JSON Patch (add, remove, replace, move, copy; test ignored) on a copy of `doc` */
47
+ export function applyPatch(doc: any, ops: any[]): any {
48
+ let root = clone(doc ?? {});
49
+ const keys = (p: string) => p.split('/').slice(1).map(s => s.replace(/~1/g, '/').replace(/~0/g, '~'));
50
+ const get = (k: string[]) => k.reduce((o, x) => o?.[x], root);
51
+ const set = (k: string[], v: any, insert: boolean) => {
52
+ if (!k.length) { root = v; return; }
53
+ const parent = get(k.slice(0, -1)), last = k[k.length - 1];
54
+ if (Array.isArray(parent)) last == '-' ? parent.push(v) : insert ? parent.splice(+last, 0, v) : (parent[+last] = v);
55
+ else if (parent && typeof parent == 'object') parent[last] = v;
56
+ };
57
+ const del = (k: string[]) => {
58
+ const parent = get(k.slice(0, -1)), last = k[k.length - 1], v = parent?.[last];
59
+ if (Array.isArray(parent)) parent.splice(+last, 1); else if (parent) delete parent[last];
60
+ return v;
61
+ };
62
+ for (const op of ops || []) {
63
+ const to = keys(String(op.path ?? ''));
64
+ if (op.op == 'add') set(to, clone(op.value), true);
65
+ else if (op.op == 'replace') set(to, clone(op.value), false);
66
+ else if (op.op == 'remove') del(to);
67
+ else if (op.op == 'move') set(to, del(keys(op.from)), true);
68
+ else if (op.op == 'copy') set(to, clone(get(keys(op.from))), true);
69
+ }
70
+ return root;
71
+ }
72
+
73
+ const textOf = (m: any) => partsOf(m).filter(p => p.type == 'text').map(p => p.text).join('');
74
+
75
+ /** a message as AG-UI messages (an assistant message per step with its toolCalls, then a tool message per result) */
76
+ export function toAgui(m: any, i: number): any[] {
77
+ const id = m.id ?? `m${i}`;
78
+ if (m.role != 'assistant') {
79
+ const files = partsOf(m).filter(p => p.type == 'file');
80
+ const text = textOf(m);
81
+ return [{id, role: m.role, content: files.length && m.role == 'user'
82
+ ? [...(text ? [{type: 'text', text}] : []), ...files.map(p => ({type: p.mediaType?.startsWith('image/') ? 'image' : 'document', source: {type: 'url', value: p.url, mimeType: p.mediaType}}))]
83
+ : text}];
84
+ }
85
+ const out: any[] = [];
86
+ let text = '', calls: any[] = [], results: any[] = [], step = 0;
87
+ const flush = () => {
88
+ if (text || calls.length) out.push({id: step ? `${id}_${step}` : id, role: 'assistant', ...(text && {content: text}), ...(calls.length && {toolCalls: calls})});
89
+ out.push(...results);
90
+ if (text || calls.length) step++;
91
+ text = ''; calls = []; results = [];
92
+ };
93
+ for (const p of partsOf(m)) {
94
+ const name = toolName(p);
95
+ if (p.type == 'text') { if (calls.length) flush(); text += p.text; }
96
+ else if (name && p.input !== undefined) {
97
+ calls.push({id: p.toolCallId, type: 'function', function: {name, arguments: JSON.stringify(p.input ?? {})}});
98
+ const o = toolOutput(p);
99
+ if (o !== undefined) results.push({id: `${p.toolCallId}_result`, role: 'tool', toolCallId: p.toolCallId, content: o, ...(p.state == 'output-error' && {error: p.errorText ?? 'error'})});
100
+ }
101
+ }
102
+ flush();
103
+ return out;
104
+ }
105
+
106
+ export function agui(url: string, options: AguiOptions = {}) {
107
+ const thread = options.threadId ?? uid('thread');
108
+ let state: any = options.state;
109
+ const headers = async (req: any) => ({accept: 'text/event-stream', ...(typeof options.headers == 'function' ? await options.headers(req) : options.headers)});
110
+ return {
111
+ async *stream(req: any, signal: AbortSignal): AsyncGenerator<any, void, any> {
112
+ const o = req.output !== undefined ? outputJsonSchema(req.output) : undefined;
113
+ const res = await post(url, {
114
+ threadId: req.chatId ?? thread,
115
+ runId: uid('run'),
116
+ state: req.state ?? state ?? {},
117
+ messages: [...(req.instructions ? [{id: 'instructions', role: 'system', content: req.instructions}] : []), ...req.messages.flatMap(toAgui)],
118
+ tools: Object.entries(req.tools || {}).map(([name, t]: [string, any]) => ({name, description: t?.description ?? '', parameters: t?.inputSchema ?? {type: 'object', properties: {}}})),
119
+ context: options.context ?? [],
120
+ forwardedProps: {...(req.model && {model: req.model}), ...(o && {output: o})},
121
+ }, req, signal, {...options, headers}, 'agui');
122
+ // tool calls by id as they stream; `open`: ended calls not yet answered, in order
123
+ const calls: Record<string, any> = {}, roles: Record<string, string> = {};
124
+ let open: any[] = [], last: any, started = false, called = 0;
125
+ const start = function* (id: any) { if (!started && id) { started = true; yield {type: 'start', id}; } };
126
+ const release = function* () { for (const c of open) { called++; yield c; } open = []; };
127
+ const take = (id: string) => {
128
+ const i = open.findIndex(c => c.id === id);
129
+ return i < 0 ? [] : [{...open.splice(i, 1)[0], executed: true}];
130
+ };
131
+ const end = (id: string) => {
132
+ const c = calls[id];
133
+ if (!c || c.done) return;
134
+ c.done = 1;
135
+ open.push({type: 'tool-call', id, name: c.name, input: json(c.args || '{}')});
136
+ };
137
+ for await (const {data} of sse(res)) {
138
+ const e = json(data), t = e?.type;
139
+ if (typeof t != 'string') continue;
140
+ if (t == 'TEXT_MESSAGE_START') { roles[e.messageId] = e.role ?? 'assistant'; yield* release(); yield* start(e.messageId); }
141
+ else if (t == 'TEXT_MESSAGE_CONTENT' || t == 'TEXT_MESSAGE_CHUNK') {
142
+ if (t == 'TEXT_MESSAGE_CHUNK' && e.role) roles[e.messageId] = e.role;
143
+ if ((roles[e.messageId] ?? 'assistant') == 'assistant' && e.delta) { yield* release(); yield* start(e.messageId); yield {type: 'text', delta: e.delta}; }
144
+ } else if (t == 'REASONING_MESSAGE_CONTENT' || t == 'REASONING_MESSAGE_CHUNK') { if (e.delta) yield {type: 'reasoning', delta: e.delta}; }
145
+ else if (t == 'TOOL_CALL_START') { yield* start(e.parentMessageId); calls[e.toolCallId] = {name: e.toolCallName, args: ''}; last = e.toolCallId; }
146
+ else if (t == 'TOOL_CALL_ARGS') { if (calls[e.toolCallId]) calls[e.toolCallId].args += e.delta ?? ''; }
147
+ else if (t == 'TOOL_CALL_CHUNK') {
148
+ const id = e.toolCallId ?? last;
149
+ if (e.toolCallId && e.toolCallId != last) { if (last) end(last); yield* start(e.parentMessageId); }
150
+ const c = calls[id] ||= {name: e.toolCallName, args: ''};
151
+ if (e.toolCallName) c.name = e.toolCallName;
152
+ c.args += e.delta ?? '';
153
+ last = id;
154
+ } else if (t == 'TOOL_CALL_END') end(e.toolCallId);
155
+ else if (t == 'TOOL_CALL_RESULT') {
156
+ end(e.toolCallId);
157
+ yield* take(e.toolCallId);
158
+ yield {type: 'tool-result', id: e.toolCallId, output: typeof e.content == 'string' ? json(e.content) : e.content};
159
+ } else if (t == 'STATE_SNAPSHOT' || t == 'STATE_DELTA') {
160
+ state = t == 'STATE_SNAPSHOT' ? e.snapshot : applyPatch(state, e.delta);
161
+ yield {type: 'data-agui-state', id: 'state', data: state};
162
+ } else if (t == 'MESSAGES_SNAPSHOT') yield {type: 'data-agui-messages', id: 'messages', data: e.messages};
163
+ else if (t == 'ACTIVITY_SNAPSHOT') yield {type: 'data-agui-activity', id: e.messageId, data: {activityType: e.activityType, content: e.content}};
164
+ else if (t == 'STEP_FINISHED') { if (last) end(last); yield* release(); }
165
+ else if (t == 'RUN_FINISHED') {
166
+ for (const id in calls) end(id);
167
+ yield* release();
168
+ const u = (e.usage || []).reduce((a: any, x: any) => ({inputTokens: a.inputTokens + (x.inputTokens ?? 0), outputTokens: a.outputTokens + (x.outputTokens ?? 0), totalTokens: a.totalTokens + (x.totalTokens ?? (x.inputTokens ?? 0) + (x.outputTokens ?? 0))}), {inputTokens: 0, outputTokens: 0, totalTokens: 0});
169
+ yield {type: 'finish', reason: e.outcome?.type == 'interrupt' ? 'other' : called ? 'tool-calls' : 'stop', ...(e.usage?.length && {usage: u})};
170
+ return;
171
+ } else if (t == 'RUN_ERROR') throw Object.assign(new Error('agui: ' + (e.message ?? 'the run failed')), {code: e.code});
172
+ }
173
+ for (const id in calls) end(id);
174
+ yield* release();
175
+ },
176
+ };
177
+ }
@@ -0,0 +1,175 @@
1
+ import {post, sse, json, partsOf, toolName, toolOutput} from './shared';
2
+ import type {HttpOptions} from './shared';
3
+ import {prepare} from './tools';
4
+ import type {StrictOption} from './tools';
5
+ import {anthropicCompatible} from '../schema/strict';
6
+
7
+ /*
8
+ * PLAN-6 L-2 (wave 2): anthropicMessages({ baseURL?, model, maxTokens?, headers?, fetch?, body?,
9
+ * strict?, serverTools?, dangerouslyAllowBrowser? }), a chat transport for Anthropic's Messages
10
+ * API SSE (checked against @anthropic-ai/sdk 0.131.0's types): Claude through the app's proxy,
11
+ * or Ollama's Anthropic-compatible endpoint. POST `${baseURL}/messages` (baseURL default '/v1')
12
+ * with `stream: true`, `anthropic-version: 2023-06-01` (and
13
+ * `anthropic-dangerous-direct-browser-access` with dangerouslyAllowBrowser).
14
+ *
15
+ * Request: `instructions` and `system` messages as `system`; user / assistant messages as content
16
+ * blocks (text; images and PDFs as image / document blocks from a URL or a data URL; signed
17
+ * reasoning parts as thinking / redacted_thinking; a tool part with a result as a tool_use block
18
+ * in the assistant message and a tool_result block in a user message after it, one assistant
19
+ * message per step; a server tool part as server_tool_use + its result block as received;
20
+ * consecutive messages of one role merged); `tools` as custom tools (`input_schema`), plus
21
+ * `serverTools` (Anthropic server tools, e.g. `{ type: 'web_search_20260209', name: 'web_search' }`)
22
+ * as is; `output` as `output_config.format` json_schema; `max_tokens` from the request's
23
+ * `maxTokens`, the option, or 16000; `body` merged in.
24
+ *
25
+ * Strict (D266, D285): `strict: strictSchemas` sends tools `strict: true` with schemas in
26
+ * Anthropic's subset and the `output` schema in it; a schema without a strict form, or one over
27
+ * Anthropic's per-request limits (20 strict tools, 24 optional and 16 union-typed parameters
28
+ * across the strict schemas, G-609), is sent non-strict on its own (SYG675).
29
+ *
30
+ * Compatibility (G-635, always on): `output_config.format` is always constrained decoding, so a
31
+ * non-strict `output` schema goes through anthropicCompatible() (../schema/strict.ts): keywords
32
+ * outside Anthropic's subset (numeric / string bounds, pattern, uniqueItems, not, if/then, ...)
33
+ * move into the description, oneOf becomes anyOf, objects get `additionalProperties: false`. The
34
+ * driver validates the reply against the original schema. Non-strict tools' `input_schema` is sent
35
+ * as is: the API takes any JSON Schema (draft 2020-12) there, and the keywords guide the model
36
+ * (A-1's normalization already makes the root an object, which the API requires).
37
+ *
38
+ * Events: message_start -> start (the message id, input usage); text_delta -> text;
39
+ * thinking_delta -> reasoning, and its signature (signature_delta) closes the part with
40
+ * `providerMetadata: { anthropic: { signature } }` (a redacted_thinking block: `{ redactedData }`)
41
+ * so the next request can send it back (Claude requires it in a tool loop); tool_use with its
42
+ * input_json_delta -> a tool call at content_block_stop; server_tool_use -> an `executed` tool
43
+ * call, its `*_tool_result` block -> that call's result (the block itself); message_delta ->
44
+ * the stop reason and output usage; message_stop (or the end of the stream) -> finish; an
45
+ * `error` event -> a thrown Error. Citations and unknown events are ignored.
46
+ */
47
+
48
+ export interface AnthropicMessagesOptions extends HttpOptions {
49
+ baseURL?: string;
50
+ model?: string;
51
+ maxTokens?: number;
52
+ strict?: StrictOption;
53
+ serverTools?: any[];
54
+ }
55
+
56
+ const REASONS: Record<string, string> = {end_turn: 'stop', stop_sequence: 'stop', pause_turn: 'stop', tool_use: 'tool-calls', max_tokens: 'length', model_context_window_exceeded: 'length', refusal: 'content-filter'};
57
+ // G-609: platform.claude.com structured outputs, "Schema complexity limits"
58
+ const LIMITS = {tools: 20, optional: 24, unions: 16};
59
+ const DATA = /^data:([^;,]+)(;base64)?,(.*)$/;
60
+
61
+ const source = (url: string) => {
62
+ const m = DATA.exec(url);
63
+ return m && m[2] ? {type: 'base64', media_type: m[1], data: m[3]} : {type: 'url', url};
64
+ };
65
+
66
+ /** a message as Anthropic messages (an assistant message with tool results: one per step, results in user messages) */
67
+ export function toAnthropic(m: any): any[] {
68
+ const out: any[] = [];
69
+ let content: any[] = [], results: any[] = [];
70
+ const flush = () => {
71
+ if (content.length) out.push({role: m.role, content});
72
+ if (results.length) out.push({role: 'user', content: results});
73
+ content = []; results = [];
74
+ };
75
+ for (const p of partsOf(m)) {
76
+ const name = toolName(p), meta = p.providerMetadata?.anthropic;
77
+ if (results.length && !(name && p.input !== undefined)) flush();
78
+ if (p.type == 'text') content.push({type: 'text', text: p.text});
79
+ else if (p.type == 'file' && m.role == 'user') {
80
+ const image = p.mediaType?.startsWith('image/');
81
+ if (image || p.mediaType == 'application/pdf') content.push({type: image ? 'image' : 'document', source: source(p.url)});
82
+ } else if (p.type == 'reasoning' && m.role == 'assistant' && meta) {
83
+ if (meta.signature) content.push({type: 'thinking', thinking: p.text, signature: meta.signature});
84
+ else if (meta.redactedData) content.push({type: 'redacted_thinking', data: meta.redactedData});
85
+ } else if (name && p.input !== undefined && m.role == 'assistant') {
86
+ if (p.providerExecuted) {
87
+ content.push({type: 'server_tool_use', id: p.toolCallId, name, input: p.input});
88
+ if (/_tool_result$/.test(p.output?.type)) content.push(p.output);
89
+ continue;
90
+ }
91
+ const o = toolOutput(p);
92
+ // a call with no result yet can't be sent: Anthropic needs its tool_result next
93
+ if (o === undefined) continue;
94
+ content.push({type: 'tool_use', id: p.toolCallId, name, input: p.input ?? {}});
95
+ results.push({type: 'tool_result', tool_use_id: p.toolCallId, content: o, ...(p.state != 'output-available' && {is_error: true})});
96
+ }
97
+ }
98
+ flush();
99
+ return out;
100
+ }
101
+
102
+ export function anthropicMessages(options: AnthropicMessagesOptions = {}) {
103
+ const seen = new Set<string>();
104
+ const headers = async (req: any) => ({
105
+ 'anthropic-version': '2023-06-01',
106
+ ...(options.dangerouslyAllowBrowser && {'anthropic-dangerous-direct-browser-access': 'true'}),
107
+ ...(typeof options.headers == 'function' ? await options.headers(req) : options.headers),
108
+ });
109
+ return {
110
+ async *stream(req: any, signal: AbortSignal): AsyncGenerator<any, void, any> {
111
+ const p = prepare(req, options.strict, seen, 'anthropicMessages', 'anthropic', LIMITS);
112
+ const system = [req.instructions, ...req.messages.filter((m: any) => m.role == 'system').map((m: any) => partsOf(m).filter(x => x.type == 'text').map(x => x.text).join(''))].filter(Boolean).join('\n\n');
113
+ const messages: any[] = [];
114
+ for (const m of req.messages.filter((m: any) => m.role != 'system').flatMap(toAnthropic)) {
115
+ const last = messages[messages.length - 1];
116
+ if (last?.role == m.role) last.content = [...last.content, ...m.content];
117
+ else messages.push(m);
118
+ }
119
+ const tools = [...p.tools.map(({name, description, parameters, strict}) => ({name, ...(description && {description}), input_schema: parameters, ...(strict && {strict})})), ...(options.serverTools || [])];
120
+ const res = await post((options.baseURL ?? '/v1').replace(/\/$/, '') + '/messages', {
121
+ model: req.model ?? options.model,
122
+ max_tokens: req.maxTokens ?? options.maxTokens ?? 16000,
123
+ stream: true,
124
+ ...(system && {system}),
125
+ messages,
126
+ ...(tools.length && {tools}),
127
+ ...(p.output && {output_config: {format: {type: 'json_schema', schema: p.output.strict ? p.output.schema : anthropicCompatible(p.output.schema)}}}),
128
+ }, req, signal, {...options, headers}, 'anthropicMessages');
129
+ // content blocks by index: tool_use / server_tool_use input JSON, thinking signatures
130
+ const blocks: any[] = [];
131
+ // usage: message_start's, updated by message_delta's (cumulative counts)
132
+ let reason: any, u: any, finished = false;
133
+ const usage = () => {
134
+ if (!u) return;
135
+ const i = (u.input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0), o = u.output_tokens ?? 0;
136
+ return {inputTokens: i, outputTokens: o, totalTokens: i + o, ...(u.output_tokens_details?.thinking_tokens != null && {reasoningTokens: u.output_tokens_details.thinking_tokens})};
137
+ };
138
+ for await (const {data} of sse(res)) {
139
+ const e = json(data), t = e?.type;
140
+ if (typeof t != 'string') continue;
141
+ if (t == 'message_start') {
142
+ u = e.message?.usage;
143
+ if (e.message?.id) yield {type: 'start', id: e.message.id};
144
+ } else if (t == 'content_block_start') {
145
+ const b = e.content_block || {};
146
+ blocks[e.index] = {...b, json: ''};
147
+ if (b.type == 'text' && b.text) yield {type: 'text', delta: b.text};
148
+ else if (b.type == 'thinking' && b.thinking) yield {type: 'reasoning', delta: b.thinking};
149
+ else if (b.type == 'redacted_thinking') yield {type: 'reasoning', delta: '', providerMetadata: {anthropic: {redactedData: b.data}}};
150
+ else if (/_tool_result$/.test(b.type) && b.tool_use_id) yield {type: 'tool-result', id: b.tool_use_id, output: b};
151
+ } else if (t == 'content_block_delta') {
152
+ const d = e.delta || {}, b = blocks[e.index];
153
+ if (d.type == 'text_delta') yield {type: 'text', delta: d.text};
154
+ else if (d.type == 'thinking_delta') yield {type: 'reasoning', delta: d.thinking};
155
+ else if (b && d.type == 'signature_delta') b.signature = d.signature;
156
+ else if (b && d.type == 'input_json_delta') b.json += d.partial_json;
157
+ } else if (t == 'content_block_stop') {
158
+ const b = blocks[e.index];
159
+ if (b?.type == 'thinking' && b.signature) yield {type: 'reasoning', delta: '', providerMetadata: {anthropic: {signature: b.signature}}};
160
+ else if (b?.type == 'tool_use') yield {type: 'tool-call', id: b.id, name: b.name, input: p.input(b.name, b.json ? json(b.json) : b.input ?? {})};
161
+ else if (b?.type == 'server_tool_use') yield {type: 'tool-call', id: b.id, name: b.name, input: b.json ? json(b.json) : b.input ?? {}, executed: true, providerExecuted: true};
162
+ } else if (t == 'message_delta') {
163
+ if (e.delta?.stop_reason) reason = REASONS[e.delta.stop_reason] ?? 'other';
164
+ if (e.usage) u = {...u, ...e.usage};
165
+ } else if (t == 'message_stop') {
166
+ finished = true;
167
+ yield {type: 'finish', reason, usage: usage()};
168
+ } else if (t == 'error') {
169
+ throw Object.assign(new Error('anthropicMessages: ' + (e.error?.message ?? 'the request failed')), {code: e.error?.type});
170
+ }
171
+ }
172
+ if (!finished) yield {type: 'finish', reason, usage: usage()};
173
+ },
174
+ };
175
+ }
@@ -0,0 +1,105 @@
1
+ import {post, sse, json, usageOf, partsOf, toolName, toolOutput} from './shared';
2
+ import type {HttpOptions} from './shared';
3
+ import {prepare} from './tools';
4
+ import type {StrictOption} from './tools';
5
+
6
+ /*
7
+ * PLAN-6 L-2: chatCompletions({ baseURL?, model, headers?, fetch?, body?, strict?,
8
+ * dangerouslyAllowBrowser? }), a chat transport for OpenAI's Chat Completions SSE, the lowest
9
+ * common denominator of local servers (Ollama, llama.cpp, LM Studio, vLLM). POST
10
+ * `${baseURL}/chat/completions` (baseURL default '/v1') with `stream: true` and
11
+ * `stream_options.include_usage`.
12
+ *
13
+ * Request: `instructions` as the first system message; each message as Chat Completions
14
+ * messages (an assistant message's text and tool calls as one message per step, followed by a
15
+ * `tool` message per result); `tools` as function tools; `output` as `response_format`
16
+ * json_schema; `body` merged in.
17
+ *
18
+ * Events: `delta.content` -> text; `delta.reasoning_content` / `delta.reasoning` (vLLM,
19
+ * DeepSeek, Ollama) -> reasoning; `delta.tool_calls` accumulated by `index`, yielded in index
20
+ * order when the choice finishes (or the stream ends); the usage chunk (empty `choices`) and
21
+ * `finish_reason` -> finish; an `error` chunk -> a thrown Error; `[DONE]` ends the stream.
22
+ */
23
+
24
+ export interface ChatCompletionsOptions extends HttpOptions {
25
+ baseURL?: string;
26
+ model?: string;
27
+ strict?: StrictOption;
28
+ }
29
+
30
+ const REASONS: Record<string, string> = {stop: 'stop', length: 'length', tool_calls: 'tool-calls', function_call: 'tool-calls', content_filter: 'content-filter'};
31
+
32
+ /** a message as Chat Completions messages */
33
+ export function toMessages(m: any): any[] {
34
+ const out: any[] = [];
35
+ let text = '', calls: any[] = [], results: any[] = [], content: any[] = [];
36
+ const flush = () => {
37
+ if (m.role == 'assistant') {
38
+ if (text || calls.length) out.push({role: 'assistant', content: text || null, ...(calls.length && {tool_calls: calls})});
39
+ } else if (text || content.length) {
40
+ out.push({role: m.role, content: content.length ? [...(text ? [{type: 'text', text}] : []), ...content] : text});
41
+ }
42
+ out.push(...results);
43
+ text = ''; calls = []; results = []; content = [];
44
+ };
45
+ for (const p of partsOf(m)) {
46
+ const name = toolName(p);
47
+ if (p.type == 'text') {
48
+ if (calls.length) flush();
49
+ text += p.text;
50
+ } else if (p.type == 'file' && m.role == 'user') {
51
+ content.push(p.mediaType?.startsWith('image/') ? {type: 'image_url', image_url: {url: p.url}} : {type: 'file', file: {file_data: p.url, ...(p.filename && {filename: p.filename})}});
52
+ } else if (name && p.input !== undefined && m.role == 'assistant') {
53
+ calls.push({id: p.toolCallId, type: 'function', function: {name, arguments: JSON.stringify(p.input ?? {})}});
54
+ const o = toolOutput(p);
55
+ if (o !== undefined) results.push({role: 'tool', tool_call_id: p.toolCallId, content: o});
56
+ }
57
+ }
58
+ flush();
59
+ return out;
60
+ }
61
+
62
+ export function chatCompletions(options: ChatCompletionsOptions = {}) {
63
+ const seen = new Set<string>();
64
+ return {
65
+ async *stream(req: any, signal: AbortSignal): AsyncGenerator<any, void, any> {
66
+ const p = prepare(req, options.strict, seen, 'chatCompletions');
67
+ const res = await post((options.baseURL ?? '/v1').replace(/\/$/, '') + '/chat/completions', {
68
+ model: req.model ?? options.model,
69
+ stream: true,
70
+ stream_options: {include_usage: true},
71
+ messages: [...(req.instructions ? [{role: 'system', content: req.instructions}] : []), ...req.messages.flatMap(toMessages)],
72
+ ...(p.tools.length && {tools: p.tools.map(({name, description, parameters, strict}) => ({type: 'function', function: {name, ...(description && {description}), parameters, ...(strict && {strict})}}))}),
73
+ ...(p.output && {response_format: {type: 'json_schema', json_schema: {name: 'output', schema: p.output.schema, ...(p.output.strict && {strict: true})}}}),
74
+ }, req, signal, options, 'chatCompletions');
75
+ // tool calls by index, as their deltas arrive
76
+ let calls: any[] = [], reason: any, usage: any, started = false;
77
+ const done = function* () {
78
+ for (const c of calls) if (c) yield {type: 'tool-call', id: c.id, name: c.name, input: p.input(c.name, json(c.args))};
79
+ calls = [];
80
+ };
81
+ for await (const {data} of sse(res)) {
82
+ if (data == '[DONE]') break;
83
+ const e = json(data);
84
+ if (!e || typeof e != 'object') continue;
85
+ if (e.error) throw Object.assign(new Error('chatCompletions: ' + (e.error.message ?? e.error)), {code: e.error.code});
86
+ if (!started && e.id) { started = true; yield {type: 'start', id: e.id}; }
87
+ if (e.usage) usage = usageOf(e.usage);
88
+ const ch = e.choices?.[0];
89
+ if (!ch) continue;
90
+ const d = ch.delta || {}, r = d.reasoning_content ?? d.reasoning;
91
+ if (typeof r == 'string' && r) yield {type: 'reasoning', delta: r};
92
+ if (typeof d.content == 'string' && d.content) yield {type: 'text', delta: d.content};
93
+ for (const t of d.tool_calls || []) {
94
+ const c = calls[t.index ?? 0] ||= {id: t.id, name: '', args: ''};
95
+ if (t.id) c.id = t.id;
96
+ if (t.function?.name) c.name = t.function.name;
97
+ if (t.function?.arguments) c.args += t.function.arguments;
98
+ }
99
+ if (ch.finish_reason) { reason = REASONS[ch.finish_reason] ?? 'other'; yield* done(); }
100
+ }
101
+ yield* done();
102
+ yield {type: 'finish', reason, usage};
103
+ },
104
+ };
105
+ }