llmlatency 0.1.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.
- llmlatency-0.1.0/LICENSE +21 -0
- llmlatency-0.1.0/PKG-INFO +96 -0
- llmlatency-0.1.0/README.md +71 -0
- llmlatency-0.1.0/pyproject.toml +43 -0
- llmlatency-0.1.0/setup.cfg +4 -0
- llmlatency-0.1.0/src/llmlatency/__init__.py +33 -0
- llmlatency-0.1.0/src/llmlatency/cli.py +99 -0
- llmlatency-0.1.0/src/llmlatency/client.py +128 -0
- llmlatency-0.1.0/src/llmlatency/py.typed +0 -0
- llmlatency-0.1.0/src/llmlatency.egg-info/PKG-INFO +96 -0
- llmlatency-0.1.0/src/llmlatency.egg-info/SOURCES.txt +13 -0
- llmlatency-0.1.0/src/llmlatency.egg-info/dependency_links.txt +1 -0
- llmlatency-0.1.0/src/llmlatency.egg-info/entry_points.txt +2 -0
- llmlatency-0.1.0/src/llmlatency.egg-info/top_level.txt +1 -0
- llmlatency-0.1.0/tests/test_client.py +118 -0
llmlatency-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 llmlatency.dev
|
|
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,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: llmlatency
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Measured latency and uptime for AI inference APIs — client for the open llmlatency.dev dataset
|
|
5
|
+
Author-email: "llmlatency.dev" <bot@mail.llmlatency.dev>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://llmlatency.dev
|
|
8
|
+
Project-URL: Methodology, https://llmlatency.dev/methodology
|
|
9
|
+
Project-URL: Machine-readable API, https://llmlatency.dev/api/rankings.json
|
|
10
|
+
Project-URL: Source, https://github.com/mazamaka/llm-latency-tracker
|
|
11
|
+
Project-URL: Citable dataset (DOI), https://doi.org/10.5281/zenodo.21954788
|
|
12
|
+
Keywords: llm,latency,benchmark,ai,inference,observability,time-to-first-token,api-monitoring
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Topic :: System :: Monitoring
|
|
19
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# llmlatency
|
|
27
|
+
|
|
28
|
+
Measured latency and uptime for AI inference APIs, as a small Python client for
|
|
29
|
+
the open dataset published at **[llmlatency.dev](https://llmlatency.dev)**.
|
|
30
|
+
|
|
31
|
+
Probes run every five minutes from four regions and are never routed through a
|
|
32
|
+
gateway or an aggregator, so the numbers describe the providers themselves
|
|
33
|
+
rather than a proxy in front of them.
|
|
34
|
+
|
|
35
|
+
- **Network probe** — DNS → TCP → TLS → time to first byte.
|
|
36
|
+
- **Inference probe** — time to first token on a real completion request.
|
|
37
|
+
|
|
38
|
+
These are different quantities, an order of magnitude apart, and this library
|
|
39
|
+
never mixes them in one ranking.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install llmlatency
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
No dependencies — standard library only.
|
|
48
|
+
|
|
49
|
+
## Use it from Python
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import llmlatency
|
|
53
|
+
|
|
54
|
+
llmlatency.regions()
|
|
55
|
+
# ['ap-tokyo', 'eu-hetzner', 'sa-east', 'us-central']
|
|
56
|
+
|
|
57
|
+
llmlatency.fastest("eu-hetzner")
|
|
58
|
+
# {'rank': 1, 'provider': 'nscale', 'p50_ms': 99, 'p95_ms': 203,
|
|
59
|
+
# 'uptime_pct': 100, 'samples': 289}
|
|
60
|
+
|
|
61
|
+
for row in llmlatency.ranking("us-central")[:3]:
|
|
62
|
+
print(row["rank"], row["provider"], row["p50_ms"], "ms")
|
|
63
|
+
|
|
64
|
+
llmlatency.provider("anthropic") # one provider across every region
|
|
65
|
+
llmlatency.generated_at() # snapshot timestamp, ISO-8601 UTC
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Fetch once and pass the snapshot around if you make several queries:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
data = llmlatency.fetch()
|
|
72
|
+
fast = {r: llmlatency.fastest(r, data=data) for r in llmlatency.regions(data)}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Use it from the shell
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
llmlatency fastest # fastest provider in every region
|
|
79
|
+
llmlatency ranking --region eu-hetzner --top 5
|
|
80
|
+
llmlatency provider openai
|
|
81
|
+
llmlatency fastest --probe inference --json
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Data, methodology, licence
|
|
85
|
+
|
|
86
|
+
- Live rankings and full methodology: <https://llmlatency.dev>
|
|
87
|
+
- Machine-readable snapshot: <https://llmlatency.dev/api/rankings.json>
|
|
88
|
+
- Citable archive with a DOI: <https://doi.org/10.5281/zenodo.21954788>
|
|
89
|
+
- Daily snapshots in git: <https://github.com/mazamaka/llm-latency-tracker>
|
|
90
|
+
|
|
91
|
+
The dataset is **CC-BY-4.0**; this client library is MIT.
|
|
92
|
+
|
|
93
|
+
**Limitations, stated up front.** Vantage points are cloud data centres, not
|
|
94
|
+
consumer networks, so absolute values are lower than an end user would see — the
|
|
95
|
+
comparison between providers is the meaningful part. Provider coverage changes
|
|
96
|
+
over time as APIs appear and shut down.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# llmlatency
|
|
2
|
+
|
|
3
|
+
Measured latency and uptime for AI inference APIs, as a small Python client for
|
|
4
|
+
the open dataset published at **[llmlatency.dev](https://llmlatency.dev)**.
|
|
5
|
+
|
|
6
|
+
Probes run every five minutes from four regions and are never routed through a
|
|
7
|
+
gateway or an aggregator, so the numbers describe the providers themselves
|
|
8
|
+
rather than a proxy in front of them.
|
|
9
|
+
|
|
10
|
+
- **Network probe** — DNS → TCP → TLS → time to first byte.
|
|
11
|
+
- **Inference probe** — time to first token on a real completion request.
|
|
12
|
+
|
|
13
|
+
These are different quantities, an order of magnitude apart, and this library
|
|
14
|
+
never mixes them in one ranking.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install llmlatency
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
No dependencies — standard library only.
|
|
23
|
+
|
|
24
|
+
## Use it from Python
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
import llmlatency
|
|
28
|
+
|
|
29
|
+
llmlatency.regions()
|
|
30
|
+
# ['ap-tokyo', 'eu-hetzner', 'sa-east', 'us-central']
|
|
31
|
+
|
|
32
|
+
llmlatency.fastest("eu-hetzner")
|
|
33
|
+
# {'rank': 1, 'provider': 'nscale', 'p50_ms': 99, 'p95_ms': 203,
|
|
34
|
+
# 'uptime_pct': 100, 'samples': 289}
|
|
35
|
+
|
|
36
|
+
for row in llmlatency.ranking("us-central")[:3]:
|
|
37
|
+
print(row["rank"], row["provider"], row["p50_ms"], "ms")
|
|
38
|
+
|
|
39
|
+
llmlatency.provider("anthropic") # one provider across every region
|
|
40
|
+
llmlatency.generated_at() # snapshot timestamp, ISO-8601 UTC
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Fetch once and pass the snapshot around if you make several queries:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
data = llmlatency.fetch()
|
|
47
|
+
fast = {r: llmlatency.fastest(r, data=data) for r in llmlatency.regions(data)}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Use it from the shell
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
llmlatency fastest # fastest provider in every region
|
|
54
|
+
llmlatency ranking --region eu-hetzner --top 5
|
|
55
|
+
llmlatency provider openai
|
|
56
|
+
llmlatency fastest --probe inference --json
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Data, methodology, licence
|
|
60
|
+
|
|
61
|
+
- Live rankings and full methodology: <https://llmlatency.dev>
|
|
62
|
+
- Machine-readable snapshot: <https://llmlatency.dev/api/rankings.json>
|
|
63
|
+
- Citable archive with a DOI: <https://doi.org/10.5281/zenodo.21954788>
|
|
64
|
+
- Daily snapshots in git: <https://github.com/mazamaka/llm-latency-tracker>
|
|
65
|
+
|
|
66
|
+
The dataset is **CC-BY-4.0**; this client library is MIT.
|
|
67
|
+
|
|
68
|
+
**Limitations, stated up front.** Vantage points are cloud data centres, not
|
|
69
|
+
consumer networks, so absolute values are lower than an end user would see — the
|
|
70
|
+
comparison between providers is the meaningful part. Provider coverage changes
|
|
71
|
+
over time as APIs appear and shut down.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "llmlatency"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Measured latency and uptime for AI inference APIs — client for the open llmlatency.dev dataset"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "llmlatency.dev", email = "bot@mail.llmlatency.dev" }]
|
|
13
|
+
keywords = ["llm", "latency", "benchmark", "ai", "inference", "observability",
|
|
14
|
+
"time-to-first-token", "api-monitoring"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
21
|
+
"Topic :: System :: Monitoring",
|
|
22
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
23
|
+
"Typing :: Typed",
|
|
24
|
+
]
|
|
25
|
+
# Зависимостей нет сознательно: клиент к одному JSON-эндпоинту, тянущий за собой
|
|
26
|
+
# requests/httpx, в чужой проект просто не ставят.
|
|
27
|
+
dependencies = []
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://llmlatency.dev"
|
|
31
|
+
Methodology = "https://llmlatency.dev/methodology"
|
|
32
|
+
"Machine-readable API" = "https://llmlatency.dev/api/rankings.json"
|
|
33
|
+
Source = "https://github.com/mazamaka/llm-latency-tracker"
|
|
34
|
+
"Citable dataset (DOI)" = "https://doi.org/10.5281/zenodo.21954788"
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
llmlatency = "llmlatency.cli:main"
|
|
38
|
+
|
|
39
|
+
[tool.setuptools.packages.find]
|
|
40
|
+
where = ["src"]
|
|
41
|
+
|
|
42
|
+
[tool.setuptools.package-data]
|
|
43
|
+
llmlatency = ["py.typed"]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""llmlatency — measured latency and uptime for AI inference APIs.
|
|
2
|
+
|
|
3
|
+
Data source: https://llmlatency.dev (CC-BY-4.0).
|
|
4
|
+
Archived dataset with a DOI: https://doi.org/10.5281/zenodo.21954788
|
|
5
|
+
"""
|
|
6
|
+
from .client import (
|
|
7
|
+
API_URL,
|
|
8
|
+
PROBES,
|
|
9
|
+
LatencyError,
|
|
10
|
+
fastest,
|
|
11
|
+
fetch,
|
|
12
|
+
generated_at,
|
|
13
|
+
provider,
|
|
14
|
+
providers,
|
|
15
|
+
ranking,
|
|
16
|
+
regions,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__version__ = "0.1.0"
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"API_URL",
|
|
23
|
+
"PROBES",
|
|
24
|
+
"LatencyError",
|
|
25
|
+
"__version__",
|
|
26
|
+
"fastest",
|
|
27
|
+
"fetch",
|
|
28
|
+
"generated_at",
|
|
29
|
+
"provider",
|
|
30
|
+
"providers",
|
|
31
|
+
"ranking",
|
|
32
|
+
"regions",
|
|
33
|
+
]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Командная строка: `llmlatency fastest`, `ranking`, `provider`, `regions`.
|
|
2
|
+
|
|
3
|
+
CLI существует не для красоты: большинство обращений к таким данным — разовые
|
|
4
|
+
(«кто сейчас быстрее в Европе»), и ради них никто не пишет скрипт. Одна команда
|
|
5
|
+
в терминале закрывает сценарий целиком.
|
|
6
|
+
|
|
7
|
+
Сеть дёргается РОВНО ОДИН РАЗ за запуск: снимок скачивается в `main()` и дальше
|
|
8
|
+
передаётся в функции. Иначе `provider` с четырьмя регионами сходил бы за одним и
|
|
9
|
+
тем же JSON четыре раза.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from . import __version__
|
|
19
|
+
from .client import (
|
|
20
|
+
LatencyError,
|
|
21
|
+
fastest,
|
|
22
|
+
fetch,
|
|
23
|
+
generated_at,
|
|
24
|
+
provider,
|
|
25
|
+
providers,
|
|
26
|
+
ranking,
|
|
27
|
+
regions,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _row(r: dict[str, Any]) -> str:
|
|
32
|
+
return (f"{r.get('rank', '?'):>3}. {r.get('provider', '?'):<18} "
|
|
33
|
+
f"p50 {r.get('p50_ms', '?'):>6} ms p95 {r.get('p95_ms', '?'):>6} ms "
|
|
34
|
+
f"uptime {r.get('uptime_pct', '?')}% n={r.get('samples', '?')}")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main(argv: list[str] | None = None) -> int:
|
|
38
|
+
ap = argparse.ArgumentParser(
|
|
39
|
+
prog="llmlatency",
|
|
40
|
+
description="Measured latency and uptime for AI inference APIs "
|
|
41
|
+
"(data: https://llmlatency.dev, CC-BY-4.0)")
|
|
42
|
+
ap.add_argument("--version", action="version", version=f"llmlatency {__version__}")
|
|
43
|
+
ap.add_argument("--json", action="store_true", help="print raw JSON instead of a table")
|
|
44
|
+
ap.add_argument("--probe", default="network", choices=("network", "inference"),
|
|
45
|
+
help="network = time to first byte, inference = time to first token")
|
|
46
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
47
|
+
|
|
48
|
+
p_fast = sub.add_parser("fastest", help="fastest provider per region")
|
|
49
|
+
p_fast.add_argument("--region", help="limit to one region (default: all)")
|
|
50
|
+
|
|
51
|
+
p_rank = sub.add_parser("ranking", help="full ranking for a region")
|
|
52
|
+
p_rank.add_argument("--region", required=True)
|
|
53
|
+
p_rank.add_argument("--top", type=int, default=10, help="how many rows (default 10)")
|
|
54
|
+
|
|
55
|
+
p_prov = sub.add_parser("provider", help="one provider across all regions")
|
|
56
|
+
p_prov.add_argument("name")
|
|
57
|
+
|
|
58
|
+
sub.add_parser("regions", help="list region ids")
|
|
59
|
+
sub.add_parser("providers", help="list provider names")
|
|
60
|
+
|
|
61
|
+
args = ap.parse_args(argv)
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
data = fetch()
|
|
65
|
+
except LatencyError as exc:
|
|
66
|
+
print(f"llmlatency: {exc}", file=sys.stderr)
|
|
67
|
+
return 2
|
|
68
|
+
|
|
69
|
+
if args.cmd == "regions":
|
|
70
|
+
out: Any = regions(data)
|
|
71
|
+
elif args.cmd == "providers":
|
|
72
|
+
out = providers(data)
|
|
73
|
+
elif args.cmd == "ranking":
|
|
74
|
+
out = ranking(args.region, args.probe, data)[: args.top]
|
|
75
|
+
elif args.cmd == "provider":
|
|
76
|
+
out = provider(args.name, args.probe, data)
|
|
77
|
+
else: # fastest
|
|
78
|
+
targets = [args.region] if args.region else regions(data)
|
|
79
|
+
out = {reg: fastest(reg, args.probe, data) for reg in targets}
|
|
80
|
+
|
|
81
|
+
if args.json:
|
|
82
|
+
print(json.dumps(out, ensure_ascii=False, indent=1))
|
|
83
|
+
return 0
|
|
84
|
+
|
|
85
|
+
print(f"llmlatency.dev — snapshot {generated_at(data)} · probe: {args.probe}\n")
|
|
86
|
+
if args.cmd in ("regions", "providers"):
|
|
87
|
+
print("\n".join(out))
|
|
88
|
+
elif args.cmd == "ranking":
|
|
89
|
+
print("\n".join(_row(r) for r in out) or "no data for this region/probe")
|
|
90
|
+
else:
|
|
91
|
+
for reg, row in out.items():
|
|
92
|
+
# Регион без замера печатается явным «—», а не пропускается: молчание
|
|
93
|
+
# читается как «данных нет вообще», хотя причина в отсутствии пробы.
|
|
94
|
+
print(f"{reg:<12} {_row(row) if row else '— (not measured)'}")
|
|
95
|
+
return 0
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
if __name__ == "__main__": # pragma: no cover
|
|
99
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Чтение публичного снимка замеров llmlatency.dev.
|
|
2
|
+
|
|
3
|
+
Зачем пакет вообще существует. У проекта есть открытый JSON с рейтингами, но
|
|
4
|
+
чтобы им пользоваться, каждый повторяет одно и то же: сходить по URL, разобрать
|
|
5
|
+
вложенные словари, вспомнить, что `network` и `inference` — разные величины.
|
|
6
|
+
Пакет закрывает эту рутину и заодно даёт стабильную точку входа, на которую
|
|
7
|
+
можно сослаться из чужого кода.
|
|
8
|
+
|
|
9
|
+
Зависимостей нет намеренно: клиент к одному JSON-эндпоинту, который тянет за
|
|
10
|
+
собой httpx или requests, в чужой проект не ставят — а значит и не используют.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import urllib.error
|
|
16
|
+
import urllib.request
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
API_URL = "https://llmlatency.dev/api/rankings.json"
|
|
20
|
+
USER_AGENT = "llmlatency-python/0.1.0 (+https://llmlatency.dev)"
|
|
21
|
+
|
|
22
|
+
#: Тип пробы. `network` — DNS→TCP→TLS→первый байт, `inference` — время до
|
|
23
|
+
#: первого токена на настоящем запросе. Величины отличаются на порядок, поэтому
|
|
24
|
+
#: они разведены по разным спискам и никогда не смешиваются в одном рейтинге.
|
|
25
|
+
PROBES = ("network", "inference")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class LatencyError(RuntimeError):
|
|
29
|
+
"""Data could not be fetched or parsed."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def fetch(url: str = API_URL, timeout: float = 10.0) -> dict[str, Any]:
|
|
33
|
+
"""Download the current snapshot as a plain dict.
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
LatencyError: the endpoint is unreachable or does not return JSON.
|
|
37
|
+
"""
|
|
38
|
+
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT,
|
|
39
|
+
"Accept": "application/json"})
|
|
40
|
+
try:
|
|
41
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
42
|
+
payload = resp.read()
|
|
43
|
+
except (urllib.error.URLError, TimeoutError, OSError) as exc:
|
|
44
|
+
raise LatencyError(f"cannot reach {url}: {exc}") from exc
|
|
45
|
+
try:
|
|
46
|
+
data = json.loads(payload)
|
|
47
|
+
except ValueError as exc:
|
|
48
|
+
raise LatencyError(f"{url} did not return JSON") from exc
|
|
49
|
+
if not isinstance(data, dict) or "regions" not in data:
|
|
50
|
+
raise LatencyError(f"{url} returned an unexpected shape")
|
|
51
|
+
return data
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def regions(data: dict[str, Any] | None = None) -> list[str]:
|
|
55
|
+
"""Region ids present in the snapshot, sorted."""
|
|
56
|
+
data = data if data is not None else fetch()
|
|
57
|
+
return sorted(data.get("regions", {}))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def ranking(region: str, probe: str = "network",
|
|
61
|
+
data: dict[str, Any] | None = None) -> list[dict[str, Any]]:
|
|
62
|
+
"""Ranked providers for one region, fastest first.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
region: region id, e.g. ``eu-hetzner``. See :func:`regions`.
|
|
66
|
+
probe: ``network`` (time to first byte) or ``inference`` (time to first
|
|
67
|
+
token). They are different quantities and are never mixed.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
A list of dicts with ``rank``, ``provider``, ``p50_ms``, ``p95_ms``,
|
|
71
|
+
``uptime_pct`` and ``samples``. Empty if the region has no such probe.
|
|
72
|
+
"""
|
|
73
|
+
if probe not in PROBES:
|
|
74
|
+
raise ValueError(f"probe must be one of {PROBES}, got {probe!r}")
|
|
75
|
+
data = data if data is not None else fetch()
|
|
76
|
+
block = data.get("regions", {}).get(region)
|
|
77
|
+
if block is None:
|
|
78
|
+
known = ", ".join(sorted(data.get("regions", {}))) or "none"
|
|
79
|
+
raise LatencyError(f"unknown region {region!r} (known: {known})")
|
|
80
|
+
rows = block.get(probe) or []
|
|
81
|
+
# Порядок из API уже правильный, но полагаться на него нельзя: снимок
|
|
82
|
+
# собирается несколькими задачами, и «отсортировано» — не гарантия формата.
|
|
83
|
+
return sorted(rows, key=lambda r: r.get("rank", 10**6))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def fastest(region: str, probe: str = "network",
|
|
87
|
+
data: dict[str, Any] | None = None) -> dict[str, Any] | None:
|
|
88
|
+
"""The single fastest provider in a region, or ``None`` if unmeasured."""
|
|
89
|
+
rows = ranking(region, probe, data)
|
|
90
|
+
return rows[0] if rows else None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def providers(data: dict[str, Any] | None = None) -> list[str]:
|
|
94
|
+
"""Every provider name that appears anywhere in the snapshot, sorted."""
|
|
95
|
+
data = data if data is not None else fetch()
|
|
96
|
+
names: set[str] = set()
|
|
97
|
+
for block in data.get("regions", {}).values():
|
|
98
|
+
for probe in PROBES:
|
|
99
|
+
for row in block.get(probe) or []:
|
|
100
|
+
name = row.get("provider")
|
|
101
|
+
if name:
|
|
102
|
+
names.add(name)
|
|
103
|
+
return sorted(names)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def provider(name: str, probe: str = "network",
|
|
107
|
+
data: dict[str, Any] | None = None) -> dict[str, dict[str, Any]]:
|
|
108
|
+
"""How one provider performs across all regions.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Mapping ``region id -> measurement row``. Regions where the provider was
|
|
112
|
+
not measured are omitted rather than filled with zeros: a missing probe
|
|
113
|
+
and a slow probe must not look the same to the caller.
|
|
114
|
+
"""
|
|
115
|
+
data = data if data is not None else fetch()
|
|
116
|
+
out: dict[str, dict[str, Any]] = {}
|
|
117
|
+
for region in data.get("regions", {}):
|
|
118
|
+
for row in ranking(region, probe, data):
|
|
119
|
+
if row.get("provider") == name:
|
|
120
|
+
out[region] = row
|
|
121
|
+
break
|
|
122
|
+
return out
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def generated_at(data: dict[str, Any] | None = None) -> str:
|
|
126
|
+
"""Timestamp of the snapshot (ISO-8601, UTC)."""
|
|
127
|
+
data = data if data is not None else fetch()
|
|
128
|
+
return str(data.get("generated", ""))
|
|
File without changes
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: llmlatency
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Measured latency and uptime for AI inference APIs — client for the open llmlatency.dev dataset
|
|
5
|
+
Author-email: "llmlatency.dev" <bot@mail.llmlatency.dev>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://llmlatency.dev
|
|
8
|
+
Project-URL: Methodology, https://llmlatency.dev/methodology
|
|
9
|
+
Project-URL: Machine-readable API, https://llmlatency.dev/api/rankings.json
|
|
10
|
+
Project-URL: Source, https://github.com/mazamaka/llm-latency-tracker
|
|
11
|
+
Project-URL: Citable dataset (DOI), https://doi.org/10.5281/zenodo.21954788
|
|
12
|
+
Keywords: llm,latency,benchmark,ai,inference,observability,time-to-first-token,api-monitoring
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Topic :: System :: Monitoring
|
|
19
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# llmlatency
|
|
27
|
+
|
|
28
|
+
Measured latency and uptime for AI inference APIs, as a small Python client for
|
|
29
|
+
the open dataset published at **[llmlatency.dev](https://llmlatency.dev)**.
|
|
30
|
+
|
|
31
|
+
Probes run every five minutes from four regions and are never routed through a
|
|
32
|
+
gateway or an aggregator, so the numbers describe the providers themselves
|
|
33
|
+
rather than a proxy in front of them.
|
|
34
|
+
|
|
35
|
+
- **Network probe** — DNS → TCP → TLS → time to first byte.
|
|
36
|
+
- **Inference probe** — time to first token on a real completion request.
|
|
37
|
+
|
|
38
|
+
These are different quantities, an order of magnitude apart, and this library
|
|
39
|
+
never mixes them in one ranking.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install llmlatency
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
No dependencies — standard library only.
|
|
48
|
+
|
|
49
|
+
## Use it from Python
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import llmlatency
|
|
53
|
+
|
|
54
|
+
llmlatency.regions()
|
|
55
|
+
# ['ap-tokyo', 'eu-hetzner', 'sa-east', 'us-central']
|
|
56
|
+
|
|
57
|
+
llmlatency.fastest("eu-hetzner")
|
|
58
|
+
# {'rank': 1, 'provider': 'nscale', 'p50_ms': 99, 'p95_ms': 203,
|
|
59
|
+
# 'uptime_pct': 100, 'samples': 289}
|
|
60
|
+
|
|
61
|
+
for row in llmlatency.ranking("us-central")[:3]:
|
|
62
|
+
print(row["rank"], row["provider"], row["p50_ms"], "ms")
|
|
63
|
+
|
|
64
|
+
llmlatency.provider("anthropic") # one provider across every region
|
|
65
|
+
llmlatency.generated_at() # snapshot timestamp, ISO-8601 UTC
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Fetch once and pass the snapshot around if you make several queries:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
data = llmlatency.fetch()
|
|
72
|
+
fast = {r: llmlatency.fastest(r, data=data) for r in llmlatency.regions(data)}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Use it from the shell
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
llmlatency fastest # fastest provider in every region
|
|
79
|
+
llmlatency ranking --region eu-hetzner --top 5
|
|
80
|
+
llmlatency provider openai
|
|
81
|
+
llmlatency fastest --probe inference --json
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Data, methodology, licence
|
|
85
|
+
|
|
86
|
+
- Live rankings and full methodology: <https://llmlatency.dev>
|
|
87
|
+
- Machine-readable snapshot: <https://llmlatency.dev/api/rankings.json>
|
|
88
|
+
- Citable archive with a DOI: <https://doi.org/10.5281/zenodo.21954788>
|
|
89
|
+
- Daily snapshots in git: <https://github.com/mazamaka/llm-latency-tracker>
|
|
90
|
+
|
|
91
|
+
The dataset is **CC-BY-4.0**; this client library is MIT.
|
|
92
|
+
|
|
93
|
+
**Limitations, stated up front.** Vantage points are cloud data centres, not
|
|
94
|
+
consumer networks, so absolute values are lower than an end user would see — the
|
|
95
|
+
comparison between providers is the meaningful part. Provider coverage changes
|
|
96
|
+
over time as APIs appear and shut down.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/llmlatency/__init__.py
|
|
5
|
+
src/llmlatency/cli.py
|
|
6
|
+
src/llmlatency/client.py
|
|
7
|
+
src/llmlatency/py.typed
|
|
8
|
+
src/llmlatency.egg-info/PKG-INFO
|
|
9
|
+
src/llmlatency.egg-info/SOURCES.txt
|
|
10
|
+
src/llmlatency.egg-info/dependency_links.txt
|
|
11
|
+
src/llmlatency.egg-info/entry_points.txt
|
|
12
|
+
src/llmlatency.egg-info/top_level.txt
|
|
13
|
+
tests/test_client.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
llmlatency
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Тесты клиента. Сеть НЕ дёргается: снимок подставляется параметром `data`.
|
|
2
|
+
|
|
3
|
+
Тест, ходящий в интернет, падает от чужого таймаута и приучает игнорировать
|
|
4
|
+
красное — а именно эти тесты защищают публичный пакет, который чинить придётся
|
|
5
|
+
новой версией на PyPI.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
import llmlatency
|
|
12
|
+
from llmlatency import cli
|
|
13
|
+
from llmlatency.client import LatencyError
|
|
14
|
+
|
|
15
|
+
SNAPSHOT = {
|
|
16
|
+
"generated": "2026-08-15T19:42:56Z",
|
|
17
|
+
"unit": "milliseconds",
|
|
18
|
+
"regions": {
|
|
19
|
+
"eu-hetzner": {
|
|
20
|
+
"label": "Europe (Hetzner)",
|
|
21
|
+
# Порядок намеренно перепутан: клиент обязан сортировать сам.
|
|
22
|
+
"network": [
|
|
23
|
+
{"rank": 2, "provider": "openai", "p50_ms": 140, "p95_ms": 300,
|
|
24
|
+
"uptime_pct": 100, "samples": 280},
|
|
25
|
+
{"rank": 1, "provider": "nscale", "p50_ms": 99, "p95_ms": 203,
|
|
26
|
+
"uptime_pct": 100, "samples": 289},
|
|
27
|
+
],
|
|
28
|
+
"inference": [
|
|
29
|
+
{"rank": 1, "provider": "cerebras", "p50_ms": 3100, "p95_ms": 5200,
|
|
30
|
+
"uptime_pct": 99, "samples": 96},
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
"us-central": {
|
|
34
|
+
"label": "US Central",
|
|
35
|
+
"network": [
|
|
36
|
+
{"rank": 1, "provider": "openai", "p50_ms": 88, "p95_ms": 150,
|
|
37
|
+
"uptime_pct": 100, "samples": 288},
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_ranking_is_sorted_by_rank_not_by_api_order():
|
|
45
|
+
"""Клиент не доверяет порядку из API.
|
|
46
|
+
|
|
47
|
+
Снимок собирается несколькими задачами; «отсортировано» — свойство текущей
|
|
48
|
+
реализации сервера, а не контракт. Положись на него — и однажды `fastest()`
|
|
49
|
+
молча вернёт второго по скорости.
|
|
50
|
+
"""
|
|
51
|
+
rows = llmlatency.ranking("eu-hetzner", data=SNAPSHOT)
|
|
52
|
+
assert [r["provider"] for r in rows] == ["nscale", "openai"]
|
|
53
|
+
assert llmlatency.fastest("eu-hetzner", data=SNAPSHOT)["provider"] == "nscale"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_network_and_inference_never_share_a_ranking():
|
|
57
|
+
"""TTFB и TTFT отличаются на порядок; общий рейтинг — правдоподобная чушь."""
|
|
58
|
+
net = llmlatency.ranking("eu-hetzner", "network", data=SNAPSHOT)
|
|
59
|
+
inf = llmlatency.ranking("eu-hetzner", "inference", data=SNAPSHOT)
|
|
60
|
+
assert {r["provider"] for r in net}.isdisjoint({r["provider"] for r in inf})
|
|
61
|
+
assert max(r["p50_ms"] for r in net) < min(r["p50_ms"] for r in inf)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_missing_probe_returns_empty_not_error():
|
|
65
|
+
"""У `us-central` нет inference-проб — это пустой список, а не исключение.
|
|
66
|
+
|
|
67
|
+
Регион без пробы — нормальное состояние снимка (провайдеры появляются и
|
|
68
|
+
исчезают). Падать здесь значило бы ломать чужой цикл по регионам.
|
|
69
|
+
"""
|
|
70
|
+
assert llmlatency.ranking("us-central", "inference", data=SNAPSHOT) == []
|
|
71
|
+
assert llmlatency.fastest("us-central", "inference", data=SNAPSHOT) is None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_unknown_region_names_the_known_ones():
|
|
75
|
+
"""Ошибка обязана перечислить доступные регионы: иначе догадываться по докам."""
|
|
76
|
+
with pytest.raises(LatencyError) as exc:
|
|
77
|
+
llmlatency.ranking("mars-1", data=SNAPSHOT)
|
|
78
|
+
assert "eu-hetzner" in str(exc.value)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_unknown_probe_is_rejected_up_front():
|
|
82
|
+
with pytest.raises(ValueError):
|
|
83
|
+
llmlatency.ranking("eu-hetzner", "ttfb", data=SNAPSHOT)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_provider_omits_regions_without_measurement():
|
|
87
|
+
"""Пропуск региона и «медленно» не должны выглядеть одинаково у вызывающего."""
|
|
88
|
+
out = llmlatency.provider("cerebras", "network", data=SNAPSHOT)
|
|
89
|
+
assert out == {}, "cerebras меряли только inference — сетевых строк быть не должно"
|
|
90
|
+
|
|
91
|
+
out = llmlatency.provider("openai", data=SNAPSHOT)
|
|
92
|
+
assert set(out) == {"eu-hetzner", "us-central"}
|
|
93
|
+
assert out["us-central"]["p50_ms"] == 88
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_providers_collects_names_from_all_regions_and_probes():
|
|
97
|
+
assert llmlatency.providers(SNAPSHOT) == ["cerebras", "nscale", "openai"]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_cli_prints_a_table_without_touching_the_network(monkeypatch, capsys):
|
|
101
|
+
"""CLI обязан ходить в сеть РОВНО один раз за запуск.
|
|
102
|
+
|
|
103
|
+
`provider` перебирает регионы; если бы снимок качался внутри каждой функции,
|
|
104
|
+
одна команда дала бы четыре запроса к сайту.
|
|
105
|
+
"""
|
|
106
|
+
calls = {"n": 0}
|
|
107
|
+
|
|
108
|
+
def fake_fetch(*a, **kw):
|
|
109
|
+
calls["n"] += 1
|
|
110
|
+
return SNAPSHOT
|
|
111
|
+
|
|
112
|
+
monkeypatch.setattr(cli, "fetch", fake_fetch)
|
|
113
|
+
rc = cli.main(["fastest"])
|
|
114
|
+
out = capsys.readouterr().out
|
|
115
|
+
|
|
116
|
+
assert rc == 0
|
|
117
|
+
assert calls["n"] == 1, "снимок обязан скачиваться один раз за запуск"
|
|
118
|
+
assert "nscale" in out and "us-central" in out
|