Skip to main content

ai_ui_stream 1.0.1 copy "ai_ui_stream: ^1.0.1" to clipboard
ai_ui_stream: ^1.0.1 copied to clipboard

Transport-agnostic Dart client for the AI SDK UI Message Stream protocol: SSE parser, message reducer and chat controller with tool approval.

ai_ui_stream #

CI pub package

A pure-Dart client for the AI SDK UI Message Stream protocol (ai v7), the SSE format behind useChat. Point it at your chat endpoint and you get typed messages that update as the model streams, with tool calls, reasoning, sources, files, data parts and human-in-the-loop tool approval. No UI framework, no backend assumptions. Not affiliated with Vercel.

Install #

dart pub add ai_ui_stream

Quick start #

import 'package:ai_ui_stream/ai_ui_stream.dart';
import 'package:ai_ui_stream/http.dart';

final chat = ChatController(
  transport: HttpChatTransport(
    endpoint: Uri.parse('https://api.example.com/chat'),
    headers: () async => {'authorization': 'Bearer ${await getToken()}'},
  ),
);

chat.snapshots.listen((snapshot) => render(snapshot.messages));

await chat.sendText('Hello!');

Render the parts #

Each UIMessage holds a list of UIParts that grow as the stream arrives. UIPart is sealed, so a switch is exhaustive:

Widget renderPart(UIPart part) => switch (part) {
      TextUIPart(:final text) => Text(text),
      ReasoningUIPart(:final text) => Text(text, style: dim),
      ToolUIPart(:final toolName, :final state) => ToolCard(toolName, state),
      FileUIPart(:final url) => Image.network(url),
      SourceUrlUIPart(:final url, :final title) => Link(url, title),
      _ => const SizedBox.shrink(),
    };

snapshot.status is submitted while waiting for the first byte, streaming during the reply, then ready or error (with snapshot.error).

ChatController #

Method Does
sendText(text) / sendMessage(message) Sends a user message (text, or parts of any type).
regenerate({messageId}) Re-answers with trigger regenerate-message: drops the last reply, or the given assistant message (or what follows a given user message).
addToolOutput / addToolOutputError Records a client tool's result locally; send with continueTurn().
continueTurn() Resends the transcript without a new message.
resumeStream() Reattaches to a running stream via ChatTransport.reconnect; no-op if it returns null.
respondToApprovals(decisions) Answers pending tool approvals and resends (see below).
addToolApprovalResponse(...) Records one approval decision locally, without sending.
clearError() Moves an error status back to ready.
stop() Cancels the turn and aborts the request; lastTurnAborted is set.
removeLastExchange() / seedMessages(...) Drops the last exchange / loads history.

Every send takes optional extraBody and extraHeaders. Pass onFinish (ChatFinishDetails) and onError to the constructor to observe each turn's end, and idGenerator if your backend needs UUID ids.

Send methods never throw for a failed turn: they complete when the turn ends, and the failure is on status and error. A turn fails with ChatProtocolException unless the response is 2xx, text/event-stream and carries x-vercel-ai-ui-message-stream: v1, or when the stream ends without a finish or abort chunk.

Tool approval #

A tool-approval-request chunk moves the tool part to ToolState.approvalRequested with a ToolApproval and lists it in chat.pendingApprovals (automatic approvals excluded). Answer with respondToApprovals:

await chat.respondToApprovals([
  for (final approval in chat.pendingApprovals)
    ApprovalDecision(
      approvalId: approval.approvalId,
      toolCallId: approval.toolCallId,
      toolName: approval.toolName,
      args: approval.input as Map<String, dynamic>,
      approved: true, // or false, with an optional `reason`
    ),
]);

Each part moves to approvalResponded and the transcript is resent with no new user message; the server continues the same assistant message, ending the part in outputAvailable or outputDenied. Pass userText to append a user message instead, for backends that reject a transcript ending in an assistant turn. For backends that predate the approval chunk, ChatController(inferApprovalOnFinish: true) treats a client tool still waiting on its input at finish as awaiting approval, using the toolCallId as the approval id.

Transport #

HttpChatTransport lives in package:ai_ui_stream/http.dart so the core library stays free of package:http. It POSTs {id, messages, trigger, ...} as JSON with accept: text/event-stream, content-type: application/json and x-vercel-ai-ui-message-stream: v1; your headers() merge over them, and body: replaces the default body builder. stop() aborts the in-flight request (AbortableRequest, package:http >= 1.5). Without a client, the transport owns one and closes it on dispose(). See example/main.dart for a runnable CLI.

To use another HTTP client, auth scheme or body shape, extend ChatTransport:

import 'dart:convert';

import 'package:ai_ui_stream/ai_ui_stream.dart';
import 'package:http/http.dart' as http;

class MyTransport extends ChatTransport {
  final _client = http.Client();

  @override
  Future<ChatResponse> send(
    ChatRequest request, {
    required Future<void> abort,
  }) async {
    final req = http.AbortableRequest(
      'POST',
      Uri.parse('https://api.example.com/chat'),
      abortTrigger: abort,
    )
      ..headers['content-type'] = 'application/json'
      ..headers['accept'] = 'text/event-stream'
      ..body = jsonEncode({
        'id': request.conversationId,
        'trigger': request.trigger,
        'messages': request.messages.map((m) => m.toJson()).toList(),
      });
    final res = await _client.send(req);
    return ChatResponse(
      statusCode: res.statusCode,
      headers: {
        for (final e in res.headers.entries) e.key.toLowerCase(): e.value,
      },
      body: res.stream,
    );
  }

  @override
  void dispose() => _client.close();
}

ChatTransport.reconnect (default null) backs resumeStream(); extend rather than implement to inherit it.

Lower-level pieces #

ChatController is built from parts you can use on their own: UiMessageStreamParser turns SSE bytes into UIStreamEvents, and MessageReducer.apply folds one event into a list of UIMessages.

Supported protocol #

Every chunk type of the UI Message Stream is parsed; unknown ones are not dropped.

Chunk Event Result
start, message-metadata, finish StartEvent, MessageMetadataEvent, FinishEvent Message id and metadata (shallow-merged)
start-step, finish-step, reset-step StartStepEvent, FinishStepEvent, ResetStepEvent StepStartUIPart; reset-step drops the step's parts
abort AbortEvent Turn ends ready; lastTurnAborted
error ErrorEvent Turn ends in error with AiStreamException
text-start / -delta / -end TextStartEvent, ... TextUIPart
reasoning-start / -delta / -end ReasoningStartEvent, ... ReasoningUIPart
source-url, source-document AppendPartEvent SourceUrlUIPart, SourceDocumentUIPart
file, reasoning-file AppendPartEvent FileUIPart (isReasoning)
tool-input-start / -delta / -available ToolInput*Event ToolUIPart (static and dynamic-tool)
tool-input-error ToolInputErrorEvent ToolUIPart in outputError, raw input kept
tool-output-available / -error / -denied ToolOutput*Event outputAvailable / outputError / outputDenied
tool-approval-request / -response ToolApprovalRequestEvent, ToolApprovalResponseEvent ToolUIPart.approval
data-* DataEvent DataUIPart, or dataEvents only if transient
custom and any unknown type UnknownStreamEvent Surfaced as an event only; the reducer leaves the message unchanged
[DONE] DoneEvent Clean end of stream

A data-* chunk becomes a DataUIPart, replacing an earlier one with the same type and id. Transient chunks never touch the message. Listen to ChatController.dataEvents to see every DataEvent, transient ones included (for progress indicators, heartbeats and the like).

Tests run against byte-exact fixtures generated from ai@7.0.133; see test/fixtures/README.md.

License #

Apache-2.0. See LICENSE.

1
likes
160
points
--
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Transport-agnostic Dart client for the AI SDK UI Message Stream protocol: SSE parser, message reducer and chat controller with tool approval.

Repository (GitHub)
View/report issues

Topics

#ai #sse #streaming #chat #vercel-ai-sdk

License

Apache-2.0 (license)

Dependencies

http

More

Packages that depend on ai_ui_stream