All numbers below are generated by benchmarks/suite/run.py and read
from benchmarks/RESULTS.json — nothing in this file is hand-typed.
- Machine: Intel(R) Core(TM) i7-1065G7 CPU @ 1.30GHz (8 CPUs), Linux 6.8.0-124-generic
- Python: 3.13.3 · uv 0.9.26 · run at 2026-06-11T02:54:35+00:00
- Each competitor is installed into its own scratch venv under
/tmp/lm15-bench-venvs/(uv venv+uv pip install); lm15 is installed from the locally built wheel. Failures are recorded and the suite continues. - Cold import = wall time of
python -c "import X"in a fresh process, best of 10 (after one warmup run to populate the page cache and.pycfiles). A barepython -c passbaseline is shown for scale. - Install size = bytes added to
site-packagesvs an empty venv, measured after byte-compilation (i.e. what sits on disk after first use). Transitive deps = installed package count minus the package itself. - Import RSS = median
VmRSSafter the import minus a bare interpreter in the same venv. - Hot path (lm15 only) = median ns/op over 2000
timed iterations after 200 warmup
iterations, in-process. Response bodies and the SSE transcript are
real pinned captures from
lm15-contract/bodies/. - TTFR tax = time-to-first-byte of a streaming generate request to
local ollama, lm15's
StdlibTransportvs rawurllib.request, identical request bytes, interleaved runs, median of N. - Steady state = per-call wall time of repeated tiny non-streaming
chat completions (
max_tokens=4): one reused lm15 adapter (pooled keep-alive transport) vs rawurllib.requestopening a fresh connection — and on Groq a fresh TLS handshake — every call; 1 warmup pair then 10 interleaved pairs, medians. - Concurrency = N tiny calls to local ollama:
AsyncOpenAIChatLMunder oneasyncio.gathervs the sync adapter run sequentially;threading.active_count()recorded around the gather proves no hidden thread pool. - Live = one Gemini Live WebSocket session (optional
websocketsextra): connect+setup, then one tiny text turn timed to the first server event and to turn end. Best-effort; skips recorded honestly.
| package | install size | transitive deps | cold import | import RSS |
|---|---|---|---|---|
| lm15 | 0.5 MiB | 0 | 152 ms | 16.6 MiB |
| openai | 18.0 MiB | 15 | 468 ms | 35.3 MiB |
| anthropic | 17.1 MiB | 15 | 589 ms | 41.2 MiB |
| google-genai | 37.2 MiB | 24 | 934 ms | 60.8 MiB |
| litellm | 133.0 MiB | 54 | 2298 ms | 161.0 MiB |
| langchain-openai | 63.3 MiB | 35 | 930 ms | 61.0 MiB |
Bare interpreter start (python -c pass): 15 ms — subtract it from every
row to get the library's own share of the cold import.
| operation | median | ops/sec |
|---|---|---|
| build_request (anthropic) | 6.8 µs | 146,951 |
| build_request (gemini) | 6.7 µs | 150,116 |
| build_request (openai) | 12.0 µs | 83,029 |
| build_request (openai_chat) | 5.9 µs | 169,162 |
| parse_response (anthropic, pinned body) | 21.8 µs | 45,845 |
| parse_response (gemini, pinned body) | 24.9 µs | 40,144 |
| parse_response (openai, pinned body) | 37.0 µs | 27,050 |
| parse_response (openai_chat, pinned body) | 22.3 µs | 44,926 |
| Request serde round-trip (1804 B wire) | 139.9 µs | 7,146 |
| stream pipeline (anthropic.streaming, 6 events) | 54.7 µs/transcript | 109,778 events/s |
Model qwen3.5:0.8b, median of 9 interleaved runs:
| client | TTFB | total |
|---|---|---|
| lm15 StdlibTransport | 589.6 ms | 1302.3 ms |
| raw urllib.request | 592.4 ms | 1313.2 ms |
lm15 transport tax over raw stdlib HTTP: -2.8 ms TTFB. Run-to-run TTFB spread: lm15 38 ms, urllib 54 ms — the model server's own jitter dwarfs any difference between the two clients; the tax is indistinguishable from zero.
Per-call wall time of repeated tiny chat completions. lm15 reuses
one pooled keep-alive connection; raw urllib.request opens a new
connection (and on Groq, a new TLS handshake) for every call.
Groq (remote, TLS) — model llama-3.1-8b-instant, median of 10 interleaved pairs:
| client | per-call wall time |
|---|---|
| lm15 (pooled keep-alive) | 99.4 ms |
| raw urllib (new connection per call) | 177.6 ms |
Delta: -78.3 ms per call (negative = lm15 faster). Spread: lm15 328 ms, urllib 614 ms.
ollama (localhost, plain HTTP) — model qwen3.5:0.8b, median of 10 interleaved pairs:
| client | per-call wall time |
|---|---|
| lm15 (pooled keep-alive) | 821.7 ms |
| raw urllib (new connection per call) | 833.7 ms |
Delta: -12.0 ms per call (negative = lm15 faster). Spread: lm15 355 ms, urllib 292 ms.
At steady state lm15 is 78 ms/call faster than raw HTTP against Groq — connection pooling amortizes the TLS handshake that urllib pays on every call.
50 tiny calls (max_tokens=4) to qwen3.5:0.8b:
| mode | wall time | calls/sec |
|---|---|---|
| AsyncOpenAIChatLM, asyncio.gather | 38.17 s | 1.3 |
| OpenAIChatLM, sequential | 40.98 s | 1.2 |
Speedup: 1.07x. Thread count across the gather: unchanged (3 before, 3 after) — the overlap is pure asyncio sockets, not a hidden thread pool.
Model gemini-3.1-flash-live-preview, one WebSocket session, one tiny text turn:
| phase | time |
|---|---|
| connect + setupComplete | 273 ms |
| text turn -> first server event | 452 ms |
| text turn -> turn complete | 1168 ms |
- Zero runtime dependencies. The whole library is stdlib-only
(
websocketsis one optional extra for live sessions). There is no pydantic, no httpx/requests, no certifi chain to install, resolve, or import. - No pydantic. Canonical types are plain
dataclasses with exact, hand-written serde — no metaclass machinery or validators executing at import time or per call. - Stdlib transport. HTTP goes through
http.client/urllib-level plumbing the interpreter already loaded, so the wire layer adds essentially nothing over raw stdlib HTTP.