toolgz 0.2.5 → 0.2.7
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 +16 -11
- package/dist/index.js +22 -1
- package/dist/recommend.js +27 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
<p><strong>Your agent spends 30–70k tokens of context on tool definitions before the user types a word. toolgz gets up to ~85% of it back.</strong></p>
|
|
4
4
|
|
|
5
|
-
<p><em>On a large tool set, at level 3. The safe default (level 1) reclaims 13–
|
|
5
|
+
<p><em>On a large tool set, at level 3. The safe default (level 1) reclaims 13–39% and gives up nothing — <code>recommendLevel()</code> picks for you.</em></p>
|
|
6
6
|
|
|
7
7
|
<p>
|
|
8
8
|
<a href="#measured-results">420-run cross-provider sweep</a> ·
|
|
@@ -39,7 +39,7 @@ const { tools, system } = forAnthropic(c); // send these instead
|
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
`compress(myTools)` with no `level` gives you **level 1** — safe, native tool calling,
|
|
42
|
-
provider schema enforcement intact, and 13–32%
|
|
42
|
+
provider schema enforcement intact, and 13–39% smaller (13–32% on the synthetic benchmark, 39% on the real 149-tool corpus). The 71–85% figures below are
|
|
43
43
|
**level 3**, which is what `recommendLevel` returns once a tool set is big enough to
|
|
44
44
|
amortise the dispatcher. Ask for it explicitly with `{ level: 3 }` if you prefer.
|
|
45
45
|
|
|
@@ -84,7 +84,7 @@ claim about real deployments.
|
|
|
84
84
|
</picture>
|
|
85
85
|
|
|
86
86
|
**All figures below are level 3** (`minified-plus`, the shipped default map style).
|
|
87
|
-
Level 1 on the same sweep saves 13–32%; level 2 is dominated by level 3.
|
|
87
|
+
Level 1 on the same sweep saves 13–32%; on the real 149-tool corpus it saves 39%; level 2 is dominated by level 3.
|
|
88
88
|
|
|
89
89
|
| Provider | Model | Tool block | Prompt tokens | Latency | Tasks |
|
|
90
90
|
|---|---|---:|---:|---:|:-:|
|
|
@@ -767,8 +767,8 @@ console.log(c.stats);
|
|
|
767
767
|
// originalChars: 61461, compressedChars: 2211, savedPct: 96.4 }
|
|
768
768
|
```
|
|
769
769
|
|
|
770
|
-
`savedPct`
|
|
771
|
-
|
|
770
|
+
`savedPct` estimates the token saving (calibrated against `count_tokens`, ~1–2pp
|
|
771
|
+
accurate). `originalChars` and `compressedChars` give the raw character counts.
|
|
772
772
|
|
|
773
773
|
**I need the real token numbers.**
|
|
774
774
|
Count them with the provider's own endpoint. Never use `tiktoken` for Claude —
|
|
@@ -842,11 +842,16 @@ Returns:
|
|
|
842
842
|
| `codeFor(name)` | `→ string` | real name → level-3 code; throws below level 3 |
|
|
843
843
|
| `stats` | `CompressStats` | `level`, `mapStyle`, `requestedMapStyle`, `fallbackReason`, `toolCount`, `wireToolCount`, `originalChars`, `compressedChars`, `savedPct` |
|
|
844
844
|
|
|
845
|
-
> **`savedPct`
|
|
846
|
-
>
|
|
847
|
-
>
|
|
848
|
-
>
|
|
849
|
-
>
|
|
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.**
|
|
851
|
+
>
|
|
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.
|
|
850
855
|
| `encodeCallForTest(name, args)` | `→ {name, args}` | build the raw call a model would emit; test aid |
|
|
851
856
|
|
|
852
857
|
### `recommendLevel(tools, namespaceOf?) → Recommendation`
|
|
@@ -955,7 +960,7 @@ and it does not get deleted.
|
|
|
955
960
|
## Development
|
|
956
961
|
|
|
957
962
|
```bash
|
|
958
|
-
npm test #
|
|
963
|
+
npm test # 277 tests, offline, no cost
|
|
959
964
|
npm run build # tsc → dist/ with .d.ts
|
|
960
965
|
|
|
961
966
|
npx tsx bench/harness/run-multi.ts --provider=all --reps=3 --variants # costs money
|
package/dist/index.js
CHANGED
|
@@ -160,6 +160,27 @@ 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.
|
|
168
|
+
//
|
|
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:
|
|
173
|
+
//
|
|
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);
|
|
163
184
|
return {
|
|
164
185
|
tools: wire,
|
|
165
186
|
systemPreamble,
|
|
@@ -183,7 +204,7 @@ export function compress(input, options = {}) {
|
|
|
183
204
|
compressedChars,
|
|
184
205
|
savedPct: originalChars === 0
|
|
185
206
|
? 0
|
|
186
|
-
: Math.round((1 -
|
|
207
|
+
: Math.round((1 - compressedTokens / originalTokens) * 1000) / 10,
|
|
187
208
|
},
|
|
188
209
|
};
|
|
189
210
|
};
|
package/dist/recommend.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { defaultNamespaceOf } from "./render/index.js";
|
|
2
|
+
import { compress } from "./index.js";
|
|
2
3
|
/**
|
|
3
4
|
* Pick a level for a given tool set.
|
|
4
5
|
*
|
|
@@ -34,23 +35,39 @@ export function recommendLevel(tools, namespaceOf = defaultNamespaceOf) {
|
|
|
34
35
|
const namespaceCount = namespaces.size;
|
|
35
36
|
const opsPerNamespace = namespaceCount ? toolCount / namespaceCount : 0;
|
|
36
37
|
const base = { toolCount, namespaceCount, opsPerNamespace };
|
|
37
|
-
|
|
38
|
+
// Size the level-1 block by asking the library what it would actually emit, rather
|
|
39
|
+
// than reconstructing it — an earlier version rebuilt the schema by hand, kept the
|
|
40
|
+
// full descriptions where level 1 emits a signature line, and overestimated by ~34%.
|
|
41
|
+
//
|
|
42
|
+
// `countSchemaTokensApprox` counts characters. Measured against `count_tokens` on
|
|
43
|
+
// real MCP tools the ratio is a stable ~2.1 chars/token (1.98 at 10 tools, 2.15 at
|
|
44
|
+
// 149), so dividing gives a fair local estimate with no API call.
|
|
45
|
+
const CHARS_PER_TOKEN = 2.1;
|
|
46
|
+
const l1Tokens = Math.round(compress(tools, { level: 1 }).stats.compressedChars / CHARS_PER_TOKEN);
|
|
47
|
+
// The old heuristic gated level 3 on `opsPerNamespace >= 4`. That is a LEVEL-2
|
|
48
|
+
// question: level 2 pays dispatcher overhead per namespace, so its shape matters
|
|
49
|
+
// there. Level 3 uses one flat dispatcher and does not care about namespaces at all.
|
|
50
|
+
//
|
|
51
|
+
// The consequence was concrete: on the 149-tool real corpus (63 namespaces, 2.4 ops
|
|
52
|
+
// each) it recommended level 1 at 41,648 tokens when level 3 measures 2,980 — leaving
|
|
53
|
+
// ~38,700 tokens on the table. Measured on real tools, level 3 is smaller at EVERY
|
|
54
|
+
// count tested, down to 5 tools (635 vs 1,178).
|
|
55
|
+
//
|
|
56
|
+
// So size never argues for level 1. What argues for level 1 is that it keeps the
|
|
57
|
+
// provider's own schema enforcement, which levels 2-3 give up. That is worth more
|
|
58
|
+
// than a saving you would not notice — hence an absolute threshold on the block,
|
|
59
|
+
// not a shape test.
|
|
60
|
+
const THRESHOLD_TOKENS = 4000;
|
|
61
|
+
if (l1Tokens < THRESHOLD_TOKENS) {
|
|
38
62
|
return {
|
|
39
63
|
...base,
|
|
40
64
|
level: 1,
|
|
41
|
-
reason: `
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
if (opsPerNamespace < 4) {
|
|
45
|
-
return {
|
|
46
|
-
...base,
|
|
47
|
-
level: 1,
|
|
48
|
-
reason: `${toolCount} tools spread across ${namespaceCount} namespaces (${opsPerNamespace.toFixed(1)} ops each). The compound-dispatcher overhead is paid per namespace, so a set this sparse stays smaller at level 1.`,
|
|
65
|
+
reason: `The tool block is only ~${l1Tokens.toLocaleString()} tokens at level 1. Level 3 would be smaller, but not by enough to be worth giving up the provider's own argument validation, which level 1 keeps. Reach for level 3 when the block is large enough that reclaiming it changes what fits in your context.`,
|
|
49
66
|
};
|
|
50
67
|
}
|
|
51
68
|
return {
|
|
52
69
|
...base,
|
|
53
70
|
level: 3,
|
|
54
|
-
reason: `${toolCount} tools
|
|
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.`,
|
|
55
72
|
};
|
|
56
73
|
}
|