Skip to main content

Adapters

The components never call a model, so something has to turn what your provider sends into AssistantEvents. Three adapters do, without importing the provider's SDK: their inputs are duck-typed shapes, so @zuilib/ai stays dependency-free and the adapters keep working across SDK versions that share the wire format.

import {fromAISDKStream, fromAISDKUIMessages} from '@zuilib/ai/adapters/ai-sdk'
import {fromAnthropicStream, fromAnthropicMessage, anthropicToolResult} from '@zuilib/ai/adapters/anthropic'
import {fromSSE, parseSSE} from '@zuilib/ai/adapters/sse'

Every from*Stream is an async generator of AssistantEvents that ends at done, so it is directly a RunResult for an AssistantDock suggestion or for useThread().consume.

AI SDK

fromAISDKStream(chunks) reads the UI message stream behind useChat (v5 chunk objects: text-delta, tool-input-start, tool-input-available, tool-output-available, tool-output-error, source-url, error, finish) or the v4 data stream protocol as lines (0:"…", 9:{…}, a:{…}, 3:"…", d:{…}). A tool is a queued tool-call at input start and a running one with args once its input is available; sources are gathered into one citation event before done.

fromAISDKUIMessage(message) / fromAISDKUIMessages(messages) map finished UIMessages (v5 parts, with v4 content and tool-invocation tolerated) to Messages for a Thread. createAISDKMapper() is the stateful chunk-to-events function and parseUIMessageStreamLine the v4 line parser, for a custom loop.

const {messages} = useChat()
<Thread messages={fromAISDKUIMessages(messages)} />

Anthropic Messages API

fromAnthropicStream(events) reads MessageStreamEvents: content_block_start (a text block, or a tool_use block that becomes a queued tool-call), content_block_delta (text_delta → text, input_json_delta gathered per block), content_block_stop (the tool call becomes running with its parsed args), message_stopdone, and error. The model's tool result comes from your app: push anthropicToolResult(toolUseId, result, {error?, elapsedMs?}) after running the tool. fromAnthropicMessage(message) maps a finished Message, or a user turn carrying tool_result content blocks, to a ZUI Message. createAnthropicMapper() is the stateful event-to-events function.

run: ({signal}) => fromAnthropicStream(client.messages.stream({...}, {signal}))

Server-sent events

parseSSE(source) reads a fetch Response, a ReadableStream<Uint8Array> or any iterable of strings / bytes into {event?, data, id?, retry?} messages, handling multi-line data:, comments and a CRLF split across chunks. fromSSE(source, map?) maps them to events. The default map: [DONE]done; an error event → error; JSON data with a known type is the event as is; JSON with a delta, text or content string, a JSON string, or plain text under no event / a text or message event → a text delta; anything else is skipped. Give map to read your own gateway's format.

run: ({signal}) => fromSSE(fetch('/api/assist', {method: 'POST', body: JSON.stringify(input), signal}))

Writing one

An adapter is a function from your stream to an async iterable of AssistantEvents (@zuilib/ai/stream-events): {type: 'text', delta}, {type: 'tool-call', id, toolName, args?, status?}, {type: 'tool-result', callId, result?, error?, elapsedMs?}, {type: 'approval', id, title, …}, {type: 'diff', id, original?, modified?, hunks?, patch?}, {type: 'citation', sources}, {type: 'error', message}, {type: 'done', summary?}. Nothing else is required.