Deuz SDK
Modules

UI Streaming

The Deuz UI wire — our own versioned SSE protocol for streaming a StreamChatResult from server to browser.

@deuz-sdk/core ships its own UI streaming protocol. The server serializes a streamChat result to Server-Sent Events with the toDeuzStreamResponse helper; the browser reads it back as typed parts with readDeuzStream. This is our wire — the canonical fullStream is reframed into stable, versioned UI parts, never a raw provider stream proxied through.

Both helpers live in the @deuz-sdk/core/ui subpath and are edge-safe (Web APIs only, no Node imports), so the server side runs on Next.js Edge, Cloudflare Workers, or Deno.

why.ts
import { toDeuzStreamResponse } from '@deuz-sdk/core/ui'; // server
import { readDeuzStream } from '@deuz-sdk/core/ui'; // client

Server: toDeuzStreamResponse

Takes a StreamChatResult (the synchronous return of streamChat) and returns a Response whose body is the Deuz SSE wire. It iterates result.fullStream, maps each canonical StreamPart to a UI part, and frames each as one data: <json>\n\n SSE event.

function toDeuzStreamResponse(
  result: StreamChatResult,
  options?: ToDeuzStreamOptions,
): Response;
OptionTypeDefaultNotes
messageIdstringId emitted in the leading start part.
generateId() => stringUsed when messageId is omitted. Pass deps.generateId for a real id.
headersRecord<string, string>Extra response headers, merged after the protocol headers.

If neither messageId nor generateId is given, the id falls back to the literal 'deuz-msg'.

The response is fixed at status: 200 with these headers (your options.headers are merged in last):

HeaderValue
content-typetext/event-stream; charset=utf-8
cache-controlno-cache
x-deuz-streamv1 (the wire version)

The wire version is exported as the constant DEUZ_STREAM_VERSION (currently 'v1'). The stream always ends with a literal data: [DONE]\n\n sentinel after the last part.

Client: readDeuzStream

Takes the Response (e.g. from fetch) and returns an async generator of DeuzUIPart. It parses the SSE body, stops at the [DONE] sentinel, and silently skips any malformed JSON line.

function readDeuzStream(response: Response): AsyncGenerator<DeuzUIPart>;

If response.body is null, it yields nothing and returns immediately.

Wire part types

DeuzUIPart is a discriminated union on type. Keep a default case when switching — the union is additive and may grow in future versions.

typeFieldsEmitted when
startmessageId: stringFirst — always the leading part.
step-startstep: numberAn agentic loop step begins.
step-finishstep: number, finishReason: FinishReason, usage: UsageA loop step ends.
text-deltatext: stringA chunk of assistant text.
reasoning-deltatext: string, signature?: stringA chunk of reasoning / thinking text.
tool-input-deltatoolCallId: string, toolName?: string, delta: stringA raw fragment of a tool call's argument JSON.
tool-calltoolCallId: string, toolName: string, input: unknownA fully parsed tool call.
tool-resulttoolCallId: string, toolName: string, output: unknown, isError?: booleanA tool finished executing (isError: true on a failed/self-healed tool).
tool-approval-requestapprovalId: string, toolCallId: string, toolName: string, input: unknownA gated tool call awaits the user's verdict — the loop broke; resume with approvalResponses. See the approval round-trip.
tool-approval-responseapprovalId: string, approved: boolean, reason?: stringClient→server direction ONLY (declared for wire symmetry): the verdict travels in the next HTTP request body as approvalResponses — the server never serializes this part.
object-deltaobject: unknownA streamObject partial (from toDeuzObjectStreamResponse) — each delta REPLACES the previous partial wholesale. useObject consumes it.
sourceid: string, url?: string, title?: stringA cited source.
finishfinishReason: FinishReason, usage: UsageLast meaningful part — the whole turn finished.
errormessage: stringA stream error occurred; message is redacted of secrets.

Usage and FinishReason are the canonical types from the core (usage breakdown). The tool-input-delta fragments accumulate into the input you receive on the matching tool-call — render the deltas for a live "typing the arguments" effect, then swap to the final input.

Backpressure and cancellation

  • Backpressure is the SSE ReadableStream's own: the server pulls the next fullStream part only when the stream controller asks for it, so a slow client throttles upstream consumption naturally.
  • Cancellation flows through the SSE reader. If the consumer breaks out of the for await over readDeuzStream early, the underlying response reader is cancelled in a finally block, which closes the body. To cancel the upstream provider request itself, pass an AbortSignal into streamChat (see Abort) — aborting it resolves the turn with finishReason: 'aborted'.

Error handling

toDeuzStreamResponse never lets the stream throw at the consumer. Any error thrown while draining fullStream is caught and emitted as a single error part, after which the [DONE] sentinel still closes the stream cleanly. The message is passed through the core's secret redactor, so API keys and bearer tokens never reach the browser. Handle it on the client by matching type: 'error'.

Example: Next.js route handler

Read keys from process.env at the app layer and pass them into the factory — the SDK core never reads env itself.

app/api/chat/route.ts
import { streamChat } from '@deuz-sdk/core';
import { createAnthropic } from '@deuz-sdk/core/anthropic';
import { toDeuzStreamResponse } from '@deuz-sdk/core/ui';

export const runtime = 'edge';

const anthropic = createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });

export async function POST(req: Request): Promise<Response> {
  const { messages } = await req.json();
  const result = streamChat({
    model: anthropic('claude-opus-4-8'),
    messages,
  });
  return toDeuzStreamResponse(result, { messageId: crypto.randomUUID() });
}

Example: vanilla React consumer

For most apps, use the ready-made useChat / useObject hooks over this wire. The manual consumer below shows what they do under the hood — useful for custom integrations or non-React frameworks. It accumulates text, renders tool calls live, and surfaces the error part.

Chat.tsx
import { useState } from 'react';
import { readDeuzStream } from '@deuz-sdk/core/ui';

type ToolCall = { id: string; name: string; input: unknown };

export function Chat() {
  const [text, setText] = useState('');
  const [tools, setTools] = useState<ToolCall[]>([]);
  const [error, setError] = useState<string | null>(null);

  async function send(prompt: string) {
    setText('');
    setTools([]);
    setError(null);

    const res = await fetch('/api/chat', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ messages: [{ role: 'user', content: prompt }] }),
    });

    for await (const part of readDeuzStream(res)) {
      switch (part.type) {
        case 'text-delta':
          setText((t) => t + part.text);
          break;
        case 'tool-call':
          setTools((prev) => [
            ...prev,
            { id: part.toolCallId, name: part.toolName, input: part.input },
          ]);
          break;
        case 'error':
          setError(part.message);
          break;
        // additive union — ignore the rest (start, step-*, tool-result, finish, ...)
        default:
          break;
      }
    }
  }

  return (
    <div>
      {error ? <p role="alert">{error}</p> : null}
      <pre>{text}</pre>
      <ul>
        {tools.map((t) => (
          <li key={t.id}>
            {t.name}: {JSON.stringify(t.input)}
          </li>
        ))}
      </ul>
      <button onClick={() => send('What is the weather in Paris?')}>Ask</button>
    </div>
  );
}

Rendering tool calls live

For a "tool is being called" indicator before arguments finish, accumulate tool-input-delta fragments by toolCallId, then replace with the final input when the tool-call arrives and mark it done when the matching tool-result lands.

accumulate.ts
import { readDeuzStream } from '@deuz-sdk/core/ui';

const pending = new Map<string, { name?: string; args: string }>();

for await (const part of readDeuzStream(res)) {
  switch (part.type) {
    case 'tool-input-delta': {
      const entry = pending.get(part.toolCallId) ?? { args: '' };
      if (part.toolName) entry.name = part.toolName;
      entry.args += part.delta;
      pending.set(part.toolCallId, entry); // render entry.args as live JSON
      break;
    }
    case 'tool-call':
      // arguments complete and parsed — render part.input
      break;
    case 'tool-result':
      pending.delete(part.toolCallId); // render part.output (part.isError flags failures)
      break;
    default:
      break;
  }
}
  • streamChat — the source StreamChatResult and its canonical fullStream.
  • Messages & Parts — the canonical message shape you send.
  • Tool Loop — how tool-call / tool-result parts are produced.

On this page