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.
- package/CHANGELOG.md +716 -0
- package/README.md +24 -9
- package/dist/ai.cjs.js +115 -0
- package/dist/ai.cjs.js.map +1 -0
- package/dist/ai.esm.js +2 -0
- package/dist/ai.esm.js.map +1 -0
- package/dist/astro/client.cjs.js +16 -9
- package/dist/astro/client.cjs.js.map +1 -1
- package/dist/astro/client.mjs +16 -9
- package/dist/astro/client.mjs.map +1 -1
- package/dist/astro/index.cjs.js +958 -92
- package/dist/astro/index.cjs.js.map +1 -1
- package/dist/astro/index.mjs +958 -93
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/server.cjs.js +3396 -165
- package/dist/astro/server.cjs.js.map +1 -1
- package/dist/astro/server.mjs +3396 -165
- package/dist/astro/server.mjs.map +1 -1
- package/dist/devtools.cjs.js +1840 -0
- package/dist/devtools.cjs.js.map +1 -0
- package/dist/devtools.esm.js +1827 -0
- package/dist/devtools.esm.js.map +1 -0
- package/dist/diagnostics.cjs.js +3190 -296
- package/dist/diagnostics.cjs.js.map +1 -1
- package/dist/diagnostics.esm.js +3190 -296
- package/dist/diagnostics.esm.js.map +1 -1
- package/dist/element.cjs.js +230 -0
- package/dist/element.cjs.js.map +1 -0
- package/dist/element.esm.js +228 -0
- package/dist/element.esm.js.map +1 -0
- package/dist/guide/accessibility.md +310 -0
- package/dist/guide/adapters.md +221 -0
- package/dist/guide/agent.md +588 -0
- package/dist/guide/ai-chat.md +693 -0
- package/dist/guide/ai-decisions.md +330 -0
- package/dist/guide/behaviors.md +312 -0
- package/dist/guide/browser-sources.md +203 -0
- package/dist/guide/drag-and-drop.md +342 -0
- package/dist/guide/element-commands.md +306 -0
- package/dist/guide/error-boundaries.md +169 -0
- package/dist/guide/forms-reference.md +409 -0
- package/dist/guide/forms.md +251 -0
- package/dist/guide/http.md +301 -0
- package/dist/guide/inputs.md +173 -0
- package/dist/guide/mcp-apps.md +272 -0
- package/dist/guide/persistence.md +245 -0
- package/dist/guide/recipes/ai-form-fill.md +163 -0
- package/dist/guide/recipes/ai-summarize.md +155 -0
- package/dist/guide/recipes/ai-support-inbox.md +190 -0
- package/dist/guide/recipes/carousel.md +155 -0
- package/dist/guide/recipes/charts.md +177 -0
- package/dist/guide/recipes/code-editor.md +135 -0
- package/dist/guide/recipes/data-grid.md +155 -0
- package/dist/guide/recipes/data-table.md +196 -0
- package/dist/guide/recipes/i18n.md +222 -0
- package/dist/guide/recipes/icons.md +115 -0
- package/dist/guide/recipes/overview.md +43 -0
- package/dist/guide/recipes/rich-text.md +154 -0
- package/dist/guide/resources.md +471 -0
- package/dist/guide/ssr.md +255 -0
- package/dist/guide/timers.md +178 -0
- package/dist/guide/ui/accordion.md +93 -0
- package/dist/guide/ui/combobox.md +144 -0
- package/dist/guide/ui/dialog.md +124 -0
- package/dist/guide/ui/disclosure.md +60 -0
- package/dist/guide/ui/menu.md +125 -0
- package/dist/guide/ui/overview.md +166 -0
- package/dist/guide/ui/popover.md +101 -0
- package/dist/guide/ui/select.md +114 -0
- package/dist/guide/ui/tabs.md +161 -0
- package/dist/guide/ui/toaster.md +152 -0
- package/dist/guide/ui/tooltip.md +103 -0
- package/dist/guide/undo.md +178 -0
- package/dist/guide/virtual-collections.md +183 -0
- package/dist/guide/web-components.md +179 -0
- package/dist/guide/webmcp.md +226 -0
- package/dist/guide/widgets.md +217 -0
- package/dist/index.cjs.js +19288 -7408
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +4466 -552
- package/dist/index.esm.js +19274 -7461
- package/dist/index.esm.js.map +1 -1
- package/dist/jsx-dev-runtime.cjs.js +26 -334
- package/dist/jsx-dev-runtime.cjs.js.map +1 -1
- package/dist/jsx-dev-runtime.esm.js +19 -327
- package/dist/jsx-dev-runtime.esm.js.map +1 -1
- package/dist/jsx-runtime.cjs.js +26 -334
- package/dist/jsx-runtime.cjs.js.map +1 -1
- package/dist/jsx-runtime.esm.js +19 -327
- package/dist/jsx-runtime.esm.js.map +1 -1
- package/dist/jsx.cjs.js +10 -314
- package/dist/jsx.cjs.js.map +1 -1
- package/dist/jsx.esm.js +8 -315
- package/dist/jsx.esm.js.map +1 -1
- package/dist/react.cjs.js +117 -0
- package/dist/react.cjs.js.map +1 -0
- package/dist/react.esm.js +115 -0
- package/dist/react.esm.js.map +1 -0
- package/dist/shims/globalthis.cjs +20 -0
- package/dist/sygnal.min.js +1 -1
- package/dist/sygnal.min.js.map +1 -1
- package/dist/ui-combobox.cjs.js +169 -0
- package/dist/ui-combobox.cjs.js.map +1 -0
- package/dist/ui-combobox.esm.js +148 -0
- package/dist/ui-combobox.esm.js.map +1 -0
- package/dist/ui-menu.cjs.js +87 -0
- package/dist/ui-menu.cjs.js.map +1 -0
- package/dist/ui-menu.esm.js +66 -0
- package/dist/ui-menu.esm.js.map +1 -0
- package/dist/ui-select.cjs.js +104 -0
- package/dist/ui-select.cjs.js.map +1 -0
- package/dist/ui-select.esm.js +83 -0
- package/dist/ui-select.esm.js.map +1 -0
- package/dist/ui.cjs.js +738 -0
- package/dist/ui.cjs.js.map +1 -0
- package/dist/ui.esm.js +727 -0
- package/dist/ui.esm.js.map +1 -0
- package/dist/vike/ClientOnly.cjs.js +4 -14
- package/dist/vike/ClientOnly.cjs.js.map +1 -1
- package/dist/vike/ClientOnly.mjs +4 -14
- package/dist/vike/ClientOnly.mjs.map +1 -1
- package/dist/vike/{+config.js → config/+config.js} +9 -1
- package/dist/vike/config/+config.js.map +1 -0
- package/dist/vike/config/package.json +6 -0
- package/dist/vike/onRenderClient.cjs.js +165 -95
- package/dist/vike/onRenderClient.cjs.js.map +1 -1
- package/dist/vike/onRenderClient.mjs +165 -95
- package/dist/vike/onRenderClient.mjs.map +1 -1
- package/dist/vike/onRenderHtml.cjs.js +59 -24
- package/dist/vike/onRenderHtml.cjs.js.map +1 -1
- package/dist/vike/onRenderHtml.mjs +60 -25
- package/dist/vike/onRenderHtml.mjs.map +1 -1
- package/dist/vite/plugin.cjs.js +919 -89
- package/dist/vite/plugin.cjs.js.map +1 -1
- package/dist/vite/plugin.mjs +919 -90
- package/dist/vite/plugin.mjs.map +1 -1
- package/dist/zag.cjs.js +408 -0
- package/dist/zag.cjs.js.map +1 -0
- package/dist/zag.esm.js +405 -0
- package/dist/zag.esm.js.map +1 -0
- package/llms.txt +172 -103
- package/package.json +112 -14
- package/src/ai.d.ts +1186 -0
- package/src/ai.ts +23 -0
- package/src/astro/client.ts +27 -16
- package/src/astro/index.d.ts +24 -0
- package/src/astro/index.ts +69 -3
- package/src/astro/server.ts +8 -2
- package/src/collection.ts +6 -93
- package/src/core/actions.ts +134 -0
- package/src/core/cell.ts +202 -0
- package/src/core/debug.ts +23 -0
- package/src/core/define.ts +242 -0
- package/src/core/hooks.ts +314 -0
- package/src/core/hosts/collection.ts +325 -0
- package/src/core/hosts/switchable.ts +146 -0
- package/src/core/instance.ts +592 -0
- package/src/core/markers/clientonly.ts +13 -0
- package/src/core/markers/lazy.ts +40 -0
- package/src/core/markers/portal.ts +96 -0
- package/src/core/markers/suspense.ts +41 -0
- package/src/core/markers/transition.ts +70 -0
- package/src/core/registry.ts +25 -0
- package/src/core/runtime.ts +573 -0
- package/src/core/statics.ts +104 -0
- package/src/core/teardown.ts +60 -0
- package/src/core/view.ts +37 -0
- package/src/cycle/dom/DocumentDOMSource.ts +13 -8
- package/src/cycle/dom/ElementFinder.ts +7 -9
- package/src/cycle/dom/EventDelegator.ts +174 -223
- package/src/cycle/dom/IsolateModule.ts +65 -35
- package/src/cycle/dom/MainDOMSource.ts +9 -2
- package/src/cycle/dom/PriorityQueue.ts +4 -2
- package/src/cycle/dom/SymbolTree.ts +18 -47
- package/src/cycle/dom/classNameModule.ts +9 -7
- package/src/cycle/dom/controlledInputModule.ts +23 -6
- package/src/cycle/dom/enrichEventStream.ts +25 -47
- package/src/cycle/dom/fragment.ts +7 -0
- package/src/cycle/dom/isolate.ts +30 -27
- package/src/cycle/dom/makeDOMDriver.ts +115 -56
- package/src/cycle/dom/mockDOMSource.ts +21 -1
- package/src/cycle/dom/modules.ts +16 -3
- package/src/cycle/dom/propsModule.ts +65 -0
- package/src/cycle/dom/selectModule.ts +22 -17
- package/src/cycle/dom/snabbdom.ts +1 -6
- package/src/cycle/dom/thunk.ts +3 -0
- package/src/cycle/dom/utils.ts +98 -0
- package/src/cycle/dom/viewTransition.ts +43 -0
- package/src/cycle/state/StateSource.ts +19 -3
- package/src/cycle/state/objIsEqual.ts +50 -0
- package/src/cycle/state/types.ts +0 -10
- package/src/defineComponent.ts +34 -0
- package/src/devtools.d.ts +170 -0
- package/src/devtools.ts +37 -0
- package/src/element.d.ts +59 -0
- package/src/element.ts +235 -0
- package/src/extra/ai/agent/index.ts +484 -0
- package/src/extra/ai/answers.ts +115 -0
- package/src/extra/ai/chat/behavior.ts +336 -0
- package/src/extra/ai/chat/driver.ts +271 -0
- package/src/extra/ai/chat/memoryTransport.ts +61 -0
- package/src/extra/ai/chat/output.ts +37 -0
- package/src/extra/ai/commandBar.ts +281 -0
- package/src/extra/ai/decide.ts +104 -0
- package/src/extra/ai/index.ts +42 -0
- package/src/extra/ai/link.ts +90 -0
- package/src/extra/ai/mcpApp.ts +314 -0
- package/src/extra/ai/messages.ts +51 -0
- package/src/extra/ai/schema/index.ts +166 -0
- package/src/extra/ai/schema/jsonSchema.ts +72 -0
- package/src/extra/ai/schema/strict.ts +152 -0
- package/src/extra/ai/transports/agui.ts +177 -0
- package/src/extra/ai/transports/anthropicMessages.ts +175 -0
- package/src/extra/ai/transports/chatCompletions.ts +105 -0
- package/src/extra/ai/transports/chromePrompt.ts +77 -0
- package/src/extra/ai/transports/encodeOpenResponses.ts +87 -0
- package/src/extra/ai/transports/fromAISDK.ts +126 -0
- package/src/extra/ai/transports/openResponses.ts +105 -0
- package/src/extra/ai/transports/shared.ts +125 -0
- package/src/extra/ai/transports/tools.ts +59 -0
- package/src/extra/ai/transports/uiMessageStream.ts +107 -0
- package/src/extra/ai/webmcp.ts +260 -0
- package/src/extra/backoff.ts +9 -0
- package/src/extra/behaviors.ts +169 -0
- package/src/extra/browserSignals.ts +23 -0
- package/src/extra/browserSources.ts +326 -0
- package/src/extra/command.ts +3 -3
- package/src/extra/controls.ts +47 -0
- package/src/extra/copyAsTest.ts +286 -0
- package/src/extra/devMcp.ts +386 -0
- package/src/extra/devtools.ts +101 -31
- package/src/extra/devtoolsActions.ts +379 -0
- package/src/extra/devtoolsHook.ts +9 -0
- package/src/extra/devtoolsNext.ts +83 -0
- package/src/extra/diagnostics/checks/actionLog.ts +108 -0
- package/src/extra/diagnostics/checks/behaviors.ts +49 -0
- package/src/extra/diagnostics/checks/browserSources.ts +96 -0
- package/src/extra/diagnostics/checks/chat.ts +71 -0
- package/src/extra/diagnostics/checks/collections.ts +1 -1
- package/src/extra/diagnostics/checks/controls.ts +148 -0
- package/src/extra/diagnostics/checks/dataset.ts +57 -0
- package/src/extra/diagnostics/checks/dom.ts +20 -11
- package/src/extra/diagnostics/checks/elementCommands.ts +175 -0
- package/src/extra/diagnostics/checks/events.ts +17 -2
- package/src/extra/diagnostics/checks/fetch.ts +135 -0
- package/src/extra/diagnostics/checks/forms.ts +171 -0
- package/src/extra/diagnostics/checks/index.ts +103 -4
- package/src/extra/diagnostics/checks/inspect.ts +140 -32
- package/src/extra/diagnostics/checks/next.ts +417 -0
- package/src/extra/diagnostics/checks/persist.ts +54 -0
- package/src/extra/diagnostics/checks/props.ts +10 -5
- package/src/extra/diagnostics/checks/public.d.ts +115 -7
- package/src/extra/diagnostics/checks/replies.ts +128 -0
- package/src/extra/diagnostics/checks/router.ts +100 -0
- package/src/extra/diagnostics/checks/rxjsHints.ts +1 -1
- package/src/extra/diagnostics/checks/shared.ts +86 -2
- package/src/extra/diagnostics/checks/shorthand.ts +115 -0
- package/src/extra/diagnostics/checks/sortable.ts +129 -0
- package/src/extra/diagnostics/checks/state.ts +100 -7
- package/src/extra/diagnostics/checks/statics.ts +48 -0
- package/src/extra/diagnostics/checks/strict.ts +10 -67
- package/src/extra/diagnostics/checks/timers.ts +80 -0
- package/src/extra/diagnostics/checks/viewTransitions.ts +47 -0
- package/src/extra/diagnostics/checks/virtual.ts +73 -0
- package/src/extra/diagnostics/checks/widgets.ts +109 -0
- package/src/extra/diagnostics/checks/wiring.ts +69 -10
- package/src/extra/diagnostics/codes.ts +335 -23
- package/src/extra/diagnostics/index.ts +34 -9
- package/src/extra/diagnostics/legacy.ts +17 -0
- package/src/extra/driverFactories.ts +83 -19
- package/src/extra/elementCommands.ts +53 -0
- package/src/extra/fetchDriver.ts +519 -0
- package/src/extra/focusWithin.ts +27 -0
- package/src/extra/form.ts +284 -0
- package/src/extra/formHelpers.ts +139 -0
- package/src/extra/formTool.ts +150 -0
- package/src/extra/head.ts +106 -0
- package/src/extra/hmr.ts +2 -3
- package/src/extra/owned.ts +24 -0
- package/src/extra/pager.ts +50 -0
- package/src/extra/persist.ts +133 -0
- package/src/extra/pwa.ts +1 -1
- package/src/extra/queryCache.ts +107 -0
- package/src/extra/reducers.ts +1 -1
- package/src/extra/reduxDevtools.ts +92 -0
- package/src/extra/replies.ts +54 -0
- package/src/extra/router.ts +323 -0
- package/src/extra/run.ts +90 -217
- package/src/extra/selection.ts +76 -0
- package/src/extra/socketDriver.ts +265 -0
- package/src/extra/sortable.ts +377 -0
- package/src/extra/ssr.ts +427 -160
- package/src/extra/standardSchema.ts +27 -0
- package/src/extra/testing.ts +2550 -175
- package/src/extra/timers.ts +95 -0
- package/src/extra/undo.ts +261 -0
- package/src/extra/viewTransitions.ts +38 -0
- package/src/extra/virtual.ts +513 -0
- package/src/extra/widget.ts +201 -0
- package/src/index.d.ts +2754 -127
- package/src/index.ts +45 -4
- package/src/jsx-runtime.ts +15 -1
- package/src/jsx.ts +2 -1
- package/src/lazy.ts +50 -7
- package/src/portal.ts +3 -1
- package/src/pragma/index.ts +256 -133
- package/src/react-peers.d.ts +5 -0
- package/src/react.d.ts +43 -0
- package/src/react.ts +110 -0
- package/src/shared.ts +57 -0
- package/src/slot.ts +1 -1
- package/src/suspense.ts +3 -1
- package/src/switchable.ts +6 -121
- package/src/transition.ts +3 -1
- package/src/ui/accordion.ts +64 -0
- package/src/ui/dialog.ts +129 -0
- package/src/ui/disclosure.ts +35 -0
- package/src/ui/popover.ts +45 -0
- package/src/ui/shared.ts +94 -0
- package/src/ui/tabs.ts +82 -0
- package/src/ui/toaster.ts +198 -0
- package/src/ui/tooltip.ts +70 -0
- package/src/ui/zag/combobox.ts +115 -0
- package/src/ui/zag/menu.ts +40 -0
- package/src/ui/zag/select.ts +54 -0
- package/src/ui/zag/shared.ts +43 -0
- package/src/ui-combobox.d.ts +20 -0
- package/src/ui-combobox.ts +6 -0
- package/src/ui-menu.d.ts +30 -0
- package/src/ui-menu.ts +6 -0
- package/src/ui-select.d.ts +17 -0
- package/src/ui-select.ts +6 -0
- package/src/ui-zag-types.d.ts +42 -0
- package/src/ui.d.ts +208 -0
- package/src/ui.ts +15 -0
- package/src/vike/+config.ts +9 -1
- package/src/vike/ClientOnly.ts +1 -1
- package/src/vike/onRenderClient.ts +144 -96
- package/src/vike/onRenderHtml.ts +69 -26
- package/src/vike/types.ts +7 -1
- package/src/vite/globalthis-shim.ts +18 -0
- package/src/vite/globalthis.ts +56 -0
- package/src/vite/mcp.ts +568 -0
- package/src/vite/plugin.d.ts +47 -3
- package/src/vite/plugin.ts +262 -25
- package/src/zag.d.ts +64 -0
- package/src/zag.ts +238 -0
- package/dist/vike/+config.cjs.js +0 -66
- package/dist/vike/+config.cjs.js.map +0 -1
- package/dist/vike/+config.js.map +0 -1
- package/src/component.ts +0 -2301
- package/src/cycle/isolate/index.ts +0 -196
- package/src/cycle/run/index.ts +0 -151
- package/src/cycle/run/internals.ts +0 -143
- package/src/cycle/state/Collection.ts +0 -184
- package/src/cycle/state/index.ts +0 -5
- package/src/cycle/state/pickCombine.ts +0 -201
- package/src/cycle/state/pickMerge.ts +0 -122
- package/src/cycle/state/withState.ts +0 -47
- 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
|
+
}
|