Skip to main content

ThinkStrip — Think-block filter for LLM streams

PyPI version Python versions License: MIT CI

thinkstrip removes <think>...</think> blocks from model output in both batch and streaming mode. It is designed for reasoning models (Qwen3, DeepSeek-R1, and others) that emit internal reasoning before their visible answer.

The streaming case is the reason this package exists: tag boundaries can split across adjacent token yields, so a correct implementation needs a stateful rolling buffer instead of a post-generation regex.


Why this exists

Reasoning models can emit output like:

<think>hidden chain of thought</think>The actual answer.

For fully materialized strings, stripping is easy. For token streams, it is not. Partial tags can arrive across multiple adjacent token yields, for example:

  • <thi then nk>
  • </thi then nk>

A naive .replace() or regex-per-token approach leaks fragments or drops visible output. thinkstrip solves this with a stateful streaming filter.


Install

Requirements:

  • Python 3.13+
  • Zero runtime dependencies — installs and runs with nothing beyond the standard library
pip install thinkstrip

Development install:

pip install -e ".[dev]"

Quick start

Streaming

from thinkstrip import ThinkStrip

stripper = ThinkStrip()
chunks   = []

for token in ['<thi', 'nk>', 'hidden', '</thi', 'nk>', 'The answer.']:
    if emitted := stripper.feed(token):
        chunks.append(emitted)

if flushed := stripper.flush():
    chunks.append(flushed)

print(''.join(chunks))
# The answer.

Batch

from thinkstrip import strip_think

clean = strip_think('<think>reasoning</think>The actual answer.')
print(clean)
# The actual answer.

Prompt pre-cleaner

Some GGUF chat templates inject <think> at the end of the rendered prompt before the model generates. This breaks the streaming filter because the model never emits its own <think>. Call strip_think_prefill on the rendered prompt to remove it:

from thinkstrip import strip_think_prefill

prompt = strip_think_prefill(prompt)
# trailing '<think>' removed if present; no-op otherwise

Public API

from thinkstrip import ThinkStrip, strip_think, strip_think_prefill

ThinkStrip

Stateful streaming filter. Create one instance per response stream.

ThinkStrip(
    open_tag:  str  = '<think>',
    close_tag: str  = '</think>',
    capture:   bool = False,
)
Method / property Description
.feed(token: str) -> str Process one token. Returns the text to emit (empty string when nothing ready yet).
.flush() -> str Call once at end-of-stream. Returns any buffered visible text. Empty if stream ended inside a think block.
.reset() -> None Reset to initial state. Use to process a second stream with the same instance.
.think_content: str Accumulated think-block text. Non-empty only when capture=True.
.in_think_block: bool True if the stream ended mid-think-block. Useful for diagnostics.
Constructor parameter Type Default Description
open_tag str <think> Opening tag to strip
close_tag str </think> Closing tag to strip
capture bool False Retain think content in .think_content instead of discarding

Buffer sizes are derived automatically: len(open_tag) - 1 chars for the opening-tag guard, len(close_tag) - 1 for the closing-tag guard. Custom tags carry no extra cost.

strip_think

Stateless helper for complete strings. Implemented via ThinkStrip — batch and streaming behavior are identical.

strip_think(
    text:      str,
    open_tag:  str = '<think>',
    close_tag: str = '</think>',
) -> str

strip_think_prefill

Removes a trailing open tag injected by some GGUF chat templates.

strip_think_prefill(
    prompt:   str,
    open_tag: str = '<think>',
) -> str

Capture mode

When capture=True, think content accumulates in .think_content instead of being discarded. Multiple think blocks per response are concatenated. Useful for surfacing the model's reasoning in a separate UI panel or for eval runs.

stripper = ThinkStrip(capture=True)

for token in stream:
    if emitted := stripper.feed(token):
        yield emitted

if flushed := stripper.flush():
    yield flushed

print(stripper.think_content)  # full reasoning text

Limitations

  • Nested tags are not supported. A <think> that arrives while already inside a think block is treated as think content and swallowed. The first </think> closes the block; any subsequent </think> with no matching open tag passes through as visible text. In practice this does not occur — Qwen3 and DeepSeek-R1 emit exactly one think block per response.

Development

git clone https://github.com/informity/thinkstrip.git
cd thinkstrip

python3 -m venv .venv
source .venv/bin/activate

pip install -e ".[dev]"

make lint
make test
make build

Contributing

See CONTRIBUTING.md.


License

MIT — see LICENSE.

Metadata

Release files for thinkstrip 0.2.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for thinkstrip 0.2.2
File Size Uploaded
thinkstrip-0.2.2.tar.gz 12.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for thinkstrip 0.2.2
File Interpreter ABI Platform
thinkstrip-0.2.2-py3-none-any.whl Python 3 none any Details

Total release size: 19.6 kB

Release files / thinkstrip-0.2.2.tar.gz

Download URL thinkstrip-0.2.2.tar.gz
Size 12.3 kB
Tags Source
SHA-256 checksum
How to use checksums
21fdd8fcd346297d904acd4fdb9c98eb2577288f44542f4dac98a7b0c14c60fe
BLAKE2b-256 checksum
How to use checksums
26c2af715f7a8ea01a65befb23f417d2aebe0df873ef1044a54a52d5c8a14dc2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Mar 26, 2026.

Transparency log

Release files / thinkstrip-0.2.2-py3-none-any.whl

Download URL thinkstrip-0.2.2-py3-none-any.whl
Size 7.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
da584dab011a30d5e5088c9002afa096f5e08f1a815237e8f1054b4bdac8ba51
BLAKE2b-256 checksum
How to use checksums
a8de81fb323c60e7addc4a569ea7979066ec0d954e20ab25553a8533fbcd5f64
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Mar 26, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.2 This release

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page