What you’ll learn
- How do I tell whether a TokenLab model is deprecated or retired?
- What HTTP status does an unknown model ID return?
- Should I retry a model_not_found error?
- Will TokenLab switch my request to another model if mine disappears?
A model string is the most dangerous constant in your codebase. Teams treat it like a label, yet it decides which checkpoint answers, what the call costs, and whether the request succeeds at all. AI model deprecation and versioning therefore belongs in your release process, not in a one-time launch decision. This guide uses TokenLab's public model API, as observed on 2026-10-03, to show what you can check automatically, what you must verify elsewhere, and what to do when an ID stops working.
Key Takeaways
- Lifecycle data lives on the model object. Read
tokenlab.lifecyclefrom Get a Model (observed 2026-10-03) instead of copying dates from a blog post. - All five models we checked on 2026-10-03 were
activewith no deprecation or retirement date. Our evidence contains no real deprecated model, so the failure path below is documented behavior, not something we watched happen. - A missing model returns
model_not_found, but the status differs by endpoint. Branch oncode, never on the status alone. - Pin exact IDs and keep them in one config layer. TokenLab says it does not silently replace the model you send.
- A fallback must be a tested, pre-approved choice. Capabilities, request formats, limits, and price all differ between models.
Where AI model deprecation and versioning shows up in the API
The Get a Model reference (observed 2026-10-03) says the lifecycle field covers availability, release date, deprecation date, and replacement model. The List Models reference (observed 2026-10-03) adds tokenlab.lifecycle, tokenlab.aliases, tokenlab.commercial, and tokenlab.badges to each list item. The same page lists tokenlab.accepted_request_formats and the token limits, which matter for any swap.
Here is what we read from the live model API for claude-sonnet-5 on 2026-10-03. This is a condensed rendering of the observed values. Our evidence recorded the values, not the raw nested key names, so confirm exact keys against a live response.
GET https://api.tokenlab.sh/v1/models/claude-sonnet-5
category: chat
lifecycle stage: active
released: 2026-06-30
replacement model: None
deprecated_at: None
retired_at: None
max input tokens: 1000000
max output tokens: 128000
accepted formats: anthropic_messages, openai_chat_completions
Map the five lifecycle concepts onto those fields like this:
| Concept | Field in the observed response | Value for claude-sonnet-5 |
|---|---|---|
| Stage | lifecycle stage | active |
| Release | released | 2026-06-30 |
| Deprecation | deprecated_at |
None |
| Retirement | retired_at |
None |
| Replacement | replacement model | None |
An empty value does not always signal a problem. For glm-5.2 and gpt-5.5, the live API (observed 2026-10-03) shows released as None while the stage is active. A check should fail on a set deprecated_at, a set retired_at, or a non-active stage. It should not fail on a missing release date.
We also read deepseek-v4-pro and deepseek-v4-flash (both released 2026-04-24, both active, no replacement). That makes five models with no deprecation signal. A deprecated model's replacement field is the one value we could not observe populated.
A script that fails when a dependency goes bad
The check below uses only the documented GET /v1/models/{model} endpoint, which needs no API key because the catalog is public. It fails on a missing ID, a set deprecated_at or retired_at, a non-active stage, or a request format the model no longer accepts.
#!/usr/bin/env python3
"""Fail when a model ID we depend on is missing, deprecated, or retired."""
import json
import sys
import urllib.error
import urllib.request
BASE = "https://api.tokenlab.sh/v1/models/"
# model ID -> the request format our code actually sends
DEPENDENCIES = {
"gpt-5.5": "openai_chat_completions",
"claude-sonnet-5": "anthropic_messages",
"deepseek-v4-pro": "openai_chat_completions",
}
def fetch(model_id):
req = urllib.request.Request(BASE + model_id, headers={"Accept": "application/json"})
try:
with urllib.request.urlopen(req, timeout=15) as resp:
return resp.status, json.load(resp)
except urllib.error.HTTPError as exc:
try:
body = json.load(exc)
except ValueError:
body = {}
return exc.code, body
def check(model_id, fmt):
try:
status, body = fetch(model_id)
except urllib.error.URLError as exc:
return [f"could not verify: {exc.reason}"], {}, []
if status in (400, 404):
return [f"missing: HTTP {status}"], {}, []
if status != 200:
return [f"could not verify: HTTP {status}"], {}, []
info = body.get("tokenlab", {})
life = info.get("lifecycle") or {}
problems = []
if not life:
problems.append("no lifecycle block returned")
stage = life.get("stage") # confirm the exact key against a live response
if stage not in (None, "active"):
problems.append(f"stage={stage}")
for key in ("deprecated_at", "retired_at"):
if life.get(key):
problems.append(f"{key}={life[key]}")
if fmt not in info.get("accepted_request_formats", []):
problems.append(f"format {fmt} not accepted")
return problems, life, info.get("aliases", [])
def main():
failed = False
for model_id, fmt in DEPENDENCIES.items():
problems, life, aliases = check(model_id, fmt)
if problems:
failed = True
print(f"FAIL {model_id}: {'; '.join(problems)}")
if life:
print(f" lifecycle: {json.dumps(life)}")
else:
print(f"ok {model_id} aliases={aliases}")
sys.exit(1 if failed else 0)
if __name__ == "__main__":
main()
Run it in CI and on a nightly schedule, because a model can change state without a commit on your side. Imagine a nightly job that exits 1 on a Tuesday while your last deploy was a week ago. That is the point: the failure shows up in a pipeline, not in a customer's chat window. The script prints the whole lifecycle block on failure so a human sees the replacement value without guessing key names.
What a retired or unknown model returns
The evidence documents unknown and unavailable models, not a separately labeled "retired" response. We therefore treat retirement as the same family of errors until you verify otherwise. Here is what the docs say, all observed 2026-10-03:
| Where | Response | Source |
|---|---|---|
GET /v1/models/{model} |
404 model_not_found |
Get a Model |
| Chat request with a misspelled or unavailable model | 400 model_not_found, optionally with did_you_mean, suggestions, hint |
Errors agents can act on |
| Operation has no supply in the selected Delivery tier | 503 all_channels_failed or 503 delivery_tier_unavailable, retryable: false |
Errors agents can act on |
| Key lacks permission for the model | 403 model_not_allowed |
Handle API errors |
The status codes disagree across endpoints, so one fix matters: our source article's fallback code treated 404 and 410 as "identifier gone." That is wrong for chat requests. The documented chat response is 400, and the error guide says to handle errors by HTTP status and code. Key your logic on code == "model_not_found".
For example, the docs show a misspelled ID returning did_you_mean: "gpt-5.6-terra". Your client should react in this order:
- Do not repeat the request unchanged. The error guide marks
400,403, and404as non-repeatable. - Treat
did_you_meanas a suggestion. Switch only if your product already has permission to change the model. - If the 503 body says
retryable: false, stop retrying. The docs say similar names do not prove availability. - Save the Request ID from the response headers with the model and your own job ID.
- Alert a human. A pinned ID that vanished is a release event, not a transient error.
Pin an exact ID or follow an alias
The best-practices guide (observed 2026-10-03) says to keep the model ID explicit in each request and not to switch models silently. It warns that a replacement can change quality, price, context length, tools, or media support. The error guide adds that TokenLab does not silently replace the model you send.
| Strategy | Behavior | Main risk |
|---|---|---|
Pin an exact ID such as gpt-5.5 |
You get that model, or an error | A hard failure if the ID is removed |
| Follow an alias | The name may point somewhere new over time | Behavior and cost can change with no code change |
| Internal route name in your config | Your code asks for primary_chat; config maps it to an exact ID |
You own and test the mapping |
On aliases, we found a gap. The List Models reference says tokenlab.aliases exists on every item. None of the five live excerpts we have (claude-sonnet-5, deepseek-v4-pro, deepseek-v4-flash, glm-5.2, gpt-5.5) shows an alias value. We cannot tell you which aliases those models have today. The script above prints aliases for each dependency, so run it and read the output.
Two more catalog facts help. The 2026-10-02 catalog lists versions as separate exact IDs, for example claude-sonnet-4-6, claude-sonnet-5, and claude-sonnet-5-5. Being listed side by side does not make one a successor. The replacement value is the only place that relationship should come from. One ID, grok-voice-latest, has "latest" in its name. We have no evidence on what it follows, so check its model page before trusting it. Marketplace-style prefixed IDs are not TokenLab aliases. The migration guide says TokenLab IDs carry no OpenRouter provider prefix, and it suggests keeping your own LiteLLM alias separate from the TokenLab ID.
A fallback pattern for a pinned model that disappears
Treat a fallback as a second pinned model that you tested in advance. Choose it per call site, record which model served each answer, and verify it with the same nightly script. Compare the two models before you pair them:
| Model (live API, observed 2026-10-03) | Input / output per 1M tokens | Max input / output tokens | Request formats | Capabilities listed |
|---|---|---|---|---|
gpt-5.5 |
$1.50 / $9.00 | 1,000,000 / 128,000 | openai_chat_completions, openai_responses |
coding, json-mode, prompt-cache, reasoning, tool-use, vision |
claude-sonnet-5 |
$0.90 / $4.50 | 1,000,000 / 128,000 | anthropic_messages, openai_chat_completions |
json-mode, prompt-cache, reasoning, tool-use, vision |
deepseek-v4-pro |
$0.66 / $1.98 off-peak; $1.32 / $3.96 peak | 1,000,000 / 384,000 | anthropic_messages, openai_chat_completions, openai_responses |
json-mode, prompt-cache, tool-use |
Pricing for all three was last updated 2026-10-02T16:53:30.068Z. The differences matter. claude-sonnet-5 does not accept openai_responses, so a Responses-based caller cannot fall back to it. deepseek-v4-pro lists neither vision nor reasoning, so an image-bearing request breaks there. DeepSeek's peak windows run 09:00-12:00 and 14:00-18:00 Beijing time.
Here is a cost estimate with the working shown. Assume a daily load of 10M input and 2M output tokens, which is our illustrative number, not a measurement:
gpt-5.5: 10 × $1.50 + 2 × $9.00 = $15 + $18 = $33 per day.claude-sonnet-5: 10 × $0.90 + 2 × $4.50 = $9 + $9 = $18 per day.deepseek-v4-prooff-peak: 10 × $0.66 + 2 × $1.98 = $6.60 + $3.96 = $10.56 per day.deepseek-v4-propeak: 10 × $1.32 + 2 × $3.96 = $13.20 + $7.92 = $21.12 per day.
These are estimates, and cached input is priced differently. The code below falls back only on errors that retrying cannot fix. A 429 or a retryable 5xx should follow the normal retry rules instead.
import logging
import os
import openai
log = logging.getLogger("model-routing")
client = openai.OpenAI(
api_key=os.environ["TOKENLAB_API_KEY"],
base_url="https://api.tokenlab.sh/v1",
timeout=30.0,
max_retries=0, # we decide what is safe to repeat
)
ROUTES = {
"primary_chat": {"model": "gpt-5.5", "fallback": "claude-sonnet-5"},
"coding_agent": {"model": "claude-sonnet-5", "fallback": "deepseek-v4-pro"},
}
def _error(exc):
body = exc.body if isinstance(exc.body, dict) else {}
err = body.get("error", body)
return err if isinstance(err, dict) else {}
def _chat(model_id, text):
resp = client.chat.completions.create(
model=model_id,
messages=[{"role": "user", "content": text}],
)
return resp.choices[0].message.content
def call_model(route, text):
"""Return (model_that_answered, text). Record the model with the result."""
cfg = ROUTES[route]
try:
return cfg["model"], _chat(cfg["model"], text)
except openai.APIStatusError as exc:
err = _error(exc)
code = err.get("code")
gone = code == "model_not_found"
no_supply = (
code in ("all_channels_failed", "delivery_tier_unavailable")
and err.get("retryable") is False
)
if not (gone or no_supply):
raise # 401/402/403/429 and the like need a different fix
log.warning("pinned model %s unavailable (%s); using %s",
cfg["model"], code, cfg["fallback"])
return cfg["fallback"], _chat(cfg["fallback"], text)
The fallback hops once and never chains. If the fallback also fails, raise the error and page someone. A silent chain of substitutions is the drift problem under another name.
Release checklist for AI model deprecation and versioning
Run this before changing a production model ID. It mirrors the best-practices guide (observed 2026-10-03) and adds the lifecycle checks.
- List every model ID in production and name the call site that uses each one. One config layer should answer this in seconds.
- Run the lifecycle script against the current and candidate IDs. Read
deprecated_at,retired_at, stage, and replacement. - Confirm the candidate accepts the request format your code sends, using
accepted_request_formatsfrom Get a Model. - Compare max input and max output tokens against your longest real request and your output cap.
- Compare capabilities. Confirm vision, tool-use, json-mode, and reasoning where your product depends on them.
- Recompute cost from current prices, with the working shown. For DeepSeek, split peak and off-peak.
- Replay representative requests with the exact candidate ID and format you will ship. Include one failure case, such as an unknown ID.
- Verify the error path. Your client should branch on
code, keep the Request ID, and not retry400,403, or404. - Ship the change as a config edit with the old ID kept as the documented rollback value.
- Check that every response record stores the model that actually served it.
- Re-run the nightly script after the release. Confirm the fallback ID passes too.
Where the docs are silent, the checklist is the safeguard. They give no deprecation lead time and no notification channel. Verify provider notices separately, and check the model's own page for any lifecycle notes.
FAQ
How do I tell whether a TokenLab model is deprecated or retired?
Call GET /v1/models/{model} and read tokenlab.lifecycle. Check the stage, deprecated_at, retired_at, and the replacement model. As of 2026-10-03, the five models we read were all active with no dates set. Poll again, because the value reflects only the moment you asked.
What HTTP status does an unknown model ID return?
It depends on the endpoint. Get a Model returns 404 model_not_found, while a chat request with a bad model returns 400 model_not_found. A model with no supply in the selected Delivery tier returns a 503. Branch on the code field.
Should I retry a model_not_found error?
No. Repeating the same request will not change the result. Check did_you_mean and suggestions if present, but switch models only when your product already has permission to. Otherwise, use a pre-approved fallback and alert your team.
Will TokenLab switch my request to another model if mine disappears?
The documentation says it does not silently replace the model you send. Send an explicit ID and handle the error yourself. Your fallback should be a tested config value, not an assumption about the platform.
Compare current IDs, limits, and prices in the model directory before your next model change.
Sources
Prices checked 2026-10-03
- TokenLab Docs: List ModelsSources checked 2026-10-03
- TokenLab Docs: Get a ModelSources checked 2026-10-03
- TokenLab Docs: Build a reliable TokenLab integrationSources checked 2026-10-03
- TokenLab Docs: Handle API errorsSources checked 2026-10-03
- TokenLab Docs: Errors agents can act onSources checked 2026-10-03
- TokenLab Docs: Migration GuidesSources checked 2026-10-03
- TokenLab live model API: claude-sonnet-5Sources checked 2026-10-03
- TokenLab live model API: deepseek-v4-proSources checked 2026-10-03
