modelalive 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,7 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ *.egg-info/
6
+ .pytest_cache/
7
+ .env
@@ -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,76 @@
1
+ # Model Alive
2
+
3
+ Pre-flight check before every LLM API call: **is this model ID still alive?**
4
+
5
+ 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.
6
+
7
+ ## Quick start
8
+
9
+ ```bash
10
+ pip install -e ".[dev]"
11
+ modelalive check claude-sonnet-4-20250514
12
+ ```
13
+
14
+ ```
15
+ DEAD: claude-sonnet-4-20250514 (retired)
16
+ replacement: claude-sonnet-4-6
17
+ ```
18
+
19
+ ## Python SDK
20
+
21
+ ```python
22
+ import modelalive
23
+
24
+ # Raises ModelRetiredError if dead
25
+ modelalive.check("claude-sonnet-4-20250514")
26
+
27
+ # Non-throwing status
28
+ result = modelalive.alive("claude-mythos-preview")
29
+ print(result.days_until_retirement) # 2 (as of 2026-06-28)
30
+
31
+ # Auto-replace before calling your provider
32
+ model_id = modelalive.resolve("claude-sonnet-4-20250514")
33
+ # → "claude-sonnet-4-6"
34
+ ```
35
+
36
+ ## HTTP API
37
+
38
+ ```bash
39
+ uvicorn api.main:app --reload --port 8787
40
+ ```
41
+
42
+ ```bash
43
+ curl "http://localhost:8787/v1/alive?model=claude-sonnet-4-20250514"
44
+ ```
45
+
46
+ ```json
47
+ {
48
+ "model": "claude-sonnet-4-20250514",
49
+ "alive": false,
50
+ "status": "retired",
51
+ "provider": "anthropic",
52
+ "retired_at": "2026-06-15",
53
+ "replacement": "claude-sonnet-4-6",
54
+ "breaking_changes": [],
55
+ "migrate_url": "https://platform.claude.com/docs/en/about-claude/model-deprecations"
56
+ }
57
+ ```
58
+
59
+ Retired models return **HTTP 410**.
60
+
61
+ ## Endpoints
62
+
63
+ | Method | Path | Description |
64
+ |--------|------|-------------|
65
+ | GET | `/v1/alive?model=` | Lifecycle check |
66
+ | GET | `/v1/resolve?model=` | Best model ID to use |
67
+ | GET | `/v1/registry` | Full registry (optional `?status=retired`) |
68
+ | GET | `/v1/health` | Health + registry version |
69
+
70
+ ## Registry
71
+
72
+ Curated deprecation data lives in `registry/models.json`. Update when providers announce retirements.
73
+
74
+ ## License
75
+
76
+ MIT
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ from fastapi import FastAPI, HTTPException, Query
4
+ from fastapi.responses import JSONResponse
5
+
6
+ from modelalive.check import alive, resolve
7
+ from modelalive.registry import load_registry, registry_version
8
+
9
+ app = FastAPI(
10
+ title="Model Alive",
11
+ description="Pre-flight API: is this LLM model ID still alive?",
12
+ version="0.1.0",
13
+ )
14
+
15
+
16
+ @app.get("/v1/health")
17
+ def health() -> dict[str, str]:
18
+ return {"status": "ok", "registry_version": registry_version() or "unknown"}
19
+
20
+
21
+ @app.get("/v1/alive")
22
+ def get_alive(model: str = Query(..., min_length=1, description="Model ID to check")):
23
+ result = alive(model)
24
+ payload = result.to_dict()
25
+ status_code = 410 if result.status == "retired" else 200
26
+ return JSONResponse(content=payload, status_code=status_code)
27
+
28
+
29
+ @app.get("/v1/resolve")
30
+ def get_resolve(model: str = Query(..., min_length=1)):
31
+ return {"model": model, "resolved": resolve(model)}
32
+
33
+
34
+ @app.get("/v1/registry")
35
+ def list_registry(
36
+ status: str | None = Query(None, description="Filter: active, deprecated, retired"),
37
+ ):
38
+ registry = load_registry()
39
+ models = registry.get("models", {})
40
+ if status:
41
+ filtered = {k: v for k, v in models.items() if v.get("status") == status}
42
+ else:
43
+ filtered = models
44
+ return {
45
+ "registry_version": registry.get("version"),
46
+ "count": len(filtered),
47
+ "models": filtered,
48
+ }
@@ -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"
@@ -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)
@@ -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."""
@@ -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
@@ -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,38 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "modelalive"
7
+ version = "0.1.0"
8
+ description = "Pre-flight check: is this LLM model ID still alive?"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [{ name = "ModelAlive" }]
13
+ keywords = ["llm", "openai", "anthropic", "deprecation", "model-id"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Topic :: Software Development :: Libraries",
20
+ ]
21
+ dependencies = []
22
+
23
+ [project.optional-dependencies]
24
+ api = ["fastapi>=0.115.0", "uvicorn[standard]>=0.32.0"]
25
+ http = ["httpx>=0.27.0"]
26
+ dev = ["pytest>=8.0", "httpx>=0.27.0", "fastapi>=0.115.0", "uvicorn[standard]>=0.32.0"]
27
+
28
+ [project.scripts]
29
+ modelalive = "modelalive.cli:main"
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/modelalive/modelalive"
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ packages = ["modelalive"]
36
+
37
+ [tool.pytest.ini_options]
38
+ testpaths = ["tests"]
@@ -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,43 @@
1
+ from datetime import date
2
+
3
+ import pytest
4
+
5
+ from modelalive import alive, check, resolve
6
+ from modelalive.exceptions import ModelRetiredError
7
+
8
+
9
+ def test_retired_model_not_alive():
10
+ result = alive("claude-sonnet-4-20250514")
11
+ assert result.alive is False
12
+ assert result.status == "retired"
13
+ assert result.replacement == "claude-sonnet-4-6"
14
+
15
+
16
+ def test_check_raises_on_retired():
17
+ with pytest.raises(ModelRetiredError) as exc:
18
+ check("claude-sonnet-4-20250514")
19
+ assert exc.value.result.replacement == "claude-sonnet-4-6"
20
+
21
+
22
+ def test_active_model_alive():
23
+ result = alive("claude-sonnet-4-6")
24
+ assert result.alive is True
25
+ assert result.status == "active"
26
+
27
+
28
+ def test_unknown_model_assumed_alive():
29
+ result = alive("some-future-model-xyz")
30
+ assert result.alive is True
31
+ assert result.status == "unknown"
32
+
33
+
34
+ def test_resolve_returns_replacement():
35
+ assert resolve("claude-sonnet-4-20250514") == "claude-sonnet-4-6"
36
+ assert resolve("claude-sonnet-4-6") == "claude-sonnet-4-6"
37
+
38
+
39
+ def test_deprecated_mythos_still_alive():
40
+ result = alive("claude-mythos-preview", today=date(2026, 6, 28))
41
+ assert result.alive is True
42
+ assert result.status == "deprecated"
43
+ assert result.days_until_retirement == 2