Skip to content

Commit 935f808

Browse files
Lookovclaude
andcommitted
docs: document the model deprecation feed
Add a Service Endpoints page for GET /v1/models/deprecations — the three states (deprecated / superseded / withdrawn), the status and since filters, ETag polling, and a sync snippet that keeps "never existed" apart from "went away". Also fix the catalogue page: a name missing from /v1/models was described as "genuinely unavailable", which is the false positive the feed exists to prevent — a superseded id still resolves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent c628d9b commit 935f808

4 files changed

Lines changed: 328 additions & 1 deletion

File tree

docs/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
* [API Key Usage](api-references/service-endpoints/api-key-usage.md)
2020
* [Usage Logs](api-references/service-endpoints/usage-logs.md)
2121
* [Complete Model List](api-references/service-endpoints/complete-model-list.md)
22+
* [Model Deprecations](api-references/service-endpoints/model-deprecations.md)
2223
* [All Model IDs](api-references/model-database.md)
2324
* [Text Models (LLM)](api-references/text-models-llm/README.md)
2425
* [AI21](api-references/text-models-llm/AI21/README.md)

docs/api-references/service-endpoints/complete-model-list.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,12 +178,16 @@ for (const model of data) {
178178
for (const alias of model.aliases ?? []) byName.set(alias, model.id);
179179
}
180180

181-
const canonical = byName.get(myModelId); // undefined ⇒ genuinely unavailable
181+
const canonical = byName.get(myModelId); // undefined ⇒ not in the catalogue
182182
```
183183
{% endcode %}
184184
185185
The same map de-duplicates your list: two names resolving to one canonical id are one model, not two.
186186
187+
{% hint style="info" %}
188+
A name missing from this map is not necessarily a model that went away — it may also be one that never existed here. [`GET /v1/models/deprecations`](model-deprecations.md) tells the two apart, and names the id to migrate to where there is one.
189+
{% endhint %}
190+
187191
## Reading prices
188192
189193
With `?include=pricing`, each model carries a `pricing` block. Read `kind` first — it decides how the rest is shaped.
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
{
2+
"openapi": "3.0.0",
3+
"info": {
4+
"title": "AIML API",
5+
"version": "1.0.0"
6+
},
7+
"servers": [
8+
{
9+
"url": "https://api.aimlapi.com"
10+
}
11+
],
12+
"paths": {
13+
"/v1/models/deprecations": {
14+
"get": {
15+
"operationId": "_v1_models_deprecations",
16+
"parameters": [
17+
{
18+
"name": "status",
19+
"in": "query",
20+
"required": false,
21+
"description": "Keep only entries in these states: `deprecated`, `superseded` or `withdrawn`. Values are case-insensitive and may be comma-separated (`?status=superseded,withdrawn`) or repeated (`?status=superseded&status=withdrawn`). Unknown values are ignored rather than rejected — a parameter holding no recognised value filters nothing and returns the whole feed.",
22+
"schema": {
23+
"type": "string",
24+
"example": "withdrawn"
25+
}
26+
},
27+
{
28+
"name": "since",
29+
"in": "query",
30+
"required": false,
31+
"description": "Keep only entries whose `deprecated_at` or `shutdown_at` falls on or after this date. Plain `YYYY-MM-DD`, no time part. Entries carrying neither date are dropped, because there is nothing to compare against. A malformed value is ignored rather than rejected, so a broken poll returns the whole feed instead of a 400.",
32+
"schema": {
33+
"type": "string",
34+
"example": "2026-08-01"
35+
}
36+
}
37+
],
38+
"responses": {
39+
"200": {
40+
"description": "The deprecation feed, newest first.",
41+
"content": {
42+
"application/json": {
43+
"schema": {
44+
"type": "object",
45+
"properties": {
46+
"object": {
47+
"type": "string",
48+
"description": "Always `list`.",
49+
"example": "list"
50+
},
51+
"generated_at": {
52+
"type": "string",
53+
"description": "When this response was built, in UTC. It moves on every call and is deliberately excluded from the ETag.",
54+
"example": "2026-08-24T13:59:21.643Z"
55+
},
56+
"data": {
57+
"type": "array",
58+
"description": "One entry per model, ordered newest-first by the most recent date the entry carries (`shutdown_at`, else `deprecated_at`), then by `id`. A model published under several endpoint types still appears once.",
59+
"items": {
60+
"type": "object",
61+
"properties": {
62+
"id": {
63+
"type": "string",
64+
"description": "The canonical public id the model was reachable under.",
65+
"example": "openai/gpt-5-codex"
66+
},
67+
"aliases": {
68+
"type": "array",
69+
"description": "Every other public id that used to reach it. Match your integrated model name against `id` and `aliases` together.",
70+
"items": {
71+
"type": "string"
72+
},
73+
"example": [
74+
"gpt-5-codex"
75+
]
76+
},
77+
"status": {
78+
"type": "string",
79+
"description": "`deprecated` — still serving, retirement announced. `superseded` — folded into another model, the id still resolves. `withdrawn` — gone, requests return 404.",
80+
"enum": [
81+
"deprecated",
82+
"superseded",
83+
"withdrawn"
84+
],
85+
"example": "withdrawn"
86+
},
87+
"deprecated_at": {
88+
"type": "string",
89+
"nullable": true,
90+
"description": "ISO date the retirement was announced, or the date we learned of it when the provider gave no notice. `null` when unknown.",
91+
"example": "2026-07-30"
92+
},
93+
"shutdown_at": {
94+
"type": "string",
95+
"nullable": true,
96+
"description": "ISO date the model stopped, or is scheduled to stop, serving. Always `null` for `superseded` — those never stop — and `null` for a `deprecated` model whose provider has named no date yet.",
97+
"example": "2026-07-30"
98+
},
99+
"replaced_by": {
100+
"type": "string",
101+
"nullable": true,
102+
"description": "Canonical id that supersedes this one, where one exists. For a `superseded` entry this is the model the old id now resolves to. `null` when there is no successor.",
103+
"example": "deepgram/aura-2"
104+
},
105+
"reason": {
106+
"type": "string",
107+
"nullable": true,
108+
"description": "`provider_delisted` — the model developer stopped serving it. `consolidated` — several ids were folded into one family model. `unknown` — no reason recorded. `null` on an entry read from a live model card.",
109+
"enum": [
110+
"provider_delisted",
111+
"consolidated",
112+
"unknown"
113+
],
114+
"example": "provider_delisted"
115+
}
116+
},
117+
"required": [
118+
"id",
119+
"aliases",
120+
"status",
121+
"deprecated_at",
122+
"shutdown_at",
123+
"replaced_by",
124+
"reason"
125+
]
126+
}
127+
}
128+
},
129+
"required": [
130+
"object",
131+
"generated_at",
132+
"data"
133+
]
134+
}
135+
}
136+
}
137+
},
138+
"304": {
139+
"description": "Not Modified — the ETag you sent as If-None-Match still matches. Empty body."
140+
}
141+
},
142+
"x-hideTryItPanel": false,
143+
"x-codeSamples": [
144+
{
145+
"lang": "cURL",
146+
"source": "curl -L \\\n --url 'https://api.aimlapi.com/v1/models/deprecations?status=withdrawn&since=2026-08-01'"
147+
},
148+
{
149+
"lang": "JavaScript",
150+
"source": "async function main() {\n const url = new URL(\"https://api.aimlapi.com/v1/models/deprecations\");\n url.searchParams.set(\"status\", \"withdrawn\");\n url.searchParams.set(\"since\", \"2026-08-01\");\n\n const response = await fetch(url);\n const data = await response.json();\n console.log(JSON.stringify(data, null, 2));\n}\n\nmain();"
151+
},
152+
{
153+
"lang": "Python",
154+
"source": "import requests\nimport json\n\ndef main():\n response = requests.get(\n \"https://api.aimlapi.com/v1/models/deprecations\",\n params={\"status\": \"withdrawn\", \"since\": \"2026-08-01\"},\n )\n\n data = response.json()\n print(json.dumps(data, indent=2, ensure_ascii=False))\n\nif __name__ == \"__main__\":\n main()"
155+
}
156+
]
157+
}
158+
}
159+
}
160+
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
# Model Deprecations
2+
3+
<table data-header-hidden data-full-width="true"><thead><tr><th width="220" valign="top"></th><th valign="top"></th></tr></thead><tbody><tr><td valign="top"><a href="model-deprecations.md#get-the-deprecation-feed">Get the deprecation feed</a></td><td valign="top"><mark style="color:$success;"><strong><code>GET</code></strong></mark> <code>https://api.aimlapi.com/v1/models/deprecations</code></td></tr></tbody></table>
4+
5+
`GET /v1/models/deprecations` lists the models that are retiring, have already been withdrawn, or were folded into another model — with the date it happened and the id to move to.\
6+
No API key is required for this request. You can also open [the feed](https://api.aimlapi.com/v1/models/deprecations) directly in a browser.
7+
8+
It exists to answer the one question [`GET /v1/models`](complete-model-list.md) structurally cannot. A model missing from the catalogue may have been withdrawn, may have been folded into another id that still works, or may never have existed — and the catalogue returns the same "not here" for all three. Treating them alike is how a model-sync job ends up disabling models that are still serving.
9+
10+
{% hint style="info" %}
11+
`https://api.aimlapi.com/models/deprecations` and `https://api.aimlapi.com/api/v1/models/deprecations` serve exactly the same data, as does `deprecated` in place of `deprecations` on any of the three. `/v1/models/deprecations` is the canonical path and the one to use in new integrations.
12+
{% endhint %}
13+
14+
### Get the deprecation feed
15+
16+
Returns the deprecation entries matching the given filters, newest first.
17+
18+
{% openapi-operation spec="model-deprecations" path="/v1/models/deprecations" method="get" %}
19+
[OpenAPI model-deprecations](https://raw.githubusercontent.com/aimlapi/api-docs/refs/heads/main/docs/api-references/service-endpoints/model-deprecations.json)
20+
{% endopenapi-operation %}
21+
22+
***
23+
24+
## The three states
25+
26+
`status` is the field to branch on, and the difference between the three is what your integration should do next.
27+
28+
| `status` | Does the id still work? | What to do |
29+
| ------------- | ------------------------------------------------ | ------------------------------------------------------------------------------ |
30+
| `deprecated` | **Yes** — still serving, retirement announced | Plan the migration before `shutdown_at`. Nothing breaks today. |
31+
| `superseded` | **Yes** — folded into another model, still resolves | Nothing is broken. Switch to `replaced_by` when convenient. |
32+
| `withdrawn` | **No** — requests return `404` | Switch to `replaced_by`, or pick a replacement from the catalogue. |
33+
34+
{% hint style="warning" %}
35+
`superseded` is not a soft `withdrawn`. The id still resolves — it lives on as an alias of `replaced_by` — and `shutdown_at` is always `null` for these entries. Conflating the two is exactly the false positive this feed exists to prevent.
36+
37+
```bash
38+
# a superseded id, resolved by the catalogue to the model it was folded into
39+
curl 'https://api.aimlapi.com/v1/models?id=aura-2-zeus-en'
40+
# -> {"object":"list","data":[{"id":"deepgram/aura-2", ...}]}
41+
```
42+
{% endhint %}
43+
44+
A `deprecated` model is read from its live model card, so it announces its own sunset while it still works. Once a real retirement date is set, it is dropped from `GET /v1/models` — but it keeps serving, and [`GET /model/{id}`](complete-model-list.md#looking-up-one-model) still returns its card with the sunset dates attached. The other two states come from a durable record instead: a withdrawn model has no card left to read.
45+
46+
## Response shape
47+
48+
```json
49+
{
50+
"object": "list",
51+
"generated_at": "2026-08-24T13:59:21.643Z",
52+
"data": [
53+
{
54+
"id": "openai/gpt-5.3-chat",
55+
"aliases": ["gpt-5.3-chat"],
56+
"status": "withdrawn",
57+
"deprecated_at": "2026-08-10",
58+
"shutdown_at": "2026-08-10",
59+
"replaced_by": "openai/gpt-5.3-codex",
60+
"reason": "provider_delisted"
61+
},
62+
{
63+
"id": "aura-2-zeus-en",
64+
"aliases": ["deepgram/aura-2-zeus-en", "#g1_aura-2-zeus-en"],
65+
"status": "superseded",
66+
"deprecated_at": "2026-06-26",
67+
"shutdown_at": null,
68+
"replaced_by": "deepgram/aura-2",
69+
"reason": "consolidated"
70+
}
71+
]
72+
}
73+
```
74+
75+
Dates are plain `YYYY-MM-DD`. `deprecated_at` is the date the provider announced the retirement, or the date we learned of it when they gave no notice; `shutdown_at` is the date it stopped, or will stop, serving. `reason` is `provider_delisted`, `consolidated` or `unknown` — and `null` on an entry read from a live model card.
76+
77+
Entries are ordered newest-first by the most recent date they carry, then by `id`. A model published under several endpoint types appears once: the sunset belongs to the model, not to the route.
78+
79+
{% hint style="warning" %}
80+
Match your integrated model name against **`id` and `aliases` together**, never `id` alone. The name you originally integrated against is often the alias rather than the canonical id, and an `id`-only comparison will miss the entry that explains what happened to it.
81+
{% endhint %}
82+
83+
## Filtering
84+
85+
Both filters are optional, and both fail open: an unrecognised value is ignored rather than rejected. This is a discovery surface, and a typo in a polling job should not turn every poll into a `400`.
86+
87+
* **`status`** — case-insensitive, comma-separated (`?status=superseded,withdrawn`) or repeated (`?status=superseded&status=withdrawn`). A parameter holding no recognised value filters nothing and returns the whole feed.
88+
* **`since`** — plain `YYYY-MM-DD`, no time part. Keeps entries whose `deprecated_at` **or** `shutdown_at` falls on or after that date. Entries carrying neither date are dropped, because there is nothing to compare against.
89+
90+
```bash
91+
# what has been withdrawn since the start of the month
92+
curl 'https://api.aimlapi.com/v1/models/deprecations?status=withdrawn&since=2026-08-01'
93+
94+
# everything still serving with a retirement already announced
95+
curl 'https://api.aimlapi.com/v1/models/deprecations?status=deprecated'
96+
```
97+
98+
## Polling and caching
99+
100+
Responses carry a weak `ETag` and `Cache-Control: public, max-age=300`. Send the tag back as `If-None-Match` and you get `304 Not Modified` with an empty body when nothing changed:
101+
102+
```bash
103+
curl -I 'https://api.aimlapi.com/v1/models/deprecations'
104+
# etag: W/"-1EFj0Jxxt7B2mx5LT-MXoCShLk"
105+
106+
curl -H 'If-None-Match: W/"-1EFj0Jxxt7B2mx5LT-MXoCShLk"' \
107+
'https://api.aimlapi.com/v1/models/deprecations'
108+
# 304
109+
```
110+
111+
{% hint style="info" %}
112+
The tag is computed over `data` only. `generated_at` changes on every call, so including it would make every poll a cache miss — the tag moves only when the feed's contents actually move. Polling daily is plenty; the payload is small and changes rarely.
113+
{% endhint %}
114+
115+
## Keeping an integration in sync
116+
117+
The feed and the catalogue answer different halves of the same question, so read both. The catalogue says what you can use today; the feed explains anything the catalogue no longer lists.
118+
119+
{% code overflow="wrap" %}
120+
```js
121+
const [catalogue, feed] = await Promise.all([
122+
fetch('https://api.aimlapi.com/v1/models').then((r) => r.json()),
123+
fetch('https://api.aimlapi.com/v1/models/deprecations').then((r) => r.json()),
124+
]);
125+
126+
// every live name -> its canonical id
127+
const live = new Map();
128+
for (const model of catalogue.data) {
129+
live.set(model.id, model.id);
130+
for (const alias of model.aliases ?? []) live.set(alias, model.id);
131+
}
132+
133+
// every retired name -> the entry explaining it
134+
const retired = new Map();
135+
for (const entry of feed.data) {
136+
retired.set(entry.id, entry);
137+
for (const alias of entry.aliases ?? []) retired.set(alias, entry);
138+
}
139+
140+
function check(modelId) {
141+
const entry = retired.get(modelId);
142+
143+
if (entry?.status === 'withdrawn') {
144+
return { ok: false, migrateTo: entry.replaced_by }; // 404s now — must move
145+
}
146+
if (entry) {
147+
// deprecated or superseded: still serving, migrate on your own schedule
148+
return { ok: true, migrateTo: entry.replaced_by, by: entry.shutdown_at };
149+
}
150+
if (live.has(modelId)) {
151+
return { ok: true }; // healthy
152+
}
153+
return { ok: false, migrateTo: null }; // unknown name — check for a typo
154+
}
155+
```
156+
{% endcode %}
157+
158+
The last branch is the one worth keeping separate. A name in neither list was never a model id here, which is a different problem from a model that went away — and it is the case a catalogue-only check silently reports as a deprecation.
159+
160+
{% hint style="info" %}
161+
Looking for a replacement by hand rather than in code? [All Model IDs](../model-database.md) lists the current catalogue alongside its [deprecated models](../model-database.md#deprecated-no-longer-supported-models) section.
162+
{% endhint %}

0 commit comments

Comments
 (0)