lap-score 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lev (lCrazyblindl)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,153 @@
1
+ Metadata-Version: 2.4
2
+ Name: lap-score
3
+ Version: 0.3.0
4
+ Summary: lap - measure & improve the token-efficiency of agent-facing APIs (OpenAPI & MCP): scorer, linter, the LAP profile, and a token benchmark
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/lCrazyblindl/lap
7
+ Project-URL: Roadmap, https://github.com/lCrazyblindl/lap/blob/main/ROADMAP.md
8
+ Project-URL: Changelog, https://github.com/lCrazyblindl/lap/blob/main/CHANGELOG.md
9
+ Project-URL: Leaderboard, https://github.com/lCrazyblindl/lap/blob/main/docs/LEADERBOARD.md
10
+ Keywords: llm,agents,openapi,mcp,tokens,api,token-efficiency
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: httpx
21
+ Requires-Dist: tiktoken
22
+ Requires-Dist: pyyaml
23
+ Provides-Extra: mcp
24
+ Requires-Dist: fastmcp; extra == "mcp"
25
+ Provides-Extra: faithful
26
+ Requires-Dist: anthropic; extra == "faithful"
27
+ Provides-Extra: all
28
+ Requires-Dist: fastmcp; extra == "all"
29
+ Requires-Dist: anthropic; extra == "all"
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ # lap — token-efficiency scorer for agent-facing APIs
35
+
36
+ `lap` is the open, neutral, standalone toolkit (no pet-zoo dependency) that measures
37
+ how many **tokens** an API's definitions cost an LLM. It answers: *is my agent-API
38
+ menu efficient, and by how much could it shrink?* — an open, reproducible number to set
39
+ beside the fast-growing MCP/OpenAPI tooling (see [`../docs/LANDSCAPE.md`](../docs/LANDSCAPE.md)
40
+ for the neighbors LAP builds on and credits).
41
+
42
+ ## Install
43
+
44
+ ```bash
45
+ pip install -e . # from the repo root (or: pip install lap-score once published)
46
+ pip install -e ".[mcp]" # + real-MCP baseline (fastmcp)
47
+ pip install -e ".[faithful]" # + faithful Anthropic count_tokens
48
+ ```
49
+
50
+ Core deps are just `httpx` + `tiktoken` + `pyyaml`; `fastmcp` and `anthropic` are optional extras.
51
+ Robust to real specs: `allOf`/`oneOf`/`anyOf`, `$ref` in params/requestBodies/responses,
52
+ path-item-level parameters, OpenAPI 3.1 `type` lists, external `$ref`s (left intact), YAML input,
53
+ **Swagger/OpenAPI 2.0** (response `schema`, `in: body` params, type-on-parameter, `#/definitions`),
54
+ and non-JSON media types (`*+json`, form, XML). Verified crash-free + non-degenerate across 175+
55
+ real APIs.guru specs — re-run with [`../experiments/fuzz_corpus.py`](../experiments/fuzz_corpus.py).
56
+
57
+ ## Quickstart
58
+
59
+ ```bash
60
+ lap score https://petstore3.swagger.io/api/v3/openapi.json # menu (bucket A) token cost
61
+ lap lint https://petstore3.swagger.io/api/v3/openapi.json # flag LAP rule violations
62
+ lap score --mcp-url http://localhost:8080/mcp # score a live MCP server's tools
63
+ lap score lap/examples/bookstore.openapi.json
64
+
65
+ # no install needed, from the repo root:
66
+ python -m lap.score lap/examples/bookstore.openapi.json
67
+ ```
68
+
69
+ Example output:
70
+
71
+ ```
72
+ LAP menu score - Bookstore API
73
+ operations: 6 referenced component schemas: 2
74
+
75
+ variant A tokens saved vs full form
76
+ openapi_full 418 +0% 6 tool(s)
77
+ compact_sig 205 +51% manifest text
78
+ numbered 168 +60% manifest text
79
+
80
+ Menu efficiency: compact signatures are +51% vs naive OpenAPI->tools (418 -> 205 tokens).
81
+ ```
82
+
83
+ When `fastmcp` is installed, the score also includes a **real-MCP baseline**
84
+ (`FastMCP.from_openapi`) — what an actual MCP generator emits — plus its
85
+ output-schema-inclusive figure (`--no-mcp` to skip). On a real public API
86
+ (**Swagger Petstore**, 19 ops) the real MCP server costs **2226** menu tokens
87
+ (**3844** with output schemas) vs **415** for compact signatures — an ~81%
88
+ reduction. The toy finding holds in the wild: a real MCP generator is *heavier*
89
+ than the naive baseline.
90
+
91
+ The score also includes a lazy **`tool_search`** form (the Anthropic Tool Search /
92
+ Cloudflare Code Mode pattern: a fixed 2-tool menu + a name index, schemas loaded on
93
+ demand). Because it doesn't preload schemas, its bucket A is ~flat in the number of
94
+ operations — on a 120-operation API it collapses the menu ~83% vs full schemas,
95
+ beating even compact signatures at scale (Petstore: 1740 → 207, −88%).
96
+
97
+ - **Faithful counts:** set `ANTHROPIC_API_KEY` (uses the free Anthropic `count_tokens`
98
+ endpoint; tool defs counted via the real `tools=` parameter). Without it, a GPT-style
99
+ `tiktoken` approximation — absolute numbers approximate, **relative ordering robust**.
100
+
101
+ ## CI gate
102
+
103
+ `--json` makes both commands machine-readable; thresholds set the exit code so LAP can fail a build:
104
+
105
+ ```bash
106
+ lap score openapi.json --gate-form compact_sig --max-menu-tokens 800 # exit 1 if the menu is too heavy
107
+ lap lint openapi.json --fail-on warn # exit 1 on any warning
108
+ lap lint openapi.json --ignore R2,A1 # suppress rules (or a ./.lapignore file)
109
+ ```
110
+
111
+ GitHub Actions:
112
+
113
+ ```yaml
114
+ - run: pip install lap-score # or: pip install -e .
115
+ - run: lap score api/openapi.json --gate-form compact_sig --max-menu-tokens 800
116
+ - run: lap lint api/openapi.json --fail-on warn
117
+ ```
118
+
119
+ …or the bundled composite **Action** (one step, no manual install):
120
+
121
+ ```yaml
122
+ - uses: lCrazyblindl/lap@v0.3.0
123
+ with:
124
+ spec: api/openapi.json
125
+ max-menu-tokens: "800" # gate the compact_sig menu (omit = report only)
126
+ fail-on: warn # fail on any lint warning (omit = report only)
127
+ ```
128
+
129
+ Already lint OpenAPI with **Spectral**? The same LAP rules ship as a ruleset —
130
+ see [`../spectral/`](../spectral/README.md).
131
+
132
+ ## What it measures (and what it doesn't)
133
+
134
+ It measures **bucket A** (the definitions/menu the model carries in context) and **estimates
135
+ C** (result size, from each response schema + an assumed `--page-size` — a structural lower
136
+ bound that captures keys/nesting/types). **B** (the call) still depends on per-API tasks; for a
137
+ full measured A/B/C run see
138
+ [`../experiments/token-bench`](../experiments/token-bench/README.md). The conventions
139
+ behind the compact form are the [LAP profile](../profile/llm-api-profile.md).
140
+
141
+ ## Files
142
+
143
+ | file | role |
144
+ | --- | --- |
145
+ | `openapi_ir.py` | load any OpenAPI (file/URL) → normalized operations + `inline_refs` |
146
+ | `menu.py` | render the menu forms (openapi_full / compact_sig / numbered) from the IR |
147
+ | `mcp_form.py` | real-MCP baseline via `FastMCP.from_openapi` (optional; `--no-mcp` to skip) |
148
+ | `mcp_client.py` | scores a live MCP server's advertised tools (`lap score --mcp-url`) |
149
+ | `estimate.py` | estimates bucket C (result size) from response schemas (`--page-size`) |
150
+ | `tokens.py` | token counting (Anthropic endpoint, or tiktoken approx) |
151
+ | `score.py` | the `lap score` CLI |
152
+ | `lint.py` | the `lap lint` CLI — checks a spec against the LAP profile rules (D3/R1/R2/R3/W1/E1/A1) |
153
+ | `examples/` | sample specs: a Bookstore API, a gnarly OpenAPI 3.1 (allOf / $ref-params / nullable / external-ref), and a Swagger 2.0 spec (`swagger2.json`) |
@@ -0,0 +1,198 @@
1
+ # lap — LLM-API Profile
2
+
3
+ [![ci](https://github.com/lCrazyblindl/lap/actions/workflows/ci.yml/badge.svg)](https://github.com/lCrazyblindl/lap/actions/workflows/ci.yml) · MIT licensed ([LICENSE](LICENSE)) · [Changelog](CHANGELOG.md) · [Roadmap](ROADMAP.md)
4
+
5
+ **Measure and improve the token-efficiency of agent-facing APIs** (OpenAPI & MCP) — a free, open, neutral toolkit: a scorer (`lap score`), a linter (`lap lint`), a convention (the **LAP profile**), and a reproducible **benchmark**. Not a product, not a gateway, not a new protocol — a public-good measuring stick for a question nobody else answers with real numbers: *how many tokens does my agent-facing API cost, and how much of that is unnecessary?*
6
+
7
+ ## Table of contents
8
+
9
+ 1. [What we did — TL;DR](#1-what-we-did--tldr)
10
+ 2. [Why this is useful](#2-why-this-is-useful)
11
+ 3. [Install & usage](#3-install--usage)
12
+ 4. [Project map](#4-project-map)
13
+ 5. [Status, license, contributing](#5-status-license-contributing)
14
+
15
+ ---
16
+
17
+ ## 1. What we did — TL;DR
18
+
19
+ ### The problem, in one paragraph
20
+
21
+ Every time an agent talks to an API — via OpenAPI-generated tools, an MCP server, or a hand-rolled integration — it pays **tokens**: once per session for the menu of what it *can* call (bucket **A**), a little for each call it makes (bucket **B**), and repeatedly for every result that comes back (bucket **C**). Nobody was publishing real, reproducible numbers for this. Vendors cite their own best-case headline figures on their own setups; nobody scores a random real API and tells you where it stands. **lap does that** — for any OpenAPI spec or live MCP server, in one command, for free.
22
+
23
+ ### The toolkit
24
+
25
+ | piece | what it does |
26
+ | --- | --- |
27
+ | **`lap score <api>`** | Reports an API's bucket-A menu cost across four rendering forms (naive OpenAPI→tools, compact signatures, a numbered dictionary, a lazy tool-search form), plus a real-MCP baseline (via FastMCP) and a bucket-C result-size estimate. Works on any OpenAPI file/URL, or a live MCP server (`--mcp-url`). |
28
+ | **`lap lint <api>`** | Flags concrete violations of the LAP profile's rules (opaque names, no pagination/filtering/projection, no aggregation, verbose writes, ambiguous errors) — each citing the measurement that justifies it. |
29
+ | **The LAP profile** ([`profile/`](profile/llm-api-profile.md)) | A short, opinionated set of conventions **on top of** HTTP/JSON/OpenAPI — not a new wire format — for exposing an API so an agent uses it in the fewest tokens. Every rule is backed by a number, not an opinion. |
30
+ | **The benchmark** ([`experiments/token-bench/`](experiments/token-bench/README.md)) | A full A/B/C token accounting across 10 tasks in 5 categories, comparing 6 interface variants (naive, real MCP, compact, numbered, code-execution, declarative query) on a real FastAPI testbed — plus an optional live run against a real model to check that compression doesn't cost accuracy. |
31
+
32
+ ### What we found on our own testbed
33
+
34
+ Six ways of exposing the same API, measured on identical tasks (tiktoken-approx; faithful Anthropic counts run ~60% higher, same ordering):
35
+
36
+ | variant | menu (bucket A) | "count females" task total |
37
+ | --- | --- | --- |
38
+ | `openapi_full` (naive OpenAPI→tools, the baseline) | 1637 | 2809 |
39
+ | `mcp_fastmcp` (a **real** MCP server, via FastMCP) | 1689 | 2865 |
40
+ | `mcp_fastmcp` + output schemas forwarded | 3762 | — |
41
+ | `compact_sig` (readable names, dense signatures) | 401 | 1573 |
42
+ | `numbered` (endpoint → integer dictionary) | 466 | 1636 |
43
+ | `code_exec` (one `run_python` tool + a compact client doc) | 183 | **217** |
44
+ | `odata_query` (one declarative `query` tool, server-side) | 219 | **239** |
45
+
46
+ **Takeaways:** numbering endpoints is a net loss (the codebook still costs bucket A, while saving ~2 tokens of the cheapest bucket); the real wins are shaping the menu (A) and the result (C); a real MCP generator is *not* a strawman — it's slightly heavier than our hand-rolled naive baseline; and declarative queries match code-execution almost everywhere, until a task needs a computed property the query DSL can't express (then code wins by ~40×). Full tables: [`experiments/token-bench/results.md`](experiments/token-bench/results.md).
47
+
48
+ A later **live success-rate matrix** (Claude Haiku, k=3 repeats) confirmed this isn't just a token story: **compression didn't cost accuracy** — `numbered`/`code_exec`/`odata_query` scored 15/15, `compact_sig` 14/15, and the naive baseline came in *last* at 13/15, while spending 3–4× more tokens. ([`validation.md`](experiments/token-bench/validation.md))
49
+
50
+ ### What we found when we stopped testing our own code and measured *real* tools
51
+
52
+ This is the part that makes the numbers worth trusting: instead of only comparing our own interface variants, we pointed the same measurement at **real OpenAPI→MCP generators, real published MCP servers, a real hosted API, and two of Anthropic's own real efficiency features** — live, with real HTTP calls and real billed tokens, not simulated.
53
+
54
+ | what we tested | result | evidence |
55
+ | --- | --- | --- |
56
+ | 3 real OpenAPI→MCP generators (FastMCP, `openapi-to-mcp`, `openapi-mcp`) | All three emit a menu **heavier than our naive baseline**, and **5–28× heavier than compact** — none ships the compact form | [`docs/GENERATORS.md`](docs/GENERATORS.md) |
57
+ | 3 real published MCP servers (git, fetch, time, over stdio) | Even small reference servers pay a real menu tax; a compact rendering of the *same* advertised tools cuts it **~89%** | [`docs/MCP-SERVERS.md`](docs/MCP-SERVERS.md) |
58
+ | End-to-end on the live hosted Swagger Petstore (real HTTP, real model) | The naive menu was both the **heaviest and the least reliable** — it failed a count task 0/3 while compact and a real FastMCP server hit 3/3 | [`validation-real.md`](experiments/token-bench/validation-real.md) |
59
+ | Anthropic's real **Tool Search**, live, on a real 290-operation API | Cut **billed** input tokens **~90%** vs. the identical schemas without it — the saving is *structural* (server-enforced) — but cost *more* than a compact menu on a small 19-op API, matching Anthropic's own "10+ tools" guidance | [`docs/TOOL-SEARCH.md`](docs/TOOL-SEARCH.md) |
60
+ | Anthropic's real **code-execution** tool, live, on a validated task | Came in **heavier** than both the naive baseline and our own sandbox on this run — its saving is *behavioral* (it only holds if the model's own code never prints the raw data), not guaranteed like Tool Search's | [`docs/CODE-EXEC.md`](docs/CODE-EXEC.md) |
61
+ | A parser fuzz pass over **175+ real specs** (APIs.guru) | Zero crashes; found and fixed real bugs (Swagger 2.0 support, non-JSON media types, a `tiktoken` crash on a literal `<|endoftext|>` in OpenAI's own spec) | [`experiments/fuzz_corpus.py`](experiments/fuzz_corpus.py) |
62
+ | Bucket-C estimate against real, enveloped API responses (`{"data":[...]}`, Kubernetes `{"items":[...]}`) | Fixed a real undercount — 15 of 20 leaderboard rows changed once envelopes were handled correctly | `lap/estimate.py` |
63
+
64
+ **We report the two real results that *don't* flatter the thesis as prominently as the ones that do.** Tool Search's saving is enforced by the server regardless of what the model does; code-execution's saving depends on the model's own discipline and, on one real run, didn't materialize. That distinction — structural vs. behavioral savings — is now written into the LAP profile itself (rules D2 and X1), not just this README.
65
+
66
+ ### The leaderboard
67
+
68
+ [**`docs/LEADERBOARD.md`**](docs/LEADERBOARD.md) scores **20 real public APIs** — Kubernetes, EC2, Jira, Stripe, GitHub, OpenAI, Slack, Notion, and more — by what their naive agent menu costs today:
69
+
70
+ | API | naive menu (bucket A) | LAP compact | saved |
71
+ | --- | ---: | ---: | ---: |
72
+ | Kubernetes | 2,818,799 | 45,015 | **+98%** |
73
+ | Amazon EC2 | 606,132 | 63,158 | **+90%** |
74
+ | Jira Cloud | 345,552 | 17,996 | **+95%** |
75
+ | Stripe | 231,586 | 32,860 | **+86%** |
76
+ | _…16 more_ | | | |
77
+
78
+ Across all 20, naive menus total **~4.9M tokens**; the compact form would cut **~86%** on average (the lazy `tool_search` form ~96%). None of these APIs ships a compact agent menu today — reproduce it with `python experiments/leaderboard.py`.
79
+
80
+ ---
81
+
82
+ ## 2. Why this is useful
83
+
84
+ - **If you run an API or MCP server:** `lap score`/`lap lint` tell you, in one command, exactly how many tokens your agent-facing menu costs and which concrete, measured rule violations are driving that cost — not vague advice, a number and a citation. Wire it into CI (`--gate-form`/`--max-menu-tokens`, `--fail-on`) so a schema change that blows up the menu fails the build instead of shipping.
85
+ - **If you build agents or pick which APIs to wire up:** the leaderboard and the real-tool findings above are due-diligence data — before you integrate an API or an MCP server, you can know whether its menu is going to eat your context window, without running your own experiment first.
86
+ - **If you already lint OpenAPI with Spectral:** the same LAP rules ship as a [Spectral ruleset](spectral/README.md) — one line in your existing setup, no new tool to adopt.
87
+ - **If you're deciding between MCP/Tool Search/code-execution/a query DSL:** this is the one place that measured all of them, on the same tasks, with the same accounting, including the vendor features' real behavior (not just their marketing numbers) — see [`docs/LANDSCAPE.md`](docs/LANDSCAPE.md) for how LAP sits next to NLWeb, MCP gateways, and the other efficiency tooling that's emerged since 2025, and what it deliberately does *not* rebuild (auth, discovery, hosting).
88
+ - **Because it's free and it stays free.** MIT-licensed, no telemetry, no paid tier, no company behind it. The goal is a shared, reproducible number the whole agentic-web ecosystem can use — a public good, not a product.
89
+
90
+ ## 3. Install & usage
91
+
92
+ ### Install
93
+
94
+ ```bash
95
+ pip install lap-score # PyPI (once published — see CHANGELOG.md for the version)
96
+ # or, from a clone of this repo:
97
+ pip install -e . # editable install for local development
98
+ pip install -e ".[mcp]" # + a real-MCP baseline in `lap score` (needs fastmcp)
99
+ pip install -e ".[faithful]" # + faithful Anthropic token counts (needs an API key)
100
+ pip install -e ".[all]" # both extras
101
+ ```
102
+
103
+ Core dependencies are just `httpx` + `tiktoken` + `pyyaml` — `fastmcp` and `anthropic` are optional. `lap` is robust to real-world spec mess: `allOf`/`oneOf`/`anyOf`, `$ref` anywhere, OpenAPI 3.1 nullable types, external refs, YAML input, **Swagger/OpenAPI 2.0**, and non-JSON media types — verified crash-free across 175+ real specs (`experiments/fuzz_corpus.py`).
104
+
105
+ ### Quickstart
106
+
107
+ ```bash
108
+ lap score https://petstore3.swagger.io/api/v3/openapi.json # menu (bucket A) token cost
109
+ lap lint https://petstore3.swagger.io/api/v3/openapi.json # flag LAP rule violations
110
+ lap score --mcp-url http://localhost:8080/mcp # score a live MCP server's tools
111
+ lap score lap/examples/bookstore.openapi.json # a bundled example, no network needed
112
+ ```
113
+
114
+ Example `lap score` output:
115
+
116
+ ```
117
+ LAP menu score - Bookstore API
118
+ operations: 6 referenced component schemas: 2
119
+
120
+ variant A tokens saved vs full form
121
+ openapi_full 418 +0% 6 tool(s)
122
+ compact_sig 205 +51% manifest text
123
+ numbered 168 +60% manifest text
124
+
125
+ Menu efficiency: compact signatures are +51% vs naive OpenAPI->tools (418 -> 205 tokens).
126
+ ```
127
+
128
+ With `fastmcp` installed, the score adds a **real-MCP baseline** (what an actual MCP generator emits) — on the live Swagger Petstore, that's **2226** menu tokens (**3844** with output schemas) vs **415** for compact signatures. With `ANTHROPIC_API_KEY` set, counts switch from a `tiktoken` approximation to Anthropic's real, free `count_tokens` endpoint — absolute numbers change (~60% higher), relative ordering doesn't.
129
+
130
+ ### Lint output
131
+
132
+ ```bash
133
+ lap lint api/openapi.json
134
+ ```
135
+
136
+ Flags rule violations (opaque operation names, missing pagination/filtering/projection, no server-side aggregation, verbose write responses, ambiguous error handling) with a plain-language message and a citation into the [LAP profile](profile/llm-api-profile.md), e.g. `[WARN R3] collection GET has no pagination (limit/offset/cursor) — agents pull the whole list`.
137
+
138
+ ### CI gate
139
+
140
+ `--json` makes both commands machine-readable; thresholds turn them into a build gate:
141
+
142
+ ```bash
143
+ lap score openapi.json --gate-form compact_sig --max-menu-tokens 800 # exit 1 if the menu is too heavy
144
+ lap lint openapi.json --fail-on warn # exit 1 on any warning
145
+ lap lint openapi.json --ignore R2,A1 # suppress specific rules (or a ./.lapignore file)
146
+ ```
147
+
148
+ As a raw GitHub Actions step:
149
+
150
+ ```yaml
151
+ - run: pip install lap-score
152
+ - run: lap score api/openapi.json --gate-form compact_sig --max-menu-tokens 800
153
+ - run: lap lint api/openapi.json --fail-on warn
154
+ ```
155
+
156
+ …or the bundled composite **Action** (one step, no manual install):
157
+
158
+ ```yaml
159
+ - uses: lCrazyblindl/lap@v0.3.0
160
+ with:
161
+ spec: api/openapi.json
162
+ max-menu-tokens: "800" # gate the compact_sig menu (omit = report only)
163
+ fail-on: warn # fail on any lint warning (omit = report only)
164
+ ```
165
+
166
+ ### Already using Spectral?
167
+
168
+ The same rules ship as a **Spectral ruleset** — no new tool, one line in your existing lint config:
169
+
170
+ ```yaml
171
+ extends:
172
+ - spectral:oas
173
+ - ./path/to/spectral/lap.spectral.yaml
174
+ ```
175
+
176
+ See [`spectral/README.md`](spectral/README.md) (custom-function rulesets must be referenced locally, not by URL).
177
+
178
+ ### What `lap score` measures (and doesn't)
179
+
180
+ It measures **bucket A** (the definitions/menu the model carries in context) and **estimates C** (result size, from each response schema — a structural lower bound, envelope-aware for patterns like `{"data": [...]}`). **B** (the call itself) needs per-API tasks; for a full measured A/B/C run with real accuracy checks, see [`experiments/token-bench`](experiments/token-bench/README.md).
181
+
182
+ ---
183
+
184
+ ## 4. Project map
185
+
186
+ - [`lap/`](lap/README.md) — the standalone, pip-installable **toolkit** (`lap score`, `lap lint`). Start here for day-to-day use.
187
+ - [`profile/`](profile/llm-api-profile.md) — the **LAP profile**: the conventions, with every rule backed by a measurement.
188
+ - [`experiments/token-bench/`](experiments/token-bench/README.md) — the full A/B/C benchmark on a real FastAPI testbed ([`pet-zoo/`](pet-zoo/README.md)), 10 tasks across 5 categories, an optional live accuracy check.
189
+ - [`docs/LEADERBOARD.md`](docs/LEADERBOARD.md) — 20 real public APIs ranked by agent-menu token cost. `experiments/leaderboard.py` regenerates it.
190
+ - [`docs/GENERATORS.md`](docs/GENERATORS.md) · [`docs/MCP-SERVERS.md`](docs/MCP-SERVERS.md) · [`docs/TOOL-SEARCH.md`](docs/TOOL-SEARCH.md) · [`docs/CODE-EXEC.md`](docs/CODE-EXEC.md) — the real-tool validation track: real generators, real MCP servers, and two of Anthropic's real efficiency features, tested live.
191
+ - [`docs/LANDSCAPE.md`](docs/LANDSCAPE.md) — where LAP sits in the June-2026 agentic-web landscape (NLWeb, llms.txt, MCP gateways, the token-efficiency tools LAP builds on and credits), and what it deliberately doesn't rebuild.
192
+ - [`spectral/`](spectral/README.md) — the LAP lint rules as a Spectral ruleset.
193
+ - [`pet-zoo/`](pet-zoo/README.md) — the small FastAPI zoo-management API used as the benchmark's testbed.
194
+ - [`ROADMAP.md`](ROADMAP.md) — the full staged history of how this was built, stop/resume friendly.
195
+
196
+ ## 5. Status, license, contributing
197
+
198
+ **v0.3.0**, pre-1.0, actively maintained. MIT licensed ([LICENSE](LICENSE)) — use it, fork it, ship its rules in your own linter, no attribution required (though a star or a mention helps the public-good goal). See [`CHANGELOG.md`](CHANGELOG.md) for release history and [`RELEASING.md`](RELEASING.md) for the release process. Issues and PRs welcome — there's no formal contributing guide yet; open an issue to discuss before a large change.
@@ -0,0 +1,120 @@
1
+ # lap — token-efficiency scorer for agent-facing APIs
2
+
3
+ `lap` is the open, neutral, standalone toolkit (no pet-zoo dependency) that measures
4
+ how many **tokens** an API's definitions cost an LLM. It answers: *is my agent-API
5
+ menu efficient, and by how much could it shrink?* — an open, reproducible number to set
6
+ beside the fast-growing MCP/OpenAPI tooling (see [`../docs/LANDSCAPE.md`](../docs/LANDSCAPE.md)
7
+ for the neighbors LAP builds on and credits).
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install -e . # from the repo root (or: pip install lap-score once published)
13
+ pip install -e ".[mcp]" # + real-MCP baseline (fastmcp)
14
+ pip install -e ".[faithful]" # + faithful Anthropic count_tokens
15
+ ```
16
+
17
+ Core deps are just `httpx` + `tiktoken` + `pyyaml`; `fastmcp` and `anthropic` are optional extras.
18
+ Robust to real specs: `allOf`/`oneOf`/`anyOf`, `$ref` in params/requestBodies/responses,
19
+ path-item-level parameters, OpenAPI 3.1 `type` lists, external `$ref`s (left intact), YAML input,
20
+ **Swagger/OpenAPI 2.0** (response `schema`, `in: body` params, type-on-parameter, `#/definitions`),
21
+ and non-JSON media types (`*+json`, form, XML). Verified crash-free + non-degenerate across 175+
22
+ real APIs.guru specs — re-run with [`../experiments/fuzz_corpus.py`](../experiments/fuzz_corpus.py).
23
+
24
+ ## Quickstart
25
+
26
+ ```bash
27
+ lap score https://petstore3.swagger.io/api/v3/openapi.json # menu (bucket A) token cost
28
+ lap lint https://petstore3.swagger.io/api/v3/openapi.json # flag LAP rule violations
29
+ lap score --mcp-url http://localhost:8080/mcp # score a live MCP server's tools
30
+ lap score lap/examples/bookstore.openapi.json
31
+
32
+ # no install needed, from the repo root:
33
+ python -m lap.score lap/examples/bookstore.openapi.json
34
+ ```
35
+
36
+ Example output:
37
+
38
+ ```
39
+ LAP menu score - Bookstore API
40
+ operations: 6 referenced component schemas: 2
41
+
42
+ variant A tokens saved vs full form
43
+ openapi_full 418 +0% 6 tool(s)
44
+ compact_sig 205 +51% manifest text
45
+ numbered 168 +60% manifest text
46
+
47
+ Menu efficiency: compact signatures are +51% vs naive OpenAPI->tools (418 -> 205 tokens).
48
+ ```
49
+
50
+ When `fastmcp` is installed, the score also includes a **real-MCP baseline**
51
+ (`FastMCP.from_openapi`) — what an actual MCP generator emits — plus its
52
+ output-schema-inclusive figure (`--no-mcp` to skip). On a real public API
53
+ (**Swagger Petstore**, 19 ops) the real MCP server costs **2226** menu tokens
54
+ (**3844** with output schemas) vs **415** for compact signatures — an ~81%
55
+ reduction. The toy finding holds in the wild: a real MCP generator is *heavier*
56
+ than the naive baseline.
57
+
58
+ The score also includes a lazy **`tool_search`** form (the Anthropic Tool Search /
59
+ Cloudflare Code Mode pattern: a fixed 2-tool menu + a name index, schemas loaded on
60
+ demand). Because it doesn't preload schemas, its bucket A is ~flat in the number of
61
+ operations — on a 120-operation API it collapses the menu ~83% vs full schemas,
62
+ beating even compact signatures at scale (Petstore: 1740 → 207, −88%).
63
+
64
+ - **Faithful counts:** set `ANTHROPIC_API_KEY` (uses the free Anthropic `count_tokens`
65
+ endpoint; tool defs counted via the real `tools=` parameter). Without it, a GPT-style
66
+ `tiktoken` approximation — absolute numbers approximate, **relative ordering robust**.
67
+
68
+ ## CI gate
69
+
70
+ `--json` makes both commands machine-readable; thresholds set the exit code so LAP can fail a build:
71
+
72
+ ```bash
73
+ lap score openapi.json --gate-form compact_sig --max-menu-tokens 800 # exit 1 if the menu is too heavy
74
+ lap lint openapi.json --fail-on warn # exit 1 on any warning
75
+ lap lint openapi.json --ignore R2,A1 # suppress rules (or a ./.lapignore file)
76
+ ```
77
+
78
+ GitHub Actions:
79
+
80
+ ```yaml
81
+ - run: pip install lap-score # or: pip install -e .
82
+ - run: lap score api/openapi.json --gate-form compact_sig --max-menu-tokens 800
83
+ - run: lap lint api/openapi.json --fail-on warn
84
+ ```
85
+
86
+ …or the bundled composite **Action** (one step, no manual install):
87
+
88
+ ```yaml
89
+ - uses: lCrazyblindl/lap@v0.3.0
90
+ with:
91
+ spec: api/openapi.json
92
+ max-menu-tokens: "800" # gate the compact_sig menu (omit = report only)
93
+ fail-on: warn # fail on any lint warning (omit = report only)
94
+ ```
95
+
96
+ Already lint OpenAPI with **Spectral**? The same LAP rules ship as a ruleset —
97
+ see [`../spectral/`](../spectral/README.md).
98
+
99
+ ## What it measures (and what it doesn't)
100
+
101
+ It measures **bucket A** (the definitions/menu the model carries in context) and **estimates
102
+ C** (result size, from each response schema + an assumed `--page-size` — a structural lower
103
+ bound that captures keys/nesting/types). **B** (the call) still depends on per-API tasks; for a
104
+ full measured A/B/C run see
105
+ [`../experiments/token-bench`](../experiments/token-bench/README.md). The conventions
106
+ behind the compact form are the [LAP profile](../profile/llm-api-profile.md).
107
+
108
+ ## Files
109
+
110
+ | file | role |
111
+ | --- | --- |
112
+ | `openapi_ir.py` | load any OpenAPI (file/URL) → normalized operations + `inline_refs` |
113
+ | `menu.py` | render the menu forms (openapi_full / compact_sig / numbered) from the IR |
114
+ | `mcp_form.py` | real-MCP baseline via `FastMCP.from_openapi` (optional; `--no-mcp` to skip) |
115
+ | `mcp_client.py` | scores a live MCP server's advertised tools (`lap score --mcp-url`) |
116
+ | `estimate.py` | estimates bucket C (result size) from response schemas (`--page-size`) |
117
+ | `tokens.py` | token counting (Anthropic endpoint, or tiktoken approx) |
118
+ | `score.py` | the `lap score` CLI |
119
+ | `lint.py` | the `lap lint` CLI — checks a spec against the LAP profile rules (D3/R1/R2/R3/W1/E1/A1) |
120
+ | `examples/` | sample specs: a Bookstore API, a gnarly OpenAPI 3.1 (allOf / $ref-params / nullable / external-ref), and a Swagger 2.0 spec (`swagger2.json`) |
@@ -0,0 +1,8 @@
1
+ """LAP - an open, neutral token-efficiency toolkit for agent-facing APIs.
2
+
3
+ `lap score <openapi>` measures the menu (bucket A) token cost of any OpenAPI spec
4
+ under several interface forms. Reuses the parsing proven in the token-bench, but
5
+ standalone (no pet-zoo dependency). See ../ROADMAP.md for the staged plan.
6
+ """
7
+
8
+ __version__ = "0.1.0"
@@ -0,0 +1,37 @@
1
+ """Unified `lap` command: `lap score <openapi>` / `lap lint <openapi>`.
2
+
3
+ Thin dispatcher so the toolkit is one console command after `pip install`. Each
4
+ subcommand reuses its module's own argument parsing.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import sys
10
+
11
+ _USAGE = "usage: lap {score|lint} <openapi-file-or-url> [options]\n" \
12
+ " score measure the menu (bucket A) token cost\n" \
13
+ " lint flag LAP profile rule violations"
14
+
15
+
16
+ def main() -> None:
17
+ argv = sys.argv[1:]
18
+ if not argv or argv[0] in ("-h", "--help"):
19
+ print(_USAGE)
20
+ return
21
+ cmd, rest = argv[0], argv[1:]
22
+ sys.argv = [f"lap {cmd}", *rest] # so each subcommand's argparse sees clean args
23
+ if cmd == "score":
24
+ from . import score
25
+
26
+ score.main()
27
+ elif cmd == "lint":
28
+ from . import lint
29
+
30
+ lint.main()
31
+ else:
32
+ print(f"lap: unknown command {cmd!r}\n{_USAGE}")
33
+ sys.exit(2)
34
+
35
+
36
+ if __name__ == "__main__":
37
+ main()
@@ -0,0 +1,115 @@
1
+ """Estimate bucket C (result tokens) from response schemas - no runtime needed.
2
+
3
+ For each operation we synthesize a representative instance from its success
4
+ response schema, serialize it compactly, and count tokens. For array (collection)
5
+ responses we multiply the per-item cost by an assumed page size. Values are a
6
+ *structural lower bound* - they capture keys + nesting + types (which dominate
7
+ repeated-JSON cost), not real string lengths, so a live payload is >= this.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+
14
+ from . import openapi_ir as ir
15
+ from . import tokens
16
+
17
+ _PLACEHOLDER = {"string": "string", "integer": 0, "number": 0, "boolean": True, "null": None}
18
+
19
+ # Common envelope keys real APIs wrap a list in, preferred in this order when
20
+ # more than one array-typed property is present (rare, but pick deterministically).
21
+ _ENVELOPE_KEYS = ("data", "items", "results", "value", "values", "content", "entries", "records")
22
+
23
+
24
+ def example_instance(spec: dict, schema, stack: frozenset = frozenset(), depth: int = 0):
25
+ if not isinstance(schema, dict) or depth > 6:
26
+ return None
27
+ if "$ref" in schema:
28
+ ref = schema["$ref"]
29
+ if not ir._local(ref) or ref in stack:
30
+ return "ref"
31
+ return example_instance(spec, ir._resolve_ref(spec, ref), stack | {ref}, depth + 1)
32
+ if "allOf" in schema:
33
+ return {
34
+ fname: example_instance(spec, prop, stack, depth + 1)
35
+ for fname, prop in ir._collect_properties(spec, schema).items()
36
+ }
37
+ for comb in ("oneOf", "anyOf"):
38
+ if schema.get(comb):
39
+ return example_instance(spec, schema[comb][0], stack, depth + 1)
40
+ if "enum" in schema:
41
+ return schema["enum"][0]
42
+ t = schema.get("type")
43
+ if isinstance(t, list):
44
+ t = next((x for x in t if x != "null"), "string")
45
+ if t == "object" or "properties" in schema:
46
+ return {
47
+ k: example_instance(spec, v, stack, depth + 1)
48
+ for k, v in schema.get("properties", {}).items()
49
+ }
50
+ if t == "array":
51
+ return [example_instance(spec, schema.get("items", {}), stack, depth + 1)]
52
+ return _PLACEHOLDER.get(t, "x")
53
+
54
+
55
+ def _success_schema(spec: dict, op: ir.Op):
56
+ # OpenAPI 3 (`content`) and 2.0 (`schema`), JSON-ish media types — see ir._response_schema.
57
+ return ir._response_schema(spec, op.raw)
58
+
59
+
60
+ def _dumps(value) -> str:
61
+ return json.dumps(value, separators=(",", ":"), default=str, ensure_ascii=False)
62
+
63
+
64
+ def _is_array_schema(spec: dict, schema) -> bool:
65
+ if not isinstance(schema, dict):
66
+ return False
67
+ deref = ir._deref(spec, schema)
68
+ return deref.get("type") == "array" or "items" in deref
69
+
70
+
71
+ def _find_envelope_key(spec: dict, schema: dict) -> str | None:
72
+ """If `schema` is an object with an array-typed property - a common
73
+ `{"data": [...]}` / k8s `{"items": [...]}` / OData `{"value": [...]}` envelope -
74
+ return that property's name. Real APIs almost always wrap collections this
75
+ way; without this, an enveloped list scores as a tiny "object" (one item deep
76
+ in the envelope) and bucket C is badly undercounted."""
77
+ props = ir._collect_properties(spec, schema)
78
+ candidates = [fname for fname, prop in props.items() if _is_array_schema(spec, prop)]
79
+ if not candidates:
80
+ return None
81
+ for key in _ENVELOPE_KEYS:
82
+ if key in candidates:
83
+ return key
84
+ return candidates[0]
85
+
86
+
87
+ def estimate(spec: dict, op: ir.Op, page_size: int = 20) -> tuple[str, int, int]:
88
+ """Returns (kind, per_unit_tokens, estimated_C_tokens) for an operation's
89
+ success response. kind in {"void", "object", "list"}.
90
+
91
+ A bare top-level array is scaled by `page_size` directly. A list wrapped in
92
+ an envelope object (`{"data": [...], "total_count": ...}`, k8s `{"items": [...],
93
+ "kind": ..., "metadata": ...}`) is detected too: the sibling fields are kept
94
+ (counted once) and the array property is scaled to `page_size` items, so the
95
+ *whole* envelope at a page is what's estimated - not just one wrapped item."""
96
+ schema = _success_schema(spec, op)
97
+ if not isinstance(schema, dict):
98
+ return ("void", 0, 0)
99
+ deref = ir._deref(spec, schema)
100
+ if schema.get("type") == "array" or "items" in deref:
101
+ per = tokens.count(_dumps(example_instance(spec, deref.get("items", {}))))
102
+ return ("list", per, per * page_size + 5) # +5 for the array brackets/commas
103
+
104
+ envelope_key = _find_envelope_key(spec, schema)
105
+ if envelope_key:
106
+ instance = example_instance(spec, schema)
107
+ arr = instance.get(envelope_key) if isinstance(instance, dict) else None
108
+ if isinstance(arr, list) and arr:
109
+ item = arr[0]
110
+ per = tokens.count(_dumps(item))
111
+ instance[envelope_key] = [item] * page_size
112
+ return ("list", per, tokens.count(_dumps(instance)))
113
+
114
+ per = tokens.count(_dumps(example_instance(spec, schema)))
115
+ return ("object", per, per)