Reshape the stream while it's still arriving.
llm-stream-reformat separates a reasoning model's thinking from its answer, collapses repetition loops, and cleans whitespace — inflight, before a single token reaches your UI.
The problem
Reasoning models don't hand you clean text.
They interleave a hidden thought-stream with the answer, stutter into repetition, and emit messy whitespace — all arriving token by token.
A black-box response opens only at the end. A stream you can shape per chunk.
01 Separate
Thinking on its own channel.
Every chunk is routed to answer or thinking. Show the reasoning, style it, or hide it — the answer stays clean either way.
Thinking
Answer
Google Vertex parts[].thought · OpenRouter delta.reasoning
02 Collapse
Repetition loops, gone.
Near-duplicate lines — a common stutter artifact — are detected with midstream's temporal-compare edit-distance similarity, then dropped.
Real crate: midstreamer-temporal-compare (DTW / LCS / EditDistance).
03 Normalize
Whitespace, tidied.
Runs of spaces and stray blank lines collapse to one clean pass — per completed line, as it streams.
Three dialects, one stream
Write your handler once.
Google Vertex, OpenRouter, and meta-llm each speak a different SSE dialect. Adapters normalize every one into a single neutral chunk stream.
How it works
One streaming pass. Nothing buffered.
It never waits for the whole response. Each chunk is transformed the moment it lands — then handed straight to your UI.
Get started
Drop it into your stream loop.
const { Reformatter } = require('llm-stream-reformat');
const rf = new Reformatter();
for (const line of sseLines) { // your provider's raw data: {...} lines
for (const ev of rf.pushSse('google', line)) {
if (ev.channel === 'thinking') showReasoning(ev.text);
else appendAnswer(ev.text);
}
}
rf.finish().forEach(ev => appendAnswer(ev.text)); // flush the tail
import { init, Reformatter } from 'llm-stream-reformat/web';
await init(); // auto-fetches the wasm
const rf = new Reformatter();
// then push provider lines exactly as above:
for (const ev of rf.pushSse('openrouter', line)) { /* … */ }
use stream_reformat::{Reformatter, Provider, Channel};
let mut rf = Reformatter::new();
for line in sse_lines {
for ev in rf.push_sse(Provider::Google, &line) {
match ev.channel {
Channel::Thinking => show_reasoning(&ev.text),
Channel::Answer => append_answer(&ev.text),
}
}
}
Each event is { channel: 'answer' | 'thinking', text }. Providers: 'google', 'openrouter', 'metallm'.