toolgz 0.2.7 → 0.2.9
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 +132 -20
- package/dist/index.js +12 -20
- package/dist/recommend.d.ts +13 -3
- package/dist/recommend.js +19 -5
- 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
|
|
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> ·
|
|
@@ -33,7 +33,7 @@ Reclaiming the room is what this does.
|
|
|
33
33
|
```ts
|
|
34
34
|
import { compress, recommendLevel, forAnthropic } from "toolgz";
|
|
35
35
|
|
|
36
|
-
const { level } = recommendLevel(myTools); // 1 for small
|
|
36
|
+
const { level } = recommendLevel(myTools); // advice: 1 for a small block, 3 for a big one
|
|
37
37
|
const c = compress(myTools, { level }); // your existing MCP/SDK tool array
|
|
38
38
|
const { tools, system } = forAnthropic(c); // send these instead
|
|
39
39
|
```
|
|
@@ -41,7 +41,11 @@ const { tools, system } = forAnthropic(c); // send these instead
|
|
|
41
41
|
`compress(myTools)` with no `level` gives you **level 1** — safe, native tool calling,
|
|
42
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
|
-
amortise the dispatcher.
|
|
44
|
+
amortise the dispatcher.
|
|
45
|
+
|
|
46
|
+
**Note the shape of those three lines: `recommendLevel` only advises, and you pass its
|
|
47
|
+
answer back in.** Nothing upgrades itself — `compress(myTools)` is level 1 whether you
|
|
48
|
+
have 2 tools or 500. See [Which level to use](#which-level-to-use) for why.
|
|
45
49
|
|
|
46
50
|
Then translate the model's call back before you dispatch:
|
|
47
51
|
|
|
@@ -226,6 +230,62 @@ tried and removed in 0.2.0 because they were smaller and still worse.
|
|
|
226
230
|
|
|
227
231
|
## Which level to use
|
|
228
232
|
|
|
233
|
+
### The whole idea, in plain English
|
|
234
|
+
|
|
235
|
+
Your tool definitions are a **menu** handed to the model at the start of every single
|
|
236
|
+
conversation. It is long, and most of it is flowery prose about each dish.
|
|
237
|
+
|
|
238
|
+
- **Level 1** — the same menu with the prose cut. It is still a real menu: the model
|
|
239
|
+
points at a dish **by name**, and *the kitchen checks the order makes sense* before
|
|
240
|
+
cooking. This is the default, and it gives up nothing.
|
|
241
|
+
- **Level 3** — throw the menu away. Hand the model a **numbered list** and one waiter.
|
|
242
|
+
It says "number 12, no onions," and the waiter knows what that means. The list is
|
|
243
|
+
tiny. But the kitchen no longer checks the order — **toolgz checks it instead**,
|
|
244
|
+
against your original schema, and hands back a readable error if it's wrong.
|
|
245
|
+
|
|
246
|
+
If the model needs to know what number 12 comes with, it asks. That's the `q()` lookup,
|
|
247
|
+
and it costs about half a turn.
|
|
248
|
+
|
|
249
|
+
So: **level 1 is smaller. Level 3 is much smaller and you take over order-checking.**
|
|
250
|
+
|
|
251
|
+
### Two things that surprise people
|
|
252
|
+
|
|
253
|
+
**1. Nothing changes level on its own. You are always the one who picks.**
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
compress(myTools) // level 1. ALWAYS — 2 tools or 500.
|
|
257
|
+
compress(myTools, { level: 3 }) // level 3, because you asked for it
|
|
258
|
+
|
|
259
|
+
const { level } = recommendLevel(myTools); // just advice: returns 1 or 3
|
|
260
|
+
compress(myTools, { level }); // now it's 3, because you passed it in
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
`recommendLevel()` **advises**; it does not act. On our 149-tool corpus,
|
|
264
|
+
`compress(myTools)` saves 45.2% and `compress(myTools, { level: 3 })` saves 96.5% — so
|
|
265
|
+
forgetting to pass the level back in quietly leaves half the win on the table.
|
|
266
|
+
|
|
267
|
+
This is deliberate. Level 3 gives up provider-side schema enforcement, and silently
|
|
268
|
+
changing a caller's correctness guarantees because their tool array grew would be a
|
|
269
|
+
worse bug than the tokens are worth.
|
|
270
|
+
|
|
271
|
+
**2. It switches on how *big* the block is, not how *many* tools you have.**
|
|
272
|
+
|
|
273
|
+
A tool can be 20 tokens or 460, so counting them tells you very little:
|
|
274
|
+
|
|
275
|
+
| Tool set | Level 1 block | Recommends |
|
|
276
|
+
|---|---:|:-:|
|
|
277
|
+
| 72 tools, one parameter each | ~5,000 tokens | **1** |
|
|
278
|
+
| 200 tools, one parameter each | ~14,100 tokens | **3** |
|
|
279
|
+
| **40** real MCP tools | ~10,600 tokens | **3** |
|
|
280
|
+
| 149 real MCP tools | ~42,700 tokens | **3** |
|
|
281
|
+
|
|
282
|
+
Forty chatty tools cross the line while 72 terse ones don't. The threshold is **10,000
|
|
283
|
+
tokens** — about 5% of a 200k window. Below that, reclaiming the block doesn't change
|
|
284
|
+
what fits, so keeping the provider's own argument validation is worth more than the
|
|
285
|
+
saving.
|
|
286
|
+
|
|
287
|
+
### The levels in full
|
|
288
|
+
|
|
229
289
|
Ask the library. It returns 1 or 3, never 2, and explains itself:
|
|
230
290
|
|
|
231
291
|
```ts
|
|
@@ -302,6 +362,11 @@ Level 3 has several map styles. Which one is cheapest turns out to depend on the
|
|
|
302
362
|
model, so you can hand `compress()` a model id and let it use what was actually
|
|
303
363
|
measured:
|
|
304
364
|
|
|
365
|
+
> **This is the one thing the library does choose for you, and only if you pass
|
|
366
|
+
> `model`.** Note the difference from levels: a map style is a pure encoding choice, so
|
|
367
|
+
> picking a better one cannot change your results. The level *can* — level 3 hands
|
|
368
|
+
> argument validation from the provider to toolgz — so that stays your explicit call.
|
|
369
|
+
|
|
305
370
|
```ts
|
|
306
371
|
compress(myTools, { level: 3, model: "gpt-5.6-sol", objective: "cost" });
|
|
307
372
|
```
|
|
@@ -346,6 +411,31 @@ style in 0.2.0.
|
|
|
346
411
|
|
|
347
412
|
---
|
|
348
413
|
|
|
414
|
+
## Runnable examples
|
|
415
|
+
|
|
416
|
+
Five files in [`examples/`](examples), all offline — no API key, no cost. Every one is
|
|
417
|
+
**executed by the test suite**, so an example that stops working is a failing test rather
|
|
418
|
+
than a bug report from you.
|
|
419
|
+
|
|
420
|
+
```bash
|
|
421
|
+
npx tsx examples/01-minimal.ts
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
| File | Shows |
|
|
425
|
+
|---|---|
|
|
426
|
+
| [`01-minimal.ts`](examples/01-minimal.ts) | the smallest useful thing: `recommendLevel` → `compress` → `resolve` |
|
|
427
|
+
| [`02-agent-loop.ts`](examples/02-agent-loop.ts) | the full loop against a scripted model, covering all three `resolve()` outcomes including a recovery |
|
|
428
|
+
| [`03-mcp-servers.ts`](examples/03-mcp-servers.ts) | 149 real tools from 14 MCP servers, all four levels, and the name-collision hazard |
|
|
429
|
+
| [`04-providers.ts`](examples/04-providers.ts) | the four provider envelopes side by side, plus the Gemini schema repairs |
|
|
430
|
+
| [`05-per-model.ts`](examples/05-per-model.ts) | `model`/`objective` selection and reading `stats` to see what was actually used |
|
|
431
|
+
|
|
432
|
+
Two things `04-providers.ts` demonstrates rather than describes, because both have bitten
|
|
433
|
+
people: `/v1/responses` needs the **flat** tool shape when you set reasoning effort, and
|
|
434
|
+
Gemini returns **one** wrapper object containing all declarations, so you count
|
|
435
|
+
`tools[0].functionDeclarations.length`.
|
|
436
|
+
|
|
437
|
+
---
|
|
438
|
+
|
|
349
439
|
## Using it: the full guide
|
|
350
440
|
|
|
351
441
|
Everything below was a separate `docs/GUIDE.md`. It is inline now, deliberately: the
|
|
@@ -758,8 +848,13 @@ Use `forGemini`, which strips the keywords Gemini won't accept. If a new one
|
|
|
758
848
|
appears, it's a one-line addition to the adapter.
|
|
759
849
|
|
|
760
850
|
**Savings look small.**
|
|
761
|
-
|
|
762
|
-
|
|
851
|
+
First check you actually passed a level: `compress(myTools)` is **level 1**, and it
|
|
852
|
+
stays level 1 no matter how many tools you hand it. `recommendLevel` advises, it does
|
|
853
|
+
not act — you have to pass its answer in as `{ level }`.
|
|
854
|
+
|
|
855
|
+
If you did, ask `recommendLevel(myTools)` and read the `reason`. It reports the size of
|
|
856
|
+
your level-1 block, and under ~10,000 tokens there is little worth reclaiming. Also
|
|
857
|
+
check `c.stats`:
|
|
763
858
|
|
|
764
859
|
```ts
|
|
765
860
|
console.log(c.stats);
|
|
@@ -767,8 +862,7 @@ console.log(c.stats);
|
|
|
767
862
|
// originalChars: 61461, compressedChars: 2211, savedPct: 96.4 }
|
|
768
863
|
```
|
|
769
864
|
|
|
770
|
-
`savedPct`
|
|
771
|
-
accurate). `originalChars` and `compressedChars` give the raw character counts.
|
|
865
|
+
`savedPct` is a character saving, a few points optimistic against tokens. `originalChars` and `compressedChars` give the raw character counts.
|
|
772
866
|
|
|
773
867
|
**I need the real token numbers.**
|
|
774
868
|
Count them with the provider's own endpoint. Never use `tiktoken` for Claude —
|
|
@@ -842,21 +936,34 @@ Returns:
|
|
|
842
936
|
| `codeFor(name)` | `→ string` | real name → level-3 code; throws below level 3 |
|
|
843
937
|
| `stats` | `CompressStats` | `level`, `mapStyle`, `requestedMapStyle`, `fallbackReason`, `toolCount`, `wireToolCount`, `originalChars`, `compressedChars`, `savedPct` |
|
|
844
938
|
|
|
845
|
-
> **`savedPct`
|
|
846
|
-
>
|
|
847
|
-
>
|
|
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.**
|
|
939
|
+
> **`savedPct` is a character saving, and runs a few points optimistic against tokens.**
|
|
940
|
+
> On the real 149-tool corpus it reports 46.8% at level 1 where `count_tokens` measures
|
|
941
|
+
> 39.2%, and 96.6% at level 3 against 95.6%.
|
|
851
942
|
>
|
|
852
|
-
>
|
|
853
|
-
>
|
|
854
|
-
>
|
|
943
|
+
> We tried making it a token estimate in 0.2.7 and reverted it in 0.2.8. Providers charge a
|
|
944
|
+
> fixed framing cost per tool definition that character counting cannot see: at 149 tools
|
|
945
|
+
> it amortises away, at 2 tools it dominates, and the calibrated estimate was off by 44% on
|
|
946
|
+
> a small level-1 block while being within 1% at scale. The plain character ratio is the
|
|
947
|
+
> smaller and more predictable error.
|
|
948
|
+
>
|
|
949
|
+
> Use it as a local signal. For anything you publish, measure with your provider's token
|
|
950
|
+
> counter. `originalChars` and `compressedChars` are on `stats` for the raw counts.
|
|
951
|
+
>
|
|
952
|
+
> A negative value is possible and correct: on a very small tool set, level 1's signature
|
|
953
|
+
> line can exceed the per-property descriptions it strips.
|
|
855
954
|
| `encodeCallForTest(name, args)` | `→ {name, args}` | build the raw call a model would emit; test aid |
|
|
856
955
|
|
|
857
956
|
### `recommendLevel(tools, namespaceOf?) → Recommendation`
|
|
858
957
|
|
|
859
|
-
`{ level, reason, toolCount, namespaceCount, opsPerNamespace }`. Returns 1 or 3.
|
|
958
|
+
`{ level, reason, toolCount, namespaceCount, opsPerNamespace }`. Returns 1 or 3, never 2.
|
|
959
|
+
|
|
960
|
+
**It advises; it does not act.** Pass the answer back in yourself —
|
|
961
|
+
`compress(tools, { level })`. Calling `compress(tools)` alone is level 1 regardless of
|
|
962
|
+
how large `tools` is.
|
|
963
|
+
|
|
964
|
+
The decision is on the **size** of the level-1 block (threshold 10,000 tokens ≈ 5% of a
|
|
965
|
+
200k window), not on `toolCount`. The three shape fields are reported for your own
|
|
966
|
+
logging; only block size drives the level.
|
|
860
967
|
|
|
861
968
|
### Provider adapters
|
|
862
969
|
|
|
@@ -945,8 +1052,13 @@ xAI is OpenAI-compatible — use `forOpenAI` with `baseURL: "https://api.x.ai/v1
|
|
|
945
1052
|
- **It has not been measured on a non-frontier model at level 3.** On Haiku 4.5, argument
|
|
946
1053
|
errors rose sharply (17 of 30 runs) — all caught and retried, no task lost, but that is the
|
|
947
1054
|
known edge.
|
|
948
|
-
- **It is not magic on
|
|
949
|
-
`recommendLevel()` will
|
|
1055
|
+
- **It is not magic on a small tool block.** Under ~10,000 tokens at level 1 there is
|
|
1056
|
+
little worth reclaiming, and `recommendLevel()` will say so and keep you on level 1.
|
|
1057
|
+
That is usually a small number of tools, but not always — it depends on how verbose
|
|
1058
|
+
your schemas are, not on the count.
|
|
1059
|
+
- **It does not pick a level for you.** `compress()` defaults to level 1 and stays there;
|
|
1060
|
+
reaching level 3 is always an explicit `{ level }`. Deliberate — level 3 trades away
|
|
1061
|
+
provider-side schema enforcement, and that is not a trade to make behind your back.
|
|
950
1062
|
|
|
951
1063
|
## Determinism
|
|
952
1064
|
|
|
@@ -960,7 +1072,7 @@ and it does not get deleted.
|
|
|
960
1072
|
## Development
|
|
961
1073
|
|
|
962
1074
|
```bash
|
|
963
|
-
npm test #
|
|
1075
|
+
npm test # 283 tests, offline, no cost
|
|
964
1076
|
npm run build # tsc → dist/ with .d.ts
|
|
965
1077
|
|
|
966
1078
|
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
|
|
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
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
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
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
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 -
|
|
199
|
+
: Math.round((1 - compressedChars / originalChars) * 1000) / 10,
|
|
208
200
|
},
|
|
209
201
|
};
|
|
210
202
|
};
|
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));
|
|
@@ -57,7 +67,11 @@ export function recommendLevel(tools, namespaceOf = defaultNamespaceOf) {
|
|
|
57
67
|
// provider's own schema enforcement, which levels 2-3 give up. That is worth more
|
|
58
68
|
// than a saving you would not notice — hence an absolute threshold on the block,
|
|
59
69
|
// not a shape test.
|
|
60
|
-
|
|
70
|
+
// 10,000 tokens is ~5% of a 200k window. Below that, reclaiming the block does not
|
|
71
|
+
// change what fits, and level 1 keeps the provider's own argument validation — worth
|
|
72
|
+
// more than a saving you would not notice. An external reviewer flagged the earlier
|
|
73
|
+
// 4,000 threshold for pushing 14-tool sets into dispatcher mode; they were right.
|
|
74
|
+
const THRESHOLD_TOKENS = 10000;
|
|
61
75
|
if (l1Tokens < THRESHOLD_TOKENS) {
|
|
62
76
|
return {
|
|
63
77
|
...base,
|
|
@@ -68,6 +82,6 @@ export function recommendLevel(tools, namespaceOf = defaultNamespaceOf) {
|
|
|
68
82
|
return {
|
|
69
83
|
...base,
|
|
70
84
|
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
|
|
85
|
+
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
86
|
};
|
|
73
87
|
}
|