Skip to content

9 min read

The useful part of a deprecation plan exists before the announcement

On this page

The email went out to both companies at the same time on a Tuesday morning: the model they were using would stop accepting requests in ninety days, with a named successor and a migration guide linked at the bottom.

At the first company it produced a Slack thread with fourteen participants, a hastily booked hour on Thursday, and an argument about whether the successor was even the right target given that a competitor had shipped something interesting in March. Nobody in the thread could say how many services called the deprecated model. The engineer who had set up the fine-tuned variant used by the classification service had left in January. A fine-tune is a copy of a vendor's model trained further on your own labelled examples and served under an identifier of its own, and it took two days to establish that the base model this one had been trained from was the model being retired, which retires the fine-tune with it.

At the second company the on-call for the model registry acknowledged the notice, opened the registry file, and read four lines: the deprecated identifier was used by six tasks, all owned by two teams; the qualified replacement was recorded with its eval results from a run six weeks earlier showing seven of nine gates green and two waived with a written reason; a compatibility test had been running the replacement against every affected suite nightly since then; and the last safe rollout date was already computed and sat 34 days before the sunset. The work that remained was real — a canary, a retune of one task, a sign-off — and it was scheduled that afternoon.

The difference between those two mornings was not built in ninety days. It was built during the eighteen months when nothing was happening.

The four things worth having standing

A model dependency registry is a small file, and its value is entirely in being complete and current rather than in being sophisticated.

yaml
# config/models/registry.yaml
- identifier: vendor-a/reason-3-20250219
  owner: platform-ml            # a team, with a rota, not a person
  used_by: [extract-invoice, classify-ticket, route-escalation,
            moderate-content, draft-reply, extract-entities]
  serving_since: 2025-03-18
  replacement:
    identifier: vendor-a/reason-4-20251106
    qualified_on: 2026-05-22
    eval_run: https://ci.internal/runs/48812
    gates: {green: 7, waived: 2}
    waivers:
      - gate: cost_per_1k_usd
        reason: "+9% accepted; offset by caching, see ADR-114"
      - gate: output_tokens_p95
        reason: "draft-reply length floor added, see PR 9930"
  compatibility_test: nightly   # runs affected suites against replacement
  announced_sunset: null
  last_safe_rollout: null       # computed when a sunset date exists

- identifier: vendor-a/embed-2
  owner: search-infra
  used_by: [rerank-search, semantic-index]
  derived_artifacts:
    - kind: vector_index
      rows: 4_200_000
      rebuild_hours: 216        # rate-limited; see runbook RB-31
  replacement:
    identifier: vendor-a/embed-3
    qualified_on: 2026-02-04
    eval_run: https://ci.internal/runs/44907
    gates: {green: 5, waived: 0}

The derived_artifacts block on the embedding entry is the field that most registries lack and most incidents need. An embedding model's retirement does not merely require a code change; it invalidates 4.2 million stored vectors, and the 216 hours is a measured number from a rehearsal rather than a guess, which is what makes it usable in backward planning.

The owner field naming a team rather than a person is deliberate for the reason the first company discovered. The waivers matter as much as the green gates, because a replacement qualified with two waived gates and a written reason is a replacement someone has actually thought about, and a replacement with nine green gates and no notes is often one nobody has looked at since the suite was written.

The compatibility test is what keeps the record true

A registry that is written once decays into fiction within a quarter, because prompts change, eval sets grow, and the replacement itself ships new checkpoints. The mechanism that keeps it honest is a scheduled job that treats the recorded replacement as a live claim.

python
# ci/lifecycle_check.py
def main() -> int:
    registry = load_registry("config/models/registry.yaml")
    catalogue = provider.list_models()          # includes deprecation metadata
    failures: list[str] = []

    for entry in registry:
        meta = catalogue.get(entry.identifier)
        if meta is None:
            failures.append(f"{entry.identifier}: no longer listed by provider")
        elif meta.deprecation_date and entry.announced_sunset is None:
            failures.append(
                f"{entry.identifier}: sunset {meta.deprecation_date} announced, "
                f"registry not updated (owner {entry.owner})")

        if entry.replacement is None:
            failures.append(f"{entry.identifier}: no qualified replacement")
            continue

        result = eval_runner.run(tasks=entry.used_by,
                                 binding=entry.replacement.identifier,
                                 compare_to=entry.identifier)
        if result.regressions_beyond(entry.replacement.waivers):
            failures.append(
                f"{entry.replacement.identifier}: regressed on "
                f"{', '.join(result.failed_gates)}")

    for f in failures:
        page(owner_of(f), f)
    return 1 if failures else 0

Two things are being checked and they are different in kind. The catalogue check asks whether the provider has said anything new about a model you depend on, which turns the announcement from an email someone might read into a build failure with an owner attached; providers publish retirement dates and notice windows in their documentation, and the Claude Platform Docs deprecations page is the canonical example of the format. The eval check asks whether the replacement is still qualified against the current version of your tasks, which catches the far more common decay where a prompt improved for the incumbent three months ago and quietly stopped working on the replacement.

Running the full suites nightly for every registry entry costs real money. This company's nightly lifecycle run was about $340 a month in model spend, which is the cheapest line item anywhere in this topic.

The date you actually plan against is not the sunset

A sunset date is when requests start failing. The date that constrains the schedule is the last day a rollout can begin and still land safely, and it is computed backwards through the work that chapter 4 itemised.

Diagram.

The property that makes day 56 the constraint is that after the sunset the incumbent is gone, so the rollback target evaporates. Every deployment before day 90 has a way back; the deployment that is still bedding in on day 89 does not. Two weeks of canary plus two weeks of full traffic with the old binding reachable is the minimum this team was willing to run for a task in the payment path, and subtracting it from 90 gives 56. If the affected set had included the embedding model, the 216 rebuild hours and the double-storage window would have pushed the constraint back another three weeks.

Company-specific friction goes in the same subtraction. A December sunset intersects a change freeze; a task under SOC 2 scope, meaning it falls inside the security-controls audit that enterprise buyers ask for evidence of, may need the model change recorded and approved before it ships; a task serving a customer with a contractual model clause needs that customer notified, which at enterprise pace is four weeks of email.

What you do first, and why it is not choosing a successor

The instinct on notice day is to pick the successor and start swapping, which skips the step that determines whether the plan is about the right things. The inventory comes first, in the form of a reconciliation between the registry and reality, because everything downstream is scoped by it.

Reconciliation means the billing-console sweep from chapter 4 — every key, every project, matched to a registry entry — plus a grep for the retired identifier across all repositories, plus the derived-artifact question asked explicitly for each entry: are there stored embeddings, cached outputs, fine-tuned variants, or evaluation baselines that were produced by this model and are compared against new output? The fine-tune case is the one that surprises people, since a fine-tuned model is a distinct identifier with its own name and does not obviously belong to the base model being retired, and retiring the base retires it.

The first company's ninety days were mostly this. The second company's inventory took a day and found one thing the registry had missed, a notebook in the data team's repo, which is roughly the hit rate you should expect even from a well-kept registry.

The reconciliation runs in a fixed order, and the order is chosen so that each step narrows the next. Start from the provider's own usage records, broken down by API key and project, because that is the only source that sees every caller including the ones outside your repositories. Match each key to a registry entry and to a human who will answer a message; keys that match nothing are the discovery list. Then grep every repository — including infrastructure, notebooks, and the internal tooling repo that nobody thinks of as production — for the retired identifier as a literal string, which catches hard-coded values that bypass the registry. Then walk each registry entry's derived_artifacts and ask what would need rebuilding, which is where embeddings, fine-tunes and stored baselines surface. Finally, check evaluation baselines, because a suite whose reference outputs were generated by the retired model will start reporting regressions that are artefacts of the baseline rather than of the candidate.

The fine-tune case deserves the extra minute it takes to check. A fine-tuned model carries its own identifier, appears in your config as an unrelated string, and is trained on a base checkpoint that the deprecation notice names. When the base retires, the derived model retires with it, and the replacement is not a config change: it is a re-run of the fine-tuning job against the new base, followed by re-qualification of a model whose behaviour has changed for two reasons at once. The company that discovers this on day 60 has lost the schedule, and the field that prevents it is a base_model line in the registry entry.

The reason the swap-first instinct is wrong is not that swapping is hard; the eleven-day binding work from chapter 4 shows it is the easy part. It is that swapping first assumes three things the inventory is there to establish: that every caller is inside the codebase you are editing, that one successor serves all of them, and that a rollback target will exist when you need it. The first assumption failed at both companies. The second failed at the second company, where two of six tasks went to a different and cheaper successor. The third fails by construction after the sunset date, which is why the schedule is planned backwards from a rollout window rather than forwards from the day the work starts.

The registry also answers a question the panic version never gets to, which is whether all six tasks should go to the same successor. Two of the six had been better served by a smaller, cheaper model in the same family for months, and the deprecation was the forcing function that made anyone check. Treating a deprecation as a per-task decision rather than a global identifier swap is available only if the seam is per task, which is the payoff for chapter 2 arriving here rather than in an incident.

The limits of standing preparation

None of this makes a deprecation cheap. The second company still spent about five engineer-weeks on the ninety days, and the difference was that the weeks were scheduled rather than emergency-staffed, and that they were spent on retuning and adjudication instead of on discovery.

Nor does it guarantee the notice window is what you planned for. Ninety days is a common figure and it is not a contract; windows vary by vendor, by whether the model is generally available or preview, and by whether the retirement is a scheduled lifecycle event or a response to something the vendor discovered. Preview and beta identifiers routinely carry much shorter guarantees, which is a reason to keep them out of any path that a customer depends on, or to accept that dependency with the reversal cost written down. The published lifecycle pages are the source of truth for the windows a vendor commits to, and reading them once when you adopt a model is cheaper than discovering the number on the day.

It also does not cover the case where there is no acceptable successor. A cheap, fast model retired in favour of one that costs 2.8x more, with no smaller option in the family, is a change to unit economics rather than a migration, and no amount of registry hygiene resolves it — that goes to whoever owns the product's margins, and the useful thing the registry contributes is the number, early.

And a registry describes dependencies on models, which is the easy half of the dependency surface. The harder half is dependence on a provider's distinctive capabilities, where nothing will ever send you a ninety-day notice, and where the decision to depend was made by a pull request that nobody framed as a decision at all. The next chapter is about writing that one down while you are making it.

Migration and portability — read the current version at https://daviddominguez.dev/learn/en/migration-and-portability/the-useful-part-of-a-deprecation-plan-exists-before-the-announcement

Back to top