Rust · WASM Node + Browser SSE · Streaming MIT

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.

gemini-3.7-flash · streamGenerateContent
Scroll to watch it work

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.

raw stream · as received● live

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.

one stream → two channelsrouted

Thinking

user wants the capital…
it's Paris, be brief

Answer

The capital of France
is Paris.

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.

near-duplicate detectiondeduped
Please double-check the result.
Please double check the result.
Please double-check the result.
×3 received → ×1 kept · similarity 0.96

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.

whitespace normalizeclean
before
Here····is···the answer.Thanks.
after
Here is the answer. Thanks.

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.

provider adaptersnormalized
Google Vertexparts[].thought · parts[].text
OpenRouterdelta.reasoning · delta.content
meta-llmdelta.content
↓
StreamChunk { text, kind } → { channel: "answer" | "thinking", text }

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.

O(1)work per chunk
0full-response buffering
2runtimes · Node & browser

Get started

Drop it into your stream loop.

$npm install llm-stream-reformat
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

Each event is { channel: 'answer' | 'thinking', text }. Providers: 'google', 'openrouter', 'metallm'.