toolgz 0.2.8 → 0.2.10
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 +106 -8
- package/dist/recommend.d.ts +13 -3
- package/dist/recommend.js +13 -3
- package/llms.txt +205 -0
- package/package.json +2 -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
|
|
5
|
+
<p><em>On a large tool set, at level 3. The default (level 1) reclaims 13–39% and gives up nothing. <code>recommendLevel()</code> tells you which one your tools want; you pass its answer in.</em></p>
|
|
6
6
|
|
|
7
7
|
<p>
|
|
8
8
|
<a href="#measured-results">420-run cross-provider sweep</a> ·
|
|
@@ -15,6 +15,17 @@ zero runtime dependencies ·
|
|
|
15
15
|
npm install toolgz
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
> ### 🤖 If a coding agent is reading this
|
|
19
|
+
>
|
|
20
|
+
> Point it at **[`llms.txt`](llms.txt)** — prescriptive integration instructions written
|
|
21
|
+
> for an agent, shipped inside the package at `node_modules/toolgz/llms.txt`. It covers
|
|
22
|
+
> the three-step contract, the rules that are not optional, per-model behaviour, and a
|
|
23
|
+
> `Do not` list of the mistakes we have actually fielded.
|
|
24
|
+
>
|
|
25
|
+
> ```
|
|
26
|
+
> Read node_modules/toolgz/llms.txt before integrating toolgz.
|
|
27
|
+
> ```
|
|
28
|
+
|
|
18
29
|
---
|
|
19
30
|
|
|
20
31
|
## The problem
|
|
@@ -33,7 +44,7 @@ Reclaiming the room is what this does.
|
|
|
33
44
|
```ts
|
|
34
45
|
import { compress, recommendLevel, forAnthropic } from "toolgz";
|
|
35
46
|
|
|
36
|
-
const { level } = recommendLevel(myTools); // 1 for small
|
|
47
|
+
const { level } = recommendLevel(myTools); // advice: 1 for a small block, 3 for a big one
|
|
37
48
|
const c = compress(myTools, { level }); // your existing MCP/SDK tool array
|
|
38
49
|
const { tools, system } = forAnthropic(c); // send these instead
|
|
39
50
|
```
|
|
@@ -41,7 +52,11 @@ const { tools, system } = forAnthropic(c); // send these instead
|
|
|
41
52
|
`compress(myTools)` with no `level` gives you **level 1** — safe, native tool calling,
|
|
42
53
|
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
54
|
**level 3**, which is what `recommendLevel` returns once a tool set is big enough to
|
|
44
|
-
amortise the dispatcher.
|
|
55
|
+
amortise the dispatcher.
|
|
56
|
+
|
|
57
|
+
**Note the shape of those three lines: `recommendLevel` only advises, and you pass its
|
|
58
|
+
answer back in.** Nothing upgrades itself — `compress(myTools)` is level 1 whether you
|
|
59
|
+
have 2 tools or 500. See [Which level to use](#which-level-to-use) for why.
|
|
45
60
|
|
|
46
61
|
Then translate the model's call back before you dispatch:
|
|
47
62
|
|
|
@@ -226,6 +241,62 @@ tried and removed in 0.2.0 because they were smaller and still worse.
|
|
|
226
241
|
|
|
227
242
|
## Which level to use
|
|
228
243
|
|
|
244
|
+
### The whole idea, in plain English
|
|
245
|
+
|
|
246
|
+
Your tool definitions are a **menu** handed to the model at the start of every single
|
|
247
|
+
conversation. It is long, and most of it is flowery prose about each dish.
|
|
248
|
+
|
|
249
|
+
- **Level 1** — the same menu with the prose cut. It is still a real menu: the model
|
|
250
|
+
points at a dish **by name**, and *the kitchen checks the order makes sense* before
|
|
251
|
+
cooking. This is the default, and it gives up nothing.
|
|
252
|
+
- **Level 3** — throw the menu away. Hand the model a **numbered list** and one waiter.
|
|
253
|
+
It says "number 12, no onions," and the waiter knows what that means. The list is
|
|
254
|
+
tiny. But the kitchen no longer checks the order — **toolgz checks it instead**,
|
|
255
|
+
against your original schema, and hands back a readable error if it's wrong.
|
|
256
|
+
|
|
257
|
+
If the model needs to know what number 12 comes with, it asks. That's the `q()` lookup,
|
|
258
|
+
and it costs about half a turn.
|
|
259
|
+
|
|
260
|
+
So: **level 1 is smaller. Level 3 is much smaller and you take over order-checking.**
|
|
261
|
+
|
|
262
|
+
### Two things that surprise people
|
|
263
|
+
|
|
264
|
+
**1. Nothing changes level on its own. You are always the one who picks.**
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
compress(myTools) // level 1. ALWAYS — 2 tools or 500.
|
|
268
|
+
compress(myTools, { level: 3 }) // level 3, because you asked for it
|
|
269
|
+
|
|
270
|
+
const { level } = recommendLevel(myTools); // just advice: returns 1 or 3
|
|
271
|
+
compress(myTools, { level }); // now it's 3, because you passed it in
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
`recommendLevel()` **advises**; it does not act. On our 149-tool corpus,
|
|
275
|
+
`compress(myTools)` saves 45.2% and `compress(myTools, { level: 3 })` saves 96.5% — so
|
|
276
|
+
forgetting to pass the level back in quietly leaves half the win on the table.
|
|
277
|
+
|
|
278
|
+
This is deliberate. Level 3 gives up provider-side schema enforcement, and silently
|
|
279
|
+
changing a caller's correctness guarantees because their tool array grew would be a
|
|
280
|
+
worse bug than the tokens are worth.
|
|
281
|
+
|
|
282
|
+
**2. It switches on how *big* the block is, not how *many* tools you have.**
|
|
283
|
+
|
|
284
|
+
A tool can be 20 tokens or 460, so counting them tells you very little:
|
|
285
|
+
|
|
286
|
+
| Tool set | Level 1 block | Recommends |
|
|
287
|
+
|---|---:|:-:|
|
|
288
|
+
| 72 tools, one parameter each | ~5,000 tokens | **1** |
|
|
289
|
+
| 200 tools, one parameter each | ~14,100 tokens | **3** |
|
|
290
|
+
| **40** real MCP tools | ~10,600 tokens | **3** |
|
|
291
|
+
| 149 real MCP tools | ~42,700 tokens | **3** |
|
|
292
|
+
|
|
293
|
+
Forty chatty tools cross the line while 72 terse ones don't. The threshold is **10,000
|
|
294
|
+
tokens** — about 5% of a 200k window. Below that, reclaiming the block doesn't change
|
|
295
|
+
what fits, so keeping the provider's own argument validation is worth more than the
|
|
296
|
+
saving.
|
|
297
|
+
|
|
298
|
+
### The levels in full
|
|
299
|
+
|
|
229
300
|
Ask the library. It returns 1 or 3, never 2, and explains itself:
|
|
230
301
|
|
|
231
302
|
```ts
|
|
@@ -302,6 +373,11 @@ Level 3 has several map styles. Which one is cheapest turns out to depend on the
|
|
|
302
373
|
model, so you can hand `compress()` a model id and let it use what was actually
|
|
303
374
|
measured:
|
|
304
375
|
|
|
376
|
+
> **This is the one thing the library does choose for you, and only if you pass
|
|
377
|
+
> `model`.** Note the difference from levels: a map style is a pure encoding choice, so
|
|
378
|
+
> picking a better one cannot change your results. The level *can* — level 3 hands
|
|
379
|
+
> argument validation from the provider to toolgz — so that stays your explicit call.
|
|
380
|
+
|
|
305
381
|
```ts
|
|
306
382
|
compress(myTools, { level: 3, model: "gpt-5.6-sol", objective: "cost" });
|
|
307
383
|
```
|
|
@@ -356,6 +432,10 @@ than a bug report from you.
|
|
|
356
432
|
npx tsx examples/01-minimal.ts
|
|
357
433
|
```
|
|
358
434
|
|
|
435
|
+
**[`examples/README.md`](examples/README.md) walks through all five** with their real
|
|
436
|
+
output and what each one is trying to teach — start there if you would rather read than
|
|
437
|
+
run.
|
|
438
|
+
|
|
359
439
|
| File | Shows |
|
|
360
440
|
|---|---|
|
|
361
441
|
| [`01-minimal.ts`](examples/01-minimal.ts) | the smallest useful thing: `recommendLevel` → `compress` → `resolve` |
|
|
@@ -783,8 +863,13 @@ Use `forGemini`, which strips the keywords Gemini won't accept. If a new one
|
|
|
783
863
|
appears, it's a one-line addition to the adapter.
|
|
784
864
|
|
|
785
865
|
**Savings look small.**
|
|
786
|
-
|
|
787
|
-
|
|
866
|
+
First check you actually passed a level: `compress(myTools)` is **level 1**, and it
|
|
867
|
+
stays level 1 no matter how many tools you hand it. `recommendLevel` advises, it does
|
|
868
|
+
not act — you have to pass its answer in as `{ level }`.
|
|
869
|
+
|
|
870
|
+
If you did, ask `recommendLevel(myTools)` and read the `reason`. It reports the size of
|
|
871
|
+
your level-1 block, and under ~10,000 tokens there is little worth reclaiming. Also
|
|
872
|
+
check `c.stats`:
|
|
788
873
|
|
|
789
874
|
```ts
|
|
790
875
|
console.log(c.stats);
|
|
@@ -885,7 +970,15 @@ Returns:
|
|
|
885
970
|
|
|
886
971
|
### `recommendLevel(tools, namespaceOf?) → Recommendation`
|
|
887
972
|
|
|
888
|
-
`{ level, reason, toolCount, namespaceCount, opsPerNamespace }`. Returns 1 or 3.
|
|
973
|
+
`{ level, reason, toolCount, namespaceCount, opsPerNamespace }`. Returns 1 or 3, never 2.
|
|
974
|
+
|
|
975
|
+
**It advises; it does not act.** Pass the answer back in yourself —
|
|
976
|
+
`compress(tools, { level })`. Calling `compress(tools)` alone is level 1 regardless of
|
|
977
|
+
how large `tools` is.
|
|
978
|
+
|
|
979
|
+
The decision is on the **size** of the level-1 block (threshold 10,000 tokens ≈ 5% of a
|
|
980
|
+
200k window), not on `toolCount`. The three shape fields are reported for your own
|
|
981
|
+
logging; only block size drives the level.
|
|
889
982
|
|
|
890
983
|
### Provider adapters
|
|
891
984
|
|
|
@@ -974,8 +1067,13 @@ xAI is OpenAI-compatible — use `forOpenAI` with `baseURL: "https://api.x.ai/v1
|
|
|
974
1067
|
- **It has not been measured on a non-frontier model at level 3.** On Haiku 4.5, argument
|
|
975
1068
|
errors rose sharply (17 of 30 runs) — all caught and retried, no task lost, but that is the
|
|
976
1069
|
known edge.
|
|
977
|
-
- **It is not magic on
|
|
978
|
-
`recommendLevel()` will
|
|
1070
|
+
- **It is not magic on a small tool block.** Under ~10,000 tokens at level 1 there is
|
|
1071
|
+
little worth reclaiming, and `recommendLevel()` will say so and keep you on level 1.
|
|
1072
|
+
That is usually a small number of tools, but not always — it depends on how verbose
|
|
1073
|
+
your schemas are, not on the count.
|
|
1074
|
+
- **It does not pick a level for you.** `compress()` defaults to level 1 and stays there;
|
|
1075
|
+
reaching level 3 is always an explicit `{ level }`. Deliberate — level 3 trades away
|
|
1076
|
+
provider-side schema enforcement, and that is not a trade to make behind your back.
|
|
979
1077
|
|
|
980
1078
|
## Determinism
|
|
981
1079
|
|
package/dist/recommend.d.ts
CHANGED
|
@@ -31,9 +31,19 @@ export type Recommendation = {
|
|
|
31
31
|
* slower and dearer than level 3. It is never recommended. It stays in the
|
|
32
32
|
* API for callers who need real operation names on the wire.
|
|
33
33
|
*
|
|
34
|
-
* The level 1 → 3 threshold is
|
|
35
|
-
*
|
|
36
|
-
*
|
|
34
|
+
* The level 1 → 3 threshold is an absolute size test on the level-1 block —
|
|
35
|
+
* neither tool count nor namespace shape. A tool is 20 to 460 tokens depending
|
|
36
|
+
* on how verbose its schema is, so 40 real MCP tools cross the line while 72
|
|
37
|
+
* one-parameter tools do not.
|
|
38
|
+
*
|
|
39
|
+
* An earlier version gated on tools-per-namespace. That is a *level 2* question
|
|
40
|
+
* (level 2 pays dispatcher overhead per namespace); level 3 uses one flat
|
|
41
|
+
* dispatcher and is indifferent to shape. See the comment on THRESHOLD_TOKENS.
|
|
42
|
+
*
|
|
43
|
+
* This function only ever *advises*. `compress()` defaults to level 1 and stays
|
|
44
|
+
* there unless the caller passes a level explicitly — level 3 trades away the
|
|
45
|
+
* provider's own schema enforcement, and that is not a trade to make on a
|
|
46
|
+
* caller's behalf because their tool array grew.
|
|
37
47
|
*/
|
|
38
48
|
export declare function recommendLevel(tools: Tool[], namespaceOf?: (n: string) => {
|
|
39
49
|
ns: string;
|
package/dist/recommend.js
CHANGED
|
@@ -25,9 +25,19 @@ import { compress } from "./index.js";
|
|
|
25
25
|
* slower and dearer than level 3. It is never recommended. It stays in the
|
|
26
26
|
* API for callers who need real operation names on the wire.
|
|
27
27
|
*
|
|
28
|
-
* The level 1 → 3 threshold is
|
|
29
|
-
*
|
|
30
|
-
*
|
|
28
|
+
* The level 1 → 3 threshold is an absolute size test on the level-1 block —
|
|
29
|
+
* neither tool count nor namespace shape. A tool is 20 to 460 tokens depending
|
|
30
|
+
* on how verbose its schema is, so 40 real MCP tools cross the line while 72
|
|
31
|
+
* one-parameter tools do not.
|
|
32
|
+
*
|
|
33
|
+
* An earlier version gated on tools-per-namespace. That is a *level 2* question
|
|
34
|
+
* (level 2 pays dispatcher overhead per namespace); level 3 uses one flat
|
|
35
|
+
* dispatcher and is indifferent to shape. See the comment on THRESHOLD_TOKENS.
|
|
36
|
+
*
|
|
37
|
+
* This function only ever *advises*. `compress()` defaults to level 1 and stays
|
|
38
|
+
* there unless the caller passes a level explicitly — level 3 trades away the
|
|
39
|
+
* provider's own schema enforcement, and that is not a trade to make on a
|
|
40
|
+
* caller's behalf because their tool array grew.
|
|
31
41
|
*/
|
|
32
42
|
export function recommendLevel(tools, namespaceOf = defaultNamespaceOf) {
|
|
33
43
|
const namespaces = new Set(tools.map((t) => namespaceOf(t.name).ns));
|
package/llms.txt
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# toolgz
|
|
2
|
+
|
|
3
|
+
> Compresses LLM tool definitions to reclaim context window. Zero runtime dependencies,
|
|
4
|
+
> TypeScript, Apache-2.0. Works with any model and any framework: it transforms a tool
|
|
5
|
+
> array before you send it, and translates the model's call back afterwards.
|
|
6
|
+
|
|
7
|
+
**This file is written for a coding agent integrating toolgz into a project.** It is
|
|
8
|
+
prescriptive on purpose. If you are contributing to toolgz itself, read `AGENTS.md` in the
|
|
9
|
+
repository instead — different audience, different rules.
|
|
10
|
+
|
|
11
|
+
Installed copy lives at `node_modules/toolgz/llms.txt`. Full prose docs: `README.md`.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## The whole integration, correctly
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { compress, recommendLevel, forAnthropic } from "toolgz";
|
|
19
|
+
|
|
20
|
+
// 1. Decide the level ONCE, at startup — not per request.
|
|
21
|
+
const { level } = recommendLevel(myTools);
|
|
22
|
+
const c = compress(myTools, { level });
|
|
23
|
+
|
|
24
|
+
// 2. Send c.tools instead of your tool array, and append the preamble to your system prompt.
|
|
25
|
+
const { tools, system } = forAnthropic(c);
|
|
26
|
+
|
|
27
|
+
// 3. Translate every tool call back before you dispatch.
|
|
28
|
+
for (const block of res.content.filter((b) => b.type === "tool_use")) {
|
|
29
|
+
const r = c.resolve(block.name, block.input);
|
|
30
|
+
if (r.kind === "call") {
|
|
31
|
+
const out = await myDispatch(r.name, r.args); // real name, real args
|
|
32
|
+
results.push({ type: "tool_result", tool_use_id: block.id, content: JSON.stringify(out) });
|
|
33
|
+
} else if (r.kind === "meta") {
|
|
34
|
+
// toolgz answered a lookup itself. DO NOT dispatch. Return its text to the model.
|
|
35
|
+
results.push({ type: "tool_result", tool_use_id: block.id, content: r.result });
|
|
36
|
+
} else {
|
|
37
|
+
// Recoverable by design: hand the message back and the model retries.
|
|
38
|
+
results.push({ type: "tool_result", tool_use_id: block.id, content: `Error: ${r.message}`, is_error: true });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Those three steps are the entire contract. `myDispatch` receives exactly what it received
|
|
44
|
+
before toolgz existed.
|
|
45
|
+
|
|
46
|
+
## Rules that are not optional
|
|
47
|
+
|
|
48
|
+
1. **`resolve()` every tool call.** At level 3 the model emits opaque codes (`t(f="a0")`),
|
|
49
|
+
so dispatching `block.name` directly will fail or dispatch the wrong tool. Route
|
|
50
|
+
everything through `resolve()` even at level 0, so the level becomes a one-line change.
|
|
51
|
+
|
|
52
|
+
2. **Handle all three `kind` values.** `call` → dispatch. `meta` → toolgz already answered
|
|
53
|
+
a lookup; return `r.result` to the model and dispatch nothing. `error` → return
|
|
54
|
+
`r.message` to the model as an error tool_result; do **not** throw. The `error` path is
|
|
55
|
+
the recovery mechanism, not a failure.
|
|
56
|
+
|
|
57
|
+
3. **Send the system preamble.** At level 3 the tool map lives in the system prompt. Drop
|
|
58
|
+
it and the model has no idea what any code means. It is `""` at levels 0–1, so
|
|
59
|
+
appending it unconditionally is always safe.
|
|
60
|
+
|
|
61
|
+
4. **`compress()` never changes level on its own.** `compress(tools)` is level 1 forever,
|
|
62
|
+
2 tools or 500. `recommendLevel()` *advises*; you pass its answer in as `{ level }`.
|
|
63
|
+
Deliberate: level 3 moves argument validation from the provider to toolgz, which is not
|
|
64
|
+
a change to make behind a caller's back.
|
|
65
|
+
|
|
66
|
+
5. **Leave `validate` on** (the default). Levels 2–3 give up the provider's constrained
|
|
67
|
+
decoding, so toolgz validating against the original schema is what catches malformed
|
|
68
|
+
arguments. On a weak model, disabling it converts roughly half of all runs from a
|
|
69
|
+
recovered retry into a bad dispatch.
|
|
70
|
+
|
|
71
|
+
6. **Compress once, reuse the result.** `compress()` is deterministic — same tools in,
|
|
72
|
+
byte-identical payload out — so the output is prompt-cache friendly. Recomputing per
|
|
73
|
+
request is wasted work; changing the tool list mid-conversation invalidates the cache
|
|
74
|
+
and strands codes the model has already seen.
|
|
75
|
+
|
|
76
|
+
## Choosing a level
|
|
77
|
+
|
|
78
|
+
Ask, don't guess: `recommendLevel(myTools)` returns `{ level, reason }` — 1 or 3, never 2.
|
|
79
|
+
|
|
80
|
+
| Level | Wire | Names | Provider schema enforcement | When |
|
|
81
|
+
|:-:|---|:-:|:-:|---|
|
|
82
|
+
| 0 | unchanged | real | yes | A/B control only |
|
|
83
|
+
| **1** | one native tool each, flattened schemas | real | **yes** | **default.** Reclaims 13–39%, gives up nothing |
|
|
84
|
+
| 2 | one tool per namespace | real | no | only if you need real op names on the wire |
|
|
85
|
+
| **3** | one dispatcher + one lookup | codes | no | **large tool sets.** Up to ~85–96% |
|
|
86
|
+
|
|
87
|
+
The switch is the **size** of the level-1 block, not the tool count — threshold 10,000
|
|
88
|
+
tokens (~5% of a 200k window). A tool is 20 to 460 tokens depending on schema verbosity,
|
|
89
|
+
so 40 real MCP tools cross it while 72 one-parameter tools do not.
|
|
90
|
+
|
|
91
|
+
Level 2 is dominated by level 3 on accuracy (six times the malformed arguments) and is
|
|
92
|
+
never recommended. It stays in the API for callers who need real operation names.
|
|
93
|
+
|
|
94
|
+
## Provider adapters — pick the right one
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
forAnthropic(c, { cache?, ttl? }) // → { tools, system } Messages API
|
|
98
|
+
forOpenAI(c) // → { tools, systemPreamble } /v1/chat/completions
|
|
99
|
+
forOpenAIResponses(c) // → { tools, systemPreamble } /v1/responses
|
|
100
|
+
forGemini(c) // → { tools, systemPreamble } generateContent
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Two mistakes that produce confusing provider errors:
|
|
104
|
+
|
|
105
|
+
- **OpenAI has two incompatible tool shapes.** `/v1/chat/completions` wants nested
|
|
106
|
+
`{type, function:{…}}`; `/v1/responses` wants flat `{type, name, …}`. If you set
|
|
107
|
+
reasoning effort on a GPT-5.x model you must use `/v1/responses`, therefore
|
|
108
|
+
`forOpenAIResponses`. Using the wrong adapter is rejected at the API.
|
|
109
|
+
- **Gemini returns ONE wrapper object,** not one entry per tool. Read
|
|
110
|
+
`tools[0].functionDeclarations.length`; `tools.length` is always 1.
|
|
111
|
+
|
|
112
|
+
`forGemini` also repairs three schema forms Gemini rejects with a 400: an array-valued
|
|
113
|
+
`type`, `type: "array"` with no `items`, and a non-string `enum`. Constraints stripped to
|
|
114
|
+
satisfy Gemini are still enforced by `resolve()` against your original schema — nothing is
|
|
115
|
+
silently dropped.
|
|
116
|
+
|
|
117
|
+
xAI is OpenAI-compatible: use `forOpenAI` with `baseURL: "https://api.x.ai/v1"`. **xAI
|
|
118
|
+
caps tool arrays at 350**; other providers accept 1,200+. Level 3 sends 2 either way.
|
|
119
|
+
|
|
120
|
+
## Model-specific behaviour
|
|
121
|
+
|
|
122
|
+
Pass an exact model id and toolgz uses the best *measured* map style for it:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
compress(myTools, { level: 3, model: "gpt-5.6-sol", objective: "cost" });
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
| Model | `objective: "cost"` | Measured vs default |
|
|
129
|
+
|---|---|---:|
|
|
130
|
+
| `gpt-5.6-sol` | `explicit` | −20.7% |
|
|
131
|
+
| `gemini-3.1-pro-preview` | `explicit` | −15.4% |
|
|
132
|
+
| `claude-opus-5` | `explicit` | −9.0% |
|
|
133
|
+
| `grok-4.5` | *(no entry)* | `explicit` is **+13.2%** there |
|
|
134
|
+
|
|
135
|
+
- Model ids are **exact, never families**. A result for `gpt-5.6-sol` says nothing about
|
|
136
|
+
`gpt-5.7`. An unknown model gets the conservative default — absence of evidence, not a
|
|
137
|
+
prediction.
|
|
138
|
+
- The default objective is `occupancy`, which has **no** entries: nothing beat the default
|
|
139
|
+
by more than 3.1% on that axis, under the 5% effect-size floor. Only `cost` has rows.
|
|
140
|
+
- This is the one thing the library picks for you, and only when you pass `model`. A map
|
|
141
|
+
style is a pure encoding choice, so a better one cannot change results. The *level*
|
|
142
|
+
can — which is why that stays explicit.
|
|
143
|
+
- Do not set `mapStyle` by hand unless you have measured your own workload. Prefer
|
|
144
|
+
`model` + `objective`.
|
|
145
|
+
|
|
146
|
+
## MCP servers
|
|
147
|
+
|
|
148
|
+
`tools/list` returns `{ name, description, inputSchema }` per tool — exactly what
|
|
149
|
+
`compress()` accepts. **There is no adapter layer.** Merge the arrays from every server
|
|
150
|
+
and pass them in.
|
|
151
|
+
|
|
152
|
+
`input_schema` and `inputSchema` are both accepted, so Anthropic-shaped and MCP-shaped
|
|
153
|
+
tools can be mixed.
|
|
154
|
+
|
|
155
|
+
Watch for name collisions when merging catalogues from independent servers. If two tool
|
|
156
|
+
names normalise identically, toolgz refuses to alias either one rather than guess — the
|
|
157
|
+
map code still works.
|
|
158
|
+
|
|
159
|
+
## API surface
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
compress(tools, options?) → CompressResult
|
|
163
|
+
recommendLevel(tools, namespaceOf?) → { level, reason, toolCount, namespaceCount, opsPerNamespace }
|
|
164
|
+
selectMapStyle(options) → { mapStyle, requestedMapStyle?, fallbackReason? } // pure
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`CompressOptions`: `level`, `mapStyle`, `namespaceOf`, `aliasOf`, `searchLimit`,
|
|
168
|
+
`validate`, `model`, `objective`.
|
|
169
|
+
|
|
170
|
+
`CompressResult`: `tools`, `systemPreamble`, `cachePreamble`, `resolve()`, `codeFor()`,
|
|
171
|
+
`encodeCallForTest()`, `stats`.
|
|
172
|
+
|
|
173
|
+
`stats`: `level`, `mapStyle`, `requestedMapStyle`, `fallbackReason`, `toolCount`,
|
|
174
|
+
`wireToolCount`, `originalChars`, `compressedChars`, `savedPct`.
|
|
175
|
+
|
|
176
|
+
`namespaceOf` must return `{ ns, op }`, **not** a bare string. Returning a string throws
|
|
177
|
+
with a corrected example — it used to fail silently and produce a plausible-looking but
|
|
178
|
+
wrong map.
|
|
179
|
+
|
|
180
|
+
## Things to tell the user, not assume
|
|
181
|
+
|
|
182
|
+
- **`savedPct` is a CHARACTER saving**, a few points optimistic against tokens. Providers
|
|
183
|
+
charge a fixed framing cost per tool definition that character counting cannot see. For
|
|
184
|
+
any published figure, measure with the provider's own token counter.
|
|
185
|
+
- **Never use `tiktoken` to count Claude tokens** — it is OpenAI's tokenizer and is wrong
|
|
186
|
+
for Claude by 15–20%+. Use Anthropic's `count_tokens`.
|
|
187
|
+
- **Token saving ≠ cost saving.** Prompt caching already makes tool tokens cheap; the claim
|
|
188
|
+
here is context-window *occupancy*. Cost follows by a variable amount, and on OpenAI —
|
|
189
|
+
where reasoning output dominates the bill — it was as little as 7%.
|
|
190
|
+
- **Level 3 costs turns.** Roughly 0.3–1.7 lookup calls per task, about half an extra turn.
|
|
191
|
+
If latency matters more than context, stay at level 1.
|
|
192
|
+
- **Below the frontier tier, argument formatting degrades at level 3.** On Haiku 4.5, 17 of
|
|
193
|
+
30 runs produced a malformed argument — all caught by `validate` and recovered, no task
|
|
194
|
+
lost, but that is the known edge. Tool *choice* did not degrade.
|
|
195
|
+
|
|
196
|
+
## Do not
|
|
197
|
+
|
|
198
|
+
- Do not dispatch `block.name` without `resolve()`.
|
|
199
|
+
- Do not throw on `kind: "error"` — return it to the model.
|
|
200
|
+
- Do not dispatch on `kind: "meta"`.
|
|
201
|
+
- Do not omit `systemPreamble` at level 3.
|
|
202
|
+
- Do not set `validate: false` to silence a malformed-argument error; fix the schema.
|
|
203
|
+
- Do not call `compress()` inside the request loop.
|
|
204
|
+
- Do not report `savedPct` as a token or cost saving.
|
|
205
|
+
- Do not assume a level was chosen for you.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolgz",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
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": {
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"files": [
|
|
46
46
|
"dist",
|
|
47
47
|
"README.md",
|
|
48
|
+
"llms.txt",
|
|
48
49
|
"LICENSE",
|
|
49
50
|
"NOTICE"
|
|
50
51
|
],
|