@effect-uai/ai-sdk
v0.11.0
Published
Vercel AI SDK (useChat) UI Message Stream compatibility for @effect-uai/core.
Maintainers
Readme
@effect-uai/ai-sdk
Vercel AI SDK compatibility for effect-uai. Keep your @ai-sdk/react
useChat frontend exactly as it is and serve it from an effect-uai agent
loop instead of streamText.
Two functions bridge the two worlds:
Messages.decodeMessagesturns theUIMessage[]auseChatclient POSTs into effect-uaiHistoryItems.UIMessageStream.toUIMessageStreamprojects the loop'sInteractionEventstream onto the AI SDK UI Message Stream protocol (v1), emittingSSE.Events.
The package owns no HTTP layer: it produces a Stream<SSE.Event> and the
required responseHeaders, so it drops into any server.
Install
pnpm add @effect-uai/ai-sdkA chat route
decodeMessages in, run your loop, toUIMessageStream out. Provide your
provider layer so the stream has no remaining requirements, then hand the
bytes to whatever server you run.
import * as Messages from "@effect-uai/ai-sdk/Messages"
import * as UIMessageStream from "@effect-uai/ai-sdk/UIMessageStream"
import * as SSE from "@effect-uai/core/SSE"
import { Stream } from "effect"
export async function POST(request: Request): Promise<Response> {
const { messages } = await request.json()
const history = Messages.decodeMessages(messages)
const events = agent(history).pipe(Stream.provide(OpenAILayer)) // your loop, unchanged
const body = events.pipe(
UIMessageStream.toUIMessageStream(crypto.randomUUID()),
SSE.toBytes,
Stream.toReadableStream,
)
return new Response(body, { headers: UIMessageStream.responseHeaders })
}With @effect/platform
Same stream, wrapped in an HttpServerResponse instead of a web Response:
import { HttpServerResponse } from "@effect/platform"
const body = events.pipe(UIMessageStream.toUIMessageStream(id), SSE.toBytes)
return HttpServerResponse.stream(body, { headers: UIMessageStream.responseHeaders })Coverage
Outbound, text and reasoning deltas, tool calls (input streaming plus
resolved output), and refusals map to their protocol parts. Alongside the
loop's InteractionEvents, toUIMessageStream also accepts dataPart and
messageMetadata emissions, so callers can interleave custom typed data
(data-<name>, optionally transient) and settled per-message metadata:
events.pipe(
Stream.map((e) => (Metrics.isThroughput(e) ? dataPart("metrics", e, { transient: true }) : e)),
UIMessageStream.toUIMessageStream(id),
)Inbound, decodeMessages reconstructs text, image (file with an image/*
media type), and tool parts: an assistant tool call becomes a function_call
item plus a function_call_output once the client carries its result.
Non-image files and unresolved tool states are dropped.
