toolgz 0.2.7 → 0.2.8

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/README.md CHANGED
@@ -346,6 +346,31 @@ style in 0.2.0.
346
346
 
347
347
  ---
348
348
 
349
+ ## Runnable examples
350
+
351
+ Five files in [`examples/`](examples), all offline — no API key, no cost. Every one is
352
+ **executed by the test suite**, so an example that stops working is a failing test rather
353
+ than a bug report from you.
354
+
355
+ ```bash
356
+ npx tsx examples/01-minimal.ts
357
+ ```
358
+
359
+ | File | Shows |
360
+ |---|---|
361
+ | [`01-minimal.ts`](examples/01-minimal.ts) | the smallest useful thing: `recommendLevel` → `compress` → `resolve` |
362
+ | [`02-agent-loop.ts`](examples/02-agent-loop.ts) | the full loop against a scripted model, covering all three `resolve()` outcomes including a recovery |
363
+ | [`03-mcp-servers.ts`](examples/03-mcp-servers.ts) | 149 real tools from 14 MCP servers, all four levels, and the name-collision hazard |
364
+ | [`04-providers.ts`](examples/04-providers.ts) | the four provider envelopes side by side, plus the Gemini schema repairs |
365
+ | [`05-per-model.ts`](examples/05-per-model.ts) | `model`/`objective` selection and reading `stats` to see what was actually used |
366
+
367
+ Two things `04-providers.ts` demonstrates rather than describes, because both have bitten
368
+ people: `/v1/responses` needs the **flat** tool shape when you set reasoning effort, and
369
+ Gemini returns **one** wrapper object containing all declarations, so you count
370
+ `tools[0].functionDeclarations.length`.
371
+
372
+ ---
373
+
349
374
  ## Using it: the full guide
350
375
 
351
376
  Everything below was a separate `docs/GUIDE.md`. It is inline now, deliberately: the
@@ -767,8 +792,7 @@ console.log(c.stats);
767
792
  // originalChars: 61461, compressedChars: 2211, savedPct: 96.4 }
768
793
  ```
769
794
 
770
- `savedPct` estimates the token saving (calibrated against `count_tokens`, ~1–2pp
771
- accurate). `originalChars` and `compressedChars` give the raw character counts.
795
+ `savedPct` is a character saving, a few points optimistic against tokens. `originalChars` and `compressedChars` give the raw character counts.
772
796
 
773
797
  **I need the real token numbers.**
774
798
  Count them with the provider's own endpoint. Never use `tiktoken` for Claude —
@@ -842,16 +866,21 @@ Returns:
842
866
  | `codeFor(name)` | `→ string` | real name → level-3 code; throws below level 3 |
843
867
  | `stats` | `CompressStats` | `level`, `mapStyle`, `requestedMapStyle`, `fallbackReason`, `toolCount`, `wireToolCount`, `originalChars`, `compressedChars`, `savedPct` |
844
868
 
845
- > **`savedPct` estimates tokens, and is accurate to a point or two.** It used to be a raw
846
- > character ratio, which overstated by 7.6 points at level 1. Characters per token differs
847
- > between the two sides uncompressed JSON is punctuation-dense, a signature line reads
848
- > more like prose — so it now divides each side by a ratio calibrated against
849
- > `count_tokens` on two unrelated corpora. Current error on the real corpus: **+1.5pp at
850
- > level 1, −0.3pp at level 2, +0.1pp at level 3.**
869
+ > **`savedPct` is a character saving, and runs a few points optimistic against tokens.**
870
+ > On the real 149-tool corpus it reports 46.8% at level 1 where `count_tokens` measures
871
+ > 39.2%, and 96.6% at level 3 against 95.6%.
872
+ >
873
+ > We tried making it a token estimate in 0.2.7 and reverted it in 0.2.8. Providers charge a
874
+ > fixed framing cost per tool definition that character counting cannot see: at 149 tools
875
+ > it amortises away, at 2 tools it dominates, and the calibrated estimate was off by 44% on
876
+ > a small level-1 block while being within 1% at scale. The plain character ratio is the
877
+ > smaller and more predictable error.
878
+ >
879
+ > Use it as a local signal. For anything you publish, measure with your provider's token
880
+ > counter. `originalChars` and `compressedChars` are on `stats` for the raw counts.
851
881
  >
852
- > It is still a local estimate with no API call. For a figure you publish, measure with
853
- > your provider's token counter; `originalChars` and `compressedChars` are also on `stats`
854
- > if you want the raw counts.
882
+ > A negative value is possible and correct: on a very small tool set, level 1's signature
883
+ > line can exceed the per-property descriptions it strips.
855
884
  | `encodeCallForTest(name, args)` | `→ {name, args}` | build the raw call a model would emit; test aid |
856
885
 
857
886
  ### `recommendLevel(tools, namespaceOf?) → Recommendation`
@@ -960,7 +989,7 @@ and it does not get deleted.
960
989
  ## Development
961
990
 
962
991
  ```bash
963
- npm test # 277 tests, offline, no cost
992
+ npm test # 283 tests, offline, no cost
964
993
  npm run build # tsc → dist/ with .d.ts
965
994
 
966
995
  npx tsx bench/harness/run-multi.ts --provider=all --reps=3 --variants # costs money
package/dist/index.js CHANGED
@@ -160,27 +160,19 @@ export function compress(input, options = {}) {
160
160
  };
161
161
  const finish = (wire, systemPreamble, cachePreamble, resolve, encode) => {
162
162
  const compressedChars = countSchemaTokensApprox(wire) + systemPreamble.length;
163
- // `savedPct` is meant to answer "how much of my tool block did I get back", which is
164
- // a TOKEN question. Deriving it from a raw character ratio answered a different one
165
- // and overstated: on the real corpus it reported 46.8% at level 1 where count_tokens
166
- // measures 39.2% — 7.6 points high, and the field was documented as "do not publish
167
- // this" rather than fixed.
163
+ // `savedPct` is a CHARACTER saving, deliberately.
168
164
  //
169
- // Characters per token is not constant across the two sides. Uncompressed JSON is
170
- // punctuation-dense; a signature line or a map row reads more like prose. Measured
171
- // against count_tokens on two unrelated corpora (149 real MCP tools and a 100-tool
172
- // synthetic fixture) the ratios are stable per level:
165
+ // 0.2.7 tried to make it a token estimate by dividing each side by a chars-per-token
166
+ // ratio calibrated against count_tokens. That was a mistake, and measurement caught
167
+ // it: providers charge a fixed framing cost per tool definition that character
168
+ // counting cannot see. At 149 tools it amortises away, at 2 tools it dominates — the
169
+ // ratio approach was off by 44% on a 2-tool level-1 block while being within 1% at
170
+ // 149. No local character-based calculation can span that range.
173
171
  //
174
- // uncompressed 2.39 / 2.45 level 2 2.20 / 1.99
175
- // level 1 2.15 / 2.19 level 3 1.92 / 1.91
176
- //
177
- // Dividing by these brings the error to ~1.5 points at level 1 and ~0.1 at level 3.
178
- // Still an estimate — it is a local calculation with no API call — but now an
179
- // estimate of the right quantity.
180
- const CHARS_PER_TOKEN = { 0: 2.42, 1: 2.17, 2: 2.1, 3: 1.92 };
181
- const estTokens = (chars, at) => chars / CHARS_PER_TOKEN[at];
182
- const originalTokens = estTokens(originalChars, 0);
183
- const compressedTokens = estTokens(compressedChars, level);
172
+ // The plain character ratio is the smaller and more predictable error: it runs a few
173
+ // points optimistic (−7.7% chars against −4.9% real tokens on a 2-tool set; 46.8%
174
+ // against 39.2% on 149 real tools). So it is reported as what it is, and the docs say
175
+ // to measure with your provider's counter for anything you publish.
184
176
  return {
185
177
  tools: wire,
186
178
  systemPreamble,
@@ -204,7 +196,7 @@ export function compress(input, options = {}) {
204
196
  compressedChars,
205
197
  savedPct: originalChars === 0
206
198
  ? 0
207
- : Math.round((1 - compressedTokens / originalTokens) * 1000) / 10,
199
+ : Math.round((1 - compressedChars / originalChars) * 1000) / 10,
208
200
  },
209
201
  };
210
202
  };
package/dist/recommend.js CHANGED
@@ -57,7 +57,11 @@ export function recommendLevel(tools, namespaceOf = defaultNamespaceOf) {
57
57
  // provider's own schema enforcement, which levels 2-3 give up. That is worth more
58
58
  // than a saving you would not notice — hence an absolute threshold on the block,
59
59
  // not a shape test.
60
- const THRESHOLD_TOKENS = 4000;
60
+ // 10,000 tokens is ~5% of a 200k window. Below that, reclaiming the block does not
61
+ // change what fits, and level 1 keeps the provider's own argument validation — worth
62
+ // more than a saving you would not notice. An external reviewer flagged the earlier
63
+ // 4,000 threshold for pushing 14-tool sets into dispatcher mode; they were right.
64
+ const THRESHOLD_TOKENS = 10000;
61
65
  if (l1Tokens < THRESHOLD_TOKENS) {
62
66
  return {
63
67
  ...base,
@@ -68,6 +72,6 @@ export function recommendLevel(tools, namespaceOf = defaultNamespaceOf) {
68
72
  return {
69
73
  ...base,
70
74
  level: 3,
71
- reason: `${toolCount} tools take ~${l1Tokens.toLocaleString()} tokens at level 1; level 3 replaces them with two dispatcher tools plus a cached map. Measured on 149 real MCP tools: 41,648 tokens at level 1 against 2,980 at level 3, and 60/60 tasks completed across four frontier providers with zero hallucinated names. The cost is roughly 0.3-1.7 lookup calls per task — so about half an extra turn — plus the loss of provider-side argument checking keep \`validate\` on, which is what catches malformed arguments instead. If latency matters more than context, stay at level 1.`,
75
+ reason: `${toolCount} tools take ~${l1Tokens.toLocaleString()} tokens at level 1; level 3 replaces them with two dispatcher tools plus a cached map. Measured on 149 real MCP tools: 41,648 tokens at level 1 against 2,980 at level 3, and 60/60 tasks completed across four frontier providers with zero hallucinated names. The cost is roughly 0.3-1.7 lookup calls per task — so about half an extra turn — plus the loss of provider-side argument checking. Keep \`validate\` on; it is what catches malformed arguments instead. If latency matters more than context, stay at level 1.`,
72
76
  };
73
77
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolgz",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "Compress LLM tool definitions to reclaim context window. Measured across Anthropic, OpenAI, Google and xAI.",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {