prefixlens 0.0.0-stage → 0.5.0
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.
- package/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +179 -2
- package/SECURITY.md +82 -0
- package/SPEC.md +312 -0
- package/dist/adapters/claude-code.d.ts +2 -0
- package/dist/adapters/claude-code.js +759 -0
- package/dist/adapters/codex.d.ts +2 -0
- package/dist/adapters/codex.js +202 -0
- package/dist/adapters/index.d.ts +40 -0
- package/dist/adapters/index.js +43 -0
- package/dist/adapters/opencode.d.ts +3 -0
- package/dist/adapters/opencode.js +349 -0
- package/dist/adapters/otel.d.ts +2 -0
- package/dist/adapters/otel.js +170 -0
- package/dist/analyze.d.ts +162 -0
- package/dist/analyze.js +589 -0
- package/dist/assert.d.ts +86 -0
- package/dist/assert.js +179 -0
- package/dist/breaks.d.ts +64 -0
- package/dist/breaks.js +114 -0
- package/dist/canonical.d.ts +58 -0
- package/dist/canonical.js +362 -0
- package/dist/catalogue.d.ts +187 -0
- package/dist/catalogue.js +459 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +347 -0
- package/dist/diff.d.ts +45 -0
- package/dist/diff.js +601 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +24 -0
- package/dist/lens.d.ts +62 -0
- package/dist/lens.js +62 -0
- package/dist/params.d.ts +29 -0
- package/dist/params.js +72 -0
- package/dist/pricing-command.d.ts +13 -0
- package/dist/pricing-command.js +198 -0
- package/dist/pricing.d.ts +135 -0
- package/dist/pricing.js +263 -0
- package/dist/printable.d.ts +10 -0
- package/dist/printable.js +27 -0
- package/dist/provider.d.ts +6 -0
- package/dist/provider.js +18 -0
- package/dist/proxy.d.ts +53 -0
- package/dist/proxy.js +155 -0
- package/dist/record/exchange.d.ts +86 -0
- package/dist/record/exchange.js +232 -0
- package/dist/record/fetch.d.ts +23 -0
- package/dist/record/fetch.js +96 -0
- package/dist/record/recorder.d.ts +95 -0
- package/dist/record/recorder.js +254 -0
- package/dist/report/format.d.ts +31 -0
- package/dist/report/format.js +99 -0
- package/dist/report/html.d.ts +7 -0
- package/dist/report/html.js +223 -0
- package/dist/report/json.d.ts +13 -0
- package/dist/report/json.js +6 -0
- package/dist/report/terminal.d.ts +9 -0
- package/dist/report/terminal.js +150 -0
- package/dist/runtime/server.d.ts +37 -0
- package/dist/runtime/server.js +175 -0
- package/dist/runtime/sqlite.d.ts +7 -0
- package/dist/runtime/sqlite.js +16 -0
- package/dist/textdiff.d.ts +32 -0
- package/dist/textdiff.js +157 -0
- package/dist/trace.d.ts +172 -0
- package/dist/trace.js +335 -0
- package/dist/usage.d.ts +4 -0
- package/dist/usage.js +84 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +4 -0
- package/package.json +73 -4
- package/pricing.json +175 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to prefixlens are recorded here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html) (before 1.0, a minor version may break compatibility).
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [0.5.0] - 2026-11-12
|
|
8
|
+
|
|
9
|
+
First public release.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Test assertions** (`prefixlens/test`). `assertPrefixStable(prev, next)` fails a test when the second of two requests your harness builds does not keep the first one's prefix. `explainPrefixDiff` returns the same explanation without throwing. A failure names the first changed block, shows a diff and suggests a fix. Options: `allowBreak`, `ignoreParams`, `hints`, `provider`. Pure functions, no network.
|
|
14
|
+
- **Recording proxy** (`prefixlens record`). A local proxy for any client whose base URL can be set. It forwards each request with its body and credentials unchanged, streams the response back, and appends one full-request record per model call. It assigns each call to a lane by shared prefix, so concurrent conversations and subagents are compared with their own previous call.
|
|
15
|
+
- **Recording library.** `wrap(client)` for the official Anthropic and OpenAI TypeScript SDKs, `createLens().fetch()` for any client that takes a fetch function, `lens.observe()` for custom clients, `buildRecord()` to build the same record from a request and a response, and `startProxy()`.
|
|
16
|
+
- **Request formats.** Anthropic Messages, OpenAI Chat Completions and OpenAI Responses, streamed or not. The assertions also read Gemini `generateContent` bodies.
|
|
17
|
+
- **Reports** (`prefixlens report`). For each call: tokens read from the cache, written and uncached, the hit rate and the cost. For each miss: the first changed block, a diff, the tokens lost and the cost. Output as a terminal table, JSON (`--json`) or one self-contained HTML file (`--html`).
|
|
18
|
+
- **Causes.** Tool-list and tool-order changes, system-prompt edits and re-rendered timestamps, ids and relative dates, history rewrites (edited, inserted, removed or reordered messages, rewritten tool output or reasoning, a sliding window, a rewind), merged or split steps, compaction, model and parameter changes, cache expiry and provider-side misses. Each is labelled confirmed, inferred or unknown.
|
|
19
|
+
- **Caching rules.** Explicit breakpoints (Anthropic `cache_control`, OpenAI `prompt_cache_breakpoint`) and implicit caches (OpenAI, Gemini), with the cache step learnt from the trace. A table per provider says which request parameters are part of the cached prefix, each entry marked verified, documented or assumed.
|
|
20
|
+
- **Intentional breaks.** A harness declares a compaction, revert, refusal, model switch or settings change with a `prefix_break` hint, or `allowBreak` in a test. A declared break is reported with its cost, totalled apart from other misses.
|
|
21
|
+
- **Trace format 1.3** ([SPEC.md](SPEC.md)). JSON Lines, one record per model call, with ordered and hashed request blocks, usage and a fidelity flag. Specified with hash test vectors, so a harness in any language can write it.
|
|
22
|
+
- **Pricing.** List prices for Anthropic, OpenAI and Gemini models in `pricing.json`, with sources, cache-write lifetimes, prompt-size tiers and dated prices. `--pricing` and `PREFIXLENS_PRICING` use another table. `prefixlens pricing update`, `check` and `show` add an optional second source, the models.dev catalogue, fetched only on request.
|
|
23
|
+
- **Importers** (`prefixlens import`). For logs you already have: OpenTelemetry GenAI spans (`otel`), and the session logs of Claude Code (`claude-code`), Codex (`codex`) and opencode (`opencode`). `prefixlens adapters` lists what each can diagnose.
|
|
24
|
+
- **Privacy defaults.** Credentials and all but four request headers are never written. Identifying request fields and the bytes of images and documents are left out unless asked for. Trace and report files are created readable by their owner only. Control characters in trace content are written visibly on the terminal. No telemetry. See [SECURITY.md](SECURITY.md).
|
|
25
|
+
- **Runtimes.** Node.js 22 or later and Bun, with type declarations and no runtime dependencies.
|
|
26
|
+
|
|
27
|
+
[Unreleased]: https://github.com/parthg1901/prefixlens/compare/v0.5.0...HEAD
|
|
28
|
+
[0.5.0]: https://github.com/parthg1901/prefixlens/releases/tag/v0.5.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Parth
|
|
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.
|
package/README.md
CHANGED
|
@@ -1,3 +1,180 @@
|
|
|
1
|
-
#
|
|
1
|
+
# prefixlens
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Explains prompt-cache hits and misses for any agent, harness or framework that calls a model API.
|
|
4
|
+
|
|
5
|
+
Cache regressions are silent and expensive: one reordered tool list or a timestamp in the system prompt, and every later request pays full price. prefixlens shows which request broke the cache, why, and what it cost. It also catches the break in CI, before it reaches your bill.
|
|
6
|
+
|
|
7
|
+
It works on the request itself. Anything that sends OpenAI Responses, OpenAI Chat Completions or Anthropic Messages requests can use it with no adapter: your own harness, an SDK, a framework such as the Vercel AI SDK or Mastra, or a gateway.
|
|
8
|
+
|
|
9
|
+
A session of an agent whose tool list grew before its fourth call:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
turn time (UTC) model prompt read write uncached out hit cost status
|
|
13
|
+
1 10:00:02 claude-opus-5-5 48.2k 0 48.2k 12 140 0% $0.24 cold
|
|
14
|
+
2 10:00:09 claude-opus-5-5 48.6k 48.2k 420 12 95 99.1% $0.01 hit
|
|
15
|
+
3 10:00:17 claude-opus-5-5 49.0k 48.6k 380 12 110 99.2% $0.01 hit
|
|
16
|
+
4 10:00:26 claude-opus-5-5 49.9k 0 49.9k 12 160 0% $0.25 miss: tool list changed (+1 tool: 3 → 4)
|
|
17
|
+
5 10:00:34 claude-opus-5-5 50.3k 49.9k 350 12 60 99.3% $0.01 hit
|
|
18
|
+
|
|
19
|
+
Misses
|
|
20
|
+
turn 4: miss, tool list changed (+1 tool: 3 → 4), ~$0.24
|
|
21
|
+
lost 49,000 of 49,000 cached tokens · vs turn 3 · confirmed
|
|
22
|
+
+ web_search
|
|
23
|
+
|
|
24
|
+
1 miss cost $0.24 at list prices; 1 from tool-list changes
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
npm install --save-dev prefixlens # or: bun add -d prefixlens
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Requires Node.js 22 or later, or Bun (tested with 1.4). There are no runtime dependencies.
|
|
34
|
+
|
|
35
|
+
## Quickstart
|
|
36
|
+
|
|
37
|
+
### 1. Test prefix stability in CI
|
|
38
|
+
|
|
39
|
+
Build the request for two consecutive steps the way your harness does, and assert that the second keeps the first one's prefix. Nothing is sent to a provider.
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { test } from "node:test" // or "bun:test"
|
|
43
|
+
import { assertPrefixStable } from "prefixlens/test"
|
|
44
|
+
|
|
45
|
+
// Stand-in for your harness: the request body it sends for a given history.
|
|
46
|
+
const buildRequest = (messages) => ({
|
|
47
|
+
model: "claude-opus-5-5",
|
|
48
|
+
max_tokens: 1024,
|
|
49
|
+
system: "You are a coding agent.",
|
|
50
|
+
tools: [{ name: "read_file", description: "Read a file", input_schema: { type: "object" } }],
|
|
51
|
+
messages,
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test("a new step extends the previous request", () => {
|
|
55
|
+
const history = [{ role: "user", content: "Fix the failing test." }]
|
|
56
|
+
const first = buildRequest(history)
|
|
57
|
+
const second = buildRequest([...history, { role: "assistant", content: "Reading it now." }, { role: "user", content: "Go on." }])
|
|
58
|
+
assertPrefixStable(first, second)
|
|
59
|
+
})
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
When the builder puts the current time in the system prompt, the test fails with the first changed block, a diff and a fix:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
PrefixBreakError: prefix not stable: system prompt changed: a timestamp value was updated (2026-01-01T10:00:00Z → 2026-01-01T10:00:01Z)
|
|
66
|
+
|
|
67
|
+
first changed block: 2 of 3 (system part 1); block 1 still matches, everything from block 2 on is re-read
|
|
68
|
+
|
|
69
|
+
type: text
|
|
70
|
+
- text: You are a coding agent. Now: 2026-01-01T10:00:00Z
|
|
71
|
+
+ text: You are a coding agent. Now: 2026-01-01T10:00:01Z
|
|
72
|
+
|
|
73
|
+
fix: keep clocks and ids out of the system prompt: send them in a new message, or round them (to the day, say).
|
|
74
|
+
if this change is intentional, pass { allowBreak: { reason: "..." } }
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
More in [assertions](https://github.com/parthg1901/prefixlens/blob/main/docs/assertions.md).
|
|
78
|
+
|
|
79
|
+
### 2. Record through the proxy
|
|
80
|
+
|
|
81
|
+
Point any client's base URL at a local proxy. It forwards every request unchanged and records each model call.
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
npx prefixlens record --provider anthropic -o run.trace.jsonl # or --provider openai; listens on 127.0.0.1:8787
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
In another terminal, set the base URL and run your agent as usual:
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
export ANTHROPIC_BASE_URL=http://127.0.0.1:8787 # with --provider openai: OPENAI_BASE_URL=http://127.0.0.1:8787/v1
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Stop the proxy with Ctrl-C, then read the report:
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
npx prefixlens report run.trace.jsonl # add --html for a self-contained page, --json for the data
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Credentials are forwarded and never written. More in [proxy](https://github.com/parthg1901/prefixlens/blob/main/docs/proxy.md).
|
|
100
|
+
|
|
101
|
+
### 3. Wrap your client
|
|
102
|
+
|
|
103
|
+
Record from inside your own process, with no proxy:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import Anthropic from "@anthropic-ai/sdk"
|
|
107
|
+
import { wrap } from "prefixlens"
|
|
108
|
+
|
|
109
|
+
const client = wrap(new Anthropic(), { trace: "run.trace.jsonl" }) // also wrap(new OpenAI())
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
For a client or framework that is not one of those SDKs:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import { createLens } from "prefixlens"
|
|
116
|
+
|
|
117
|
+
const lens = createLens({ trace: "run.trace.jsonl" })
|
|
118
|
+
const recordingFetch = lens.fetch() // pass it to any client or framework that accepts a fetch function
|
|
119
|
+
lens.observe({ request: body, response }) // or report each call yourself: the body as sent, the response as received
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`buildRecord` builds the same record from a request and a response you already have. From another language, write the [trace format](SPEC.md) or use the proxy. More in [library](https://github.com/parthg1901/prefixlens/blob/main/docs/library.md).
|
|
123
|
+
|
|
124
|
+
## Works with
|
|
125
|
+
|
|
126
|
+
Any client of the Anthropic Messages, OpenAI Chat Completions or OpenAI Responses request formats, on a provider with implicit or explicit prompt caching. There is no per-harness adapter.
|
|
127
|
+
|
|
128
|
+
| | |
|
|
129
|
+
|---|---|
|
|
130
|
+
| Assertions | Request bodies in those three formats, and Gemini `generateContent` bodies with `provider: "gemini"`. |
|
|
131
|
+
| Proxy, `wrap`, `lens.fetch`, `lens.observe`, `buildRecord` | Calls in those three formats, streamed or not. A gateway's or another provider's compatible endpoint is recorded in the format it uses. |
|
|
132
|
+
| Caching | Implicit caches (OpenAI, Gemini) and explicit breakpoints (Anthropic `cache_control`, OpenAI `prompt_cache_breakpoint`). |
|
|
133
|
+
| Other languages | The proxy, or a trace written to [SPEC.md](SPEC.md). |
|
|
134
|
+
|
|
135
|
+
## What it tells you
|
|
136
|
+
|
|
137
|
+
For each call: prompt tokens read from the cache, written and uncached, the hit rate and the cost. For each miss: the first changed block, a diff, the tokens lost, the cost, and one of these causes.
|
|
138
|
+
|
|
139
|
+
| cause | what happened |
|
|
140
|
+
|---|---|
|
|
141
|
+
| Tool-list changes | A tool added, removed, redefined or reordered. |
|
|
142
|
+
| System-prompt changes | The system prompt edited, or a timestamp, id or relative date in it rendered again. |
|
|
143
|
+
| History rewrites | A sent message edited, inserted, removed or reordered; a tool output or reasoning block rewritten on replay; a sliding window; a rewind. |
|
|
144
|
+
| Reshaped steps | Messages merged or split on replay. |
|
|
145
|
+
| Compaction | History replaced by a summary. |
|
|
146
|
+
| Model or parameter changes | Another model, or a change to a parameter the provider caches on. |
|
|
147
|
+
| Cache expiry | The prefix is intact, but the call came after the cache lifetime. |
|
|
148
|
+
| Provider-side misses | The prefix is intact and in time: the provider evicted the entry or routed the request elsewhere. |
|
|
149
|
+
|
|
150
|
+
More in [reports](https://github.com/parthg1901/prefixlens/blob/main/docs/report.md).
|
|
151
|
+
|
|
152
|
+
## Declaring intentional breaks
|
|
153
|
+
|
|
154
|
+
A compaction, a revert or a model change the user asked for breaks the prefix on purpose. Declare it, and it is reported as an intentional break, with its cost totalled apart from the misses that point at bugs.
|
|
155
|
+
|
|
156
|
+
In a test: `assertPrefixStable(prev, next, { allowBreak: "compaction" })`. In a trace: a `prefix_break` hint on the first call after the break.
|
|
157
|
+
|
|
158
|
+
A declaration excuses one call, and only a change its reason allows. See [intentional breaks](https://github.com/parthg1901/prefixlens/blob/main/docs/intentional-breaks.md).
|
|
159
|
+
|
|
160
|
+
## Limits
|
|
161
|
+
|
|
162
|
+
- It sees the request your code sends, not what the provider does with it. Server-side context editing, eviction and routing show up as a miss with the prefix intact.
|
|
163
|
+
- Costs are API list prices from a bundled table, not your bill. Subscriptions, discounts and batch tiers are not applied, and the table can be out of date. See [pricing](https://github.com/parthg1901/prefixlens/blob/main/docs/pricing.md).
|
|
164
|
+
- Gemini's native API and WebSocket transports are not recorded. Chat Completions streams are recorded only when they report usage (`stream_options: {"include_usage": true}`).
|
|
165
|
+
- A trace holds your prompts, tool output and the files your agent read. Keep it private and never commit it. See [SECURITY.md](SECURITY.md).
|
|
166
|
+
- The fields of `report --json` may change in a minor version before 1.0. What has been checked on real traffic is listed in [verification](https://github.com/parthg1901/prefixlens/blob/main/docs/verification.md).
|
|
167
|
+
|
|
168
|
+
## Documentation
|
|
169
|
+
|
|
170
|
+
- [Assertions](https://github.com/parthg1901/prefixlens/blob/main/docs/assertions.md): `assertPrefixStable`, `explainPrefixDiff`, options, example failures.
|
|
171
|
+
- [Proxy](https://github.com/parthg1901/prefixlens/blob/main/docs/proxy.md): options, the base URL to set, lanes and sessions.
|
|
172
|
+
- [Library](https://github.com/parthg1901/prefixlens/blob/main/docs/library.md): `wrap`, `createLens`, `buildRecord`, `startProxy`, other exports.
|
|
173
|
+
- [Reports](https://github.com/parthg1901/prefixlens/blob/main/docs/report.md): options, the JSON report, how verdicts are decided.
|
|
174
|
+
- [Intentional breaks](https://github.com/parthg1901/prefixlens/blob/main/docs/intentional-breaks.md) and [pricing](https://github.com/parthg1901/prefixlens/blob/main/docs/pricing.md).
|
|
175
|
+
- [Importers](https://github.com/parthg1901/prefixlens/blob/main/docs/adapters.md): for logs you already have, there are importers for OpenTelemetry GenAI spans and for the session logs of a few coding agents.
|
|
176
|
+
- [Trace format](SPEC.md), [security and privacy](SECURITY.md), [changelog](CHANGELOG.md), [development](https://github.com/parthg1901/prefixlens/blob/main/docs/development.md).
|
|
177
|
+
|
|
178
|
+
## Licence
|
|
179
|
+
|
|
180
|
+
MIT; see [LICENSE](LICENSE).
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Security and privacy
|
|
2
|
+
|
|
3
|
+
prefixlens sits between an agent harness and its model provider, or inside the harness's own process, and writes what it sees to a trace file. This page says exactly what it sees, what it writes, and what it never writes. Each statement about what is written, stripped or refused is checked by a test in `test/privacy.test.ts`, `test/record.test.ts`, `test/proxy-guard.test.ts`, `test/binary.test.ts`, `test/cli-private.test.ts` or `test/printable.test.ts`.
|
|
4
|
+
|
|
5
|
+
## Reporting a vulnerability
|
|
6
|
+
|
|
7
|
+
Please report security problems privately: [open a private security advisory](https://github.com/parthg1901/prefixlens/security/advisories/new) on the project's repository (the repository's **Security** tab, then **Report a vulnerability**). Do not open a public issue for a vulnerability. Include the version (`prefixlens --version`), what you ran, and what was exposed.
|
|
8
|
+
|
|
9
|
+
Only the latest released version is supported.
|
|
10
|
+
|
|
11
|
+
## Traces contain your prompts and code
|
|
12
|
+
|
|
13
|
+
**Treat every trace like source code plus personal data: keep it private, and never commit one.** By design, a trace records the full request of every model call, as sent: the full system prompt, every tool definition, the instruction files and memory the harness sends with it (`CLAUDE.md`, `AGENTS.md`, memory files), and every message in the conversation, including your prompts, the contents of files the agent read, tool outputs (shell output, search results, anything a tool returned), and earlier assistant replies, which the harness sends back as history. That usually includes working-directory paths, and often your email address, which harnesses put in their system prompts and git context.
|
|
14
|
+
|
|
15
|
+
Left out by default:
|
|
16
|
+
|
|
17
|
+
- request fields that identify a person, account, device or installation and are not model input: `metadata` (Anthropic's `metadata.user_id`, where Claude Code puts its account id, a persistent device id and the session id; OpenAI's developer key-value pairs), `user`, `safety_identifier`, and Codex's `client_metadata` (`IDENTIFYING_PARAMS` in `src/params.ts`). The importers drop the same fields. `keepIdentifiers` (`--keep-identifiers`) keeps them. Leaving them out changes no block hash; OpenAI documents `user` as a cache-bucketing key, so without it a miss caused by changing `user` reads as one with the prefix intact;
|
|
18
|
+
- the bytes of base64 images, documents, audio and files, which are written as their SHA-256 and decoded size (SPEC.md section 4.5). Block hashes are still those of the request as sent. `keepBinary` (`--keep-binary`) keeps the bytes.
|
|
19
|
+
|
|
20
|
+
Kept, because the analysis needs them: `prompt_cache_key` and the `session-id` header, which providers route requests to a cache by. A harness may derive them from a session or user id.
|
|
21
|
+
|
|
22
|
+
The same applies to traces made by `prefixlens import`, which copy the content of the harness's own session logs, and to `--html` and `--json` reports, which include the changed blocks of each miss.
|
|
23
|
+
|
|
24
|
+
The repository's `.gitignore` ignores `*.trace.jsonl`, `*.trace.html` and `*.report.html`, and CI fails if a trace, session log or credential is committed. Neither helps if you write a trace under another name.
|
|
25
|
+
|
|
26
|
+
Trace files prefixlens creates (the proxy, `wrap`, `lens.fetch`, `lens.observe` with a trace path, `prefixlens import -o`) and `report --html` reports are created readable and writable by their owner only (mode 0600), and directories it creates for them owner-only (0700). A file or directory that already exists keeps its permissions: prefixlens never changes a mode you set. The proxy and `import -o` print a warning when such a file is readable by other users. A report printed to stdout (`--json`, the terminal report) or a trace written to stdout by `import` gets whatever permissions you redirect it into.
|
|
27
|
+
|
|
28
|
+
## Trace content on the terminal
|
|
29
|
+
|
|
30
|
+
A trace holds whatever the agent read: web pages, file contents, tool output, tool names from third-party servers. Printed as it is, an escape sequence in that content could retitle your terminal, hide or recolour later output, or rewrite lines already printed. So what prefixlens prints from a trace for people to read has its control characters (C0 and C1, and DEL; newline and tab are kept) written visibly, as `\x1b`: the terminal report, the diff excerpt under a miss (in every report form), the message of a `PrefixBreakError` from `prefixlens/test`, which ends up in CI logs, and the proxy's line per call. The HTML report escapes the content it shows. The JSON report (`--json`) keeps the content as it was and prints every control character in it as a JSON escape (`\u001b`, `\u009b`), so it parses to the same values.
|
|
31
|
+
|
|
32
|
+
A trace itself is not changed, and is not covered: it is JSON Lines, in which the C0 characters are escaped and DEL and the C1 characters are not. That includes a trace `prefixlens import` prints to stdout when `-o` is not given. If you print a trace with another tool (`cat`, `less -r`), that tool decides what reaches the terminal.
|
|
33
|
+
|
|
34
|
+
## What is never written
|
|
35
|
+
|
|
36
|
+
From the request headers, only these four are written, under the record's `headers` field: `session-id`, `anthropic-beta`, `anthropic-version` and `openai-beta` (`RECORDED_HEADERS` in `src/record/exchange.ts`). They route a request to a cache or change what is cached. Every other header is dropped before the record is built, including `authorization`, `x-api-key`, `proxy-authorization`, `cookie`, `openai-organization`, `openai-project`, `chatgpt-account-id`, `user-agent` and any custom header. This holds for the proxy, `wrap`, `lens.fetch`, `lens.observe` and `buildRecord`, which all build records with the same function.
|
|
37
|
+
|
|
38
|
+
From the response, only the usage counts, the response id and the model name are written. The response's content (the model's reply, tool calls, reasoning) is not. It reaches the trace only when the harness sends it back as history in a later request.
|
|
39
|
+
|
|
40
|
+
Nothing is written for calls that are not model calls (anything other than `POST …/messages`, `…/chat/completions` and `…/responses`), for error responses, or for requests that fail to reach the provider.
|
|
41
|
+
|
|
42
|
+
## The recording proxy
|
|
43
|
+
|
|
44
|
+
`prefixlens record` forwards every request to the provider and streams the response back. It sees everything the harness sends and receives, API keys included. The method, path, query, body and credentials go upstream as the harness sent them. The headers and the framing are not byte for byte what the harness sent:
|
|
45
|
+
|
|
46
|
+
- it removes the hop-by-hop headers (`host`, `connection`, `keep-alive`, `content-length`, `transfer-encoding`) and its own `x-prefixlens-lane` and `x-prefixlens-session` before forwarding;
|
|
47
|
+
- it narrows the client's `accept-encoding` to the encodings the runtime's fetch can decode (this drops `zstd` on Node before 24), because it passes the response on decoded;
|
|
48
|
+
- the runtime's fetch, which sends the request, sets the hop-by-hop headers again for its own connection and adds its defaults for headers the client did not send. With a client that sent none of them, the upstream received on Bun 1.4.0 `accept: */*`, `accept-encoding: gzip, deflate, br, zstd` and `user-agent: Bun/1.4.0`, and on Node 24 `accept: */*`, `accept-encoding: gzip, deflate`, `user-agent: node`, `accept-language: *` and `sec-fetch-mode: cors`. A header the client did send is forwarded with the client's value;
|
|
49
|
+
- it reads each request body whole before forwarding it, so a request is not streamed to the provider as it arrives;
|
|
50
|
+
- the response is streamed back chunk by chunk as it arrives, but decompressed, without the provider's `content-encoding` and `content-length`.
|
|
51
|
+
|
|
52
|
+
It listens on `127.0.0.1:8787` by default, so only processes on the same machine can reach it. It has no authentication of its own: any local process can send requests through it (with its own credentials; the proxy adds none), and those requests are written to your trace.
|
|
53
|
+
|
|
54
|
+
A web page in your browser can also send requests to a loopback port. The proxy refuses, with 403 and without forwarding:
|
|
55
|
+
|
|
56
|
+
- a request whose `Host` header is not a loopback name (`localhost`, `*.localhost`, `127.0.0.0/8`, `::1`) or the address given with `--host`, which defeats DNS rebinding;
|
|
57
|
+
- a request with an `Origin` header, which harness CLIs do not send, unless `--allow-origin` (`allowedOrigins`) names that origin. Browsers send `Origin` on cross-origin `fetch` and XHR requests, on their preflights and on cross-origin `POST`s, which covers every request that could be recorded as a model call.
|
|
58
|
+
|
|
59
|
+
What the `Origin` check does not cover: a browser sends no `Origin` on a plain `GET` navigation or on an image or script load. Such a `GET`, with a loopback `Host`, is forwarded to the upstream like any other `GET`. Nothing is recorded for it (only `POST`s to the model endpoints are), and the proxy adds no credentials to it.
|
|
60
|
+
|
|
61
|
+
`--allow-origin` only stops the proxy refusing: the proxy adds no CORS response headers of its own and forwards a preflight upstream like any other request, so a page from an allowed origin can read the answer only if the upstream's CORS headers allow it. `Origin: null` is what every sandboxed frame and every page opened from a file sends, so allowing `null` allows all of them.
|
|
62
|
+
|
|
63
|
+
This does not make the proxy safe to expose: it does not authenticate the client, and a local process is trusted.
|
|
64
|
+
|
|
65
|
+
`--host` binds it to another address. Do this only on a network you trust, and understand what it means:
|
|
66
|
+
|
|
67
|
+
- the harness talks to the proxy over plain HTTP, so API keys and request content cross the network unencrypted;
|
|
68
|
+
- anyone who can reach the port can send requests through it to the provider (with their own credentials; the proxy adds none), and their requests are written to your trace.
|
|
69
|
+
|
|
70
|
+
`--upstream` sends requests, credentials included, wherever you point it. An `http://` upstream receives them unencrypted.
|
|
71
|
+
|
|
72
|
+
The proxy prints a warning at start-up when `--host` is not a loopback address and when `--upstream` is plain HTTP to another machine.
|
|
73
|
+
|
|
74
|
+
## wrap, lens.fetch and lens.observe
|
|
75
|
+
|
|
76
|
+
`wrap(client)` and `lens.fetch()` run inside your process. They send each request exactly as the client built it, through the client's own fetch, and record a copy as the response streams back; they make no other request. `lens.observe` and `buildRecord` make no network request at all: they build a record from the request and response you pass in. All of them write the same record as the proxy, with the same headers and identifying fields dropped and binary data elided. `wrap` and `lens.fetch` write to `$PREFIXLENS_TRACE`, else `prefixlens.trace.jsonl` in the working directory, unless you pass `trace` (a path, or `false` to write no file).
|
|
77
|
+
|
|
78
|
+
## No telemetry
|
|
79
|
+
|
|
80
|
+
prefixlens sends nothing anywhere on its own. The only outgoing network requests in its source are the proxy forwarding each request to the configured upstream, `wrap`/`lens.fetch` passing requests to the fetch you gave them, and the price fetch below. Importers read local files (the opencode database is opened read-only), reports are written locally, and the HTML report is self-contained and makes no external requests. The pricing table is read from a local file.
|
|
81
|
+
|
|
82
|
+
`prefixlens pricing update` and `prefixlens pricing check` fetch the models.dev price catalogue, and only when you run one of them without `--from`. Each makes one HTTP GET to `https://models.dev/api.json` and to no other URL: it does not follow redirects. The request has no body, cookies or credentials and carries nothing about your traces; its headers are `host`, `accept: application/json`, `accept-encoding`, `connection` and the runtime's default `user-agent` (`Bun/<version>` on Bun, as checked in `test/catalogue.test.ts`; `node` on Node, whose fetch also adds `accept-language: *` and `sec-fetch-mode: cors`). The response is read as data (prices, ids and dates; ids with control characters are dropped), limited to 32 MB and 30 seconds. `update` writes only the converted prices to your user cache directory (see [docs/pricing.md](https://github.com/parthg1901/prefixlens/blob/main/docs/pricing.md#commands)). The data is public, but the file is created owner-only (0600) like every file prefixlens creates, in a directory created owner-only (0700) if it did not exist; each update replaces the file, mode included. `report` and `pricing show` read that file if it exists and never fetch.
|