modelalive 0.1.0__py3-none-any.whl

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.
modelalive/__init__.py ADDED
@@ -0,0 +1,15 @@
1
+ """Model Alive — pre-flight check for LLM model IDs."""
2
+
3
+ from modelalive.check import alive, check, resolve
4
+ from modelalive.exceptions import ModelDeprecatedError, ModelRetiredError
5
+ from modelalive.types import AliveResult
6
+
7
+ __all__ = [
8
+ "alive",
9
+ "check",
10
+ "resolve",
11
+ "AliveResult",
12
+ "ModelRetiredError",
13
+ "ModelDeprecatedError",
14
+ ]
15
+ __version__ = "0.1.0"
modelalive/check.py ADDED
@@ -0,0 +1,125 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import date
4
+ from pathlib import Path
5
+
6
+ from modelalive.exceptions import ModelDeprecatedError, ModelRetiredError
7
+ from modelalive.registry import days_until, get_model_entry, registry_version
8
+ from modelalive.types import AliveResult
9
+
10
+
11
+ def alive(
12
+ model: str,
13
+ *,
14
+ registry_path: str | Path | None = None,
15
+ today: date | None = None,
16
+ ) -> AliveResult:
17
+ """Return lifecycle status for a model ID without raising."""
18
+ entry = get_model_entry(model, registry_path=registry_path)
19
+ version = registry_version(registry_path=registry_path)
20
+
21
+ if entry is None:
22
+ return AliveResult(
23
+ model=model,
24
+ alive=True,
25
+ status="unknown",
26
+ message="Model not in registry — assumed alive. Register at modelalive.dev or pin a known ID.",
27
+ registry_version=version,
28
+ )
29
+
30
+ status = entry.get("status", "unknown")
31
+ retired_at = entry.get("retired_at")
32
+ days_left = days_until(retired_at, today=today)
33
+
34
+ if status == "retired":
35
+ return AliveResult(
36
+ model=model,
37
+ alive=False,
38
+ status="retired",
39
+ provider=entry.get("provider"),
40
+ deprecated_at=entry.get("deprecated_at"),
41
+ retired_at=retired_at,
42
+ replacement=entry.get("replacement"),
43
+ breaking_changes=list(entry.get("breaking_changes") or []),
44
+ migrate_url=entry.get("migrate_url"),
45
+ days_until_retirement=days_left,
46
+ message=_retired_message(model, entry),
47
+ registry_version=version,
48
+ )
49
+
50
+ if status == "deprecated":
51
+ return AliveResult(
52
+ model=model,
53
+ alive=True,
54
+ status="deprecated",
55
+ provider=entry.get("provider"),
56
+ deprecated_at=entry.get("deprecated_at"),
57
+ retired_at=retired_at,
58
+ replacement=entry.get("replacement"),
59
+ breaking_changes=list(entry.get("breaking_changes") or []),
60
+ migrate_url=entry.get("migrate_url"),
61
+ days_until_retirement=days_left,
62
+ message=_deprecated_message(model, entry, days_left),
63
+ registry_version=version,
64
+ )
65
+
66
+ return AliveResult(
67
+ model=model,
68
+ alive=True,
69
+ status="active",
70
+ provider=entry.get("provider"),
71
+ replacement=entry.get("replacement"),
72
+ breaking_changes=list(entry.get("breaking_changes") or []),
73
+ migrate_url=entry.get("migrate_url"),
74
+ registry_version=version,
75
+ )
76
+
77
+
78
+ def check(
79
+ model: str,
80
+ *,
81
+ warn_deprecated: bool = False,
82
+ registry_path: str | Path | None = None,
83
+ today: date | None = None,
84
+ ) -> AliveResult:
85
+ """Check model lifecycle; raise on retired (and optionally deprecated)."""
86
+ result = alive(model, registry_path=registry_path, today=today)
87
+
88
+ if result.status == "retired":
89
+ raise ModelRetiredError(result)
90
+ if warn_deprecated and result.status == "deprecated":
91
+ raise ModelDeprecatedError(result)
92
+ return result
93
+
94
+
95
+ def resolve(
96
+ model: str,
97
+ *,
98
+ registry_path: str | Path | None = None,
99
+ today: date | None = None,
100
+ ) -> str:
101
+ """Return replacement model ID if retired/deprecated, else the original."""
102
+ result = alive(model, registry_path=registry_path, today=today)
103
+ if result.replacement:
104
+ return result.replacement
105
+ return model
106
+
107
+
108
+ def _retired_message(model: str, entry: dict) -> str:
109
+ replacement = entry.get("replacement")
110
+ if replacement:
111
+ return f"Model '{model}' was retired. Use '{replacement}' instead."
112
+ return f"Model '{model}' was retired."
113
+
114
+
115
+ def _deprecated_message(model: str, entry: dict, days_left: int | None) -> str:
116
+ replacement = entry.get("replacement")
117
+ retired_at = entry.get("retired_at")
118
+ parts = [f"Model '{model}' is deprecated."]
119
+ if retired_at:
120
+ parts.append(f"Retires on {retired_at}.")
121
+ if days_left is not None and days_left >= 0:
122
+ parts.append(f"{days_left} day(s) remaining.")
123
+ if replacement:
124
+ parts.append(f"Migrate to '{replacement}'.")
125
+ return " ".join(parts)
modelalive/cli.py ADDED
@@ -0,0 +1,64 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+
7
+ from modelalive.check import alive, check, resolve
8
+ from modelalive.exceptions import ModelDeprecatedError, ModelRetiredError
9
+
10
+
11
+ def main(argv: list[str] | None = None) -> int:
12
+ parser = argparse.ArgumentParser(
13
+ prog="modelalive",
14
+ description="Pre-flight check: is this LLM model ID still alive?",
15
+ )
16
+ sub = parser.add_subparsers(dest="command", required=True)
17
+
18
+ check_cmd = sub.add_parser("check", help="Check a model ID")
19
+ check_cmd.add_argument("model", help="Model ID to check")
20
+ check_cmd.add_argument(
21
+ "--warn-deprecated",
22
+ action="store_true",
23
+ help="Exit non-zero if model is deprecated",
24
+ )
25
+ check_cmd.add_argument("--json", action="store_true", help="Output JSON")
26
+
27
+ resolve_cmd = sub.add_parser("resolve", help="Return best model ID to use")
28
+ resolve_cmd.add_argument("model", help="Model ID to resolve")
29
+
30
+ args = parser.parse_args(argv)
31
+
32
+ if args.command == "check":
33
+ try:
34
+ result = check(args.model, warn_deprecated=args.warn_deprecated)
35
+ except (ModelRetiredError, ModelDeprecatedError) as exc:
36
+ result = exc.result
37
+ if args.json:
38
+ print(json.dumps(result.to_dict(), indent=2))
39
+ else:
40
+ print(result.message or result.model, file=sys.stderr)
41
+ return 1
42
+
43
+ if args.json:
44
+ print(json.dumps(result.to_dict(), indent=2))
45
+ else:
46
+ status = "ALIVE" if result.alive else "DEAD"
47
+ print(f"{status}: {result.model} ({result.status})")
48
+ if result.replacement:
49
+ print(f" replacement: {result.replacement}")
50
+ if result.breaking_changes:
51
+ print(" breaking_changes:")
52
+ for change in result.breaking_changes:
53
+ print(f" - {change}")
54
+ return 0
55
+
56
+ if args.command == "resolve":
57
+ print(resolve(args.model))
58
+ return 0
59
+
60
+ return 2
61
+
62
+
63
+ if __name__ == "__main__":
64
+ raise SystemExit(main())
@@ -0,0 +1,128 @@
1
+ {
2
+ "version": "2026-06-28",
3
+ "models": {
4
+ "claude-sonnet-4-20250514": {
5
+ "provider": "anthropic",
6
+ "status": "retired",
7
+ "deprecated_at": "2026-04-14",
8
+ "retired_at": "2026-06-15",
9
+ "replacement": "claude-sonnet-4-6",
10
+ "breaking_changes": [],
11
+ "migrate_url": "https://platform.claude.com/docs/en/about-claude/model-deprecations"
12
+ },
13
+ "claude-opus-4-20250514": {
14
+ "provider": "anthropic",
15
+ "status": "retired",
16
+ "deprecated_at": "2026-04-14",
17
+ "retired_at": "2026-06-15",
18
+ "replacement": "claude-opus-4-8",
19
+ "breaking_changes": [
20
+ "temperature, top_p, and top_k are rejected on Claude Opus 4.7+ — remove these parameters"
21
+ ],
22
+ "migrate_url": "https://platform.claude.com/docs/en/about-claude/model-deprecations"
23
+ },
24
+ "claude-opus-4-1-20250805": {
25
+ "provider": "anthropic",
26
+ "status": "deprecated",
27
+ "deprecated_at": "2026-06-05",
28
+ "retired_at": "2026-08-05",
29
+ "replacement": "claude-opus-4-8",
30
+ "breaking_changes": [
31
+ "temperature, top_p, and top_k are rejected on Claude Opus 4.7+ — remove these parameters"
32
+ ],
33
+ "migrate_url": "https://platform.claude.com/docs/en/about-claude/model-deprecations"
34
+ },
35
+ "claude-mythos-preview": {
36
+ "provider": "anthropic",
37
+ "status": "deprecated",
38
+ "deprecated_at": "2026-06-01",
39
+ "retired_at": "2026-06-30",
40
+ "replacement": "claude-mythos-5",
41
+ "breaking_changes": [],
42
+ "migrate_url": "https://platform.claude.com/docs/en/about-claude/model-deprecations"
43
+ },
44
+ "claude-3-7-sonnet-20250219": {
45
+ "provider": "anthropic",
46
+ "status": "retired",
47
+ "deprecated_at": "2025-10-28",
48
+ "retired_at": "2026-02-19",
49
+ "replacement": "claude-sonnet-4-6",
50
+ "breaking_changes": [],
51
+ "migrate_url": "https://platform.claude.com/docs/en/about-claude/model-deprecations"
52
+ },
53
+ "claude-3-5-haiku-20241022": {
54
+ "provider": "anthropic",
55
+ "status": "retired",
56
+ "deprecated_at": "2025-12-19",
57
+ "retired_at": "2026-02-19",
58
+ "replacement": "claude-haiku-4-5-20251001",
59
+ "breaking_changes": [],
60
+ "migrate_url": "https://platform.claude.com/docs/en/about-claude/model-deprecations"
61
+ },
62
+ "claude-3-haiku-20240307": {
63
+ "provider": "anthropic",
64
+ "status": "retired",
65
+ "deprecated_at": "2026-02-19",
66
+ "retired_at": "2026-04-20",
67
+ "replacement": "claude-haiku-4-5-20251001",
68
+ "breaking_changes": [],
69
+ "migrate_url": "https://platform.claude.com/docs/en/about-claude/model-deprecations"
70
+ },
71
+ "claude-sonnet-4-6": {
72
+ "provider": "anthropic",
73
+ "status": "active",
74
+ "replacement": null,
75
+ "breaking_changes": []
76
+ },
77
+ "claude-opus-4-8": {
78
+ "provider": "anthropic",
79
+ "status": "active",
80
+ "replacement": null,
81
+ "breaking_changes": [
82
+ "temperature, top_p, and top_k are rejected — omit these parameters"
83
+ ]
84
+ },
85
+ "claude-haiku-4-5-20251001": {
86
+ "provider": "anthropic",
87
+ "status": "active",
88
+ "replacement": null,
89
+ "breaking_changes": []
90
+ },
91
+ "gpt-4-0314": {
92
+ "provider": "openai",
93
+ "status": "retired",
94
+ "retired_at": "2024-06-06",
95
+ "replacement": "gpt-4o",
96
+ "breaking_changes": [],
97
+ "migrate_url": "https://developers.openai.com/api/docs/deprecations"
98
+ },
99
+ "gpt-4-32k": {
100
+ "provider": "openai",
101
+ "status": "retired",
102
+ "retired_at": "2024-06-06",
103
+ "replacement": "gpt-4o",
104
+ "breaking_changes": [],
105
+ "migrate_url": "https://developers.openai.com/api/docs/deprecations"
106
+ },
107
+ "gpt-3.5-turbo-0301": {
108
+ "provider": "openai",
109
+ "status": "retired",
110
+ "retired_at": "2024-09-13",
111
+ "replacement": "gpt-3.5-turbo",
112
+ "breaking_changes": [],
113
+ "migrate_url": "https://developers.openai.com/api/docs/deprecations"
114
+ },
115
+ "gpt-4o": {
116
+ "provider": "openai",
117
+ "status": "active",
118
+ "replacement": null,
119
+ "breaking_changes": []
120
+ },
121
+ "gpt-4o-mini": {
122
+ "provider": "openai",
123
+ "status": "active",
124
+ "replacement": null,
125
+ "breaking_changes": []
126
+ }
127
+ }
128
+ }
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ from modelalive.types import AliveResult
4
+
5
+
6
+ class ModelLifecycleError(Exception):
7
+ """Base error for model lifecycle issues."""
8
+
9
+ def __init__(self, result: AliveResult) -> None:
10
+ self.result = result
11
+ super().__init__(result.message or result.model)
12
+
13
+
14
+ class ModelRetiredError(ModelLifecycleError):
15
+ """Raised when a model is retired and API calls will fail."""
16
+
17
+
18
+ class ModelDeprecatedError(ModelLifecycleError):
19
+ """Raised when a model is deprecated but not yet retired."""
modelalive/registry.py ADDED
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from datetime import date, datetime
5
+ from functools import lru_cache
6
+ from importlib import resources
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ _REGISTRY_FILENAME = "models.json"
11
+
12
+
13
+ def _bundled_registry_text() -> str:
14
+ return resources.files("modelalive.data").joinpath(_REGISTRY_FILENAME).read_text(encoding="utf-8")
15
+
16
+
17
+ def _dev_registry_path() -> Path:
18
+ return Path(__file__).resolve().parent.parent / "registry" / _REGISTRY_FILENAME
19
+
20
+
21
+ @lru_cache(maxsize=1)
22
+ def load_registry(path: str | Path | None = None) -> dict[str, Any]:
23
+ if path is not None:
24
+ return json.loads(Path(path).read_text(encoding="utf-8"))
25
+ try:
26
+ return json.loads(_bundled_registry_text())
27
+ except (FileNotFoundError, OSError, TypeError):
28
+ return json.loads(_dev_registry_path().read_text(encoding="utf-8"))
29
+
30
+
31
+ def get_model_entry(model: str, *, registry_path: str | Path | None = None) -> dict[str, Any] | None:
32
+ registry = load_registry(registry_path)
33
+ return registry.get("models", {}).get(model)
34
+
35
+
36
+ def registry_version(*, registry_path: str | Path | None = None) -> str | None:
37
+ return load_registry(registry_path).get("version")
38
+
39
+
40
+ def _parse_date(value: str | None) -> date | None:
41
+ if not value:
42
+ return None
43
+ return datetime.strptime(value, "%Y-%m-%d").date()
44
+
45
+
46
+ def days_until(value: str | None, *, today: date | None = None) -> int | None:
47
+ target = _parse_date(value)
48
+ if target is None:
49
+ return None
50
+ current = today or date.today()
51
+ return (target - current).days
modelalive/types.py ADDED
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict, dataclass, field
4
+ from typing import Any
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class AliveResult:
9
+ model: str
10
+ alive: bool
11
+ status: str # active | deprecated | retired | unknown
12
+ provider: str | None = None
13
+ deprecated_at: str | None = None
14
+ retired_at: str | None = None
15
+ replacement: str | None = None
16
+ breaking_changes: list[str] = field(default_factory=list)
17
+ migrate_url: str | None = None
18
+ days_until_retirement: int | None = None
19
+ message: str | None = None
20
+ registry_version: str | None = None
21
+
22
+ def to_dict(self) -> dict[str, Any]:
23
+ return asdict(self)
@@ -0,0 +1,102 @@
1
+ Metadata-Version: 2.4
2
+ Name: modelalive
3
+ Version: 0.1.0
4
+ Summary: Pre-flight check: is this LLM model ID still alive?
5
+ Project-URL: Homepage, https://github.com/modelalive/modelalive
6
+ Author: ModelAlive
7
+ License-Expression: MIT
8
+ Keywords: anthropic,deprecation,llm,model-id,openai
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Software Development :: Libraries
14
+ Requires-Python: >=3.10
15
+ Provides-Extra: api
16
+ Requires-Dist: fastapi>=0.115.0; extra == 'api'
17
+ Requires-Dist: uvicorn[standard]>=0.32.0; extra == 'api'
18
+ Provides-Extra: dev
19
+ Requires-Dist: fastapi>=0.115.0; extra == 'dev'
20
+ Requires-Dist: httpx>=0.27.0; extra == 'dev'
21
+ Requires-Dist: pytest>=8.0; extra == 'dev'
22
+ Requires-Dist: uvicorn[standard]>=0.32.0; extra == 'dev'
23
+ Provides-Extra: http
24
+ Requires-Dist: httpx>=0.27.0; extra == 'http'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # Model Alive
28
+
29
+ Pre-flight check before every LLM API call: **is this model ID still alive?**
30
+
31
+ Hardcoded model IDs break silently until production fails. Anthropic retired Claude Sonnet 4 and Opus 4 on **June 15, 2026**. Mythos Preview retires **June 30, 2026**. Model Alive answers in one call.
32
+
33
+ ## Quick start
34
+
35
+ ```bash
36
+ pip install -e ".[dev]"
37
+ modelalive check claude-sonnet-4-20250514
38
+ ```
39
+
40
+ ```
41
+ DEAD: claude-sonnet-4-20250514 (retired)
42
+ replacement: claude-sonnet-4-6
43
+ ```
44
+
45
+ ## Python SDK
46
+
47
+ ```python
48
+ import modelalive
49
+
50
+ # Raises ModelRetiredError if dead
51
+ modelalive.check("claude-sonnet-4-20250514")
52
+
53
+ # Non-throwing status
54
+ result = modelalive.alive("claude-mythos-preview")
55
+ print(result.days_until_retirement) # 2 (as of 2026-06-28)
56
+
57
+ # Auto-replace before calling your provider
58
+ model_id = modelalive.resolve("claude-sonnet-4-20250514")
59
+ # → "claude-sonnet-4-6"
60
+ ```
61
+
62
+ ## HTTP API
63
+
64
+ ```bash
65
+ uvicorn api.main:app --reload --port 8787
66
+ ```
67
+
68
+ ```bash
69
+ curl "http://localhost:8787/v1/alive?model=claude-sonnet-4-20250514"
70
+ ```
71
+
72
+ ```json
73
+ {
74
+ "model": "claude-sonnet-4-20250514",
75
+ "alive": false,
76
+ "status": "retired",
77
+ "provider": "anthropic",
78
+ "retired_at": "2026-06-15",
79
+ "replacement": "claude-sonnet-4-6",
80
+ "breaking_changes": [],
81
+ "migrate_url": "https://platform.claude.com/docs/en/about-claude/model-deprecations"
82
+ }
83
+ ```
84
+
85
+ Retired models return **HTTP 410**.
86
+
87
+ ## Endpoints
88
+
89
+ | Method | Path | Description |
90
+ |--------|------|-------------|
91
+ | GET | `/v1/alive?model=` | Lifecycle check |
92
+ | GET | `/v1/resolve?model=` | Best model ID to use |
93
+ | GET | `/v1/registry` | Full registry (optional `?status=retired`) |
94
+ | GET | `/v1/health` | Health + registry version |
95
+
96
+ ## Registry
97
+
98
+ Curated deprecation data lives in `registry/models.json`. Update when providers announce retirements.
99
+
100
+ ## License
101
+
102
+ MIT
@@ -0,0 +1,11 @@
1
+ modelalive/__init__.py,sha256=OYp5mLHET38U5SaBZTweV2mv0xr93Ridor7rjx6HsNw,375
2
+ modelalive/check.py,sha256=hHeiHt0aV8CvLpOCEP89dDkY-JOUHejBn7tX8b01Yp4,4133
3
+ modelalive/cli.py,sha256=E1R_k_iV5_55zE_IB7W7ZT5NO_p-BAt_xy8k_UXlP38,2087
4
+ modelalive/exceptions.py,sha256=lF9gitdXb-T5YkEK8TxmLCIp6hb8s1jLHoOPzGQr1Io,536
5
+ modelalive/registry.py,sha256=2OpMXTVLY5FVroZHRMKnqkrAIr5NPhPWjz6dePTj4d0,1574
6
+ modelalive/types.py,sha256=HLDaz5y7GnUiz2hIoyXeXMaKfsksri01odRfGMywd7w,664
7
+ modelalive/data/models.json,sha256=yhRUqHq6bBKREzCDYrBLw--mYaj40JKjOoqL7bXBKL8,4182
8
+ modelalive-0.1.0.dist-info/METADATA,sha256=Xhh-QPJ1-3rhBTW0c-7Ow3CHoeZU4hMxkSbPXj8X_8I,2776
9
+ modelalive-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
10
+ modelalive-0.1.0.dist-info/entry_points.txt,sha256=k6-jjSdowRF-uGoWdhypSyii9WSFjNhQVlPfUrLikzk,51
11
+ modelalive-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ modelalive = modelalive.cli:main