bench

package
v0.51.0-rc.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 15, 2026 License: MIT Imports: 27 Imported by: 0

README

mcpproxy benchmark harness

The reproducible numbers behind mcpproxy's marketing claims — token reduction, discovery accuracy, and latency — comparing three ways an agent can be wired to upstream MCP tools, plus the discovery-effectiveness profiler (Spec 083): encoding-arm comparisons, real-MCP response cost, break-even analysis, session-cost estimates, public-corpus validation, and an independent LAP verdict.

Roadmap item #19 (MCP-42) + Spec 083. In-repo (bench/), reproducible, intended to be refreshed on release. Reports are never committed (Spec 065 CN-003); only code, fixtures, and this methodology are versioned.

The three modes

Mode What the agent sees in context mcpproxy server
baseline Every upstream tool definition, loaded directly (no proxy discovery)
retrieve_tools retrieve_tools + call_tool_read/write/destructive + read_cache + code_execution + management tools; tools found on demand via BM25 callToolServer
code_execution code_execution + retrieve_tools + management tools; many tools orchestrated from sandboxed JS in one round-trip codeExecServer

Both proxy modes also append the shared management tool setupstream_servers, quarantine_security, search_servers, list_registries — that the live routing-mode servers expose. These count against the proxy context cost: omitting them undercounts that cost and inflates the savings.

The per-mode catalog is derived directly from the live tool builders (buildCallToolModeTools / buildCodeExecModeTools in internal/server/mcp_routing.go, via server.ProxyModeToolDefs), so it can never drift from production.

What ships today (deterministic, offline)

The token-reduction measurement is fully deterministic and runs with no network or LLM:

go run ./bench/cmd/bench            # scores the committed Spec 065 corpus (v1 report)
make bench-discovery                # Spec 083 profiler: all encoding arms on corpus_v2 (v2 report)
go test ./bench/...                 # unit + invariant tests

It counts the context-token cost of each mode over a frozen tool corpus and reports the savings of each proxy mode versus the baseline. Output: a report.json and a self-contained dashboard.html in bench/results/ (gitignored).

Current deterministic result

Over the 45-tool Spec 065 reference corpus, counting tool name + description only (schemas excluded uniformly — see limitations), cl100k_base:

Mode Context tools Tokens Savings vs. baseline
baseline 45 1730
retrieve_tools 10 1431 ~17%
code_execution 6 986 ~43%

These are deliberately modest: the proxy context here is the full per-mode tool set (discovery + call-tool variants + management tools), and the corpus is small. Savings grow toward the asymptote as the upstream tool count rises (the baseline grows linearly while the proxy context stays fixed) — always quote the corpus size alongside a percentage. Reproduce with go run ./bench/cmd/bench.

Scoring rubric — token reduction
  • Tool universe: the frozen Spec 065 snapshot specs/065-evaluation-foundation/datasets/corpus_v1.tools.json — 45 tools across 7 no-auth reference servers. Frozen + versioned so scoring never runs against a drifting corpus (CN-002).
  • Tokenizer: tiktoken cl100k_base, a widely-used reproducible BPE (already a repo dependency). It is a model-agnostic estimator — see the tokenizer caveat under "Known limitations" before quoting an absolute number.
  • Proxy-mode tools: the complete per-mode catalog, derived from the live server builders — discovery, the call-tool variants, code_execution, and the shared management tool set (upstream_servers, quarantine_security, search_servers, list_registries). Nothing the agent actually sees is dropped from the proxy cost.
  • Cost of a tool: name + "\n" + description. JSON input schemas are excluded uniformly across all modes (the committed corpus snapshot does not carry schemas). The Spec 083 profiler measures schema-bearing renderings on corpus_v2 instead.
  • Savings for a mode m: 1 - tokens(m) / tokens(baseline).

Discovery-effectiveness profiler (Spec 083)

The profiler answers three questions the v1 harness could not: what does a retrieve_tools response actually cost over the real MCP protocol, which tool-definition encoding is cheapest without wrecking retrieval quality, and do the in-house numbers survive external corpora and an independent linter. Output is a versioned v2 report (report.json conforming to specs/083-discovery-profiler/contracts/report-v2.schema.json) plus a self-contained dashboard.html, where every headline number carries a provenance badge: measured (observed on the wire), computed (deterministic arithmetic over measurements), or estimated (a model with documented assumptions).

Quickstart

One command (SC-008):

make bench-discovery
# = npm ci --prefix bench/tscg  (pinned @tscg/core, one-time)
#   + go run ./bench/cmd/bench -corpus-v2 ... -arms all -out bench/results
# → bench/results/report.json (v2) + bench/results/dashboard.html
# CI runs this same target.

Offline (deterministic, no network):

# All mandatory arms on the schema-bearing frozen corpus
go run ./bench/cmd/bench \
  -corpus-v2 specs/083-discovery-profiler/datasets/corpus_v2.tools.json \
  -arms baseline_json,compact_sig,tscg,toon_listing \
  -out bench/results

TSCG arm prerequisite (covered by make bench-discovery): npm ci --prefix bench/tscg (Node ≥20). Without it the arm reports skipped: node runtime unavailable (allowed locally, fails SC-002 in CI).

Live (response cost + break-even + latency):

# 1. Boot the snapshot proxy (same as existing bench live mode)
go build -o mcpproxy ./cmd/mcpproxy
./mcpproxy serve --config specs/065-evaluation-foundation/datasets/snapshot-servers.config.json \
  --listen 127.0.0.1:8092 &   # MCPPROXY_API_KEY=eval-corpus-snapshot

# 2. Run live measurement (real MCP retrieve_tools calls per golden query)
go run ./bench/cmd/bench -live \
  -proxy http://127.0.0.1:8092 -api-key eval-corpus-snapshot \
  -golden specs/065-evaluation-foundation/datasets/retrieval_golden_v1.json \
  -out bench/results

Public corpora:

# ToolRet (runtime fetch — never committed; license unstated upstream)
./scripts/fetch-toolret.sh                # uv-run parquet→JSON into bench/results/cache/toolret/
go run ./bench/cmd/bench -toolret bench/results/cache/toolret \
  -subset 250 -seed 42 -arms baseline_json,compact_sig -out bench/results

# LiveMCPTool (committed Apache-2.0 snapshot) — token/scale measurement
go run ./bench/cmd/bench \
  -livemcptool specs/083-discovery-profiler/datasets/livemcptool_snapshot \
  -arms baseline_json,compact_sig,tscg,toon_listing -out bench/results

Independent LAP verdict (what CI runs):

uvx --from lap-score==0.8.0 lap lint \
  --mcp-url "http://127.0.0.1:8092/mcp?apikey=eval-corpus-snapshot" --json > bench/results/lap.json

Regenerating fixtures (maintainers only):

./scripts/gen-corpus-v2.sh     # schema-bearing frozen corpus from booted snapshot proxy
# result_fixtures_v1.json: captured once from reference servers; see datasets/README

Verifying success criteria:

  • SC-003 gate: baseline arm on in-house golden set must report recall@5 = 0.68 ± 0.05.
  • SC-001: report.json .response_cost.p50/.p95, .break_even.break_even_calls populated after a live run.
  • SC-005: dashboard shows provenance badge + tokenizer caveat on every headline number.
Flags
Flag Default Meaning
-corpus specs/065-evaluation-foundation/datasets/corpus_v1.tools.json Legacy v1 corpus (name+description only); used when no profiler flag is set
-out bench/results Output directory for report.json + dashboard.html
-encoding cl100k_base tiktoken encoding name
-live off Live benchmark against a running proxy: full-schema tokens, accuracy, latency, and real-MCP response cost + break-even
-proxy http://127.0.0.1:8092 Live proxy base URL
-api-key eval-corpus-snapshot Live proxy API key (X-API-Key; the MCP session uses ?apikey=)
-golden specs/065-evaluation-foundation/datasets/retrieval_golden_v1.json Retrieval golden set (the SC-003 gate corpus)
-arms (empty) Comma-separated encoding arms, or all — enables profiler mode. toon_results is selectable here as a fixture-driven pseudo-arm
-corpus-v2 (empty) Spec 083 schema-bearing frozen corpus (corpus_v2.tools.json)
-toolret (empty) ToolRet cache dir from scripts/fetch-toolret.sh (revision dir, or its parent with exactly one revision) — runtime fetch, never committed
-livemcptool (empty) LiveMCPTool committed snapshot directory (or its tools.json)
-subset 250 Seeded ToolRet query-subset size (FR-014)
-seed 42 Seed for the deterministic query subset — same revision+seed+size ⇒ same subset
-lap-json (empty) Path to a LAP lint artifact to merge as the independent verdict
-result-fixtures specs/083-discovery-profiler/datasets/result_fixtures_v1.json Tool-result fixture set for the toon_results arm

Any of -arms / -corpus-v2 / -toolret / -livemcptool switches the CLI into profiler mode (v2 report); -live runs the live benchmark and also emits a v2 report; with none of them the legacy v1 offline run executes.

Encoding arms

An arm is one deterministic way of rendering tool definitions into agent-context text (bench/arms/; behavioral contract in specs/083-discovery-profiler/contracts/arm-interface.md). Arms are byte-deterministic (FR-010), fail explicitly instead of truncating silently (an unencodable tool is a counted skip, with examples in the report), and declare whether they alter what the retrieval index ingests — index-altering arms are scored for retrieval quality through the production Bleve index (bench/armindex.go), where the baseline arm must reproduce the golden-set recall@5 = 0.68 ± 0.05 (SC-003 gate).

Arm What it renders (one-line example) Index-altering Lower-bound
baseline_json Full definition, canonical JSON schema — fetchFetches a URL from the internet…{"properties":{"url":{…}},"required":["url"],…}. THE canonical renderer: it is also the naive-menu count, every savings denominator, and the break-even input no no
compact_sig Flat signature fetch(url:string, max_length?:int, raw?:bool, start_index?:int)|Fetches a URL from the internet… — required vs opt? params, nested objects collapse to obj, descriptions preserved verbatim yes no
tscg Reference TSCG compiler (pinned @tscg/core@1.4.3 via the committed Node shim bench/tscg/shim.mjs, JSONL protocol) — fetch: Fetches a URL… max_length (integer): Maximum number of characters… | raw (bool): …. Rewrites/elides description filler, so savings are a lower bound. Without node/node_modules the whole arm is a skip-with-reason row (never per-tool failures) yes yes
toon_listing Official TOON encoding (toon-go) of {name, description, inputSchema} per tool — - name: fetch description: "Fetches a URL…" inputSchema: properties: … yes no
tron_dedup In-tree TRON-style named-class schema dedup (no upstream Go impl): byte-identical canonical schemas declared once in the listing preamble — class Cbae0cfe3 = {…canonical schema…} — and referenced per tool as fetch|Fetches a URL…|Cbae0cfe3. Content-addressed class names, exact-bytes dedup key: deliberately conservative vs. the paper (deviations documented in bench/arms/tron.go) yes no
toon_results Pseudo-arm over tool-call outputs, not definitions: the committed result_fixtures_v1.json payloads encoded as TOON vs. a compact-JSON baseline of the same payloads, split tabular vs. non-tabular. Selectable via -arms; included automatically by -arms all when the fixture file exists n/a no

Listing-level amortization (TOON header, TRON class preamble) lives only in EncodeListing; per-tool means stay comparable across arms. Each arm row also reports skip counts with examples, the top-N heaviest tools, and the corpus's degenerate-description count (FR-020: description missing, shorter than 20 runes, or matching stub patterns) so cheap-but-useless corpora are visible.

Live response cost, break-even, and session estimates (US1)

The -live run now measures what the v1 REST-based run could not — the response an agent pays for on every discovery call:

  • Real MCP protocol. bench/mcpcall.go speaks streamable-HTTP MCP (mark3labs/mcp-go) against the proxy's /mcp endpoint and calls retrieve_tools for every golden query — capturing the exact response text an agent's context ingests (REST /api/v1/index/search misses usage_instructions, call_with, annotations, session_risk), plus client-measured latency. Provenance: measured.
  • Span-based component attribution (bench/respcost.go). Per-query tokens split into input_schemas / descriptions / usage_instructions / metadata / other. BPE is not additive across concatenation boundaries, so the response is partitioned into labeled byte spans, tokenized once, and each token attributed to the span owning its starting byte — components sum exactly to the total (FR-002), by construction. Percentiles: p50/p95/max + mean.
  • Break-even (bench/breakeven.go): break_even_calls = (naive_full_menu_tokens − proxy_menu_tokens) / mean_response_tokens, all three inputs echoed in the row so the number is recomputable. Both menus come from the same canonical renderer. Numerator ≤ 0 ⇒ explicit no_break_even row; break-even is withheld when menu token counts are non-authoritative (see the MCP-3161 safety valve below). Provenance: computed.
  • Session-cost estimator (bench/session.go) — an ESTIMATE, not a measurement: session_cost(arm, calls) = proxy_menu + calls × mean_response(arm) × (1 + retry_rate(arm)) for calls ∈ {1,3,5,10}. retry_rate defaults are literature-derived (0.0 for format-native JSON/compact/TSCG/TRON; 0.05 for toon_listing, per the parsing-cascade evidence in arXiv:2605.29676 §5) and echoed in every row. Provenance: estimated, always.
LAP independent verdict (US4)

CI runs the version-pinned external linter lap-score==0.8.0 against the booted proxy (command in the quickstart) and merges its artifact via -lap-json: the report's lap block carries LAP's letter grade and menu-token count, compared against the in-house count of the same surface. Both use cl100k_base but frame the menu differently, so divergence within ±15% is expected; beyond it the report shows a non-blocking warning (FR-016). LAP being absent or broken never fails the benchmark (FR-015): every failure path yields executed: false with an explicit skip reason.

Live run — full schemas + accuracy + latency

The live run boots mcpproxy over the Spec 065 reference-server config and measures the headline claims against a running proxy. Everything here is still deterministic and LLM-free.

# 1. Boot the reproducible substrate (proxy + 7 no-auth reference servers)
docker compose -f bench/docker-compose.yml up --build -d

# 2. Score against the running proxy (writes bench/results/live_report.json + v2 report)
go run ./bench/cmd/bench -live -proxy http://127.0.0.1:8092 -api-key eval-corpus-snapshot

What it adds over the offline token run:

  • Exact token number (full schemas). Pulls GET /api/v1/tools for the upstream tools with their full JSON input schemas and counts them against the proxy modes — whose management-tool schemas come from the same live builders as the offline run (server.ProxyModeToolDefs). Because schemas are counted on both sides, the savings is authoritative.
    • Safety valve (MCP-3161): if any proxy tool is missing a schema, counting the baseline's schemas alone would overstate savings, so the run withholds the headline % and reports raw token totals only (authoritative_headline: false) — and withholds break-even, which depends on the same menus. Never quote a withheld run.
  • Accuracy. Replays retrieval_golden_v1.json through the proxy's BM25 search (GET /api/v1/index/search) and scores Recall@{1,3,5,10}, MRR, nDCG@10, MAP against the graded labels. Deterministic (BM25), so a single run is reported (runs_averaged: 1). The emitted retrieval block conforms to the Spec 065 score-report.schema.json shape — nested metrics + gate (verified by a schema-validation test). A standalone live run has no stored baseline to regress against, so gate.passed is true by construction; CI regression-gating against a committed baseline is the MCP-3133 lane.
  • Latency. Client-measured per-query search latency (p50/p95/p99/max) vs. the one-shot cost of loading all tools. Measured client-side on purpose: the server's SearchToolsResponse.took field is currently a "0ms" stub.
  • Response cost + break-even (Spec 083 US1) — see the profiler section above. The live run emits both the v1 live_report.json and the v2 report.json/dashboard.html.
Compact arm / flip gates (Spec 085, -flip-gates)

-live -flip-gates additionally replays the golden set through the proxy's MCP retrieve_tools tool twice per query — once with detail=full, once with detail=compact (same pipeline, FR-017) — and emits the Spec 085 flip-gate metrics under flip_gates in live_report.json (FR-018):

  • per-query ranked-ID identity across modes (gate: 100%, SC-002 — any mismatch is listed and fails the gate),
  • full vs compact response-token distributions (p50/p95/max) and the median reduction (SC-001 gates on ≥50% on a real corpus),
  • the lossy-signature rate via the shared internal/toolsig grammar (gate: <20%, SC-005), with the heaviest signatures for triage,
  • describe_tool usage per completed task (informational; populated by the E2E suite, not a live run).
go run ./bench/cmd/bench -live -flip-gates -proxy http://127.0.0.1:8092 -api-key eval-corpus-snapshot

What is scoped but not yet built (follow-ups)

These require decisions and/or other roles, so they are tracked as child issues rather than landed here:

  • End-to-end task success with a pinned LLM — requires a pinned model + an LLM-call budget; this is the only part that costs spend. Until then, the session-cost estimator above is the honest substitute — and it is labeled estimated, never measured.
  • CI publish-on-release-tag → public static dashboard — Release/DevOps lane.

Dataset sources & provenance

Full details and regeneration procedures: specs/083-discovery-profiler/datasets/README.md (immutability rule: a refresh is *_v3.*, never an edit of a committed *_v2.* file).

  • Spec 065 datasets (specs/065-evaluation-foundation/datasets/): the 45-tool corpus_v1 snapshot (name+description) and retrieval_golden_v1.json, generated from 7 permissively reachable no-auth reference servers (filesystem, git, memory, sqlite, fetch, time, sequential-thinking).
  • corpus_v2.tools.json (committed): the same 45 tools exported with full JSON input schemas — the universe for all encoding-arm comparisons. Captured 2026-07-14 (corpus_v2@2026-07-14), canonicalized (sorted tool IDs, sorted object keys). Provenance note: the export reads the Bleve index (scripts/gen-corpus-v2.sh), not GET /api/v1/tools — that REST endpoint currently serves stub schemas (supervisor StateView never parses ParamsJSON); the index is the authoritative record of what the retrieval funnel ingests, which is exactly what the arm comparison must measure.
  • result_fixtures_v1.json (committed): 6 deterministic tool-call outputs captured once through the proxy from 5 reference servers for the toon_results arm — 2 tabular, 4 non-tabular; wall-clock values and capture-host specifics stripped. License-clean: outputs of our own reference servers over our own seed data (fetch_fixture.html is the committed fetch input).
  • LiveMCPTool snapshot (specs/083-discovery-profiler/datasets/livemcptool_snapshot/, committed): frozen, normalized copy of the LiveMCPBench tool corpus — 70 servers / 527 tools, pinned Hugging Face revision ddea2d24 (ICIP/LiveMCPBench, arXiv:2508.01780). Apache-2.0, redistributed with the mandatory attribution in the snapshot's ATTRIBUTION.md. Used for token/scale measurement only: its task annotations name tools as unqualified free text (5/150 names resolve to no corpus tool, 13/150 to multiple servers), so relevance labels are not derivable — the loader records that absence explicitly rather than guessing (FR-011).
  • ToolRet (never committed): the upstream mangopy/ToolRet-Tools + ToolRet-Queries datasets carry no stated license, so redistribution is not clearly permitted — the harness fetches at runtime only (scripts/fetch-toolret.sh, pinned revision, uv run parquet→JSON) into the gitignored bench/results/cache/ and never writes ToolRet bytes into git (FR-013). Retrieval scoring runs on a seeded query subset (-subset/-seed): same revision + seed + size ⇒ byte-identical subset on every machine. Attribution: ToolRet tool-retrieval benchmark (Hugging Face; ACL 2025).
  • Proxy + management tool definitions: derived at run time from the live server tool builders (internal/server/mcp_routing.gobuildCallToolModeTools / buildCodeExecModeTools, exposed via internal/server.ProxyModeToolDefs). No hand-maintained fixture — the benchmark cannot drift from the tools the proxy actually serves.
  • TSCG shim (bench/tscg/): committed shim.mjs + package-lock.json pinning @tscg/core@1.4.3; npm ci --prefix bench/tscg (Node ≥20) reproduces the exact runtime everywhere, including CI.

Known limitations (read before quoting a number)

  • Tokenizer caveat — absolute numbers are estimates. All token counts use tiktoken cl100k_base as a reproducible, model-agnostic, offline estimator. It can underestimate other tokenizers (e.g. Claude's) by up to ~60%, so never quote an absolute token count as a Claude cost — but relative savings between arms and modes are stable across vocabularies, and those are the headline numbers. The v2 dashboard renders this caveat next to every absolute number (SC-005).
  • The session estimator is an ESTIMATE, not a measurement. The multi-turn session cost is a model — proxy_menu + calls × mean_response × (1 + retry_rate) — whose retry rates are literature-derived defaults, not observations of a live agent loop. Every estimator row is provenance-labeled estimated and echoes its inputs; treat it as a decision aid, not a benchmark result. A measured agent loop is the pinned-LLM follow-up above.
  • Lower-bound arms. Arms that rewrite or elide description text (TSCG) report savings as a lower bound: what they drop cannot be priced back in.
  • v1 offline run: schemas excluded — direction is not clean. In the legacy name+description-only run, input schemas are dropped from both sides, so that number is its own well-defined metric, not unambiguously conservative. The live run adds full schemas for the exact headline number; the Spec 083 arms measure schema-bearing renderings on corpus_v2. Quote those, not the v1 offline estimate.
  • Savings scale with tool count. The 45-tool reference corpus is small; the 527-tool LiveMCPTool snapshot and the ToolRet subset exist precisely to test scale. Quote the corpus size alongside any percentage.
  • LiveMCPTool has no relevance labels (see provenance above): it validates token/scale claims only, never retrieval quality.
  • LAP divergence is expected. LAP frames the tool menu its own way; within ±15% of the in-house count is normal, beyond it is a non-blocking warning — never a hidden discrepancy, never a build failure.

Reproducible live run

docker-compose.yml boots mcpproxy over the frozen reference-server config so the corpus and live tool list are reproducible across machines. The live accuracy/latency/full-schema/response-cost scorers attach to it via -live (see "Live run" above). Pin the upstream-server images before publishing headline numbers (image drift can change the tool corpus).

Reviewer contact

Methodology questions / disputes: open an issue in smart-mcp-proxy/mcpproxy-go and tag the maintainers, or comment on the roadmap benchmark ticket (MCP-42).

Documentation

Overview

armindex.go — temp-dir retrieval index for arm-aware quality scoring (Spec 083 US2, FR-008, research D7).

For index-altering arms the profiler must answer "does this encoding move recall?", which is only meaningful if the index under test IS the production index: same field mappings, same analyzers, same six-branch SearchTools query shape. So the builder funnels through the production internal/index.Manager (BatchIndexTools → BleveIndex.BatchIndex) and the SearchFunc calls the production search path (Manager.SearchTools → BleveIndex.SearchTools) — nothing here re-implements the mapping or the query. The SC-003 parity test (bench/armindex_test.go) proves the baseline arm through this funnel reproduces the recorded golden-set baseline (recall@5 = 0.68 ± 0.05) before any other arm's score is trusted.

armrun.go — the encoding-arm runner (Spec 083 US2, T024): orchestrates arms over one corpus and assembles the ArmResult report rows (FR-005/006/007/ 008/009/020).

Per arm it measures: total tokens over the amortized whole-corpus listing (EncodeListing, so shared preambles/dictionaries are paid once — contract rule 6), per-tool mean and p95 from EncodeTool (comparable across arms), savings % against the baseline_json arm's listing total (the single denominator, research D7b), per-tool skip counting with capped examples (FR-009), the heaviest-tools top-N (FR-020), and — for index-altering arms when a golden set is supplied — retrieval quality through the production index funnel (armindex.go, FR-008).

Determinism (FR-010): results follow the caller's arm order, per-tool iteration is corpus order, skip examples are the first failures in corpus order, and heaviest-tools ties break on tool_id — no map iteration anywhere.

breakeven.go — break-even analysis for proxy-mediated tool discovery (Spec 083 US1, FR-003/FR-004, research D8).

LAP independent verdict (Spec 083 US4, FR-015/016, SC-006).

CI runs the version-pinned external linter against the booted proxy:

uvx --from 'lap-score[mcp]==0.8.0' lap lint --mcp-url "$PROXY/mcp" --json > lap.json

and this file turns that artifact into the report's LapVerdict: parse the JSON, extract LAP's menu-token count (bucket A) and letter grade, and compare against the in-house count for the same surface. LAP being absent or broken must never fail the benchmark (FR-015): every failure path yields Executed=false with a skip reason instead of an error.

mcpcall.go — real MCP retrieve_tools invocation (Spec 083 US1, FR-001, research D1).

The REST /api/v1/index/search endpoint used by live.go is NOT the payload an agent pays for: it misses usage_instructions, call_with, annotations, and session_risk. This file speaks the actual MCP protocol (streamable-http, mark3labs/mcp-go client) against the proxy's /mcp endpoint and returns the raw text content of each retrieve_tools call — the exact bytes an agent's context ingests — plus the client-measured latency (FR-023; the server-side timing fields are stubs, same rationale as LiveClient.Search).

offline_report.go — assembly of the versioned v2 report for offline profiler runs (Spec 083, research D12): encoding-arm rows over one or more frozen corpora, corpus descriptors with license/attribution provenance, and provenance labels for every headline number (SC-005). The same assembly path serves the bench CLI and the end-to-end report-validation test (bench/reportv2_e2e_test.go), so what CI validates is what the CLI ships.

respcost.go — span-based component attribution for retrieve_tools responses (Spec 083 US1, FR-001/FR-002, research D7b).

The FR-002 invariant — component token counts sum EXACTLY to the response total — is mathematically unsatisfiable if each field is tokenized on its own, because BPE is not additive across concatenation boundaries. Instead the response text is partitioned into contiguous labeled byte spans, the WHOLE text is tokenized once, and each token is attributed to the span that contains its starting byte. The sum then equals the total by construction.

Package bench is the mcpproxy benchmark harness (roadmap #19 / MCP-42).

It produces the reproducible numbers behind mcpproxy's marketing claims — token reduction, discovery accuracy, and latency — by comparing three ways an agent can be wired to upstream MCP tools:

  • baseline: every upstream tool definition is loaded directly into the agent's context (no proxy discovery).
  • retrieve_tools: only mcpproxy's discovery + call_tool variants occupy the context; tools are found on demand via BM25 search.
  • code_execution: only code_execution + retrieve_tools occupy the context; the agent orchestrates many tools from sandboxed JS in one round-trip.

The token-reduction measurement in this file is fully deterministic and offline: it counts the context cost of each mode over a frozen tool corpus using the tiktoken cl100k_base encoding (a reproducible, model-agnostic estimator). It reuses the Spec 065 frozen corpus (specs/065-evaluation-foundation/datasets/corpus_v1.tools.json) as its tool universe so the benchmark scores a versioned, non-drifting snapshot (CN-002).

Methodology, limitations, and the live (docker-compose) run that adds full JSON input schemas and end-to-end accuracy/latency are documented in bench/README.md.

Index

Constants

View Source
const (
	ProvenanceMeasured  = "measured"
	ProvenanceComputed  = "computed"
	ProvenanceEstimated = "estimated"
)

Provenance labels for headline metrics (SC-005): a number is either measured (observed over the real protocol), computed (derived arithmetic over measured inputs), or estimated (model with documented assumptions).

View Source
const (
	// ComponentInputSchemas is each result's "inputSchema" pair — the raw JSON
	// input schema, including nested property descriptions (schema cost, not
	// tool-description cost).
	ComponentInputSchemas = "input_schemas"
	// ComponentDescriptions is each result's tool-level "description" pair.
	ComponentDescriptions = "descriptions"
	// ComponentUsageInstructions is the top-level "usage_instructions" pair.
	ComponentUsageInstructions = "usage_instructions"
	// ComponentMetadata is every other pair inside a result object: name,
	// score, server, call_with, annotations, usage stats.
	ComponentMetadata = "metadata"
	// ComponentOther is the response envelope: query/total/session_risk/notice
	// and the structural braces, brackets, and separators between spans.
	ComponentOther = "other"
)

Component labels of a retrieve_tools response (data-model DiscoveryResponseMeasurement.Components). Every token of the response text lands in exactly one of these buckets.

View Source
const (
	ModeBaseline      = "baseline"
	ModeRetrieveTools = "retrieve_tools"
	ModeCodeExecution = "code_execution"
)

Routing modes the benchmark compares. The mode names mirror the mcpproxy MCP servers in internal/server/mcp.go (codeExecServer, callToolServer).

View Source
const DefaultEncoding = "cl100k_base"

DefaultEncoding is the tiktoken encoding used for token estimation. cl100k_base is a widely-used, reproducible BPE; exact counts for a specific pinned model (e.g. Claude) will differ, but the *relative* savings between modes are stable.

View Source
const DefaultHeaviestN = 10

DefaultHeaviestN is the FR-020 default size of the heaviest-tools list.

View Source
const LapDivergenceTolerancePct = 15.0

LapDivergenceTolerancePct is the documented tolerance (FR-016) between LAP's menu-token count and the in-house count of the same surface. Both use cl100k_base, but LAP frames the menu its own way, so small divergence is expected; beyond ±15% the report shows a (non-blocking) warning.

View Source
const LapPinnedVersion = "0.8.0"

LapPinnedVersion is the lap-score release the CI step installs (research D9). The lint artifact itself carries no version field, so the pin is recorded here and stamped into the verdict on successful parse.

View Source
const ReportVersion2 = 2

ReportVersion2 is the report_version const of the v2 envelope.

View Source
const SessionEstimateProvenance = ProvenanceEstimated

SessionEstimateProvenance is the provenance label the report layer attaches to session-estimate rows (SC-005): always "estimated", never "measured" — the estimator is a model with documented assumptions, not an observation.

View Source
const TokenizerCaveatText = "" /* 283-byte string literal not displayed */

TokenizerCaveatText is the mandatory accuracy caveat rendered wherever absolute token numbers appear (research D11, SC-005).

Variables

This section is empty.

Functions

func AttributeTokens

func AttributeTokens(tk *Tokenizer, text string, spans []Span) (int, map[string]int, error)

AttributeTokens tokenizes the WHOLE text once and attributes each token to the span containing its starting byte (research D7b). It returns the total token count and the per-label component counts; all five canonical components are always present in the map. Invariant, by construction: sum(components) == total.

Token byte offsets are recovered by decoding each token id individually — cl100k is a byte-level BPE, so per-token decoding is lossless and the concatenated pieces reproduce the input bytes exactly (verified at runtime).

func AveragePrecision

func AveragePrecision(ranked []string, labels []Label) float64

AveragePrecision is the mean of the precision values computed at each rank where a relevant tool is retrieved, divided by the total number of relevant tools (so unretrieved relevant tools lower the score). Binary relevance (relevance >= 1) is used, matching the standard MAP definition.

func CanonicalJSON

func CanonicalJSON(raw json.RawMessage) (string, error)

CanonicalJSON re-encodes raw JSON bytes into the canonical form used everywhere in the bench (research D7b): object keys sorted lexicographically at every depth, array order preserved, number literals preserved verbatim (via json.Number — no float round-trip), compact (no insignificant whitespace), and no HTML escaping. Identical JSON content in any key order canonicalizes to identical bytes (FR-010).

It lives in the bench package (not bench/arms) because canonicalization happens at every schema INGESTION boundary — LoadCorpusV2, the live /api/v1/tools fetch, and the corpusio loaders — so the plain Tokenizer.CountToolWithSchema and the arm renderers always count the same canonical bytes (contract parity). bench/arms re-exports it for the arm encoders (arms imports bench, never the reverse).

func DefaultCallsPerSession

func DefaultCallsPerSession() []int

DefaultCallsPerSession returns the default calls-per-session grid {1,3,5,10} (research D8) as a fresh slice, so callers cannot mutate the shared default.

func DefaultStubPatterns

func DefaultStubPatterns() []string

DefaultStubPatterns returns the FR-020 default stub-description pattern list as a fresh slice (callers may append their own patterns).

func GeneratedAtNow

func GeneratedAtNow() string

GeneratedAtNow returns the RFC 3339 UTC timestamp the CLI stamps into GeneratedAt. It lives at the CLI boundary on purpose: no library encoder below this call reads the wall clock (FR-010), and tests pass fixed values.

func InHouseProxyMenuTokens

func InHouseProxyMenuTokens(tk *Tokenizer) int

InHouseProxyMenuTokens is the in-house count of the proxy's retrieve_tools menu surface (management tools WITH schemas) — the same surface LAP lints, used as the divergence comparison base (FR-016) in offline runs where no live menu was measured.

func InvokeRetrieveTools

func InvokeRetrieveTools(ctx context.Context, baseURL, apiKey, query string) (string, time.Duration, error)

InvokeRetrieveTools is the one-shot form: open a session, perform a single retrieve_tools call with the proxy's default limit, and close. The live harness prefers one MCPCaller across the golden set; this entry point serves smoke tests and single-query probes.

func NDCGAtK

func NDCGAtK(ranked []string, labels []Label, k int) float64

NDCGAtK is the normalized discounted cumulative gain at k using the graded relevance as the gain (linear gain, log2 position discount). 1.0 means the ranking is in ideal (relevance-descending) order; 0 means no gain in top-k.

func RecallAtK

func RecallAtK(ranked []string, labels []Label, k int) float64

RecallAtK is the fraction of the query's relevant tools (relevance >= 1) that appear in the top-k of the ranking. Returns 0 when there are no relevant tools (a degenerate query that should not be scored).

func ReciprocalRank

func ReciprocalRank(ranked []string, labels []Label) float64

ReciprocalRank is 1/rank of the first relevant tool in the ranking, or 0 if none of the ranked tools are relevant.

func RequireEOF

func RequireEOF(dec *json.Decoder) error

RequireEOF verifies dec has no data left after the first decoded top-level value. dec.More() is NOT a valid trailing-data check at the top level: it merely peeks for a byte other than ']' or '}', so streams like `{}}` or `{}]` slip through (and `{} {}` is a second value, not "more of" the first). Draining one more token and requiring io.EOF rejects them all.

func RetryRateForArm

func RetryRateForArm(arm string) float64

RetryRateForArm returns the documented retry-rate default for an arm. Unknown arms default to 0.0 (no retry evidence → no penalty; the rate is echoed in every estimate row, so the assumption stays visible).

Types

type ArmIndex

type ArmIndex struct {
	// contains filtered or unexported fields
}

ArmIndex is a scratch-directory retrieval index built through the production index code for one arm's encoding of one corpus. Close releases the underlying Bleve index; the scratch directory itself is owned by the caller (tests pass t.TempDir()).

func BuildArmIndex

func BuildArmIndex(dir string, metas []*config.ToolMetadata) (*ArmIndex, error)

BuildArmIndex creates a fresh production index.Manager rooted at dir (the index lands in dir/index.bleve) and batch-indexes the given per-tool metadata — the output of an arm's EncodeIndexMetadata over a corpus. dir must be an empty scratch directory: an existing index there would be silently reused and contaminate the measurement.

func (*ArmIndex) Close

func (ai *ArmIndex) Close() error

Close releases the underlying index.

func (*ArmIndex) SearchFunc

func (ai *ArmIndex) SearchFunc() SearchFunc

SearchFunc returns the ScoreRetrieval adapter over the production search path: each query runs through Manager.SearchTools (the exact BM25 query shape retrieve_tools uses) and the hits are returned as ranked "server:tool" IDs — the golden set's tool_id format, matching how the live REST path assembles IDs (LiveClient.Search).

type ArmResult

type ArmResult struct {
	Arm                  string           `json:"arm"`
	CorpusID             string           `json:"corpus_id"`
	Skipped              bool             `json:"skipped"`
	SkipReason           string           `json:"skip_reason,omitempty"`
	LowerBound           bool             `json:"lower_bound,omitempty"`
	IndexAltering        bool             `json:"index_altering"`
	PayloadClass         string           `json:"payload_class,omitempty"` // "listing" | "results"
	FixtureID            string           `json:"fixture_id,omitempty"`
	TabularCount         *int             `json:"tabular_count,omitempty"`
	NonTabularCount      *int             `json:"non_tabular_count,omitempty"`
	TotalTokens          int              `json:"total_tokens"`
	MeanTokens           float64          `json:"mean_tokens"`
	P95Tokens            int              `json:"p95_tokens"`
	SavingsVsBaselinePct float64          `json:"savings_vs_baseline_pct"`
	SkippedTools         int              `json:"skipped_tools"`
	SkipExamples         []SkipExample    `json:"skip_examples,omitempty"`
	HeaviestTools        []ToolTokenEntry `json:"heaviest_tools,omitempty"`
	Quality              *RetrievalScore  `json:"quality"`
}

ArmResult is one encoding arm measured on one corpus (FR-005/006/007/009).

Contract conditionals: a skipped row requires SkipReason; a non-skipped row requires the token stats; a results-class row requires FixtureID and the tabular/non-tabular split (pointers so an explicit 0 still serializes); a non-skipped index-altering row requires the quality key (nullable only when the corpus has no relevance labels, with MetricNote explaining the absence).

func RunArms

func RunArms(tk *Tokenizer, corpus *Corpus, armsToRun []EncodingArm, opts ArmRunOptions) ([]ArmResult, error)

RunArms measures every given arm on the corpus and returns one ArmResult per arm, in caller order. The baseline_json arm MUST be among armsToRun — its listing total is the savings denominator (research D7b). Arms whose runtime is absent never reach this function: the harness records them via SkippedArmResult after registry resolution fails (contract rule 5).

func SkippedArmResult

func SkippedArmResult(armName, corpusID, reason string) ArmResult

SkippedArmResult is the arm-level skip row (FR-006, contract rule 5): the arm's external runtime is absent, so no tool was processed and only the identity + reason fields are meaningful.

type ArmRunOptions

type ArmRunOptions struct {
	// CorpusID identifies the corpus in report rows (e.g. "corpus_v2@<sha>");
	// empty defaults to the corpus's own Version.
	CorpusID string
	// Golden enables retrieval-quality scoring for index-altering arms. nil
	// means the corpus has no relevance labels: index-altering rows then carry
	// a quality block with only an explanatory MetricNote (FR-008/011).
	Golden *GoldenSet
	// HeaviestN sizes the heaviest-tools list; <=0 defaults to DefaultHeaviestN.
	HeaviestN int
	// IndexDir is the scratch parent directory for per-arm retrieval indexes.
	// Empty creates (and removes) a temp directory per run.
	IndexDir string
}

ArmRunOptions configures one RunArms invocation.

type BreakEvenAnalysis

type BreakEvenAnalysis struct {
	NaiveFullMenuTokens int     `json:"naive_full_menu_tokens"`
	ProxyMenuTokens     int     `json:"proxy_menu_tokens"`
	MeanResponseTokens  float64 `json:"mean_response_tokens"`
	BreakEvenCalls      float64 `json:"break_even_calls"`
	NoBreakEven         bool    `json:"no_break_even,omitempty"`
}

BreakEvenAnalysis is the FR-003 break-even computation with its inputs echoed (FR-004): break_even_calls = (naive − proxy_menu) / mean_response.

func ComputeBreakEven

func ComputeBreakEven(naiveFullMenuTokens, proxyMenuTokens int, meanResponseTokens float64) (*BreakEvenAnalysis, error)

ComputeBreakEven computes the number of retrieve_tools calls at which the proxy's cumulative context cost (proxy menu + N discovery responses) equals the naive full-menu cost:

break_even_calls = (naive_full_menu_tokens − proxy_menu_tokens) / mean_response_tokens

Below break_even_calls discovery calls the proxy is strictly cheaper; each call beyond it spends part of the menu savings. All three inputs are echoed in the result (FR-004) so a reader can recompute the number from the row alone. Both menu inputs MUST come from the same canonical full-definition renderer (research D7b) or the numerator compares different token shapes.

When the numerator is ≤ 0 the proxy menu is already at least as expensive as the naive menu — there are no savings to amortize — and the result is a NoBreakEven row (BreakEvenCalls stays 0), whatever the response cost. Otherwise meanResponseTokens must be positive: a zero mean means no responses were measured, and dividing by it is undefined, so that is an error rather than a fabricated verdict.

type Corpus

type Corpus struct {
	Version string `json:"version"`
	Tools   []Tool `json:"tools"`
}

Corpus is a frozen, versioned set of tool definitions.

func LoadCorpus

func LoadCorpus(path string) (*Corpus, error)

LoadCorpus reads a frozen corpus snapshot (e.g. the Spec 065 corpus_v1.tools.json) from disk.

func LoadCorpusV2

func LoadCorpusV2(path string) (*Corpus, error)

LoadCorpusV2 reads the Spec 083 schema-bearing frozen corpus (specs/083-discovery-profiler/datasets/corpus_v2.tools.json). On top of LoadCorpus it enforces the corpus_v2 contract: every tool MUST carry a non-empty JSON input schema (arm comparison over a schema-less corpus is meaningless — research D4), and each schema is CANONICALIZED at load (CanonicalJSON: sorted keys, compact, verbatim numbers) so the in-memory bytes match what the arm renderers produce — CountToolWithSchema and the baseline arm count identical bytes even for a non-canonical input file (the committed file is pretty-printed for git diffability; its keys are already sorted by the generator's jq -S, so this is a no-op for it).

type CorpusDescriptor

type CorpusDescriptor struct {
	ID                     string                  `json:"id"`
	Name                   string                  `json:"name"`
	Version                string                  `json:"version"`
	ToolCount              int                     `json:"tool_count"`
	License                string                  `json:"license"`
	Attribution            string                  `json:"attribution,omitempty"`
	Committed              bool                    `json:"committed"`
	DegenerateDescriptions *DegenerateDescriptions `json:"degenerate_descriptions,omitempty"`
}

CorpusDescriptor identifies one measured corpus with license/attribution provenance (FR-011/012/013).

type DegenerateDescriptions

type DegenerateDescriptions struct {
	Count int      `json:"count"`
	Rules []string `json:"rules,omitempty"`
}

DegenerateDescriptions counts corpus tools whose descriptions trip the FR-020 quality rules, with the rule list echoed for reproducibility.

func CountDegenerateDescriptions

func CountDegenerateDescriptions(tools []Tool, stubPatterns []string) (*DegenerateDescriptions, error)

CountDegenerateDescriptions counts corpus tools whose descriptions trip any FR-020 rule — empty/whitespace-only, shorter than 20 characters, or matching a stub pattern — each tool counted once, with the applied rule list echoed for reproducibility. An invalid pattern is an explicit error.

type DescribeToolUsage

type DescribeToolUsage struct {
	Calls          int     `json:"calls"`
	CompletedTasks int     `json:"completed_tasks"`
	PerTask        float64 `json:"per_task"`
}

DescribeToolUsage is the informational FR-018 metric: describe_tool calls per completed task, collected by the E2E suite (not by a live bench run).

type DiscoveryResponseMeasurement

type DiscoveryResponseMeasurement struct {
	QueryID     string         `json:"query_id"`
	TotalTokens int            `json:"total_tokens"`
	ResultCount int            `json:"result_count,omitempty"`
	LatencyMs   float64        `json:"latency_ms,omitempty"`
	Components  map[string]int `json:"components"`
}

DiscoveryResponseMeasurement is one golden query's retrieve_tools response cost with its span-attributed component breakdown (FR-001/002). Invariant: the component values sum EXACTLY to TotalTokens — enforced by construction in the span attributor (bench/respcost.go), never by re-tokenizing fields.

func MeasureRetrieveToolsResponse

func MeasureRetrieveToolsResponse(tk *Tokenizer, queryID, raw string, latencyMs float64) (*DiscoveryResponseMeasurement, error)

MeasureRetrieveToolsResponse is the one-call US1 measurement: partition the raw response text, attribute tokens, and assemble the report row. queryID and latencyMs are caller-observed (the harness measures latency around the MCP call, FR-023).

type EncodingArm

type EncodingArm interface {
	Name() string
	IndexAltering() bool
	LowerBound() bool
	EncodeTool(t Tool) (string, error)
	EncodeListing(ts []Tool) (string, error)
	EncodeIndexMetadata(t Tool) (config.ToolMetadata, error)
}

EncodingArm is the bench-side mirror of the arms.Arm contract (specs/083-discovery-profiler/contracts/arm-interface.md). It is declared structurally here because bench cannot import bench/arms (arms imports bench); every arms.Arm value satisfies it as-is.

type FlipGateReport

type FlipGateReport struct {
	Encoding       string                `json:"encoding"`
	RankedIdentity *RankedIdentityGate   `json:"ranked_identity"`
	Tokens         *TokenReductionReport `json:"tokens"`
	Lossy          *LossySignatureGate   `json:"lossy,omitempty"`
	DescribeTool   *DescribeToolUsage    `json:"describe_tool_usage,omitempty"`
	Notes          []string              `json:"notes,omitempty"`
}

FlipGateReport bundles every FR-018 flip-gate metric.

func RunFlipGates

func RunFlipGates(retrieve RetrieveToolsFunc, golden *GoldenSet, tk *Tokenizer, limit int) (*FlipGateReport, error)

RunFlipGates replays every golden query through retrieve_tools twice — once per detail mode, same pipeline (FR-017) — and computes the ranked-ID identity gate plus the full/compact token distributions. The lossy gate is computed separately (ComputeLossyGate) because it needs a schema-bearing corpus, which a golden set does not carry.

func RunLiveFlipGates

func RunLiveFlipGates(ctx context.Context, mcpURL string, golden *GoldenSet, upstream []Tool, corpusVersion string, tk *Tokenizer, limit int) (*FlipGateReport, error)

RunLiveFlipGates is the live compact arm (FR-017): it connects an MCP client to a running proxy, replays the golden set through retrieve_tools in both detail modes, and computes the full flip-gate report. The lossy gate runs over the live upstream tool corpus (with schemas from GET /api/v1/tools); the frozen corpus_v2 re-baseline follows the 083 rebase (tasks T040/T043).

type GoldenQuery

type GoldenQuery struct {
	ID     string  `json:"id"`
	Query  string  `json:"query"`
	Labels []Label `json:"labels"`
}

GoldenQuery is one labelled query -> relevant-tool(s) judgement.

type GoldenSet

type GoldenSet struct {
	CorpusVersion string        `json:"corpus_version"`
	Queries       []GoldenQuery `json:"queries"`
}

GoldenSet is the frozen Spec 065 retrieval golden set (retrieval_golden_v1.json).

func LoadGoldenSet

func LoadGoldenSet(path string) (*GoldenSet, error)

LoadGoldenSet reads the Spec 065 retrieval golden set (retrieval_golden_v1.json) from disk.

type HeaviestSignature

type HeaviestSignature struct {
	ToolID string `json:"tool_id"`
	Tokens int    `json:"tokens"`
	Sig    string `json:"sig"`
	Lossy  bool   `json:"lossy"`
}

HeaviestSignature is one corpus signature ranked by token cost, for triage of what compact mode still spends tokens on.

type IndexMetadataFunc

type IndexMetadataFunc func(t Tool) (config.ToolMetadata, error)

IndexMetadataFunc maps one corpus tool to the exact metadata the production retrieval index ingests for an arm. It is the bench-side shape of arms.Arm.EncodeIndexMetadata (bench cannot import bench/arms — arms imports bench — so the mapping crosses the boundary as a function value).

type Label

type Label struct {
	ToolID    string `json:"tool_id"`
	Relevance int    `json:"relevance"`
}

Label is a graded relevance judgement for one tool against one query, taken from the Spec 065 retrieval golden set (relevance 2 = primary, 1 = related, 0 / absent = irrelevant).

type LapVerdict

type LapVerdict struct {
	Executed          bool    `json:"executed"`
	SkipReason        string  `json:"skip_reason,omitempty"`
	Version           string  `json:"version,omitempty"`
	MenuTokens        int     `json:"menu_tokens,omitempty"`
	InHouseMenuTokens int     `json:"in_house_menu_tokens,omitempty"`
	DivergencePct     float64 `json:"divergence_pct,omitempty"`
	Grade             string  `json:"grade,omitempty"`
	ArtifactPath      string  `json:"artifact_path,omitempty"`
}

LapVerdict is the pinned independent LAP run (FR-015/016, SC-006).

func ParseLapJSON

func ParseLapJSON(path string) LapVerdict

ParseLapJSON reads a LAP lint artifact from disk and returns the verdict. It never returns an error: a missing, unreadable, corrupt, or menu-token-free artifact yields Executed=false with a SkipReason (SC-006 requires an explicit skip reason over a silent absence).

func (*LapVerdict) Compare

func (v *LapVerdict) Compare(inHouseMenuTokens int) (divergencePct float64, warn bool)

Compare records the in-house menu-token count for the same proxy surface on the verdict and computes the divergence of LAP's count from it, in percent (positive = LAP counted more). It returns the divergence and whether it exceeds LapDivergenceTolerancePct (FR-016 warning; non-blocking). When the verdict was not executed or the in-house count is not positive there is nothing to compare: divergence is 0 and no warning is raised.

type LatencyAggregate

type LatencyAggregate struct {
	P50Ms float64 `json:"p50_ms"`
	P95Ms float64 `json:"p95_ms"`
	P99Ms float64 `json:"p99_ms"`
	MaxMs float64 `json:"max_ms"`
}

LatencyAggregate is one nearest-rank percentile summary of client-measured latencies (FR-023) for a single measured surface.

type LatencyReport

type LatencyReport struct {
	Samples        int     `json:"samples"`
	P50ms          float64 `json:"p50_ms"`
	P95ms          float64 `json:"p95_ms"`
	P99ms          float64 `json:"p99_ms"`
	MaxMs          float64 `json:"max_ms"`
	LoadAllToolsMs float64 `json:"load_all_tools_ms"`
}

LatencyReport summarizes the REST /api/v1/index/search (BM25 search endpoint) round-trips replayed for retrieval scoring, versus the fixed one-shot cost of loading every tool. Times are client-measured (milliseconds); the server's SearchToolsResponse "took" field is a "0ms" stub. NOTE: this is NOT the MCP retrieve_tools discovery latency — that is aggregated separately in LiveReport.MCPDiscoveryLatency (FR-023).

type LatencyV2

type LatencyV2 struct {
	P50Ms float64 `json:"p50_ms"`
	P95Ms float64 `json:"p95_ms"`
	P99Ms float64 `json:"p99_ms"`
	MaxMs float64 `json:"max_ms"`
	// RESTSearch labels the flat fields' surface explicitly:
	// GET /api/v1/index/search round-trips.
	RESTSearch *LatencyAggregate `json:"rest_search,omitempty"`
	// MCPDiscovery aggregates the MCP retrieve_tools call latencies from the
	// per-query DiscoveryResponseMeasurement rows.
	MCPDiscovery *LatencyAggregate `json:"mcp_discovery,omitempty"`
}

LatencyV2 is the client-measured latency block (FR-023). Two DIFFERENT surfaces are measured and must never be conflated: the REST /api/v1/index/search calls used for retrieval scoring, and the MCP retrieve_tools calls the discovery-response rows time. The flat fields summarize the REST search calls (kept additive for existing consumers and mirrored in RESTSearch); MCPDiscovery, when present, is the retrieve_tools aggregate over the real MCP protocol.

type LiveClient

type LiveClient struct {
	BaseURL string
	APIKey  string
	HTTP    *http.Client
}

LiveClient talks to a running mcpproxy instance (e.g. the bench docker-compose substrate on 127.0.0.1:8092) over its REST API. It is used by the live benchmark run to pull the exact tool definitions (with schemas) and to replay the retrieval golden set through the proxy's BM25 search.

func NewLiveClient

func NewLiveClient(baseURL, apiKey string) *LiveClient

NewLiveClient builds a LiveClient for baseURL (e.g. "http://127.0.0.1:8092") authenticating with apiKey via the X-API-Key header.

func (*LiveClient) FetchUpstreamTools

func (c *LiveClient) FetchUpstreamTools(ctx context.Context) ([]Tool, error)

FetchUpstreamTools pulls the consolidated tool list (GET /api/v1/tools) and returns every upstream tool with its full JSON input schema, ready to feed into schema-aware token counting for the baseline.

func (*LiveClient) Search

func (c *LiveClient) Search(ctx context.Context, query string, limit int) (ranked []string, latency time.Duration, err error)

Search replays one query through the proxy's BM25 tool search (GET /api/v1/index/search) and returns the ranked tool IDs (server:tool, best first) plus the client-measured round-trip latency.

Latency is measured client-side on purpose: the server's SearchToolsResponse "took" field is currently a hardcoded "0ms" stub (internal/httpapi handleSearchTools), so it cannot be trusted as the proxy-side timing.

type LiveModeResult

type LiveModeResult struct {
	Mode         string  `json:"mode"`
	ContextTools int     `json:"context_tools"`
	Tokens       int     `json:"tokens"`
	SavingsRatio float64 `json:"savings_vs_baseline,omitempty"`
}

LiveModeResult is the per-mode context-token cost from the live run.

type LiveReport

type LiveReport struct {
	Proxy     string            `json:"proxy"`
	Encoding  string            `json:"encoding"`
	Tokens    *LiveTokenReport  `json:"tokens"`
	Retrieval *RetrievalMetrics `json:"retrieval"`
	Latency   *LatencyReport    `json:"latency"`

	// ProxyInfo records the measured proxy's identity and discovery
	// configuration (FR-021): version, tool count, tools_limit, routing_mode.
	// Collected best-effort from /api/v1/info, /api/v1/config, /api/v1/status;
	// fields the proxy does not expose stay zero-valued.
	ProxyInfo *ProxyInfo `json:"proxy_info,omitempty"`
	// ResponseCost is the per-golden-query retrieve_tools response-cost
	// summary over the real MCP protocol (FR-001). nil when the measurement
	// was skipped — see ResponseCostNote.
	ResponseCost *ResponseCostSummary `json:"response_cost,omitempty"`
	// BreakEven is the FR-003 analysis over the SAME token report as the
	// headline (naive full menu vs proxy menu, both schema-counted). nil when
	// responses were not measured or the menu counts are non-authoritative.
	BreakEven *BreakEvenAnalysis `json:"break_even,omitempty"`
	// SessionEstimates are the FR-019 estimator rows for the measured live
	// encoding (baseline_json — the proxy's own full-JSON responses).
	SessionEstimates []SessionCostEstimate `json:"session_estimates,omitempty"`
	// MCPDiscoveryLatency aggregates the MCP retrieve_tools call latencies
	// from the per-query response-cost rows (FR-023). It is a DIFFERENT
	// surface than Latency, which times REST /api/v1/index/search calls —
	// the two are never conflated. nil when responses were not measured.
	MCPDiscoveryLatency *LatencyAggregate `json:"mcp_discovery_latency,omitempty"`
	// ResponseCostNote explains a skipped or degraded response-cost /
	// break-even measurement (never silent, SC-006 spirit). Empty on success.
	ResponseCostNote string `json:"response_cost_note,omitempty"`
	// FlipGates (Spec 085 FR-017/FR-018) is populated only when the live run
	// was invoked with the compact arm enabled (-flip-gates).
	FlipGates *FlipGateReport `json:"flip_gates,omitempty"`
}

LiveReport is the full live benchmark result: exact-token comparison, retrieval accuracy, and search latency, all gathered from one running proxy — plus, since Spec 083 (T016), the retrieve_tools RESPONSE cost measured over the real MCP protocol (per-golden-query rows with span-based component attribution, FR-001/002), the break-even analysis (FR-003/004), the FR-021 proxy identity fields, and the session-cost estimate rows for the measured live encoding. FlipGates (Spec 085 FR-017/FR-018) is populated only when the live run was invoked with the compact arm enabled (-flip-gates).

func RunLive

func RunLive(ctx context.Context, client *LiveClient, golden *GoldenSet, opts LiveRunOptions) (*LiveReport, error)

RunLive gathers the full live benchmark from a running proxy: it pulls the exact tool definitions (with schemas) for the token comparison, replays the golden set through the proxy's BM25 search for accuracy, and records the per-query search latency.

func (*LiveReport) ToReportV2

func (r *LiveReport) ToReportV2(generatedAt string) *ReportV2

ToReportV2 projects a live run into the versioned v2 report envelope (research D12): the live proxy toolset becomes the single corpus row, arm rows stay empty (arms are offline-computable, not live-measured), and every headline number carries its provenance label (SC-005).

func (*LiveReport) WriteJSON

func (r *LiveReport) WriteJSON(dir string) (string, error)

WriteJSON writes the live report as indented JSON into dir/live_report.json (the dir is gitignored — reports are never committed, per Spec 065 CN-003).

type LiveRunOptions

type LiveRunOptions struct {
	// CorpusV2Path, when non-empty, is the Spec 083 schema-bearing frozen
	// corpus used as the full-definition schema SOURCE for the naive
	// full-menu count: live tools are joined by tool id with the corpus_v2
	// schemas. This exists because GET /api/v1/tools can serve stub schemas
	// ({"type":"object","properties":{}} — see scripts/gen-corpus-v2-dump),
	// which would silently hollow out the FR-004 baseline. Live tools missing
	// from the corpus fall back to name+description and the headline is
	// withheld (the MCP-3161/3167 guard pattern).
	CorpusV2Path string
	// ExpectedToolCount, when > 0, is the tool count the frozen corpus
	// documents; it is recorded in ProxyInfo.ExpectedToolCount and drives the
	// corpus-drift warning when the live catalog differs (FR-021).
	ExpectedToolCount int
}

LiveRunOptions are the optional knobs of a live benchmark run.

type LiveTokenReport

type LiveTokenReport struct {
	Encoding               string           `json:"encoding"`
	UpstreamTools          int              `json:"upstream_tools"`
	BaselineTokens         int              `json:"baseline_tokens"`
	Modes                  []LiveModeResult `json:"modes"`
	ProxySchemasCounted    bool             `json:"proxy_schemas_counted"`
	BaselineSchemasCounted bool             `json:"baseline_schemas_counted"`
	AuthoritativeHeadline  bool             `json:"authoritative_headline"`
	// SchemaSource records where the baseline upstream schemas came from
	// (provenance, SC-005): the live GET /api/v1/tools response, or a
	// corpus_v2 join when the live endpoint serves stub schemas (FR-004).
	SchemaSource string   `json:"schema_source,omitempty"`
	Notes        []string `json:"notes"`
}

LiveTokenReport is the exact-token comparison from a live proxy, with the baseline upstream tools counted WITH their full JSON input schemas.

AuthoritativeHeadline gates the savings percentage: it is only true when schemas were counted on BOTH sides — the proxy management tools carry schemas (ProxySchemasCounted) AND the baseline upstream tools carry schemas (BaselineSchemasCounted). Counting schemas on one side only overstates or distorts savings — the exact error corrected in MCP-3161 — so when either side is schema-less the savings ratio is withheld and only raw token totals are reported. BaselineSchemasCounted also guards against a /api/v1/tools response that silently dropped upstream schemas (MCP-3167).

type LossySignatureGate

type LossySignatureGate struct {
	CorpusVersion string              `json:"corpus_version"`
	Tools         int                 `json:"tools"`
	LossyTools    int                 `json:"lossy_tools"`
	Rate          float64             `json:"rate"`
	Pass          bool                `json:"pass"`
	Heaviest      []HeaviestSignature `json:"heaviest,omitempty"`
}

LossySignatureGate is the corpus lossy-rate result (gate: <20%, SC-005).

func ComputeLossyGate

func ComputeLossyGate(tk *Tokenizer, corpusVersion string, tools []Tool, topN int) *LossySignatureGate

ComputeLossyGate renders every corpus tool through the SHARED internal/toolsig grammar (FR-019) and reports the lossy-signature rate (gate: <20%) plus the topN heaviest signatures by token cost. A tool with no schema is a parameter-less tool (renders "()", never lossy) — matching the live normalizeSchema semantics — not an unparseable one.

type MCPCaller

type MCPCaller struct {
	// contains filtered or unexported fields
}

MCPCaller is one initialized MCP session against a running proxy. The live run reuses a single session for every golden query (one initialize, N retrieve_tools calls) so per-query latency measures the call, not session setup.

func NewMCPCaller

func NewMCPCaller(ctx context.Context, baseURL, apiKey string) (*MCPCaller, error)

NewMCPCaller connects to the proxy's /mcp streamable-http endpoint and performs the MCP initialize handshake. Callers must Close() the session.

func (*MCPCaller) Close

func (c *MCPCaller) Close() error

Close terminates the MCP session.

func (*MCPCaller) RetrieveTools

func (c *MCPCaller) RetrieveTools(ctx context.Context, query string, limit int) (string, time.Duration, error)

RetrieveTools calls the proxy's retrieve_tools tool with query and returns the raw text content of the result — the exact JSON string marshaled by internal/server/mcp.go handleRetrieveToolsWithMode — plus the client-side round-trip latency of the call itself (FR-023). limit <= 0 omits the limit argument so the proxy's configured tools_limit default applies, matching what a real agent pays per call.

type MCPRetrieveCaller

type MCPRetrieveCaller struct {
	// contains filtered or unexported fields
}

MCPRetrieveCaller is the live compact arm's transport (Spec 085 FR-017): it drives a running proxy's MCP endpoint and replays retrieve_tools calls with a per-call `detail` override, so full and compact responses are measured through the SAME pipeline against the same live index. The serialized response text it returns is exactly what an agent would receive — that is what gets tokenized.

func NewMCPRetrieveCaller

func NewMCPRetrieveCaller(ctx context.Context, mcpURL string) (*MCPRetrieveCaller, error)

NewMCPRetrieveCaller connects and initializes an MCP client against mcpURL (e.g. "http://127.0.0.1:8092/mcp"). Callers must Close it.

func (*MCPRetrieveCaller) Close

func (m *MCPRetrieveCaller) Close() error

Close shuts the underlying MCP client down.

func (*MCPRetrieveCaller) RetrieveFunc

func (m *MCPRetrieveCaller) RetrieveFunc(ctx context.Context) RetrieveToolsFunc

RetrieveFunc adapts the caller to the RetrieveToolsFunc signature RunFlipGates consumes.

type ModeResult

type ModeResult struct {
	Mode         string  `json:"mode"`
	ContextTools int     `json:"context_tools"`
	Tokens       int     `json:"tokens"`
	SavingsRatio float64 `json:"savings_vs_baseline"`
}

ModeResult is the per-mode context-cost outcome.

type OfflineSection

type OfflineSection struct {
	// Corpus is the frozen tool corpus to run Arms over; nil skips the arm
	// runner and keeps only Descriptor + SkippedArms + ExtraArmRows.
	Corpus *Corpus
	// Descriptor identifies the corpus in the report (FR-011/012/013).
	// ToolCount defaults to len(Corpus.Tools) and DegenerateDescriptions is
	// computed with the FR-020 default rules when unset.
	Descriptor CorpusDescriptor
	// Golden enables retrieval-quality scoring for index-altering arms; nil
	// means the corpus has no relevance labels (FR-011 explicit absence).
	Golden *GoldenSet
	// Arms are the resolved encoding arms to measure (must include
	// baseline_json when non-empty — the savings denominator).
	Arms []EncodingArm
	// SkippedArms are arm-level skip rows for arms whose runtime is absent
	// (contract rule 5), recorded with the section's corpus ID.
	SkippedArms []ArmResult
	// ExtraArmRows are pre-computed rows appended verbatim (e.g. the
	// toon_results fixture rows, which do not run over a tool corpus).
	ExtraArmRows []ArmResult
}

OfflineSection is one corpus measured by a set of encoding arms (or, with a nil Corpus, a container for pre-computed rows such as the fixture-driven toon_results measurement).

type ProxyInfo

type ProxyInfo struct {
	Version           string `json:"version,omitempty"`
	ToolCount         int    `json:"tool_count,omitempty"`
	ExpectedToolCount int    `json:"expected_tool_count,omitempty"`
	ToolsLimit        int    `json:"tools_limit,omitempty"`
	RoutingMode       string `json:"routing_mode,omitempty"`
}

ProxyInfo records the measured proxy's identity and discovery configuration (FR-021): interpreting response cost requires knowing tools_limit and routing_mode; tool_count vs expected_tool_count surfaces corpus drift.

type QueryModeMismatch

type QueryModeMismatch struct {
	QueryID    string   `json:"query_id"`
	Query      string   `json:"query"`
	FullIDs    []string `json:"full_ids"`
	CompactIDs []string `json:"compact_ids"`
}

QueryModeMismatch records one golden query whose ranked ids differ between modes — any entry here fails the SC-002 gate.

type RankedIdentityGate

type RankedIdentityGate struct {
	Queries    int                 `json:"queries"`
	Identical  int                 `json:"identical"`
	Pass       bool                `json:"pass"`
	Mismatches []QueryModeMismatch `json:"mismatches,omitempty"`
}

RankedIdentityGate is the per-query ranked-ID identity result (gate: 100%).

type Report

type Report struct {
	Encoding      string       `json:"encoding"`
	CorpusVersion string       `json:"corpus_version"`
	CorpusTools   int          `json:"corpus_tools"`
	Modes         []ModeResult `json:"modes"`
	Notes         []string     `json:"notes"`
}

Report is the full token-reduction benchmark result.

func ComputeReport

func ComputeReport(tk *Tokenizer, corpus *Corpus) *Report

ComputeReport computes the per-mode context-token cost over the corpus and the savings of each proxy mode versus the baseline (all tools loaded directly).

func (*Report) WriteHTML

func (r *Report) WriteHTML(path string) error

WriteHTML renders the report as a self-contained static dashboard. The output is a single file with no external assets so it can be published as-is to a static host (CI release-tag publishing is tracked as a follow-up).

func (*Report) WriteJSON

func (r *Report) WriteJSON(path string) error

WriteJSON writes the report as indented JSON to path.

func (*Report) WriteReports

func (r *Report) WriteReports(dir string) (jsonPath, htmlPath string, err error)

WriteReports writes both report.json and dashboard.html into dir.

type ReportV2

type ReportV2 struct {
	ReportVersion    int                   `json:"report_version"`
	GeneratedAt      string                `json:"generated_at"`
	Tokenizer        TokenizerInfo         `json:"tokenizer"`
	Proxy            *ProxyInfo            `json:"proxy,omitempty"`
	Corpora          []CorpusDescriptor    `json:"corpora"`
	Arms             []ArmResult           `json:"arms"`
	ResponseCost     *ResponseCostSummary  `json:"response_cost,omitempty"`
	BreakEven        *BreakEvenAnalysis    `json:"break_even,omitempty"`
	SessionEstimates []SessionCostEstimate `json:"session_estimates,omitempty"`
	Latency          *LatencyV2            `json:"latency,omitempty"`
	Lap              *LapVerdict           `json:"lap,omitempty"`
	Subset           *SubsetInfo           `json:"subset,omitempty"`
	Provenance       map[string]string     `json:"provenance"`
}

ReportV2 is the versioned report envelope (research D12). Additive over the v1 report: existing consumers are unaffected (reports are never committed, Spec 065 CN-003).

func BuildOfflineReportV2

func BuildOfflineReportV2(tk *Tokenizer, generatedAt string, sections []OfflineSection) (*ReportV2, error)

BuildOfflineReportV2 measures every section and assembles the v2 envelope. Sections are processed in caller order; within a section rows follow the caller's arm order, then skip rows, then extra rows — fully deterministic (FR-010). Lap/Subset merging stays with the caller (CLI flags own those).

func (*ReportV2) WriteHTML

func (r *ReportV2) WriteHTML(path string) error

WriteHTML renders the v2 report as a self-contained static dashboard (Spec 083 T035, FR-017/018, SC-005): arms table, corpora table, response-cost percentiles, break-even, session estimates, LAP row, provenance badges on every headline section, and the tokenizer caveat banner. Single file, inline CSS only, no external resource loads — bench/report_test.go asserts self-containment.

func (*ReportV2) WriteJSON

func (r *ReportV2) WriteJSON(dir string) (string, error)

WriteJSON writes the v2 report as indented JSON into dir/report.json.

func (*ReportV2) WriteReports

func (r *ReportV2) WriteReports(dir string) (jsonPath, htmlPath string, err error)

WriteReports writes report.json and dashboard.html for a v2 run into dir.

type ResponseCostSummary

type ResponseCostSummary struct {
	P50      int                            `json:"p50"`
	P95      int                            `json:"p95"`
	Max      int                            `json:"max"`
	Mean     float64                        `json:"mean"`
	PerQuery []DiscoveryResponseMeasurement `json:"per_query,omitempty"`
}

ResponseCostSummary aggregates per-query response measurements (FR-001).

func SummarizeResponseCost

func SummarizeResponseCost(perQuery []DiscoveryResponseMeasurement) *ResponseCostSummary

SummarizeResponseCost aggregates per-query measurements into the FR-001 summary with nearest-rank percentiles over TotalTokens (same rank rule as the latency percentiles in live_report.go). PerQuery keeps the caller's order (golden-set order), so reports stay deterministic (FR-010).

type RetrievalGate

type RetrievalGate struct {
	Passed    bool    `json:"passed"`
	Metric    string  `json:"metric,omitempty"`
	Tolerance float64 `json:"tolerance,omitempty"`
}

RetrievalGate is the `retrieval.gate` object of the score-report contract.

A standalone live run has no stored baseline to regress against, so the gate cannot fail by construction: Passed is true and Metric/Tolerance are empty. Regression gating against a committed baseline is the CI lane's job (MCP-3133) — that run fills Metric/Tolerance and can set Passed=false.

type RetrievalMetricValues

type RetrievalMetricValues struct {
	RecallAt map[int]float64 `json:"recall_at"`
	MRR      float64         `json:"mrr"`
	NDCGAt10 float64         `json:"ndcg_at_10"`
	MAP      float64         `json:"map"`
}

RetrievalMetricValues holds the aggregated metric numbers. It is the `retrieval.metrics` object of the Spec 065 score-report.schema.json contract.

type RetrievalMetrics

type RetrievalMetrics struct {
	CorpusVersion string                `json:"corpus_version"`
	GoldenVersion string                `json:"golden_version,omitempty"`
	RunsAveraged  int                   `json:"runs_averaged"`
	QueryCount    int                   `json:"query_count,omitempty"`
	Metrics       RetrievalMetricValues `json:"metrics"`
	Gate          RetrievalGate         `json:"gate"`
}

RetrievalMetrics is the aggregated retrieval-quality report over a golden set. Its JSON shape IS the Spec 065 score-report.schema.json `retrieval` block (nested `metrics` + `gate`), so a live report's retrieval payload validates against that contract directly.

func ScoreRetrieval

func ScoreRetrieval(golden *GoldenSet, search SearchFunc, ks []int) (*RetrievalMetrics, error)

ScoreRetrieval replays every golden query through search and aggregates Recall@k (for each k in ks), MRR, nDCG@10 and MAP as the mean over all queries. The search is deterministic (BM25), so a single run is averaged.

type RetrievalScore

type RetrievalScore struct {
	RecallAt1  float64 `json:"recall_at_1"`
	RecallAt3  float64 `json:"recall_at_3"`
	RecallAt5  float64 `json:"recall_at_5"`
	RecallAt10 float64 `json:"recall_at_10"`
	MRR        float64 `json:"mrr"`
	NDCGAt10   float64 `json:"ndcg_at_10"`
	MAP        float64 `json:"map"`
	// MetricNote documents the gain formula (FR-012), or — for an
	// index-altering arm scored on a corpus without relevance labels —
	// explains the absence of numbers.
	MetricNote string `json:"metric_note,omitempty"`
}

RetrievalScore is the flat retrieval-quality DTO of the v2 contract. It is mapped from the existing nested RetrievalMetrics by MapRetrievalMetrics — the single conversion point — so the report schema stays flat and stable.

func MapRetrievalMetrics

func MapRetrievalMetrics(m *RetrievalMetrics) *RetrievalScore

MapRetrievalMetrics converts the existing nested bench.RetrievalMetrics (recall_at as a map) into the flat report DTO. nil in, nil out — a nil score marks a quality-neutral arm (FR-008).

type RetrieveToolsFunc

type RetrieveToolsFunc func(query string, limit int, detail string) (rankedIDs []string, responseText string, err error)

RetrieveToolsFunc replays one query through the proxy's MCP retrieve_tools tool with the given per-call detail override ("full" | "compact") and returns the ordered entry ids (server:tool, best first) plus the raw serialized response text — exactly what gets tokenized.

type SearchFunc

type SearchFunc func(query string, limit int) (ranked []string, err error)

SearchFunc replays one query through the retrieval system under test and returns the ranked tool IDs (most relevant first), limited to `limit`.

type SessionCostEstimate

type SessionCostEstimate struct {
	Arm             string  `json:"arm"`
	CallsPerSession int     `json:"calls_per_session"`
	RetryRate       float64 `json:"retry_rate"`
	EstimatedTokens int     `json:"estimated_tokens"`
}

SessionCostEstimate is one row of the FR-019 session estimator (provenance is always "estimated"; retry-rate defaults documented in research D8).

func EstimateSessionCost

func EstimateSessionCost(arm string, proxyMenuTokens int, meanResponseTokens float64, calls int) SessionCostEstimate

EstimateSessionCost computes one estimator row for a single arm and calls-per-session point (formula and rounding policy in the package comment). meanResponseTokens is the arm's mean retrieve_tools response cost — measured for the live baseline, derived from arm token ratios otherwise.

func EstimateSessionCosts

func EstimateSessionCosts(proxyMenuTokens int, meanResponseTokensByArm map[string]float64) []SessionCostEstimate

EstimateSessionCosts produces the full session-estimate table: one row per arm per default calls-per-session point, in deterministic order (arms sorted lexicographically, then calls ascending) regardless of map iteration order (FR-010). meanResponseTokensByArm maps arm name → mean retrieve_tools response tokens for that arm.

type SkipExample

type SkipExample struct {
	ToolID string `json:"tool_id"`
	Error  string `json:"error"`
}

SkipExample is one (tool, error) pair from an arm's skipped tools (FR-009).

type Span

type Span struct {
	Label string
	Start int
	End   int
}

Span is one contiguous labeled byte range [Start, End) of a response text. A valid span list is a partition: sorted, non-empty, gap-free, covering exactly [0, len(text)).

func PartitionRetrieveToolsResponse

func PartitionRetrieveToolsResponse(raw string) ([]Span, int, error)

PartitionRetrieveToolsResponse partitions the raw retrieve_tools MCP text content (the exact JSON string produced by internal/server/mcp.go handleRetrieveToolsWithMode) into labeled byte spans, and returns the number of tools in the response. The walk is offset-exact over the ORIGINAL bytes (json.Decoder.InputOffset) — the text is never re-marshaled, so the token total measured over these spans is the cost of the wire payload itself.

A key/value pair's span runs from the end of the previous value through the end of its own value, so it includes its preceding separator and its key bytes: the "inputSchema" bucket is the full cost of carrying schemas, key included. Structural bytes not owned by any pair land in "other".

type SubsetInfo

type SubsetInfo struct {
	Seed int `json:"seed"`
	Size int `json:"size"`
}

SubsetInfo records the seeded query subset of a public-corpus run (FR-014): same revision + seed + size ⇒ same subset.

type TokenDistribution

type TokenDistribution struct {
	Samples int `json:"samples"`
	P50     int `json:"p50"`
	P95     int `json:"p95"`
	Max     int `json:"max"`
}

TokenDistribution summarizes per-query response-token counts for one mode.

type TokenReductionReport

type TokenReductionReport struct {
	Encoding        string            `json:"encoding"`
	Full            TokenDistribution `json:"full"`
	Compact         TokenDistribution `json:"compact"`
	MedianReduction float64           `json:"median_reduction"`
}

TokenReductionReport compares full vs compact response-token distributions. MedianReduction = 1 - compact.P50/full.P50 (SC-001 gates on >= 0.50).

type Tokenizer

type Tokenizer struct {
	// contains filtered or unexported fields
}

Tokenizer wraps a tiktoken encoding for reproducible token estimation.

func NewTokenizer

func NewTokenizer(encoding string) (*Tokenizer, error)

NewTokenizer constructs a Tokenizer for the given tiktoken encoding name.

func (*Tokenizer) Count

func (t *Tokenizer) Count(text string) int

Count returns the number of tokens in text.

func (*Tokenizer) CountTool

func (t *Tokenizer) CountTool(tl Tool) int

CountTool returns the context-token cost of a single tool definition.

It counts the tool name and description only. Input JSON schemas are excluded uniformly across every mode because the committed Spec 065 corpus snapshot does not carry schemas. Schemas are dropped from BOTH sides — the baseline's upstream tools and the proxy modes' management tools (e.g. upstream_servers carries a large multi-field schema) — so this is a well-defined name+description-only metric, not an unambiguously conservative one. The live docker-compose run (README.md) adds full schemas from GET /api/v1/tools for the exact headline number.

func (*Tokenizer) CountToolWithSchema

func (t *Tokenizer) CountToolWithSchema(tl Tool) int

CountToolWithSchema returns the context-token cost of a tool definition INCLUDING its JSON input schema (name + description + schema). This is the authoritative per-tool context cost an agent actually pays. A tool with no schema counts identically to CountTool, so mixing schema-bearing (live) and schemaless tools in one report is well-defined. Used by the live run, where both the baseline upstream tools AND the proxy management tools carry their real schemas — counting schemas on BOTH sides is what keeps the headline savings honest rather than overstated.

type TokenizerInfo

type TokenizerInfo struct {
	Name   string `json:"name"`
	Caveat string `json:"caveat"`
}

TokenizerInfo names the token estimator and carries the mandatory accuracy caveat rendered wherever absolute numbers appear (research D11, SC-005).

type Tool

type Tool struct {
	ToolID      string          `json:"tool_id"`
	Server      string          `json:"server"`
	Name        string          `json:"tool"`
	Description string          `json:"description"`
	Schema      json.RawMessage `json:"schema,omitempty"`
}

Tool is a single tool definition the benchmark scores token cost over. It matches the shape of both the Spec 065 corpus snapshot and the embedded proxy-tool fixture. Schema is optional: the committed corpus snapshot is description-only (nil schema), while the live run (live.go) populates it with each tool's full JSON input schema for the exact-token headline.

func ProxyToolsForMode

func ProxyToolsForMode(mode string) []Tool

ProxyToolsForMode returns the built-in mcpproxy proxy + management tool definitions that occupy the agent's context window in the given routing mode.

The catalog is derived directly from the live server tool builders (internal/server.ProxyModeToolDefs → buildCallToolModeTools / buildCodeExecModeTools in internal/server/mcp_routing.go). This is the single source of truth: both routing modes append the shared management tool set (upstream_servers, quarantine_security, search_servers, list_registries), so deriving from the builders guarantees the benchmark counts the real per-mode context cost and can never drift from production by re-introducing the undercount that inflated the headline savings (MCP-3161).

type ToolTokenEntry

type ToolTokenEntry struct {
	ToolID string `json:"tool_id"`
	Tokens int    `json:"tokens"`
}

ToolTokenEntry is one heaviest-tools row (FR-020).

Directories

Path Synopsis
Package arms implements the encoding arms of the discovery-effectiveness profiler (spec 083).
Package arms implements the encoding arms of the discovery-effectiveness profiler (spec 083).
cmd
bench command
Command bench runs the mcpproxy benchmark.
Command bench runs the mcpproxy benchmark.
Package corpusio loads public tool-retrieval corpora into the bench types (spec 083, US3).
Package corpusio loads public tool-retrieval corpora into the bench types (spec 083, US3).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL