|
| 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