@kunal_dutt/token-lens 0.0.0-stage → 0.2.16

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.
Files changed (40) hide show
  1. package/GATEWAY.md +491 -0
  2. package/LICENSE +21 -0
  3. package/PRIVACY.md +36 -0
  4. package/README.md +80 -2
  5. package/agent-cost-check.yml +49 -0
  6. package/check-agent.mjs +3298 -0
  7. package/cli.mjs +22 -0
  8. package/gateway.mjs +3365 -0
  9. package/package.json +45 -4
  10. package/sdk.mjs +207719 -0
  11. package/server.mjs +3392 -0
  12. package/token-lens.mcpb +0 -0
  13. package/types/src/core/advice.d.ts +27 -0
  14. package/types/src/core/budget.d.ts +28 -0
  15. package/types/src/core/compress.d.ts +29 -0
  16. package/types/src/core/cost.d.ts +32 -0
  17. package/types/src/core/guard.d.ts +20 -0
  18. package/types/src/core/hotspots.d.ts +124 -0
  19. package/types/src/core/index.d.ts +74 -0
  20. package/types/src/core/media.d.ts +29 -0
  21. package/types/src/core/models.d.ts +81 -0
  22. package/types/src/core/plans.d.ts +88 -0
  23. package/types/src/core/redos.d.ts +32 -0
  24. package/types/src/core/review.d.ts +46 -0
  25. package/types/src/core/rules.d.ts +47 -0
  26. package/types/src/core/tasks.d.ts +44 -0
  27. package/types/src/core/tokens.d.ts +19 -0
  28. package/types/src/core/verify.d.ts +19 -0
  29. package/types/src/gateway/config.d.ts +217 -0
  30. package/types/src/gateway/guard.d.ts +56 -0
  31. package/types/src/gateway/images.d.ts +40 -0
  32. package/types/src/gateway/metrics.d.ts +287 -0
  33. package/types/src/gateway/optimize.d.ts +102 -0
  34. package/types/src/gateway/protocol.d.ts +68 -0
  35. package/types/src/gateway/runs.d.ts +182 -0
  36. package/types/src/gateway/shared.d.ts +57 -0
  37. package/types/src/gateway/signals.d.ts +38 -0
  38. package/types/src/gateway/storage.d.ts +88 -0
  39. package/types/src/gateway/verify.d.ts +51 -0
  40. package/types/src/sdk/index.d.ts +228 -0
package/GATEWAY.md ADDED
@@ -0,0 +1,491 @@
1
+ # Token Lens gateway
2
+
3
+ A self-hosted gateway that cuts the tokens your AI features, workflows and
4
+ agents spend, on any model: OpenAI, Anthropic, Azure OpenAI, or a model you
5
+ run yourself behind an OpenAI-compatible server (vLLM, TGI, SGLang, Ollama,
6
+ LiteLLM, an in-house serving stack).
7
+
8
+ It speaks the OpenAI and Anthropic APIs, so nothing in your app changes except
9
+ its **base URL**. It runs inside your own network; prompts never leave it except
10
+ to the upstream you configure, and it stores no prompt text (logs and reports
11
+ hold counts only).
12
+
13
+ ```
14
+ your app / agent / workflow ──► Token Lens gateway ──► OpenAI · Anthropic · Azure · your own model
15
+ (any language, any SDK) optimize · measure · report
16
+ ```
17
+
18
+ **Success criterion: fewer tokens, same work.** The gateway never changes the
19
+ model a call asks for and never suggests a smaller one. It cuts what the agent
20
+ sends (repeated prompts, history, tool definitions and results, retries), shows
21
+ the saving in tokens and money before and after, and backs it with output
22
+ checks: sampled calls are re-run as originally written and the answers compared.
23
+
24
+ ## Run it
25
+
26
+ ```bash
27
+ docker build -t token-lens-gateway token-lens
28
+ docker run -p 8080:8080 \
29
+ -e OPENAI_API_KEY=sk-... -e ANTHROPIC_API_KEY=sk-ant-... \
30
+ -e TOKEN_LENS_CLIENT_KEYS="payments-team:tl-key-1,support-agent:tl-key-2" \
31
+ token-lens-gateway
32
+ ```
33
+
34
+ or without Docker (Node 20+): `npm install && npm run build:gateway && node dist-gateway/gateway.mjs`.
35
+ The bundle is one file with no runtime dependencies.
36
+
37
+ For several upstreams, routing and your own models, mount a config file and set
38
+ `TOKEN_LENS_CONFIG` (see [deploy/gateway.example.json](deploy/gateway.example.json)
39
+ and [deploy/docker-compose.yml](deploy/docker-compose.yml)).
40
+
41
+ ## Point your app at it
42
+
43
+ | Client | Change |
44
+ | --- | --- |
45
+ | OpenAI SDK (Python, Node, Java, Go, .NET…) | `base_url="http://token-lens:8080/v1"`, `api_key=<your Token Lens client key>` |
46
+ | Anthropic SDK | `base_url="http://token-lens:8080"`, `api_key=<client key>` |
47
+ | LangChain / LangGraph | `ChatOpenAI(base_url=…/v1)` or `ChatAnthropic(base_url=…)` |
48
+ | LlamaIndex, CrewAI, AutoGen, Semantic Kernel, Vercel AI SDK, Spring AI | their OpenAI or Anthropic provider's base URL setting |
49
+ | OpenAI Agents SDK | `AsyncOpenAI(base_url=…/v1)` as the client (Responses API is supported) |
50
+ | Anything else | `OPENAI_BASE_URL` / `ANTHROPIC_BASE_URL` environment variables, or raw HTTP |
51
+
52
+ ```python
53
+ from openai import OpenAI
54
+ client = OpenAI(base_url="http://token-lens:8080/v1", api_key="tl-key-1")
55
+ client.chat.completions.create(model="acme-llm-70b", messages=[...]) # routed to your own model
56
+ ```
57
+
58
+ Endpoints optimized: `/v1/chat/completions`, `/v1/responses`, `/v1/messages`,
59
+ streaming included. Every other `/v1/...` path (models, embeddings, files,
60
+ batches, count_tokens) is passed through unchanged.
61
+
62
+ ### One app, any model: calls between the two APIs
63
+
64
+ An app written with the Anthropic SDK can call a model behind an
65
+ OpenAI-compatible API (an in-house model on vLLM, Azure, any OpenAI-style
66
+ server), and an app written with the OpenAI SDK can call Claude. Route the model
67
+ to the upstream in the config (`"routes": [{ "model": "acme-*", "upstream": "inhouse" }]`)
68
+ and keep calling the endpoint your SDK uses.
69
+
70
+ - **What is translated:** system prompts, text, images, tool definitions, tool
71
+ calls and results, `tool_choice`, stop sequences, temperature and token limits,
72
+ in both directions. Answers, usage (including cached tokens) and errors come
73
+ back in your SDK's shape, and streams are translated event by event.
74
+ - **What stays the same:** the model. Every Token Lens fix and output check works
75
+ the same on translated calls. The gateway's prompt caching becomes Anthropic
76
+ cache breakpoints when an OpenAI-style call goes to Claude.
77
+ - **Keys:** if callers bring their own key, it is sent the way the other API
78
+ expects (`Authorization: Bearer` becomes `x-api-key`, and the reverse).
79
+
80
+ ## Your own model
81
+
82
+ A company model isn't in anyone's built-in list, so register it: its context
83
+ window, tokenizer and price. Matching is by model id, so `acme-llm` also covers
84
+ `acme-llm-70b-instruct`.
85
+
86
+ ```json
87
+ "models": [
88
+ { "id": "acme-llm", "match": ["acme-llm", "/^acme-.*-instruct$/"], "contextWindow": 32768,
89
+ "tokenizer": "heuristic", "charsPerToken": 3.6,
90
+ "pricing": { "inputPerMTok": 0.2, "outputPerMTok": 0.6 } }
91
+ ],
92
+ "upstreams": { "inhouse": { "type": "openai", "baseUrl": "http://llm.internal:8000/v1" } },
93
+ "routes": [{ "model": "acme-*", "upstream": "inhouse" }]
94
+ ```
95
+
96
+ - `tokenizer`: `"o200k"` if the model uses OpenAI's o200k tokenizer, otherwise
97
+ `"heuristic"`. Heuristic counts **calibrate themselves**: each time the
98
+ upstream reports usage, the gateway compares it with its estimate and adjusts
99
+ that model's factor (see `calibration` in `/token-lens/stats`).
100
+ - The same entry can override a built-in model, for example to add exact pricing
101
+ for `gpt-4o-mini`.
102
+ - Models can also be added at runtime: `POST /token-lens/models` with the admin
103
+ key (`TOKEN_LENS_ADMIN_KEY`). Runtime entries live in memory; put permanent
104
+ ones in the config file.
105
+
106
+ ## What it does to each request
107
+
108
+ The gateway sorts every request's input into parts: **system** prompt,
109
+ **tools** definitions, **history**, **tool outputs**, **context** (material
110
+ pasted or retrieved into the newest message) and the **prompt** itself. It
111
+ counts each part in the model's own tokens, tracks the parts across the calls
112
+ of a run, and applies the fix that fits the part that is growing.
113
+
114
+ Changes that keep the meaning are applied automatically:
115
+
116
+ | Fix | Part | What it does |
117
+ | --- | --- | --- |
118
+ | `prompt` | user messages | The Token Lens optimizer, adapted to the model and task. Code, quotes, URLs, XML-tagged and pasted material stay verbatim. Every user message is rewritten the same way on every call, so the model sees one consistent history and prompt caches keep hitting. |
119
+ | `minify-json` | tool outputs | Pretty-printed JSON re-serialized without whitespace. Same data. |
120
+ | `collapse-repeats` | tool outputs | Runs of identical lines become one line plus `[previous line repeated N more times]`. |
121
+ | `dedupe-tool-outputs` | tool outputs | When the agent gets the same result twice (same tool, same input), the result is sent once; later copies point back to the first, which stays in place so caching still works. |
122
+ | `dedupe-passages` | context, history, tool outputs | A paragraph of 200+ characters repeated within a request (the same retrieved chunk, a policy pasted twice) is sent once. Later copies point back to the first. |
123
+ | `dedupe-images` | images | An image sent again later in the conversation (a repeated screenshot or upload) is sent once. Later copies become a one-line pointer to the first. |
124
+ | `prompt-caching` | system, tools, history | Once a run re-sends a prefix of 1,024+ tokens: Anthropic cache breakpoints after the tools, the system prompt and the conversation; OpenAI's `prompt_cache_key` per run. Cached input costs about a tenth. Not added to one-off calls, where a cache write would cost more. |
125
+ | retry replay | whole call | An identical, non-streamed, temperature-0 request within 2 minutes gets the earlier answer, at no token cost. Sampled requests (temperature unset or > 0) are always sent. |
126
+ | embeddings cache | `/v1/embeddings` | A text this team already embedded with the same model and settings is answered from memory; only new texts go upstream. See [Embeddings](#embeddings-unchanged-documents-once). |
127
+
128
+ Changes that could change the answer wait for an opt-in, either for everyone
129
+ in the config's `optIn` or per request with `x-token-lens-allow`:
130
+
131
+ | Opt-in | What it does |
132
+ | --- | --- |
133
+ | `elide-tool-outputs` | Tool outputs older than the newest 3 (and over 300 tokens) are replaced with a one-line stub. |
134
+ | `lazy-tools` | Loads tool definitions only when the agent needs them (the idea behind Atlassian's mcp-compressor). See [Large tool lists](#large-tool-lists-lazy-tools). |
135
+ | `trim-tool-fields` | In tool outputs older than the newest 3, JSON fields the agent never referred to afterwards (no value and not the field's name appears in any later message or tool call) are left out and listed in `_omitted_by_token_lens`. Small fields stay. |
136
+ | `compact-json` | In JSON tool outputs, a list of 5+ records with the same keys is written once as `{"_columns": [...], "_rows": [[...], ...]}` instead of repeating every key in every record. Nothing is dropped. |
137
+ | `park-tool-outputs` | Older tool outputs over 1,500 tokens stay outside the conversation: their first lines and an id remain, plus a `token_lens_read_output` tool the model calls to search them or read a slice. The gateway answers those look-ups itself and re-sends, so the app only gets the model's real answer; after 3 look-ups the request goes again with the outputs in full. Non-streamed OpenAI Chat and Anthropic requests. |
138
+ | `flex` | The provider's cheaper, slower processing on the same model: OpenAI's `service_tier: "flex"`, about half price. Offered on api.openai.com upstreams (set `"flex": true` on others that support it). If flex is busy or not offered for the model, the same request goes again at the normal tier. Doesn't change the request's content. |
139
+ | `warm-cache` | When several calls with the same 1,024+ token prefix arrive at once, the first goes alone and the rest wait for it (up to 15 seconds, `optIn.warmCache.maxWaitMs`), so they read its cache instead of each paying for the prefix. Trades a little speed; doesn't change requests. |
140
+ | run budget | A cap per run on calls and/or spend (`runBudget` in the config, or `x-token-lens-max-calls` / `x-token-lens-max-cost-usd`). Calls past the cap get an error instead of being sent, which stops a looping agent. |
141
+
142
+ Per-request controls (headers): `x-token-lens-mode: optimize | observe | off`,
143
+ `x-token-lens-level: light | balanced | aggressive`, `x-token-lens-allow`,
144
+ `x-token-lens-run` (names the run; otherwise calls are grouped by app, model,
145
+ system prompt and first user message), `x-token-lens-app` (when no client keys
146
+ are configured).
147
+
148
+ Every response carries `x-token-lens-run`, `-profile`, `-input-tokens`,
149
+ `-saved-tokens`, `-actions`, `-tips` and `-growing` (the part that grew most in
150
+ this run). `x-token-lens-price-tier: near 200000` or `above 200000` warns that a
151
+ call is close to or past the size where its model bills the whole request at a
152
+ higher rate (see [Price tiers](#price-tiers)).
153
+
154
+ ## Savings report: tokens and money, before and after
155
+
156
+ `GET /token-lens/report` is a page anyone can open in a browser (with
157
+ `?key=<client key>` when keys are set): tokens and cost before and after, money
158
+ saved, and the share of checked calls whose output was unchanged, broken down by
159
+ team, agent, model and run. `GET /token-lens/report.json` has the same data.
160
+
161
+ - **Before** is what the same calls would have cost without Token Lens: the
162
+ tokens it removed added back, cached input priced as normal input, and
163
+ replayed retries paid in full. **After** is what the provider billed, with
164
+ cached reads and cache writes at their own rates.
165
+ - Built-in model families carry public API list prices. For your own models, and
166
+ to use your negotiated rates, give the model `pricing` in the config. Calls on
167
+ models without a price are counted in tokens only, and the report says how many.
168
+ - Name the agent or workflow with the `x-token-lens-agent` header to get a row
169
+ per agent; the team is the client key's name.
170
+
171
+ ## Output checks: evidence the work is unchanged
172
+
173
+ For a sample of calls the gateway changed, it also sends the original request
174
+ to the same model in the background and compares the two answers. Streamed
175
+ calls are checked too: the streamed answer is put back together and compared
176
+ with an unstreamed original.
177
+
178
+ - A check passes when the tool calls are identical (same tools, same arguments)
179
+ and the text says the same thing (word and word-pair overlap of at least
180
+ `minSimilarity`, 0.8 by default). A changed number counts as a different answer.
181
+ - `verify.sampleRate` (default 5%; `TOKEN_LENS_VERIFY_RATE`) sets how many
182
+ calls are checked; calls using an opt-in fix are checked at least 25% of the
183
+ time. Checks cost a call each, so after the first 5 per team and agent they
184
+ stop whenever check tokens would exceed 20% of the tokens saved
185
+ (`maxShareOfSavings`). Their cost is shown separately and the report's net
186
+ saving subtracts it.
187
+ - `x-token-lens-verify: 1` checks a specific call. Results appear per run
188
+ (`outputCheck`, `outputChecks`), in the report and in `/metrics`.
189
+ - Even unchanged requests don't always get word-for-word identical answers when
190
+ sampling is on, so treat the rate as a lower bound on how often the work is the
191
+ same; temperature-0 calls give the cleanest signal.
192
+
193
+ ### The rule: no fix may change an answer
194
+
195
+ - **A changed answer switches the fix off.** When a check finds a different
196
+ answer, the gateway asks the model the original request once more. If the
197
+ model's own two answers agree, the fixes that were in play are switched off for
198
+ that team and agent (the log says which). If the model's own answers differ just
199
+ as much, the difference isn't the fixes' and they stay on (the check shows
200
+ `modelVaries: true`). Switched-off fixes are listed at `GET /token-lens/guard`,
201
+ in `/token-lens/stats` and in the report, and stay off until an admin calls
202
+ `POST /token-lens/guard/reset` (body `{}` for all, or `{"app": "...", "agent": "..."}`).
203
+ - **Opt-in fixes prove themselves first.** `elide-tool-outputs` and `lazy-tools`
204
+ can change an answer by design, so for each team and agent they start on
205
+ probation: the original request goes up alongside the optimized one, and the app
206
+ gets the optimized answer only when the two match. If they differ, the app gets
207
+ the original's answer (`x-token-lens-answer: original`) and the fix is switched
208
+ off as above. After `verify.probation` matches in a row (5 by default) the fix
209
+ runs on its own, still checked at least 25% of the time. During probation the
210
+ extra call is counted as check cost, and opt-in fixes are left out of streamed
211
+ and batched calls, whose answers can't be compared before the app gets them.
212
+ - Fixes that never change the request's content (prompt caching, batching on the
213
+ same model, replaying an identical retry) aren't switched off. Images are never
214
+ shrunk: providers already resize them to their own limit before counting tokens.
215
+
216
+ ## Optimizing a running agent's whole run
217
+
218
+ Send the agent's model calls through the gateway (base URL) and give each run a
219
+ name with `x-token-lens-run` and the agent a name with `x-token-lens-agent`.
220
+ Every step of the run is then optimized as above, and the run is judged as a
221
+ whole, not call by call:
222
+
223
+ - **Total cost of the run**: `/token-lens/runs/<id>` adds up every step,
224
+ replayed retries (free) and output checks (shown separately): `costBeforeUsd`,
225
+ `costUsd`, `savedUsd`, `checkCostUsd`, `netSavedUsd`, and each step's own cost.
226
+ - **What the agent does across steps**: repeated tool calls (same tool, same
227
+ input), loops (the same call several times in a row), retries, output that
228
+ costs more than input, and context that keeps growing.
229
+ - **What happens about it**: the gateway fixes what it can without changing the
230
+ answer (identical tool results sent once, caching, retry replay, compact tool
231
+ output). What only the agent's code can change, such as skipping a repeated
232
+ step, caching tool results or adding a step limit, is named in the run's
233
+ findings with the tokens it costs. A run budget is there for runaway loops when
234
+ you turn it on.
235
+
236
+ ### Test it: the same agent with and without Token Lens
237
+
238
+ `scripts/agent-benchmark.mjs` runs a multi-step customer-care agent (4 tools,
239
+ about 10 tool calls, refunds two delayed orders) through the gateway twice:
240
+ once observed only, with requests sent as written, and once optimized. It
241
+ compares the actions taken, the final answer, tokens and the run's cost,
242
+ including the output checks.
243
+
244
+ ```bash
245
+ npm install && npm run build:gateway
246
+ npm run benchmark:agent # stand-in model, no key, runs locally and in CI
247
+ npm run benchmark:agent -- --api anthropic # the same stand-in behind the Anthropic API (simulates prompt caching)
248
+ npm run benchmark:agent -- --extra-tools 30 --allow lazy-tools # 30 more tools the agent doesn't need, with lazy-tools
249
+ ANTHROPIC_API_KEY=... npm run benchmark:agent -- --provider anthropic --model claude-opus-5-5
250
+ OPENAI_API_KEY=... npm run benchmark:agent -- --provider openai --model <your-model-id>
251
+ ```
252
+
253
+ With a real provider `--model` is required. Pass the exact model your agent
254
+ runs (for example `claude-opus-5-5` or `claude-sonnet-5-5`, or the OpenAI model
255
+ id shown in your account), so the result is about that model, not a smaller one.
256
+
257
+ With a real model it also runs the baseline a second time as a control, so the
258
+ model's own run-to-run variation is visible next to any difference Token Lens
259
+ makes. `--out result.json` saves everything. Checks run on every optimized step
260
+ here (capped by `maxShareOfSavings`) to give the most evidence, so on a short run
261
+ their cost can exceed the saving; in production about 5% of calls are checked.
262
+
263
+ ### Work that can wait: half price (`x-token-lens-batch`)
264
+
265
+ Overnight reports, evaluations and bulk summaries don't need an answer in
266
+ seconds. Send them with `x-token-lens-batch: true`:
267
+
268
+ - **What happens:** the gateway applies its usual fixes, then queues the call
269
+ for the provider's batch option on the same model. That is OpenAI's Batch API
270
+ or Anthropic's Message Batches. Both bill about half the normal price and
271
+ answer within 24 hours.
272
+ - **What the app gets back:** `202` and a job id straight away. Fetch the answer
273
+ later from `GET /token-lens/batches/<id>`. It returns `queued`, `submitted`,
274
+ `completed` with the provider's normal response body, or `failed` with the
275
+ reason. Each result is handed over once and kept for a day.
276
+ - **Reports:** batched calls show at the batch price next to what they would have
277
+ cost, and carry a `batch` action.
278
+ - **When it's refused:** streamed requests, and upstreams without a batch option.
279
+ In-house servers get `"batch": true` on their upstream if they speak OpenAI's
280
+ Batch API. A refused request returns an error instead of being sent at full
281
+ price.
282
+ - **Timing:** queued calls go out every `batch.flushSeconds` (60) and results are
283
+ checked every `batch.pollSeconds` (60). `batch.discount` (0.5) is the price
284
+ factor used in reports.
285
+
286
+ ### Embeddings: unchanged documents once
287
+
288
+ Search and retrieval pipelines often re-embed every document on each sync,
289
+ changed or not. On `/v1/embeddings` the gateway keeps each vector it gets, keyed
290
+ by a hash of the input with the team, upstream, model, `dimensions` and
291
+ `encoding_format`, and sends only new inputs upstream. The response is the
292
+ provider's own vectors in the request's order, with
293
+ `x-token-lens-cached-embeddings` and `-saved-tokens` headers.
294
+
295
+ - Texts aren't stored, only their hash and vector, in this gateway's memory up to
296
+ `embeddingsCacheMB` (256), least recently used out first.
297
+ - Teams don't share entries. Turn it off with `"safe": { "cacheEmbeddings": false }`.
298
+
299
+ ### Price tiers
300
+
301
+ Some models bill the whole request at a higher rate once its input passes a
302
+ size: Gemini Pro and Claude Sonnet's 1M-token window above 200K input tokens,
303
+ for example. Built-in models carry their list price for this, and `priceTiers`
304
+ in the config adds or overrides models by id. The gateway:
305
+
306
+ - prices calls past the size at the higher rate in every report, and prices the
307
+ "without Token Lens" cost at that rate when the original request was past it;
308
+ - adds `x-token-lens-price-tier` to calls within 10% of it or past it;
309
+ - reports a `price-tier` run finding with what to drop first.
310
+
311
+ Nothing is cut automatically, since that would change the answer.
312
+
313
+ ### Images
314
+
315
+ The gateway reads each image's size and estimates its tokens with the
316
+ provider's published rules: about w×h/750 for Anthropic, and 512 px tiles for
317
+ OpenAI. Run reports show those tokens, and a repeated image is sent once
318
+ (`dedupe-images`).
319
+
320
+ Hosted providers shrink large images before counting them. Anthropic fits them
321
+ to 1,568 px; OpenAI fits them to 2,048 px with a 768 px short side. So sending a
322
+ smaller image than that saves upload time, not tokens, and the gateway doesn't
323
+ claim a saving for it. The changes that do save tokens are in the run report's
324
+ "images" finding: keep a description instead of an old screenshot, crop to the
325
+ part that matters, or use low detail where it is enough. Low detail can change
326
+ answers, so check it with output checks on.
327
+
328
+ ### Large tool lists (`lazy-tools`)
329
+
330
+ Agents connected to large MCP servers often send dozens of tool definitions on
331
+ every call and use only a few. With `x-token-lens-allow: lazy-tools`, or
332
+ `"optIn": { "lazyTools": { "minTools": 8 } }` in the config:
333
+
334
+ - **What goes up:** tools the run hasn't used yet are sent as a name and a
335
+ one-line summary, plus a small `token_lens_load_tools` tool.
336
+ - **Loading on demand:** when the model calls the loader, or calls a summarized
337
+ tool directly, the gateway adds those tools' full definitions and sends the same
338
+ request again. The app only ever gets the final answer. Any tool call in it was
339
+ made with that tool's full definition, and the loader never reaches the app.
340
+ - **Later calls:** tools a run has loaded or used stay in full on its later calls,
341
+ so each tool is loaded at most once per run.
342
+ - **When it applies:** non-streamed requests with at least 8 plain function tools.
343
+ A tool forced with `tool_choice` is always sent in full.
344
+ - **Cost accounting:** the extra loading calls are counted in the run's cost.
345
+ The saving shown is net of them, and can be negative when a run uses most of
346
+ its tools.
347
+
348
+ It can change which tool the model picks, so it is opt-in, and output checks
349
+ sample it more often. In the stand-in benchmark with 34 tools, the run went from
350
+ 94,741 to 48,452 tokens (49% fewer). The agent took the same actions and gave an
351
+ identical answer, and 5 of 5 output checks matched.
352
+
353
+ ### What else the run report spots
354
+
355
+ These need a change in the agent, so the gateway shows them with the fix and
356
+ its saving, and changes nothing itself:
357
+
358
+ - **`cache-breaker`:** the system prompt or tool list starts the same as the
359
+ previous call's and then changes: a timestamp, a date, an id, a number, or
360
+ tools in a different order. The provider's cache misses from that point, so
361
+ those tokens are billed in full instead of at about a tenth. The report names
362
+ the kind of change, never the text, and gives the change to make (move it to the
363
+ newest message, or sort tools once).
364
+ - **`json-retries`:** the model's JSON reply didn't parse and the agent asked
365
+ again, each time a whole extra step. The fix is the provider's strict
366
+ structured output on the same model, written for the API in use (`json_schema`
367
+ response format on OpenAI, a forced tool on Anthropic).
368
+ - **`shared-reads`:** files or documents this run read that other runs of the
369
+ team (other sub-agents) read too, matched by a hash of the path. The fix loads
370
+ them once and passes them on, or puts them at the top of each agent's prompt so
371
+ the provider's cache serves them.
372
+ - **`price-tier`:** see [Price tiers](#price-tiers).
373
+
374
+ ### Several gateway copies, and restarts: shared storage
375
+
376
+ By default reports live in memory and start empty after a restart. To share
377
+ them across copies behind a load balancer, and keep them across restarts, point
378
+ the gateway at your own Redis or Valkey:
379
+
380
+ ```bash
381
+ TOKEN_LENS_REDIS_URL=rediss://:password@redis.internal:6380/0 npm run gateway
382
+ ```
383
+
384
+ or `"storage": { "redisUrl": "env:REDIS_URL" }` in the config. A single gateway
385
+ can use a file instead (`"storage": { "file": "/var/lib/token-lens/reports.json" }`
386
+ or `TOKEN_LENS_STORAGE_FILE`).
387
+
388
+ - **Totals:** every copy adds its own changes to one set of counters every
389
+ `storage.flushSeconds` (10), so the report, CSV and stats show all copies.
390
+ - **Run reports:** kept `storage.runDays` (7) days. Any copy can show any run.
391
+ - **Switched-off fixes:** shared, so a fix that changed an answer on one copy is
392
+ off on all of them, and an admin reset turns it back on everywhere.
393
+ - **What is stored:** only what reports already show: totals, run summaries,
394
+ findings and per-call counts, with tool inputs hashed. Never prompts or
395
+ answers. Keys start with `storage.keyPrefix` (`token-lens:`).
396
+
397
+ ### Savings export for finance
398
+
399
+ `GET /token-lens/report.csv` downloads tokens and money before and after, output
400
+ check cost and net saving, one row per team, agent and model.
401
+ `/token-lens/report.csv?by=run` gives one row per recent run. The HTML report
402
+ links both.
403
+
404
+ ## Where the tokens go: run reports
405
+
406
+ `GET /token-lens/runs/<run id>` explains a run: its calls, the parts of each, and
407
+ findings ranked by how much they would save. Each finding says what the gateway
408
+ already does about it, what a run like this one would save in tokens and
409
+ dollars, and the exact changes that cut it, each with where it goes (gateway
410
+ config, request header, agent code, system prompt, model server) and what to
411
+ paste. None of them switch to a different or smaller model. For example:
412
+
413
+ ```
414
+ - The agent calls the same tool with the same input more than once - saves ~3,377 tokens ($0.0068) per run
415
+ agent code: Cache tool results for the run, so get_order with the same input runs once
416
+ const cache = new Map(); const run = (name, args) => { ... };
417
+ system prompt: Tell the agent results don't change within a run
418
+ Tool results stay valid for this whole task. Do not call a tool again with the same input; use the earlier result.
419
+ - Tool outputs take a large share of every call
420
+ request header: Replace older tool outputs with a stub (opt-in; output checks run on a sample)
421
+ x-token-lens-allow: elide-tool-outputs
422
+ agent code: Return only the fields the agent uses from each tool
423
+ return JSON.stringify(pick(result, ["id", "status", "total"]));
424
+ ```
425
+
426
+ The HTML report (`/token-lens/report`) shows the same under "What to change
427
+ next" for each recent run.
428
+
429
+ Findings cover repeated prefixes, growing history, tool outputs, large
430
+ retrieved context, retries, repeated tool calls, loops, output-heavy runs, hidden
431
+ reasoning that outweighs the answer, and runs nearing the context limit.
432
+
433
+ - `GET /token-lens/runs` lists recent runs (`?app=` to filter).
434
+ - `GET /token-lens/stats` has the report's totals plus tokenizer calibration.
435
+ - `GET /metrics` exposes the same totals to Prometheus, labelled by team, agent
436
+ and model.
437
+ - `POST /token-lens/analyze` with `{"prompt", "model"}` returns the Token Lens
438
+ analysis of one prompt without calling a model.
439
+
440
+ ## Security
441
+
442
+ - Set client keys (`TOKEN_LENS_CLIENT_KEYS="name:key,…"` or `clientKeys` in the
443
+ config). Each key's name becomes the `app` in logs, stats and metrics. Without
444
+ keys, anyone who can reach the port can use the upstream keys; the gateway
445
+ warns about this at startup.
446
+ - Upstream keys are read from the environment (`"env:NAME"` in the config) and
447
+ never logged. When an upstream has no key, the caller's own key is passed
448
+ through.
449
+ - Logs are one JSON line per call with counts, model, status and the fixes
450
+ applied; never prompt or response text. Run reports hold counts only. Tool
451
+ inputs, which can hold customer data, show only as a short hash (for example
452
+ `get_order(input #3f2a9c1e)`) unless you set `"reportToolArgs": true`.
453
+ - `/token-lens/*` and `/metrics` need a client key (when keys are set); model
454
+ registration needs the admin key.
455
+
456
+ ## Privacy: nothing leaves your systems
457
+
458
+ The gateway runs entirely on your own servers and needs no account, licence
459
+ server or Token Lens service.
460
+
461
+ - **Outbound requests:** it makes requests only to the upstreams you configure.
462
+ There is no telemetry, no update check and no call home.
463
+ - **Reports and metrics:** stats, run reports, the HTML report and `/metrics` are
464
+ kept in memory on your gateway (or in your own Redis or file, if you turn on
465
+ shared storage), and only your own clients can read them.
466
+ - **What providers see:** the provider receives the same request your app sent,
467
+ minus the `x-token-lens-*` headers. OpenAI's prompt cache key is a hash, so app
468
+ and run names never reach it.
469
+ - **Output checks:** these re-send a sample of requests to the same upstream your
470
+ app already uses. Set `"verify": { "sampleRate": 0 }` to turn them off.
471
+ - **Embeddings and file reads:** cached embeddings are kept as a hash of the text
472
+ and its vector, in memory, per team. Files several agents read are matched by a
473
+ hash of the path; reports show only how many and which agents.
474
+ - **Test coverage:** a test checks all of this on every change. It records every
475
+ outbound request and asserts that customer data never appears in reports or
476
+ logs.
477
+
478
+ ## Limits
479
+
480
+ - Without shared storage, run reports and stats are kept in memory per gateway
481
+ copy (the most recent 2,000 runs). With it, totals are combined exactly; a run
482
+ served by two copies at once is reported by whichever wrote last, so route a run
483
+ to one copy (by the `x-token-lens-run` header) where that matters. `/metrics`
484
+ is always per copy.
485
+ - Translation between the two APIs covers Anthropic Messages and OpenAI Chat
486
+ Completions. The OpenAI Responses API reaches only OpenAI-compatible upstreams.
487
+ - Counts for models without a known tokenizer are estimates until the upstream
488
+ has reported usage a few times.
489
+ - Switched-off fixes and probation counts are kept in memory per gateway
490
+ instance, so a restart puts opt-in fixes back on probation.
491
+ - A streamed answer longer than 16 MB isn't checked.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kunal Dutt
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/PRIVACY.md ADDED
@@ -0,0 +1,36 @@
1
+ # Token Lens privacy policy
2
+
3
+ _Last updated: 13 October 2026_
4
+
5
+ Token Lens collects no data. Not from individuals, not from companies, not from
6
+ anyone.
7
+
8
+ - **No accounts, analytics, telemetry, crash reports, ads or trackers.** Token
9
+ Lens never "phones home". There is no Token Lens server that receives your
10
+ usage, and the publisher cannot see who uses it or how.
11
+ - **Your prompts and chats never leave your device.** Every count, rewrite and
12
+ tip is computed on your own computer, browser or servers.
13
+ - **Nothing is sold or shared,** because nothing is collected.
14
+
15
+ ## Each way of running Token Lens
16
+
17
+ | Where it runs | What it reads | What it keeps | What it sends |
18
+ | --- | --- | --- | --- |
19
+ | **Browser extension** (Chrome, Edge, Firefox) | The prompt box, model picker and visible messages on the AI chat sites listed in its permissions, to count tokens. | Your settings, prices, budget and savings totals, in your browser's own extension storage. Uninstalling deletes them. | Nothing. Its only network request is on claude.ai: at most once a minute it reads your own session and weekly usage from claude.ai, signed in as you, so plan tips use real numbers. The result is not stored and goes nowhere else. |
20
+ | **MCP server on your computer** (`npx @kunal_dutt/token-lens`, Claude Desktop extension) | Only the text an AI app passes to its tools. | Nothing. | Nothing by default. Only if you set your own `ANTHROPIC_API_KEY` or `GEMINI_API_KEY` for it does it send the text to that provider's token-counting endpoint, with your key, for an exact count. |
21
+ | **Your own connector** (`npx @kunal_dutt/token-lens --http`) | Only the text sent to its tools. | Nothing. | Nothing beyond the reply. |
22
+ | **Public hosted connector** (`/api/mcp`) | Only the text an AI app passes to its tools. | Nothing. Each request is handled in memory and discarded; requests are not logged or stored. The hosting provider keeps standard request metadata (time, IP address, URL) for security, as any website does. | Nothing beyond the reply. Prefer running your own copy if even that metadata matters. |
23
+ | **Developer library** (`@kunal_dutt/token-lens/sdk` on npm, `token-lens` on PyPI, inside your app) | Your app's requests to OpenAI or Anthropic, in your own process. | Savings counters in your app's memory. | Nothing of its own. Your requests still go only to the provider your code already calls. |
24
+ | **Gateway** (Docker image or `npx @kunal_dutt/token-lens gateway`, on your servers) | Your apps' model requests, which you route through it. | Savings counters in memory on your server. | Only to the model endpoints *you* configure (your OpenAI, Anthropic, Azure or in-house model). Nothing goes to Token Lens or anyone else. |
25
+
26
+ ## Stores and registries
27
+
28
+ Chrome Web Store, Microsoft Edge Add-ons, Firefox Add-ons, npm, PyPI, GitHub
29
+ and the MCP Registry may record installs or downloads under their own privacy policies.
30
+ Token Lens receives none of that.
31
+
32
+ ## Changes and contact
33
+
34
+ If this ever changed, the policy would be updated here first and the change
35
+ would ship as a new version, never silently. Questions: contact
36
+ the publisher, Kunal Dutt, through the support link on any store listing.
package/README.md CHANGED
@@ -1,3 +1,81 @@
1
- # Temporary Holding Version
1
+ # Token Lens
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Count and cut the tokens your prompts, chats and agents use, adapted to the
4
+ model and the task, without switching your model or changing the answer. Every
5
+ saving is shown in tokens and dollars, priced for the model you use.
6
+
7
+ **Collects no data.** No accounts, analytics or telemetry. It makes no network
8
+ requests of its own, except an exact token count from Anthropic or Google when
9
+ you set your own `ANTHROPIC_API_KEY` or `GEMINI_API_KEY` for it. See [PRIVACY.md](PRIVACY.md).
10
+
11
+ ## Add it to your AI app
12
+
13
+ It runs on your own computer with Node 20 or newer. `@latest` means
14
+ each start picks up the newest version, so there's nothing to update.
15
+
16
+ | App | How |
17
+ | --- | --- |
18
+ | **Claude Code** | `claude mcp add token-lens -- npx -y @kunal_dutt/token-lens@latest` |
19
+ | **Claude Desktop** | Add the JSON below to `claude_desktop_config.json` (keeps itself up to date), or for a one-click install without Node, download [token-lens.mcpb](https://unpkg.com/@kunal_dutt/token-lens@latest/token-lens.mcpb) and open it. |
20
+ | **Cursor, VS Code, Windsurf, Zed, any MCP app** | Add a stdio server with the JSON below. |
21
+ | **ChatGPT, claude.ai and mobile apps** | Need a connector URL: use the public one from the Token Lens listing, or run your own (below). |
22
+
23
+ ```json
24
+ {
25
+ "mcpServers": {
26
+ "token-lens": { "command": "npx", "args": ["-y", "@kunal_dutt/token-lens@latest"] }
27
+ }
28
+ }
29
+ ```
30
+
31
+ Then ask, for example: "optimize this prompt with Token Lens", "how full is this
32
+ chat?" or "optimize this agent's system prompt and tools".
33
+
34
+ Tools: `optimize_prompt`, `optimize_agent`, `count_tokens`,
35
+ `check_conversation`, `list_model_profiles`.
36
+
37
+ ## Run your own connector
38
+
39
+ ```bash
40
+ npx -y @kunal_dutt/token-lens@latest --http 8787 # serves http://localhost:8787/mcp
41
+ ```
42
+
43
+ Put it behind HTTPS (any reverse proxy or tunnel) and add `https://<host>/mcp`
44
+ as a custom connector in ChatGPT or claude.ai. It stores nothing.
45
+
46
+ ## Cost check for agent builders
47
+
48
+ Every pull request that changes an agent's prompts or tool definitions gets a
49
+ comment with the token and dollar change per call (and per month), priced for
50
+ the model the agent runs on, plus what could be trimmed with the same output.
51
+
52
+ - **GitHub:** copy [agent-cost-check.yml](https://unpkg.com/@kunal_dutt/token-lens@latest/agent-cost-check.yml)
53
+ to `.github/workflows/`, then set `--model` and your agent file paths.
54
+ - **Any other CI or locally:**
55
+ `npx -y @kunal_dutt/token-lens@latest check-agent --model claude-opus-5-5 --base origin/main 'agents/**'`
56
+ prints the same report as Markdown (`--out file.md` to save it,
57
+ `--fail-above 500` to fail when a change adds more than 500 tokens per call).
58
+
59
+ Agent files are `.json` specs (`system`, `tools`, `examples`, `messages`,
60
+ `prompt`; OpenAI- and Anthropic-style tool lists work as-is) or any text file,
61
+ read as a system prompt. It runs inside your CI and sends nothing anywhere.
62
+
63
+ ## Gateway for companies
64
+
65
+ A self-hosted proxy that speaks the OpenAI and Anthropic APIs and optimizes
66
+ every call your apps, workflows and agents make, with any model, including your
67
+ own. It talks only to the model endpoints you configure. See
68
+ [GATEWAY.md](GATEWAY.md).
69
+
70
+ ```bash
71
+ docker run -p 8080:8080 -e OPENAI_API_KEY ghcr.io/duttkunal101-lab/token-lens-gateway:latest
72
+ # or
73
+ OPENAI_API_KEY=... npx -y @kunal_dutt/token-lens@latest gateway
74
+ ```
75
+
76
+ ## Browser extension
77
+
78
+ For ChatGPT, Claude, Gemini, DeepSeek, Le Chat and Grok on the web: search
79
+ "Token Lens" in the Chrome Web Store, Microsoft Edge Add-ons or Firefox Add-ons.
80
+
81
+ MIT licensed.