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.
- modelalive-0.1.0/.gitignore +7 -0
- modelalive-0.1.0/PKG-INFO +102 -0
- modelalive-0.1.0/README.md +76 -0
- modelalive-0.1.0/api/main.py +48 -0
- modelalive-0.1.0/modelalive/__init__.py +15 -0
- modelalive-0.1.0/modelalive/check.py +125 -0
- modelalive-0.1.0/modelalive/cli.py +64 -0
- modelalive-0.1.0/modelalive/data/models.json +128 -0
- modelalive-0.1.0/modelalive/exceptions.py +19 -0
- modelalive-0.1.0/modelalive/registry.py +51 -0
- modelalive-0.1.0/modelalive/types.py +23 -0
- modelalive-0.1.0/pyproject.toml +38 -0
- modelalive-0.1.0/registry/models.json +128 -0
- modelalive-0.1.0/tests/test_check.py +43 -0
|
@@ -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
|