Skip to content

the intake filter

Never let the bytes into the cache

The one position that pays nothing.

A pruner edits a conversation that is already cached, so it pays 1.9·S − 2·D once and has to earn it back. winnow filter is a local pass-through proxy that keeps the bytes out of the cached prefix in the first place: a tool result a rule would strip is sent in full on the one request the model acts on it, placed after the last cache_control breakpoint so the API never writes it to cache, and is dropped on the next request.

It runs. winnow filter is src/winnow/filter.py and proxy.py — stdlib only, about 1,350 lines with 804 tests, of which a property test over generated conversations asserts the cache-write invariant and a golden pins the emitted bytes.

position

Where it acts in a turn

Position is the whole mechanism. The full send sits past the last cache_control breakpoint, so the API never writes it — and what is never written is never read back at 0.1× on any later turn either.

Nothing in the cached prefix is edited, so nothing is invalidated. That is the difference the arithmetic turns on, and it is a difference of position rather than of policy: the rules are the same rules.

Illustration of where the filter acts in a turn. Five requests go out: two carry a tool result in full, placed past the last cache_control breakpoint, and three carry a pointer instead, each marked with the winnow rule that fired — C1, C3 and B2. A meter then splits one request into the cached prefix, read at a tenth of the input rate, and the uncached tail the full result sits in. A pruner instead edits the prefix and pays 1.9 S minus 2 D once; that half is hatched, because winnow fork writes it but no fork has been resume-tested. Illustration, on invented request numbers and proportions. Only the three rules needing no hindsight can fire on the wire — C1, C3 and B2. C2, B1 and A1 all need to see the conversation's future, and a policy that did would change the prefix under the cache. The hatched half is software you can run — winnow fork writes it — but the 100-fork resume test has never produced a result.

rules

Three of the six can fire on the wire

The filter reaches less than the pruner, and the reason is structural rather than a matter of tuning: a rule that has to see the conversation's future cannot run on a request that is going out now.

C1 locator
Glob and LS, and Grep where output_mode is files_with_matches or count. The output is a list of paths whose only consumer is the call that followed it.
C3 passing verification
Bash matching the verification pattern — npm test, pytest, go test, cargo test, tsc, ruff, mypy and their neighbours — and only where is_error is false. A failing verification is never stripped: the failure is the information.
B2 Bash inspection
Bash where the first token of the first segment before && or | matches the inspection pattern, so a pipeline headed by ls or git status counts and one headed by python does not, whatever follows.

And three of them cannot

Each of these decides what to strip by looking at something that has not happened yet. A policy that did would also change the prefix under the cache, which is the cost the filter exists to avoid.

C2 exact duplicate
Strips the earlier of two byte-identical results, which means knowing a later one arrives.
B1 superseded read
Strips a Read because a later Read covers it. The later one has not happened yet.
A1 read then written
Strips a Read because a later Edit or Write made it stale. Opt-in even in the pruner, and the rule docs/SPEC.md section 4 itself calls the most likely to be wrong.

Section 4 of docs/SPEC.md sets five universal guards, applied before any rule at any tier: the last results in the session are kept, a result under --min-bytes is kept, is_error: true is never stripped, a pointer longer than the content it replaces is not written, and every tool_use in the output keeps its matching tool_result. The filter's own floor is 256 bytes rather than the pruner's 2,048, because it sends a candidate once in full and the pointer then lives in the cached prefix.

the cost model

What it avoids, term by term

Per result of D tokens over T following turns, the baseline is a 2.0× cache write plus a 0.1× read on every later turn. The filter pays 1.0× once and nothing after.

There is no break-even term. It is cheaper from the first request, at every S/D, which is the one thing the pruner cannot say.

The terms stay apart rather than blending into one number, because that is how winnow savings reads them out — the avoided write and the avoided reads are different quantities and only one of them grows with the length of the session.

Illustration of the intake filter's cost model with its terms left apart. Doing nothing costs a 2.0 times cache write once plus a 0.1 times read on every later turn. The filter pays 1.0 times once and nothing after. What it avoids is therefore two terms — the write and the reads — and there is no third, because there is no break-even term to pay off. A pruner removing the same result pays 1.9 S minus 2 D once and earns it back over 19 times S over D minus 20 turns. The README's cost model, per result, with the terms unblended — which is how winnow savings reads them out. D and T are left symbolic: the model is stated for any result over any number of following turns, and supplying a number for either would be one this site invented.

the replay

The table is a simulation

The 8.21% comes from replaying the three no-hindsight rules over 175 historical sessions — what the filter would have done, not a record of anything it did. It is stated here with that attached, because a share of a bill reads like an invoice line and this one is not.

It also describes a floor the filter has left behind. The replay used --min-bytes 2048, inherited from the pruner; the filter now ships 256, because it sends a candidate once in full and the pointer then lives in the cached prefix, so its break-even is between one and two pointer lengths rather than two thousand bytes. Re-measured over one install's 869 transcripts, moving the floor takes the reach from 8.03% to 10.82% and the net up 31% at 224 following turns. The dollar figures below have not been recomputed.

A replay over 175 historical sessions at a 2,048-byte floor, from README.md, 'The intake filter', with the detail in docs/COZEMPIC.md section 3.5. Modelled, not billed. The ratio is 1.1×, and what separates the two is variance rather than size: the filter cannot be negative.
what was replayedreachesshare of the billsessions where it pays
intake filter8.21%+3.76%175 of 175
pruner, tier CB10.17%+3.27%97 of 168

Running both is possible and nearly pointless. The filter takes the shared mass first, leaving the pruner 2.2% against an unchanged S.

the instrument

What the ledger says it did

winnow savings reads ~/.winnow/filter.jsonl, joins each line to the Claude Code transcript on request_id to recover which session it belongs to and how many API turns followed it, and prices it. The two numbers are not comparable and the command does not try: the simulation is a corpus average, this is one install's ledger over however long it has been on.

It is src/winnow/savings.py — stdlib only, about 575 lines with 34 tests. The readout splits the avoided write from the avoided reads rather than blending them, and names the lines it could not join or could not price.

python -m winnow savings

--json for the machine-readable form. The figure is modelled, not billed, and the command says so in its own output: the bytes were never sent, so no invoice line corresponds to them.

The two things it has to get right

The filter is stateless. It re-drops the same result on every later request that still carries it, so a ledger of 1,283 removal events on one install holds 49 distinct results — summing bytes_dropped over lines would report 27× what was removed. The repeats are not removals; they are the 0.1·D·T term, and are priced at 0.1×. De-duplication is on tool_use_id, with a conservative (tool, rule, bytes) fallback for lines written before that field existed.

One API request is one turn, however many records it left on disk. Claude Code writes a response as one record per content-block group — the text, then each tool_use — and stamps every one of them with the same requestId and the same message.usage. Counting records instead of requests inflates both T and the bill it is compared against, by 1.7 to 2.4× on that install's transcripts.

before you run it

It is in your credential path

It relays your auth headers upstream, holds none of its own and logs none — but an operator running it has put a process of their own in front of their own key.

It refuses to start without WINNOW_FILTER=1 for that reason, and forwards the original bytes unchanged on any failure to parse or rewrite: it must not be the thing that breaks a run.

export WINNOW_FILTER=1python -m winnow filter --ledger ~/.winnow/filter.jsonlexport ANTHROPIC_BASE_URL=http://127.0.0.1:8789

The third line is what the proxy prints. Point the client at it and every request goes through the filter.

turning it off

The two ways are not equivalent

Set the toggle blank and restart for the full off. On a running install, what can be turned off is the rewriting and not the proxy: touch ~/.winnow/filter-off and the next request is relayed untouched. Remove the file to resume.

Killing the process is not the off switch. ANTHROPIC_BASE_URL is fixed in a client's environment when it starts, so a listener that goes away takes every request with it.

touch ~/.winnow/filter-off