toolgz 0.2.9 โ 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 +15 -0
- package/llms.txt +205 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -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
|
|
@@ -421,6 +432,10 @@ than a bug report from you.
|
|
|
421
432
|
npx tsx examples/01-minimal.ts
|
|
422
433
|
```
|
|
423
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
|
+
|
|
424
439
|
| File | Shows |
|
|
425
440
|
|---|---|
|
|
426
441
|
| [`01-minimal.ts`](examples/01-minimal.ts) | the smallest useful thing: `recommendLevel` โ `compress` โ `resolve` |
|
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
|
],
|