Benchmark Results¶
Every number on this page, and in the README benchmark tables, is copied
from one run of the end-to-end harness (make harness, tests/harness,
issue #301). Nothing here is hand-edited or estimated. The run's report is
written to bench-results/harness.md, and CI uploads the same file as the
harness-results artifact on every push and pull request.
TL;DR. Measured through the real binary: a session costs 71–86% fewer tokens than native MCP with
list_toolsdiscovery, and 65–94% withsearch_toolsdiscovery, once LeanProxy's own discovery calls are counted. Discovery adds 3–4 extra LLM turns per session withlist_toolsand 4–10 withsearch_tools(one search per new tool). Asearch_toolslookup costs 152 tokens on average, against 907 forlist_tools(server), and finds the right tool in its top 5 for 85.5% of 83 labeled intents. The router the client loads is 318 tokens, against 10,049 for the five nativetools/listpayloads. The proxy adds 0.74 ms at p95, and a 500-call burst finished with 0 errors.
The README used to claim 81.5–93.7% session savings, ~12 µs overhead and ~25,000 q/s. Those figures came from benchmarks that did not run the proxy (see §6).
1. How to reproduce¶
- The harness builds
leanproxy-mcpand the catalog mock itself. It needs no network, no real MCP server and no credentials. - Every process runs with a scratch
HOMEandLEANPROXY_TOOLCACHE_DIR, so the harness never touches your real config, tool cache or status file. - The tests use the
harnessbuild tag, sogo test ./...does not run them.
2. What the harness runs¶
- Proxy. The real
leanproxy-mcp server run --stdiobinary. It is built with the release flags (-trimpath -ldflags="-s -w") and driven over stdin/stdout pipes, the way an IDE drives it. - Upstream servers.
tests/harness/catalogmcpis a Go stdio MCP server that serves one server of a realistic 118-tool catalog (tests/harness/testdata/catalog.json):
| Server | Tools |
|---|---|
| GitHub | 42 |
| Jira/Confluence | 24 |
| Slack | 14 |
| Garmin | 28 |
| Postgres | 10 |
The catalog was ported from the audit prototype
docs/audit/experiments/catalog.py. The tool names, descriptions and
parameter names follow the real servers. Every parameter is a string with
a one-line description, so real schemas are usually larger.
- Mock flags.
- --server: which catalog server to serve.
- --delay-ms: slow down each tools/call.
- --response-bytes: pad each result.
- --secrets: embed fake credentials in each result. The keys are built
at runtime, so no literal key is committed.
- --concurrent: answer out of order.
- Proxy config. Defaults, plus the prompt-injection guard switched on:
- redaction on, with the built-in patterns;
- injection guard: block at risk ≥ 80, redact at risk 50–79;
- no rate limit;
- no response cache.
3. Methodology¶
3.1 Token unit¶
pkg/reporter.Estimator counts 1 token per 4 characters. It is the same
estimator the runtime cost tracker uses. The harness applies it to the
full JSON-RPC response line as it crosses the pipe, so both sides pay
for the envelope. The chars/4 rule approximates BPE tokenizers. It is not
exact, but it is consistent, and consistency is what the ratios need.
3.2 Discovery payloads¶
- Native. Each catalog server's own
tools/listresponse, read directly from the mock. - Router. LeanProxy's
tools/listresponse (search_tools,list_servers,list_tools,invoke_tool). list_serversandlist_tools(server). The real outputs of those router tools.list_serversis read once the background tool refresh has filled in every server's tool count.invoke_tooloverhead. The token difference between aninvoke_toolrequest and the same call sent natively astools/call, and the same difference for the response.search_tools. Each of the 83 labeled intents ofpkg/toolsearch/testdata/intents.jsonis sent as onesearch_toolscall (k = 5, BM25, default config). The harness reports the average and maximum response size, and compares it with thelist_tools(server)output of each intent's server, which is what thelist_toolsflow reads for the same lookup. It also reports recall@1 and recall@5 of those answers. 44 intents come from the audit prototype; 39 were written from the user's side without reusing the catalog descriptions.
3.3 Session model¶
A session is a list of prompts. Each prompt names the (server, tool) that
serves it and a plain-words query a model would search with. The harness
replays each session through the proxy: every list_servers, list_tools,
search_tools and invoke_tool call below really runs. Tokens are counted
per LLM turn.
Native MCP. All 5 configured servers' tools/list payloads are in
context on every turn. The first turn pays for them at full price. Later
turns pay the provider's cache-read rate of 0.25×. There are no extra
turns.
LeanProxy with list_tools.
- The router is in context from the first turn.
- A turn that needs discovery adds that discovery output at full price:
list_serverson the first turn, because the router tells the model to call it first;list_tools(server)the first time the session uses a server.- Each discovery output stays in context.
- Every turn after the first re-reads the carried context (the router plus every earlier discovery output) at 0.25×.
LeanProxy with search_tools. The same rules, but the discovery is one
search_tools(query) the first time the session needs a tool (a tool found
earlier in the session is already in context). No list_servers call. When
the tool is not in the top 5, the model is assumed to fall back to
list_tools(server): that output is added at full price too, with one more
extra turn. The queries are not tuned to the index; a miss is paid for, not
rewritten.
Extra turns. Each discovery call is one extra LLM round-trip. The harness reports them in their own column. Their token cost (another re-read of the context) is not added to the LeanProxy total, so the savings column is an upper bound.
Assumptions.
- Tool results are identical on both paths, so both sides leave them out.
The
invoke_toolrequest wrapper adds 14 tokens per call; that is also left out. - Cache reads cost 0.25× (Anthropic's prompt-cache pricing), with a 100% cache hit after the first turn on both sides.
- On a native client, every configured server's schemas are loaded on every turn, whether or not the session uses that server.
Sessions.
| Session | Prompts |
|---|---|
| Morning Sport | 3 Garmin, then 1 Slack |
| Dev Workflow | GitHub ×2, Jira ×2, GitHub |
| Full Day | GitHub ×2, Jira ×2, Slack ×2, GitHub |
The prompts and their queries are in tests/harness/harness_test.go.
3.4 Latency, throughput and memory¶
- Paced sequential.
- 200
invoke_toolcalls, one at a time, 5 ms apart, after 20 warm-up calls. - Every call has a unique argument, so no cache can serve it.
- The same 200 calls are also sent directly to the same mock.
- Overhead is the proxied percentile minus the direct percentile.
- Burst. 500
invoke_toolcalls, written back to back without waiting, round-robin over the 5 servers. The harness counts errors, checks that every answer carries its own request's argument, and measures wall time. - Parallel.
- 50 calls sent at once to a single mock server that sleeps 100 ms per call and answers concurrently.
- The pool's default
max_in_flightof 32 per server splits them into two waves. - Large response. One 5 MiB tool result, relayed through the proxy and also read directly from the mock.
- RSS.
VmRSSof the proxy process, taken once all 5 servers are warm and again after the burst.
3.5 Safety¶
The mock embeds three fake credentials in every result, and in the description of its first tool. The harness then checks five things:
- Server → client. No fake credential reaches the client.
- Server → client via
search_tools. A search that returns the leaky tool shows no fake credential. - Client → server. The mock counts the fake credentials that arrive in the request it received. The count must be 0.
- Injection. A block-level payload is refused with -32600.
- Server-initiated requests. The mock sends
roots/listin the middle of a call. The proxy must answer it, and the call must complete.
TestHarness_RedactionAssertionTrips runs the same checks with
bouncer: {enabled: false} and requires the redaction assertion to fail.
The harness therefore cannot pass vacuously.
4. Results¶
Run metadata:
| Field | Value |
|---|---|
| Commit | 430f9a8 |
| Date (UTC) | 2026-09-23 |
| Host | linux/amd64, 4 CPUs, go1.25.5 |
Latency depends on the host; token counts do not.
4.1 Assertions (a failure fails make harness and CI)¶
| Assertion | Measured | Result |
|---|---|---|
| Redaction active (both directions) | both directions clean | PASS |
| Injection payload blocked | JSON-RPC error -32600 returned | PASS |
| Server→client request does not hang | answered in 1 ms | PASS |
| 0 errors in a 500-call pipelined burst | 0 errors | PASS |
| 50 × 100 ms parallel calls < 1 s | 205 ms | PASS |
| 5 MB response relayed | 5,242,881 bytes | PASS |
| p95 proxy overhead < 5 ms | 0.74 ms | PASS |
4.2 Discovery payloads¶
| Payload | Tokens |
|---|---|
LeanProxy tools/list (router, 4 tools) |
318 |
LeanProxy list_servers (5 servers) |
80 |
LeanProxy search_tools (k=5), per lookup: average / max over 83 labeled intents |
152 / 260 |
LeanProxy list_tools(server) for the same intents' servers, per lookup: average |
907 |
invoke_tool request overhead vs a native tools/call |
+14 |
invoke_tool response overhead vs a native tools/call |
+0 |
4.3 Per server¶
| Server | Tools | Native tools/list |
LeanProxy list_tools(server) |
LeanProxy router | Router vs native |
|---|---|---|---|---|---|
| github | 42 | 4,443 | 1,638 | 318 | −92.8% |
| jira | 24 | 2,043 | 772 | 318 | −84.4% |
| slack | 14 | 1,128 | 441 | 318 | −71.8% |
| garmin | 28 | 1,795 | 760 | 318 | −82.3% |
| postgres | 10 | 640 | 282 | 318 | −50.3% |
| all 5 | 118 | 10,049 | 3,893 | 318 | −96.8% |
"Router vs native" compares only what sits in context before any tool is used. A real session also pays for discovery; that cost is in §4.5.
The router grew from 237 to 318 tokens when search_tools was added
(#305).
4.4 search_tools ranking through the binary¶
| Measurement | Value |
|---|---|
| Recall@1 (right tool first) | 55/83 (66.3%) |
| Recall@5 (right tool in the answer) | 71/83 (85.5%) |
Tokens per lookup vs list_tools(server) |
−83.2% |
On the 44 audit intents alone recall@5 is 88.6%; on the 39 independent
intents it is 82.1% (go test ./pkg/toolsearch -run TestRecall -v). The
independent intents miss mostly on vocabulary the catalog does not use
("time booked" for a worklog, "watches" for devices); the optional hybrid
mode (tool_search.hybrid, off by default and not measured here) targets
those.
4.5 Session replay¶
| Session | Prompts | Servers used | Native tokens | LeanProxy list_tools tokens |
Savings | Extra LLM turns | LeanProxy search_tools tokens |
Savings | Extra LLM turns | Search misses |
|---|---|---|---|---|---|---|---|---|---|---|
| Morning Sport | 4 | 2 | 17,586 | 2,470 | −86.0% | +3 | 1,148 | −93.5% | +4 | 0 |
| Dev Workflow | 5 | 2 | 20,098 | 5,234 | −74.0% | +3 | 3,143 | −84.4% | +6 | 1 |
| Full Day | 7 | 3 | 25,123 | 7,302 | −70.9% | +4 | 8,793 | −65.0% | +10 | 3 |
With list_tools, the savings shrink as a session uses more servers. Each
newly used server brings its list_tools output into context; GitHub's
output alone is 1,638 tokens.
With search_tools, a session that finds its tools pays about 150 tokens
per new tool instead of a whole server's list, and saves more (Morning
Sport, Dev Workflow). A miss is expensive: the modeled fallback reads the
whole list_tools output on top of the search. Full Day misses 3 of its 7
distinct tools (a README read, a Jira search phrased "find my tickets in
progress", a Slack post phrased "status update"), so it ends up above the
list_tools flow. search_tools also adds more extra turns than
list_tools: one per distinct tool rather than one per server.
4.6 Latency, throughput and memory¶
| Measurement | Value |
|---|---|
Paced sequential invoke_tool, 200 unique calls: p50 / p95 / p99 via proxy |
0.73 / 1.04 / 1.42 ms |
| Same calls direct to the mock: p50 / p95 / p99 | 0.17 / 0.30 / 0.38 ms |
| Proxy overhead (proxy − direct): p50 / p95 / p99 | 0.56 / 0.74 / 1.04 ms |
| 500-call pipelined burst over 5 servers: wall / throughput / errors | 51 ms / 9,723 req/s / 0 |
50 parallel calls to a 100 ms tool on one server (default max_in_flight 32, so two waves): wall |
205 ms |
| 5 MB tool response: via proxy / direct | 581 ms / 51 ms |
| Proxy RSS: idle (5 servers warm) / after the burst | 20.4 MiB / 22.5 MiB |
Binary size (linux/amd64, -trimpath -ldflags="-s -w") |
16.4 MiB |
The 5 MB relay costs about 0.5 s more through the proxy than directly.
Most of that is response-side secret redaction, which scans the whole
result. A one-off check with bouncer.enabled: false relayed the same
response several times faster; the harness does not report that
configuration.
5. Micro-benchmarks that remain in tests/bench¶
make bench still runs these. Each one measures only what its name says.
None of them is a proxy-level number, and none is quoted in the README.
| Benchmark | What it measures |
|---|---|
BenchmarkSchemaTax_LeanProxyRouter |
Size of the real router tools/list payload (pkg/mcp.GetAllToolDefinitions) |
BenchmarkSchemaTax_StubSchema |
Size of one lazy-loading registry.ToolStub |
BenchmarkJSONParse_Literal |
json.Unmarshal of two literal strings plus one cost-tracker update, in process |
BenchmarkEstimateTokens_50MB |
The chars/4 estimator over a 50 MB buffer |
TestBinarySize_NFR3 |
Size of the dist/ binaries from make build (< 20 MB) |
6. What changed from the previous numbers¶
| Previous claim | Where it came from | Now (harness) |
|---|---|---|
| Session savings 81.5–93.7% | SessionReplay_* charged LeanProxy the router plus one ~26-token stub per prompt. It ignored the list_tools output and the extra turns, and its timing loop was empty. |
−70.9% to −86.0% with list_tools (3–4 extra turns), −65.0% to −93.5% with search_tools (4–10 extra turns), discovery outputs counted |
| Per-server savings 79.0–97.9% (GitHub 41 tools = 4,570 tokens, ...) | Synthetic tools/list payloads padded with dots to a byte count from a seeded snapshot |
Real catalog payloads: router vs native −50.3% to −92.8% per server, −96.8% for all 5 |
| Proxy overhead ~12 µs/op (p50) | BenchmarkProxyOverhead_NFR1: two json.Unmarshal calls on literals, in process |
0.56 ms p50 / 0.74 ms p95 through the binary, measured against a direct baseline |
| Throughput ~25,000 q/s | BenchmarkThroughput_MockMCP: an in-process mock, with no proxy involved |
9,723 req/s for a 500-call pipelined burst through the binary (one run; varies by host) |
| 50 MB payload estimate ~7 ms | Token estimator over a 50 MB buffer; no relay | 5 MB relayed in 581 ms (estimator benchmark renamed BenchmarkEstimateTokens_50MB) |
| Binary 15.8 MB (darwin-arm64) | dist/ build on the maintainer's machine |
16.4 MiB (linux/amd64, harness build) |
7. Response governor (large results)¶
The response governor (#319, response: block, off by default) caps each
tool result at a token budget and keeps the full result for read_result.
The harness measures it with catalogmcp --large-results, where five tools
answer with realistic large results:
| Call | Result |
|---|---|
github.get_file_contents |
a ~200 KB Go source file |
github.list_issues |
300 issues with user objects, URLs, labels and bodies |
github.search_code |
400 code-search hits with text matches |
jira.jira_search |
250 issues with custom fields |
postgres.pg_query |
1,500 rows |
The same calls go through two proxies: the default config (governor off)
and response: {enabled: true, max_tokens: 4000}. Tokens are those of the
whole JSON-RPC response line. The session row applies the session model of
§3: each result enters the context once and is re-read at 0.25× on every
later turn.
| Call | Governor off | Governor on | Savings |
|---|---|---|---|
github.get_file_contents |
54,850 | 3,445 | −93.7% |
github.list_issues |
57,949 | 3,748 | −93.5% |
github.search_code |
41,635 | 3,784 | −90.9% |
jira.jira_search |
35,069 | 3,739 | −89.3% |
postgres.pg_query |
45,197 | 3,799 | −91.6% |
| Total (each result once) | 234,700 | 18,515 | −92.1% |
| Session (5 turns) | 362,597 | 27,598 | −92.4% |
The harness also asserts that:
- every governed response line stays within
max_tokens(largest: 3,799); read_resultpages the file back (13 pages of 4,000 tokens) and the concatenation is byte-identical to the ungoverned result;- the 16 normal-size calls of the replayed sessions are byte-identical with
the governor on (a result under budget is not even parsed), and the paced
invoke_toollatency is unchanged within noise (p50 0.54 ms off, 0.65 ms on, same run; the stage costs about 3 µs per call inBenchmarkGovernor_SmallResult).
The savings are an upper bound for what the model keeps in context: a
model that needs the omitted part pays for the read_result pages it
reads (the file above is 13 pages).
8. Field projection (large results)¶
Field projection (#320, response.projections and
response.default_projections, off by default) drops JSON fields the model
does not need before the budget is applied. The harness measures it on the
same large-results calls, with the github.* drop pack of the issue
(**.node_id, **.*_url, **.url, **.reactions, **.avatar_url,
**.gravatar_id) and the built-in default pack for the other servers,
through three proxies: projection only (max_tokens: 0), truncation only
(the §7 row) and projection then truncation (max_tokens: 4000). "Items"
is the number of list elements the model sees within the budget.
| Call | Off | Projection only | Truncation only (items) | Projection + truncation (items) |
|---|---|---|---|---|
github.get_file_contents |
54,850 | 54,850 (not JSON) | 3,445 | 3,445 |
github.list_issues |
57,949 | 41,020 (−29.2%) | 3,748 (19) | 3,719 (26) |
github.search_code |
41,635 | 33,303 (−20.0%) | 3,784 (36) | 3,715 (43) |
jira.jira_search |
35,069 | 31,350 (−10.6%) | 3,739 (27) | 3,670 (29) |
postgres.pg_query |
45,197 | 45,197 (nothing to drop) | 3,799 (126) | 3,799 (126) |
| Total | 234,700 | 205,720 (−12.3%) | 18,515 | 18,348 |
- On the three noisy listings, projection alone saves 21.5% (134,653 → 105,673 tokens); the file and the SQL rows have nothing to drop and come back unchanged.
- On top of truncation, the budget is the same, so the token totals barely move (the projection note and its link take part of the budget); what changes is how much of the list fits: 26 issues instead of 19, 43 code hits instead of 36.
- With the model's
fieldsargument ([].number,[].title,[].state,[].labels[].name,[].user.login,[].updated_at),list_issuesshows 64 issues in 3,738 tokens, against 19 with truncation only. - The catalog fixtures carry little noise. On a realistic GitHub
list_issuesresponse (pkg/mcp/governor/testdata/github_list_issues.json, 30 issues with the REST API's full user objects, URL templates and reactions), the same drop pack saves 72.2% (29,536 → 8,225 tokens), and the default pack alone 59.6% (TestProjection_GitHubDropPackOnListIssues). read_resulton the projection's result id returns the full, redacted result with the dropped fields (harness assertion).
Cost: projection works on the bytes, without decoding values. Projecting
the 118 KB fixture takes about 1.6 ms (BenchmarkProjection_DropPack), and
the governor stage with projection only about 1 ms on a 30-issue result
(BenchmarkGovernor_ProjectionOnly). Per-call latency in the harness is
within noise of truncation only (for example github.list_issues: 15.9 ms
truncation only, 19.1 ms projection then truncation, 22.0 ms projection
only, which returns a 164 KB line).
9. In-session dedup (repeated reads)¶
In-session dedup (#321, response.dedup: on, off by default) replaces a
result byte-identical to one already returned earlier in the same session
with a short stub. The harness simulates the common "re-read the same file
after a failed edit" pattern: github.get_file_contents read once, then
re-read three more times in the same session, with response.dedup: on on
top of the max_tokens: 4000 truncation baseline (§7).
| Read | Dedup off (truncation only, §7) | Dedup on | Savings |
|---|---|---|---|
| First (not yet seen this session) | 3,445 | 3,445 | −0.0% |
| Repeat (already seen this session) | 3,445 | 65 | −98.1% |
| Whole session (1 read + 3 repeats) | 13,780 | 3,640 | −73.6% |
- The first read is unaffected (there is nothing to dedup against yet); a repeat read is a two-line stub naming the earlier result's id, a 98.1% reduction over truncation alone.
- Cross-session isolation is checked by the e2e suite, not the harness: two
Streamable HTTP sessions read the same (byte-identical, from the fake
upstream) content, and the second session never receives a dedup marker
for content only the first session saw
(
TestResponseGovernorDedup_CrossSessionHTTP_NotDeduped).
Summarization (#321, response.summarize, off by default, no default
allowlist) is not part of the harness's token-savings table: it needs a
local Ollama model to measure honestly, which the harness's fixed-catalog,
no-network design deliberately avoids (the same reasoning as the embedding
hybrid search of #305). It is covered instead by unit and e2e tests with a
fake Ollama server (httptest): replacing an over-threshold result with a
summary and a read_result pointer, falling back to truncation on a
timeout or an error, and the summary being run back through the injection
guard's response scan before being returned.
10. Exposure modes (#322)¶
Clients with native tool search (Claude Code, clientInfo.name
claude-code) get passthrough by default: every upstream tool listed as
<server>__<tool> with its full metadata, instead of LeanProxy's 4-tool
router (see Configuration). The
harness initializes server run --stdio as claude-code over the same
118-tool catalog, checks that every tool is listed under a valid name and that
every replayed session call routes by its namespaced name, and compares the
token cost with the router's search_tools flow (§4.5).
| Payload | Tokens |
|---|---|
Native: every server's own tools/list, summed |
10,049 |
Router tools/list (4 gateway tools) |
318 |
Passthrough tools/list (every tool, namespaced, full metadata) |
10,229 (+1.8% vs native) |
Hybrid tools/list (passthrough + search_tools) |
10,333 |
| Passthrough names only (what a deferring client keeps in context) | 754 |
| One tool's full definition, loaded on first use: average | 87 |
Call request line: invoke_tool envelope / namespaced name |
50 / 38 |
Session model: the same three sessions and 0.25× cache-read rate as §4.5,
tool results excluded. Router is the search_tools column of §4.5.
Passthrough, deferred models what Claude Code does with a large tool
list: the 118 names are in context from turn 1, and a tool's full definition
is loaded (at 1×, then carried at 0.25×) the first time it is used, with one
client-side search turn. The client's own search tool definition and search
requests are not counted (they exist with or without LeanProxy).
Passthrough, eager is a client that loads the whole list every turn.
| Session | Native | Router (search_tools) |
Passthrough, deferred | Deferred vs router | Deferred loads (extra turns) | Passthrough, eager |
|---|---|---|---|---|---|---|
| Morning Sport (4 prompts) | 17,586 | 1,148 | 1,686 | +46.9% (more tokens) | +4 | 17,901 (+1.8% vs native) |
| Dev Workflow (5 prompts) | 20,098 | 3,143 | 2,460 | −21.7% | +5 | 20,458 (+1.8% vs native) |
| Full Day (7 prompts) | 25,123 | 8,793 | 3,089 | −64.9% | +7 | 25,573 (+1.8% vs native) |
What this says, honestly:
- Passthrough never shrinks the wire payload. Its
tools/listis the whole catalog, 1.8% more than the native lists (the<server>__prefixes), sent once per session and again after eachtools/list_changed. What a capable client pays is decided by the client, not by LeanProxy. - With deferral, it wins on longer sessions and loses on short ones. A
deferring client carries 754 tokens of names from turn 1, more than the
318-token router; a tool definition then costs 87 tokens on average,
versus a ranked
search_toolsanswer of about 150 tokens (§4.2) and, on a miss, alist_toolsfallback. Morning Sport (4 prompts, all found by the first search) is 47% cheaper on the router; Full Day, where 3 of 7 searches miss, is 65% cheaper in passthrough. These numbers depend on the client's own search quality, which the harness does not measure. - Without deferral, passthrough costs what native MCP costs. That is why
unknown clients stay on the router, and why the built-in table only maps
clients known to defer or select tools themselves.
--exposure router(orexposure.clients) keeps any client on the router. - Calls are cheaper to write in passthrough (38 vs 50 tokens for the same
create_issuecall): noinvoke_toolenvelope.
11. Auditable savings report (#324)¶
leanproxy-mcp report (see docs/savings-report.md and
docs/commands.md) rebuilds a savings breakdown from the same real,
running-pipeline counters this harness's own assertions read from — the
response governor's GovernorStats (§7-§9) and the schema/discovery
telemetry counters (§4, §10) — persisted to an offline JSONL store instead
of only living in one process's memory. tests/harness/savings_report_test.go
cross-checks report --export json's totals against an independent,
harness-measured baseline (an ungoverned proxy over the same calls) and
asserts they agree within 1%, and that the report's mechanism breakdown
sums to its total. Unlike every number in this document, report's numbers
are not benchmark averages over a synthetic catalog: they are the exact
counters of whatever real session(s) produced them.
12. Known limits and follow-ups¶
search_tools. The audit prototype estimated about −92% for Full Day; the harness measures −65.0%, because 3 of Full Day's 7 queries miss the top 5 and pay thelist_toolsfallback. Hybrid mode is not measured (the harness has no network and no embedder), and the fallback model is a conservative guess: a model might retry the search instead.- Extra-turn cost. The extra turns are counted but not costed. Costing them needs a model of what the model re-sends on a discovery turn.
- Transports. Only the stdio front end and stdio upstreams are measured.
serve, HTTP and SSE are not. - Catalog. It is realistic but synthetic. Real servers often have
larger schemas, with enums, nested objects and long descriptions. That
raises both the native cost and the
list_toolscost.