useChat speaks two protocols. Mismatching client and server is the number one streaming parse error.
UI message stream (default, what you want 95% of the time):
- Server: createUIMessageStreamResponse({ stream: toUIMessageStream({ stream: result.stream }) }) or result.toUIMessageStreamResponse().
- Client: useChat({ transport: new DefaultChatTransport({ api: '/api/chat' }) }).
- Carries text, tool calls, tool results, reasoning, sources, data parts, and error parts. Required for anything with tools.
Text stream (simple cases, custom backends):
- Server: createTextStreamResponse({ stream: toTextStream({ stream: result.stream }) }).
- Client: useChat with new TextStreamChatTransport({ api: '/api/chat' }).
- Plain text chunks only. No tool calls, no structured parts. Use it when a non-AI-SDK backend (Python FastAPI, etc.) serves the stream, or for useCompletion-style single responses.
Rules:
1. Default to the UI message stream. It is the only protocol that supports tools, and useChat expects it unless you say otherwise.
2. If the client shows 0:"..." chunks or Failed to parse stream string, the server is sending the UI message protocol to a client expecting raw text, or vice versa. Align the pair.
3. Custom backends in other languages should implement the text stream protocol; it is just chunked text. The UI message stream protocol is documented for custom frontends too.
4. useObject uses its own object-streaming protocol over a text stream; do not point useChat at a useObject endpoint.
5. When proxying streams (nginx, CF), the protocol does not matter, but buffering must be off for both.