Zum Inhalt springen
EN DE

Challenge 7.1: Custom Data Parts

Was wenn Du neben Text auch strukturierte Daten im Stream transportieren willst — z.B. einen Fortschrittsbalken, der anzeigt wie viele von 5 Recherche-Schritten abgeschlossen sind?

Frontend, API Route und LLM streamen Text-Deltas mit Custom Data Parts für Fortschrittsanzeige

Im Stream fliessen nicht nur Text-Chunks. Custom Data Parts lassen Dich strukturierte Daten — Fortschritt, Quellen, Status — parallel zum Text an das Frontend senden.

Ohne Custom Data Parts: Dein Stream liefert nur Text. Wenn Du dem User Fortschritt, Quellen oder Status anzeigen willst, musst Du diese Informationen entweder in den Text einbetten (haesslich, schwer parsbar) oder über einen separaten Kanal senden (komplex, Race Conditions).

Mit Custom Data Parts: Strukturierte Daten fliessen im selben Stream wie der Text. Das Frontend bekommt typisierte Objekte und kann sie direkt in der UI rendern — Fortschrittsbalken, Quellenangaben, Status-Badges. Ein Stream, ein Kanal, alles synchron.

Jeder Chunk im fullStream hat einen type. Die wichtigsten Standard-Typen:

TypeBeschreibungDaten
text-deltaText-Chunkchunk.textDelta
reasoningReasoning/Chain-of-Thoughtchunk.textDelta
sourceQuellenangabechunk.source
tool-callTool wird aufgerufenchunk.toolName, chunk.args
tool-resultTool-Ergebnischunk.toolName, chunk.result
dataCustom Data Partchunk.data (beliebiges Objekt)

Die ersten fuenf kennst Du teilweise aus Level 1 und 3. Der data-Type ist neu — damit sendest Du eigene strukturierte Daten.

Du sendest Custom Data Parts über sendDataPart auf dem Stream-Result oder über die onChunk/onStepFinish Callbacks. Der einfachste Weg ist mergeIntoDataStream in Kombination mit createDataStream:

import { createDataStream, streamText } from 'ai';
export async function POST(req: Request) {
const { messages } = await req.json();
const dataStream = createDataStream({
execute(dataStream) { // ← dataStream Controller
// Custom Data Part senden BEVOR der LLM-Stream startet
dataStream.writeData({ status: 'searching' }); // ← Beliebiges JSON-Objekt
const result = streamText({
model: anthropic('claude-sonnet-4-5-20250514'),
messages,
onFinish() {
dataStream.writeData({ status: 'done' }); // ← Nach Abschluss
},
});
result.mergeIntoDataStream(dataStream); // ← LLM-Stream einfuegen
},
});
return dataStream.toDataStreamResponse();
}

createDataStream oeffnet einen Kanal, in den Du sowohl eigene Daten (writeData) als auch den LLM-Stream (mergeIntoDataStream) einspeisen kannst. Das Frontend bekommt alles in der richtigen Reihenfolge.

Schicht 3: Custom Data Parts im Frontend konsumieren

Abschnitt betitelt „Schicht 3: Custom Data Parts im Frontend konsumieren“

Web-App Kontext: Der folgende Code zeigt Custom Data Parts in einer Next.js/React App. Deine TRY-Uebung weiter unten arbeitet im Terminal (CLI).

Im Frontend empfaengst Du Custom Data Parts über den data-Array im useChat-Hook:

'use client';
import { useChat } from '@ai-sdk/react';
export function Chat() {
const { messages, input, handleInputChange, handleSubmit, data } = useChat();
// data enthaelt alle Custom Data Parts als Array
const latestStatus = data?.at(-1); // ← Letztes Data Part
return (
<div>
{latestStatus?.status === 'searching' && (
<div className="status-bar">Suche laeuft...</div>
)}
{messages.map((m) => (
<div key={m.id}>
<strong>{m.role}:</strong> {m.content}
</div>
))}
{latestStatus?.status === 'done' && (
<div className="status-bar">Fertig!</div>
)}
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} />
</form>
</div>
);
}

Du kannst beliebige JSON-Objekte senden. Hier ein Beispiel mit Fortschrittsanzeige und Quellenangaben:

// Fortschritt senden
dataStream.writeData({
type: 'progress', // ← Eigener Typ zur Unterscheidung
current: 2,
total: 5,
label: 'Recherche-Schritt 2 von 5',
});
// Quellenangabe senden
dataStream.writeData({
type: 'source',
url: 'https://ai-sdk.dev/docs/streaming',
title: 'AI SDK Streaming Docs',
});
// Status-Update senden
dataStream.writeData({
type: 'status',
phase: 'analyzing',
timestamp: Date.now(),
});

Im Frontend filterst Du dann nach Typ:

const progressParts = data?.filter((d) => d.type === 'progress') ?? [];
const sourceParts = data?.filter((d) => d.type === 'source') ?? [];
const latestProgress = progressParts.at(-1);

Aufgabe: Baue einen Stream, der per createDataStream einen Status (“searching”, “analyzing”, “done”) UND den LLM-Stream sendet. Konsumiere die Data Parts im Terminal.

Erstelle die Datei custom-data-parts.ts:

import { createDataStream, streamText } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
// TODO 1: Erstelle einen dataStream mit createDataStream
// const dataStream = createDataStream({
// execute(dataStream) {
// // TODO 2: Sende ein Data Part mit status: 'searching'
// // dataStream.writeData({ ??? });
//
// // TODO 3: Starte streamText
// // const result = streamText({
// // model: anthropic('claude-sonnet-4-5-20250514'),
// // prompt: 'Erklaere Custom Data Parts im AI SDK.',
// // onFinish() {
// // // TODO 4: Sende ein Data Part mit status: 'done'
// // },
// // });
//
// // TODO 5: Merge den LLM-Stream in den dataStream
// // result.mergeIntoDataStream(dataStream);
// },
// });
// Für CLI-Test: Den dataStream als ReadableStream konsumieren
// const reader = dataStream.toDataStream().getReader();
// const decoder = new TextDecoder();
// while (true) {
// const { done, value } = await reader.read();
// if (done) break;
// console.log(decoder.decode(value));
// }

Checkliste:

  • createDataStream erstellt und execute Callback implementiert
  • Mindestens ein writeData Aufruf mit einem JSON-Objekt
  • streamText gestartet und in den dataStream gemerged
  • Data Part mit Status “done” wird in onFinish gesendet

Fuehre aus: npx tsx custom-data-parts.ts

Lösung anzeigen
import { createDataStream, streamText } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
const dataStream = createDataStream({
execute(dataStream) {
// Status: Suche startet
dataStream.writeData({ type: 'status', status: 'searching', timestamp: Date.now() });
const result = streamText({
model: anthropic('claude-sonnet-4-5-20250514'),
prompt: 'Erklaere Custom Data Parts im AI SDK in 3 Saetzen.',
onFinish() {
// Status: Fertig
dataStream.writeData({ type: 'status', status: 'done', timestamp: Date.now() });
},
});
result.mergeIntoDataStream(dataStream);
},
});
// Den Stream als ReadableStream konsumieren
const reader = dataStream.toDataStream().getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
process.stdout.write(chunk);
}
console.log('\n--- Stream beendet ---');

Erklärung: createDataStream oeffnet einen gemischten Kanal. writeData sendet Custom Data Parts als JSON, mergeIntoDataStream fuegt den LLM-Text-Stream ein. Im Output siehst Du abwechselnd Data Parts (als JSON-Zeilen) und Text-Chunks — alles in einem Stream, in der richtigen Reihenfolge.

Erwarteter Output (ungefaehr):

2:["status","searching"]
0:"Custom Data Parts "
0:"sind ein Mechanismus "
0:"im AI SDK..."
2:["status","done"]
--- Stream beendet ---

Die Zeilen mit 2: sind Data Parts (JSON-Objekte), die mit 0: sind Text-Chunks. Der genaue Text variiert (LLM-Output).

Combine: User Prompt und Status Parts fliessen über streamText() und createDataStream() in die Frontend UI

Uebung: Kombiniere Custom Data Parts mit dem streamText-Wissen aus Level 1.4. Baue einen Stream, der:

  1. Einen Status “thinking” sendet, bevor der LLM-Call startet
  2. Waehrend der LLM-Generierung einen Fortschritts-Counter alle 2 Sekunden hochzaehlt (via setInterval im execute Callback)
  3. Nach Abschluss den finalen Token-Verbrauch als Data Part sendet
  4. Den LLM-Text-Stream parallel dazu streamt

Optional Stretch Goal: Baue ein Frontend mit useChat, das den Fortschritts-Counter als animierten Fortschrittsbalken rendert und den Token-Verbrauch am Ende anzeigt.

Part of AI Learning — free courses from prompt to production. Jan on LinkedIn