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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,2 @@
1
+ [console_scripts]
2
+ llmlatency = llmlatency.cli:main
@@ -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