ThinkStrip — Think-block filter for LLM streams
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:
<thithennk></thithennk>
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)
| File | Size | Uploaded | |
|---|---|---|---|
| thinkstrip-0.2.2.tar.gz | 12.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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