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.
- lap_score-0.3.0/LICENSE +21 -0
- lap_score-0.3.0/PKG-INFO +153 -0
- lap_score-0.3.0/README.md +198 -0
- lap_score-0.3.0/lap/README.md +120 -0
- lap_score-0.3.0/lap/__init__.py +8 -0
- lap_score-0.3.0/lap/cli.py +37 -0
- lap_score-0.3.0/lap/estimate.py +115 -0
- lap_score-0.3.0/lap/examples/bookstore.openapi.json +79 -0
- lap_score-0.3.0/lap/examples/gnarly.openapi.json +68 -0
- lap_score-0.3.0/lap/examples/swagger2.json +65 -0
- lap_score-0.3.0/lap/lint.py +149 -0
- lap_score-0.3.0/lap/mcp_client.py +65 -0
- lap_score-0.3.0/lap/mcp_form.py +45 -0
- lap_score-0.3.0/lap/menu.py +108 -0
- lap_score-0.3.0/lap/openapi_ir.py +331 -0
- lap_score-0.3.0/lap/score.py +181 -0
- lap_score-0.3.0/lap/tokens.py +80 -0
- lap_score-0.3.0/lap_score.egg-info/PKG-INFO +153 -0
- lap_score-0.3.0/lap_score.egg-info/SOURCES.txt +24 -0
- lap_score-0.3.0/lap_score.egg-info/dependency_links.txt +1 -0
- lap_score-0.3.0/lap_score.egg-info/entry_points.txt +2 -0
- lap_score-0.3.0/lap_score.egg-info/requires.txt +16 -0
- lap_score-0.3.0/lap_score.egg-info/top_level.txt +1 -0
- lap_score-0.3.0/pyproject.toml +42 -0
- lap_score-0.3.0/setup.cfg +4 -0
- lap_score-0.3.0/tests/test_lap.py +315 -0
lap_score-0.3.0/LICENSE
ADDED
|
@@ -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.
|
lap_score-0.3.0/PKG-INFO
ADDED
|
@@ -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
|
+
[](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)
|