prefixwise 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- prefixwise-0.3.0/.gitignore +13 -0
- prefixwise-0.3.0/CHANGELOG.md +31 -0
- prefixwise-0.3.0/LICENSE +21 -0
- prefixwise-0.3.0/PKG-INFO +290 -0
- prefixwise-0.3.0/README.md +266 -0
- prefixwise-0.3.0/SECURITY.md +17 -0
- prefixwise-0.3.0/docs/USE_CASES.md +143 -0
- prefixwise-0.3.0/examples/openai_agent.py +41 -0
- prefixwise-0.3.0/examples/session.jsonl +4 -0
- prefixwise-0.3.0/examples/turn1.json +41 -0
- prefixwise-0.3.0/examples/turn2.json +49 -0
- prefixwise-0.3.0/experiments/dogfood/2026-11-06-opencode.md +30 -0
- prefixwise-0.3.0/experiments/e1_hosted/README.md +41 -0
- prefixwise-0.3.0/experiments/e1_hosted/e1_hosted.py +505 -0
- prefixwise-0.3.0/experiments/e1_hosted/runs/2026-11-06-anthropic-claude-haiku-5-5.md +44 -0
- prefixwise-0.3.0/experiments/e1_hosted/runs/2026-11-06-openai-probes-round3.md +22 -0
- prefixwise-0.3.0/experiments/e1_hosted/runs/2026-11-06-probes.md +56 -0
- prefixwise-0.3.0/experiments/e1_hosted/runs/2026-11-06-round4.md +43 -0
- prefixwise-0.3.0/experiments/e1_open/README.md +84 -0
- prefixwise-0.3.0/experiments/e1_open/colab_e1_open.ipynb +1 -0
- prefixwise-0.3.0/experiments/e1_open/e1_open.py +418 -0
- prefixwise-0.3.0/experiments/e1_open/runs/2026-11-06-colab-l4-qwen3-4b.json +1499 -0
- prefixwise-0.3.0/experiments/e1_open/runs/2026-11-06-colab-l4-qwen3-4b.md +20 -0
- prefixwise-0.3.0/experiments/e1_open/vllm_server.sh +79 -0
- prefixwise-0.3.0/experiments/e4/README.md +38 -0
- prefixwise-0.3.0/experiments/e4/e4.py +357 -0
- prefixwise-0.3.0/experiments/e4/runs/2026-11-06-live.md +56 -0
- prefixwise-0.3.0/experiments/e4/sessions/tool-order-drift.jsonl +60 -0
- prefixwise-0.3.0/pyproject.toml +49 -0
- prefixwise-0.3.0/src/prefixwise/__init__.py +10 -0
- prefixwise-0.3.0/src/prefixwise/__main__.py +3 -0
- prefixwise-0.3.0/src/prefixwise/classifier.py +283 -0
- prefixwise-0.3.0/src/prefixwise/cli.py +338 -0
- prefixwise-0.3.0/src/prefixwise/diagnostics.py +168 -0
- prefixwise-0.3.0/src/prefixwise/explain.py +155 -0
- prefixwise-0.3.0/src/prefixwise/fingerprint.py +159 -0
- prefixwise-0.3.0/src/prefixwise/fixes.py +105 -0
- prefixwise-0.3.0/src/prefixwise/ledger.py +294 -0
- prefixwise-0.3.0/src/prefixwise/prices.json +61 -0
- prefixwise-0.3.0/src/prefixwise/pricing.py +165 -0
- prefixwise-0.3.0/src/prefixwise/proxy.py +1075 -0
- prefixwise-0.3.0/tests/test_classifier.py +187 -0
- prefixwise-0.3.0/tests/test_cli.py +141 -0
- prefixwise-0.3.0/tests/test_diagnostics.py +69 -0
- prefixwise-0.3.0/tests/test_e1_hosted.py +97 -0
- prefixwise-0.3.0/tests/test_e1_open.py +179 -0
- prefixwise-0.3.0/tests/test_e4.py +54 -0
- prefixwise-0.3.0/tests/test_examples.py +21 -0
- prefixwise-0.3.0/tests/test_fingerprint.py +36 -0
- prefixwise-0.3.0/tests/test_fixes.py +83 -0
- prefixwise-0.3.0/tests/test_ledger.py +92 -0
- prefixwise-0.3.0/tests/test_pricing.py +99 -0
- prefixwise-0.3.0/tests/test_proxy.py +555 -0
- prefixwise-0.3.0/tests/test_routing.py +147 -0
- prefixwise-0.3.0/tests/test_security.py +123 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.0: 2026-11-07
|
|
4
|
+
|
|
5
|
+
- **Fixes the proxy can apply:** `prefixwise serve --fix canonical_tools` sorts tools so a drifting tool list stops breaking the cache. It is opt-in and reversible, and the ledger records which requests it rewrote.
|
|
6
|
+
- **`prefixwise fixes`:** ranks fixes by the tokens your own ledger says they would save.
|
|
7
|
+
- **Found by dogfooding (opencode):** `/messages`-style paths without `/v1` are accepted. Query strings are forwarded. Endpoints Prefixwise does not analyse (`/v1/models`, `count_tokens`, ...) pass through unrecorded. `HEAD` is answered. Sonnet 4.6 is priced.
|
|
8
|
+
- **Dollar costs:** every request is priced from a dated price table. Built in: Claude Haiku, Sonnet and Opus 5.5 and Fable 5.1, verified from Anthropic's pricing page on 2026-11-06, including Haiku's over-100k tier and 1-hour cache writes. Add your own with `PREFIXWISE_PRICES`. `report` shows cost, avoidable cost and cost per successful task; `fixes` shows savings in dollars; `prefixwise prices` shows the table. Unknown rates stay empty, never estimated.
|
|
9
|
+
- **E4 harness** (`experiments/e4`): replays recorded sessions with a fix off and on, and measures predicted vs. realised savings and task success. **`canonical_tools` is proven:** 99.9% (Anthropic) and 97.5% (vLLM) of predicted savings realised, with task success flat. `prefixwise fixes` shows the evidence.
|
|
10
|
+
- **E1-hosted:** on Anthropic, Prefixwise agreed with Anthropic's own miss diagnosis on 37 of 37 misses. OpenAI's diagnostics returned `unavailable` (recorded as an open question).
|
|
11
|
+
- **Fixed:** the moving Anthropic `cache_control` breakpoint, and string vs. text-block content, were both misread as `history_edit`.
|
|
12
|
+
- **Fixed:** Anthropic's real diagnostics shape (`diagnostics.cache_miss_reason`) is now read.
|
|
13
|
+
- **Fixed:** a request with no usage is now `unknown`, not `below_min_prefix`.
|
|
14
|
+
- **Security:**
|
|
15
|
+
- `PREFIXWISE_AUTH_TOKEN`, and the proxy refuses to expose an API key without one
|
|
16
|
+
- input validation and body limits
|
|
17
|
+
- the ledger file is owner-only
|
|
18
|
+
- `SECURITY.md`
|
|
19
|
+
|
|
20
|
+
## 0.2.0: 2026-11-06
|
|
21
|
+
|
|
22
|
+
- **CLI:** `prefixwise serve | report | task | request | explain`.
|
|
23
|
+
- **Proxy:** a drop-in pass-through for OpenAI chat completions, the Responses API and Anthropic messages, streaming included. Verdicts come back in `X-Prefixwise-*` headers.
|
|
24
|
+
- **Tokens per successful task:** measured per task, not per request, with a `/tasks` endpoint.
|
|
25
|
+
- **Causes:** tool-result, TTL and fork causes now fire, and the timing rules no longer guess.
|
|
26
|
+
- **E1-open** passed live on vLLM: 120 turns, 100%.
|
|
27
|
+
- Docker image and CI on Python 3.11 to 3.13.
|
|
28
|
+
|
|
29
|
+
## 0.1.0: 2026-11-05
|
|
30
|
+
|
|
31
|
+
- First version: fingerprinting, the 19-cause classifier, the ledger and the proxy.
|
prefixwise-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Teja Chavali
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: prefixwise
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Name why your LLM prompt cache missed, price the miss, and measure tokens per successful task. A local proxy and CLI for OpenAI, Anthropic and vLLM.
|
|
5
|
+
Project-URL: Homepage, https://github.com/tejachavali/prefixwise
|
|
6
|
+
Project-URL: Issues, https://github.com/tejachavali/prefixwise/issues
|
|
7
|
+
Author: Teja Chavali
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agents,anthropic,kv-cache,llm,openai,prompt-caching,proxy,token-efficiency,vllm
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
+
Classifier: Topic :: System :: Monitoring
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# Prefixwise
|
|
26
|
+
|
|
27
|
+
**Find out why your LLM prompt cache missed, what it cost, and how many tokens each successful task really takes.**
|
|
28
|
+
|
|
29
|
+
Prefixwise is a local proxy and CLI that sits between your agent and the model API (OpenAI, Anthropic, or a self-hosted vLLM). For every request it names the cause of any cache miss (a timestamp in the system prompt, a reordered tool list, a re-rendered tool result, an expired TTL...), says whether that miss was waste or a legitimate change, and prices it in tokens. Join requests to tasks and it reports the number that matters: **tokens per successful task**.
|
|
30
|
+
|
|
31
|
+
- Drop-in: change your SDK's base URL. Responses come back unchanged; the verdict rides in headers.
|
|
32
|
+
- Offline too: `prefixwise explain` diagnoses saved request bodies with no proxy and no API key.
|
|
33
|
+
- Standard library only. Hashes are stored, never prompts.
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
$ prefixwise report
|
|
37
|
+
Requests 1,284
|
|
38
|
+
Prompt tokens 9,412,330 (cache hit rate 61.0%)
|
|
39
|
+
Completion tokens 402,118
|
|
40
|
+
Avoidable tokens 2,106,904 (22.4% of prompt tokens)
|
|
41
|
+
Tasks 212 succeeded / 240 resolved
|
|
42
|
+
Tokens per successful task 46,293
|
|
43
|
+
|
|
44
|
+
cause requests prompt cached avoidable waste reqs tokens/success
|
|
45
|
+
-------------------- -------- --------- ------ --------- ---------- --------------
|
|
46
|
+
volatile_system 301 2,288,600 12.0% 1,766,040 301 48,110
|
|
47
|
+
tool_result_mutation 88 702,000 55.1% 251,320 88 47,904
|
|
48
|
+
none 790 5,925,000 84.0% 0 0 44,980
|
|
49
|
+
...
|
|
50
|
+
```
|
|
51
|
+
<sub>Illustrative output: a timestamp in the system prompt was costing a fifth of all prompt tokens.</sub>
|
|
52
|
+
|
|
53
|
+
## Quickstart
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install "git+https://github.com/tejachavali/prefixwise"
|
|
57
|
+
prefixwise serve --mock # no API key, synthetic responses; try it first
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Point any OpenAI- or Anthropic-compatible client at it:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from openai import OpenAI
|
|
64
|
+
client = OpenAI(base_url="http://127.0.0.1:8080/v1", api_key="sk-...",
|
|
65
|
+
default_headers={"X-Prefixwise-Task-Id": "fix-bug-42", "X-Prefixwise-Session-Id": "fix-bug-42"})
|
|
66
|
+
raw = client.chat.completions.with_raw_response.create(model="gpt-5.6", messages=[...])
|
|
67
|
+
print(raw.headers["x-prefixwise-cause"], raw.headers["x-prefixwise-avoidable-tokens"])
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Then:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
prefixwise task fix-bug-42 --success # when the task ends (or POST /tasks)
|
|
74
|
+
prefixwise report # where the tokens went
|
|
75
|
+
prefixwise report --by agent_id # compare agent / prompt versions per successful task
|
|
76
|
+
prefixwise explain examples/session.jsonl # offline: diagnose a saved session (examples/ is in this repo)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
For real traffic, drop `--mock` and name the upstream; your client's own API key is passed through:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
prefixwise serve --upstream https://api.openai.com # OpenAI: /v1/chat/completions, /v1/responses
|
|
83
|
+
prefixwise serve --upstream https://api.anthropic.com # Anthropic: /v1/messages
|
|
84
|
+
prefixwise serve --upstream http://127.0.0.1:8000 # vLLM / any OpenAI-compatible server
|
|
85
|
+
docker build -t prefixwise . && docker run -p 127.0.0.1:8080:8080 -v pw:/data prefixwise # mock in Docker
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**More: [docs/USE_CASES.md](docs/USE_CASES.md)** walks through eight concrete scenarios: a bill that jumped, a CI gate on prompt changes, A/B-testing prompt versions per successful task, Anthropic TTL choice, self-hosted vLLM, and more.
|
|
89
|
+
|
|
90
|
+
## What it tells you
|
|
91
|
+
|
|
92
|
+
One cause per request: the first thing, in prefix order, that broke reuse. Waste is avoidable; legitimate changes cost tokens but are not waste.
|
|
93
|
+
|
|
94
|
+
| Cause | Meaning | Waste? | Typical fix |
|
|
95
|
+
|---|---|---|---|
|
|
96
|
+
| `none` | Prefix reused; only new content was uncached | no | - |
|
|
97
|
+
| `first_write` | First request of a session | no | - |
|
|
98
|
+
| `volatile_system` | Only dates, times, ids or numbers changed in the system prompt | **yes** | Move volatile values to the end of the prompt |
|
|
99
|
+
| `system_edit` | The system prompt's wording changed | depends | Batch prompt edits; avoid per-turn rewording |
|
|
100
|
+
| `tool_reserialize` | Same tools, different order or bytes | **yes** | Sort tools; serialize deterministically |
|
|
101
|
+
| `tool_catalog_change` | A tool was added or removed | no | Mask tools instead of removing them, if frequent |
|
|
102
|
+
| `history_edit` | Earlier messages were rewritten without a compaction | **yes** | Append, do not rewrite history |
|
|
103
|
+
| `tool_result_mutation` | An earlier tool result was re-rendered or removed | **yes** | Freeze tool output once sent |
|
|
104
|
+
| `compaction_rewrite` | Context was compacted (send `X-Prefixwise-Compaction-Event: 1`) | no | Compact at task boundaries, not per turn |
|
|
105
|
+
| `param_rendered` | Blocks equal, but tool_choice / thinking / reasoning / format changed | depends | Keep those params stable within a session |
|
|
106
|
+
| `model_switch` | Different model than the previous turn | no | - |
|
|
107
|
+
| `retry_replay` | Identical request within 60 s with nothing cached | **yes** | Check retry logic |
|
|
108
|
+
| `breakpoint_misplaced` | Anthropic: no `cache_control` breakpoint | **yes** | Add `cache_control` after the stable prefix |
|
|
109
|
+
| `below_min_prefix` | Prompt below the provider's minimum cacheable length | depends | - |
|
|
110
|
+
| `ttl_expired` | Gap since the last turn exceeded the cache TTL | no | Longer TTL (e.g. Anthropic `ttl: "1h"`) if gaps are common |
|
|
111
|
+
| `routing_miss` | Prompt and timing fine, still nothing cached | no | Set a stable `prompt_cache_key` / session affinity |
|
|
112
|
+
| `fork_boundary` | First turn of a forked session (`X-Prefixwise-Fork-Of`) | no | - |
|
|
113
|
+
| `evicted` | Engine evicted this session's blocks | depends | More KV cache memory |
|
|
114
|
+
| `unknown` | Not enough evidence; never guessed | - | Send TTL / session headers, or check engine events |
|
|
115
|
+
|
|
116
|
+
Avoidable tokens for a waste turn = the part of the previous turn's prompt that could have been read from cache but was not. Appended content is never counted.
|
|
117
|
+
|
|
118
|
+
## Reference
|
|
119
|
+
|
|
120
|
+
**CLI**
|
|
121
|
+
|
|
122
|
+
| Command | |
|
|
123
|
+
|---|---|
|
|
124
|
+
| `prefixwise serve [--upstream URL] [--mock] [--fix canonical_tools] [--host H] [--port P] [--ledger FILE]` | Run the proxy. Ledger defaults to `./prefixwise.db`. `--fix` applies an opt-in, reversible rewrite to every request (see below). |
|
|
125
|
+
| `prefixwise prices [--json]` | The dated price table behind dollar costs, and where each rate came from. |
|
|
126
|
+
| `prefixwise fixes [--json]` | The fixes that would save the most, ranked by the avoidable tokens your own ledger attributes to each cause. |
|
|
127
|
+
| `prefixwise report [--by cause\|session_id\|agent_id\|model\|provider\|task_id] [--json]` | Totals and a breakdown, most avoidable first. |
|
|
128
|
+
| `prefixwise task ID --success\|--failure` | Record a task outcome (safe while the proxy runs). |
|
|
129
|
+
| `prefixwise request REQUEST_ID [--json]` | One request's ledger entry (from `X-Prefixwise-Request-Id`). |
|
|
130
|
+
| `prefixwise explain A.json B.json` / `explain SESSION.jsonl [--json] [--fail-on-waste]` | Offline diagnosis of saved request bodies, or `{"request": ..., "usage": ..., "session_id": ..., "gap_seconds": ...}` records. |
|
|
131
|
+
|
|
132
|
+
**HTTP endpoints:** `POST /v1/chat/completions`, `POST /v1/responses`, `POST /v1/messages` (streaming supported on all three; the same paths without `/v1` also work, and query strings are forwarded). Any other path, such as `GET /v1/models` or `count_tokens`, passes through to the upstream unchanged and is not recorded. Also: `POST /tasks {"task_id", "success"}`, `GET /requests/<id>`, `GET /ledger`, `GET /health`.
|
|
133
|
+
|
|
134
|
+
**Request headers** (all optional; stripped before forwarding):
|
|
135
|
+
|
|
136
|
+
| Header | Effect |
|
|
137
|
+
|---|---|
|
|
138
|
+
| `X-Prefixwise-Session-Id` | Groups turns into a session (also: `session_id` body field, or Anthropic `metadata.user_id`). Without it, all traffic is one session. |
|
|
139
|
+
| `X-Prefixwise-Task-Id` | Joins the request to a task for tokens per successful task (also: `task_id` body field). |
|
|
140
|
+
| `X-Prefixwise-Agent-Id` | Labels the agent or prompt version, for `report --by agent_id`. |
|
|
141
|
+
| `X-Prefixwise-Compaction-Event: 1` | This turn follows a context compaction. |
|
|
142
|
+
| `X-Prefixwise-Fork-Of: <session>` | This session was forked from another. |
|
|
143
|
+
| `X-Prefixwise-Cache-Ttl: <seconds>` | The cache lifetime, when known. |
|
|
144
|
+
|
|
145
|
+
**Response headers:** `X-Prefixwise-Request-Id` always; `X-Prefixwise-Cause`, `X-Prefixwise-Legit`, `X-Prefixwise-Avoidable-Tokens` on non-streaming responses; `X-Prefixwise-Fixes` when a fix rewrote the request. Streamed requests are classified when the stream ends: read them from `GET /requests/<id>`.
|
|
146
|
+
|
|
147
|
+
**Environment:** `PREFIXWISE_UPSTREAM_URL`, `PREFIXWISE_API_KEY` (overrides client auth; `x-api-key` for Anthropic), `PREFIXWISE_LEDGER_PATH`, `PREFIXWISE_HMAC_KEY` (HMAC short blocks so their hashes cannot be dictionary-guessed), `PREFIXWISE_MAX_SESSIONS` (default 10,000), `PREFIXWISE_FIXES` (e.g. `canonical_tools`), `PREFIXWISE_AUTH_TOKEN` (require `X-Prefixwise-Token`), `PREFIXWISE_MAX_BODY_BYTES`, `PREFIXWISE_PRICES` (your own rates file), `PORT`.
|
|
148
|
+
|
|
149
|
+
**Dollar costs.** Each request is priced from the provider's own usage breakdown (uncached input, cache reads, cache writes by TTL, output), using a dated price table: `prefixwise prices` shows it. *Avoidable cost* is the avoidable tokens times (input price − cache-read price): what they would have cost as cache hits. A model with no rates shows `-`, never an estimate. To add OpenAI models or your negotiated rates, write a JSON file in the same format (`prefixwise prices --json` prints one) and set `PREFIXWISE_PRICES=/path/prices.json`. Your entries override the built-in ones. `prefixwise report` and `prefixwise fixes` then show dollars, and say how many requests were priced.
|
|
150
|
+
|
|
151
|
+
**Fixes the proxy can apply.** `canonical_tools` sorts tools by name and sorts each tool's keys before forwarding, so a tool list that drifts between turns (plugin reload, dict rebuild) stops breaking the cache. It is off by default; with it off, requests pass through unchanged. Prefixwise classifies the request the provider actually received. Fixes that would change what the model reads, like moving a timestamp out of the system prompt, are advice only: `prefixwise fixes` lists them. `canonical_tools` is **proven**: in [E4](experiments/e4/runs/2026-11-06-live.md), turning it on realised 99.9% (Anthropic Haiku 5.5) and 97.5% (vLLM Qwen3-4B) of the savings Prefixwise predicted, with task success flat.
|
|
152
|
+
|
|
153
|
+
## Security
|
|
154
|
+
|
|
155
|
+
- **What it stores:** the ledger keeps fingerprints of prompt blocks (hashes), token counts, causes, and your session, task and agent labels. It never stores prompt text or API keys. The ledger file is created readable by its owner only. Set `PREFIXWISE_HMAC_KEY` so hashes of short, guessable blocks are keyed.
|
|
156
|
+
- **Where it listens:** `127.0.0.1` by default. To listen beyond localhost (a team server, or Docker with a published port), set `PREFIXWISE_AUTH_TOKEN`; clients then send it as `X-Prefixwise-Token`, and every endpoint but `/health` requires it. With `PREFIXWISE_API_KEY` set, the proxy refuses to listen beyond localhost without a token, because anyone who reached the port could spend your key. Publish Docker ports on `127.0.0.1` (`-p 127.0.0.1:8080:8080`).
|
|
157
|
+
- **Keys:** the client's own auth headers pass through to the upstream untouched and are never logged. `X-Prefixwise-*` headers are never forwarded.
|
|
158
|
+
- **Limits:** request bodies over 32 MiB (`PREFIXWISE_MAX_BODY_BYTES`) get a 413, and malformed requests get a 400 naming the field.
|
|
159
|
+
|
|
160
|
+
Found a vulnerability? See [SECURITY.md](SECURITY.md).
|
|
161
|
+
|
|
162
|
+
## Limits
|
|
163
|
+
|
|
164
|
+
- **What has been verified live (2026-11-06):**
|
|
165
|
+
- **E1-open, vLLM:** Qwen3-4B on vLLM 0.33.0, Colab L4. 120 turns, 9 injected causes including append-only growth. Agreement 100%, UNKNOWN 0%. See [`experiments/e1_open/runs/`](experiments/e1_open/runs/).
|
|
166
|
+
- **E1-hosted, Anthropic:** claude-haiku-5-5. Across two runs, on 37 misses diagnosed by Anthropic's own API (compaction, model switch, parameter change, system edit, tool catalog change, tool reorder, volatile system), Prefixwise named a cause Anthropic's reason covers 37 out of 37 times. All 60 cache hits were classified as hits. UNKNOWN 0%. See [`experiments/e1_hosted/runs/`](experiments/e1_hosted/runs/).
|
|
167
|
+
- **E4, fixes:** `canonical_tools` realised 99.9% (Anthropic) and 97.5% (vLLM) of its predicted savings with task success flat. On Qwen3-4B, 1 task of 60 changed; on Anthropic, none. See [`experiments/e4/runs/`](experiments/e4/runs/).
|
|
168
|
+
- **Not verified yet:**
|
|
169
|
+
- **OpenAI native agreement.** OpenAI's `prompt_cache_diagnostics` returned `unavailable` for every comparison on gpt-5.6, -luna and -sol. That held with turn delays of 1 s, 30 s and 120 s, and on a minimal standalone call with a completed baseline. OpenAI's dashboard shows 0 diagnosed responses. Prefixwise still classifies OpenAI traffic; only the cross-check against OpenAI's own verdict is missing.
|
|
170
|
+
- **Causes E1 does not inject:** TTL expiry, routing, eviction, tool-result mutation.
|
|
171
|
+
- **Real agent traffic,** which is messier than scripted turns.
|
|
172
|
+
- **Divergence is located per block, not per token.** An edit at the very end of a long system prompt is reported as `system_edit`, even if the provider could have reused everything before it. In one live OpenAI call, appending three words to 2,100 tokens of instructions gave 0 cached tokens, which looks more like routing than the edit itself.
|
|
173
|
+
- One proxy process keeps session state in memory: run one instance per agent fleet, or send all of a session's turns to the same instance.
|
|
174
|
+
- `evicted` needs vLLM KV events, which the proxy does not consume yet.
|
|
175
|
+
- OpenAI's default in-memory cache TTL is not a fixed number, so after a long gap a cold OpenAI cache is `unknown` unless you send `X-Prefixwise-Cache-Ttl`.
|
|
176
|
+
- **Dollar costs:** only models in the dated price table are priced: Claude Haiku, Sonnet and Opus 5.5, Fable 5.1 and Sonnet 4.6, read from Anthropic's pricing page (2026-11-06/07). Everything else, OpenAI included, shows `-` until you add rates with `PREFIXWISE_PRICES`. Batch, fast-mode and data-residency multipliers are not modelled.
|
|
177
|
+
|
|
178
|
+
## Working on Prefixwise
|
|
179
|
+
|
|
180
|
+
Agents and people working on this repo: read [`WORKLOG.md`](WORKLOG.md) first. It has the current goal, why, what to run, and what happens with each result. `build.log` records outcomes.
|
|
181
|
+
|
|
182
|
+
## Development
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
pip install -e ".[dev]"
|
|
186
|
+
python -m pytest tests -q # 75 tests, about 10 s
|
|
187
|
+
python experiments/e1_open/e1_open.py --mock # harness smoke test
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Design notes
|
|
191
|
+
|
|
192
|
+
The research and architecture behind Prefixwise.
|
|
193
|
+
|
|
194
|
+
### What is missing elsewhere
|
|
195
|
+
|
|
196
|
+
Every provider and engine reports how many tokens missed. None, on its own, names the app-side cause across providers, and none joins spend to task success.
|
|
197
|
+
|
|
198
|
+
- OpenAI and Anthropic now report a native miss reason (OpenAI Responses, GPT-5.6+, 9 reasons; Anthropic, 6 reasons, GA Sep 23, 2026), but only the earliest divergence, in a coarse taxonomy, and neither names TTL expiry, routing, or eviction.
|
|
199
|
+
- vLLM and SGLang report hit rates and per-request cached tokens, not per-request causes.
|
|
200
|
+
- LMCache sees chunk hashes, not sessions, agents, or tasks.
|
|
201
|
+
- Gemini has no miss diagnostics, hit count only.
|
|
202
|
+
- Single-provider cause classification already exists (CacheCanary for Bedrock Claude, Copilot Cost Analyzer for Copilot Chat). The unbuilt part is the cross-provider, cross-harness version, joined to the engine, priced per successful task.
|
|
203
|
+
|
|
204
|
+
### Architecture (v2, verified Nov 6, 2026)
|
|
205
|
+
|
|
206
|
+
Design confidence 85/100, proven 79/100. The rise from v1 (79/100) is earned by the verified v2 corrections; 90 and 99 need the E1-E5 experiments below, with running code against real traffic. Full survey and verification: `../repo-survey-and-architecture.md` and `../v2-verification-check.md`.
|
|
207
|
+
|
|
208
|
+
#### 1. Capture: proxy first, hooks second
|
|
209
|
+
|
|
210
|
+
Only the proxy sees the exact bytes on the wire, the bytes the provider actually hashes. Hooks see what the harness constructed; the provider may re-serialize before hashing. Hooks (LangGraph middleware, OpenAI Agents SDK, Claude Agent SDK including PreCompact, CrewAI) supply session, agent, and task identity the proxy cannot see. OTel is third, roll-up only.
|
|
211
|
+
|
|
212
|
+
The proxy also auto-chains native diagnostics: Anthropic `diagnostics.previous_message_id` (no beta header, GA Sep 23, 2026) and OpenAI `prompt_cache_options.comparison_response_id`. Neither changes caching. The provider reason is a partial agreement signal, not ground truth.
|
|
213
|
+
|
|
214
|
+
#### 2. Fingerprinting: raw, canonical, and masked hashes
|
|
215
|
+
|
|
216
|
+
Three hashes per block, in prefix order. Order is per provider, not universal: Anthropic is tools, system, messages; OpenAI injects tools before developer instructions.
|
|
217
|
+
|
|
218
|
+
- `raw`: exact bytes as sent. TOOL_RESERIALIZE is defined as canonical-equal, raw-different, so without raw it cannot be seen.
|
|
219
|
+
- `canonical`: sorted keys, fixed separators, NFC unicode.
|
|
220
|
+
- `masked`: canonical bytes with volatile values masked (dates, times, ids, numbers). A system block whose raw hash changed but whose masked hash is equal is VOLATILE_SYSTEM, not SYSTEM_EDIT.
|
|
221
|
+
|
|
222
|
+
Hashes are stored, never raw prompts, by default. A bare SHA-256 of a short or predictable block is dictionary-guessable, so when an HMAC key is supplied, every hash of a short block is an HMAC with the per-install key, replacing bare SHA-256 entirely for that block. The short-block threshold (default 64 tokens, approximate) is a policy choice, not a verified fact. Soft tokens are fingerprinted by type and count only, never hashed as text.
|
|
223
|
+
|
|
224
|
+
#### 3. Classifier: 19 causes, one per miss, legitimacy separate
|
|
225
|
+
|
|
226
|
+
First divergence in prefix order wins; the rest are recorded as secondary. UNKNOWN when evidence is absent, never a guess. Legitimacy is a separate column: a legitimate change still costs tokens, but it is not waste.
|
|
227
|
+
|
|
228
|
+
`FIRST_WRITE`, `MODEL_SWITCH`, `VOLATILE_SYSTEM`, `SYSTEM_EDIT`, `TOOL_RESERIALIZE`, `TOOL_CATALOG_CHANGE`, `COMPACTION_REWRITE`, `HISTORY_EDIT`, `TOOL_RESULT_MUTATION`, `FORK_BOUNDARY`, `PARAM_RENDERED`, `BELOW_MIN_PREFIX`, `BREAKPOINT_MISPLACED`, `TTL_EXPIRED`, `EVICTED`, `ROUTING_MISS`, `RETRY_REPLAY`, `NONE`, `UNKNOWN`
|
|
229
|
+
|
|
230
|
+
`PARAM_RENDERED` is the case Anthropic reports as `unavailable`: blocks equal, but tool_choice, thinking, context_management, output_config, output_format, or beta headers differ. Minimum cacheable prefix is model-dependent: OpenAI 1,024 tokens, Anthropic 512-4,096, Gemini 3.x 4,096 (2,048 on 2.5).
|
|
231
|
+
|
|
232
|
+
Two scoring metrics ship with the classifier: agreement (share of turns where Prefixwise's cause maps onto the native reason when both exist) and coverage (share of misses not UNKNOWN).
|
|
233
|
+
|
|
234
|
+
#### 4. Engine join: request id, not hash recomputation
|
|
235
|
+
|
|
236
|
+
vLLM >= v0.30.0 (Sep 22, 2026, PR #51381) stamps `session_id` onto every `BlockStored` KV event; the proxy sets `X-Request-Id` and `session_id`. LMCache `--lookup-hash-log-dir` logs the same request id with chunk hashes. SGLang accepts `rid` and `session_id` and, with `return_cached_tokens_details`, reports cached tokens split by device, host, and storage tier. Join on request id; time-window fallback is flagged low confidence only. Hash recomputation stays out: vLLM 16-token blocks, LMCache 256-token chunks, and SGLang radix trees cannot be reproduced from one hash.
|
|
237
|
+
|
|
238
|
+
The production-stack `LookupMsg` preflight is NOT claimed to work against current LMCache: its import path (`lmcache.v1.cache_controller.message`) 404s at LMCache 012965f (real path `lmcache.inprocess.v1.cache_controller.message`), and the pinned version is unverified.
|
|
239
|
+
|
|
240
|
+
#### 5. Compaction: frozen boundary, rare and timed rewrites
|
|
241
|
+
|
|
242
|
+
"Append-only compaction" as written in v1 is incoherent: any compaction that shrinks the window rewrites the prefix after the boundary by definition. The replacement: never touch the system/tool prefix; compact only when the window is near its limit or the cache would expire anyway; prefer task boundaries over per-turn compaction; snapshot the pre-compaction fingerprint at the PreCompact hook and price the rewrite exactly once.
|
|
243
|
+
|
|
244
|
+
#### 6. Fix toggles and the ledger
|
|
245
|
+
|
|
246
|
+
Toggles are opt-in and reversible: canonical tool ordering, volatile fields out of the prefix, breakpoint placement, TTL selection. Each ships as "recommended" on predicted savings and graduates to "proven" only after realised savings are measured with task success flat (realised >= 50% of predicted over 50 requests). The reuse-adjusted saving (k = write multiplier / (1 - read multiplier)) is labelled as a model, not a verified fact, because the read multiplier is model-dependent.
|
|
247
|
+
|
|
248
|
+
Headline metric: tokens per successful task, with cost as a secondary column. Rate tables are versioned snapshots with dates; if a rate is unknown, cost is null, never invented.
|
|
249
|
+
|
|
250
|
+
#### 7. Distribution: local first
|
|
251
|
+
|
|
252
|
+
The OSS core is the ledger, diffing, and one engine join, as a local CLI and proxy. Fleet roll-up is hosted and optional, redacted entries only, never raw prompts.
|
|
253
|
+
|
|
254
|
+
### What Prefixwise will not build
|
|
255
|
+
|
|
256
|
+
Not a cost dashboard, not a prompt optimiser or compressor, not a router or gateway, not a second observability platform, not a hosted-provider feature. It cannot fix OpenAI's per-machine routing or Anthropic's automatic-caching opacity; it records those misses as ROUTING_MISS and moves on.
|
|
257
|
+
|
|
258
|
+
### E1-open: classifier validation without API credits
|
|
259
|
+
|
|
260
|
+
E1-open validates the classifier on open Hugging Face models served by vLLM, with controlled injected causes as ground truth. No Anthropic or OpenAI API credits needed. See `experiments/e1_open/README.md`.
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
# Mock mode (no GPU, no server): scoring path smoke test only
|
|
264
|
+
PYTHONPATH=src python3 experiments/e1_open/e1_open.py --mock
|
|
265
|
+
|
|
266
|
+
# Live, on a GPU box (vLLM >= 0.30.0; Qwen3-8B fits on one 24 GB GPU)
|
|
267
|
+
MODEL=Qwen/Qwen3-8B bash experiments/e1_open/vllm_server.sh
|
|
268
|
+
PYTHONPATH=src python3 experiments/e1_open/e1_open.py \
|
|
269
|
+
--vllm-url http://127.0.0.1:8000 --model Qwen/Qwen3-8B \
|
|
270
|
+
--model-alias Qwen/Qwen3-8B-alias
|
|
271
|
+
|
|
272
|
+
# Or on Colab (L4 GPU, Qwen3-4B default): open experiments/e1_open/colab_e1_open.ipynb
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
120 turns, 9 injected causes, including two append-only sessions where every turn grows the conversation (FIRST_WRITE, NONE, VOLATILE_SYSTEM, TOOL_RESERIALIZE, TOOL_CATALOG_CHANGE, SYSTEM_EDIT, COMPACTION_REWRITE, RETRY_REPLAY, MODEL_SWITCH). Pass: agreement >= 95%, UNKNOWN <= 10% of misses. Mock mode scores 100% by construction (simulated cached_tokens); only the live run is evidence about vLLM.
|
|
276
|
+
|
|
277
|
+
Live run Nov 5-6, 2026 (Colab L4, vLLM 0.33.0, Qwen3-4B): agreement 90.0%, Pass NO, only retry_replay failed (0/10). Two fixes since: the server now starts with `--enable-auto-tool-choice --tool-call-parser hermes` (vLLM 0.33.0 rejected tool-carrying requests with HTTP 400 without them), and retry turns use a fresh upstream session + fresh `cache_salt` instead of POST `/reset_prefix_cache` (HTTP 404 on vLLM 0.33.0). Qwen3-8B failed to start on a Colab L4 in that run, so 4B is the Colab default; 8B needs a larger GPU.
|
|
278
|
+
|
|
279
|
+
### Build order (E1 first)
|
|
280
|
+
|
|
281
|
+
1. **E1-open: passed live 2026-11-06** (Qwen3-4B, vLLM 0.33.0; 100%). **E1-hosted: Anthropic passed 2026-11-06** (37/37 native agreement over two runs); OpenAI diagnostics returned `unavailable` throughout. Original plan: 120 turns on vLLM + Qwen3-8B with injected causes as ground truth; agreement >= 95%, UNKNOWN <= 10%. No API credits. Then **E1-hosted:** 100 consecutive turns on Anthropic and OpenAI with native diagnostics chained; Prefixwise's first-diverged block agrees with the native reason >= 95% of the time; UNKNOWN <= 10% of misses on real coding-agent traffic. Needs API credits and is still the provider-facing proof.
|
|
282
|
+
2. **E2/E3:** vLLM >= v0.30.0 + LMCache on one GPU box, ids set by the proxy; >= 95% of requests join on ids within 5 s; lookup-hash lines join on request id for >= 99% of requests.
|
|
283
|
+
3. **E4: passed live 2026-11-06** (`canonical_tools`: 99.9% of predicted savings realised on Anthropic, 97.5% on vLLM; task success flat). Original plan: one toggle (canonical tools or volatile-to-tail) on a recorded agent session replayed live; realised cache_read >= 50% of predicted over 50 requests; task success flat.
|
|
284
|
+
4. **E5:** PreCompact snapshot on one harness; rewrite priced exactly once; compaction frequency reduced to task boundaries without a drop in a 10-task pass rate.
|
|
285
|
+
5. **SGLang adapter** (rid, session_id, return_cached_tokens_details).
|
|
286
|
+
6. Hosted fleet roll-up, only if anyone asks.
|
|
287
|
+
|
|
288
|
+
## License
|
|
289
|
+
|
|
290
|
+
MIT. See LICENSE.
|