02 · 1/6

Module 2 · Lesson 1 of 6

Pin Your Model Like You Pin Your Packages

7 min read · July 13, 2026

Your package.json pins every dependency to a version. Your Dockerfile pins base images. Your Terraform pins provider versions. Then the same codebase calls an LLM with model: "gpt-5" and ships it to production: an unpinned dependency that the vendor upgrades, rewrites, and eventually deletes on a schedule you don't control, with no lockfile anywhere in your repo.

This module treats the LLM API as what it is: a third-party dependency on your hot path. Module 1 gave you the mental model and a framework for choosing a model. This lesson is about what happens after you choose: how to hold onto the model you tested, and how to move off it on your schedule instead of the provider's.

Two dependencies, one of them unpinned

An LLM integration actually has two versioned dependencies, and most teams pin only one:

  • The SDK (openai, @anthropic-ai/sdk): a normal npm package. Your lockfile pins it. It buys you typed requests and responses, default retries with backoff, and streaming helpers, which is why the SDK is the right default over raw fetch. Raw HTTP earns its place in two cases: you're building a gateway seam that normalizes providers anyway, or you're in an environment where a fat dependency hurts (edge runtimes, tight cold-start budgets).
  • The model. Not a package. There is no lockfile, no semver, no npm audit. The string you pass as model is the only pin you get, and whether it actually pins anything depends on which kind of string it is.

Model IDs are versions: snapshots vs aliases

Providers publish two kinds of model identifiers, and the difference is exactly nginx:1.27.4 vs nginx:latest:

Pinned snapshot

claude-sonnet-4-5-20250929, gpt-5-2025-08-07

  • Frozen weights: behavior changes only when the provider's serving stack changes, not the model itself
  • Has a published deprecation and retirement schedule
  • What your evals actually measured
  • The Docker digest of LLM land

Alias

claude-sonnet-4-5, gpt-5, anything -latest

  • A pointer the provider moves to a newer snapshot, sometimes with notice, sometimes with little
  • Output distribution can shift under you with zero code changes on your side
  • Convenient in dev, a silent regression channel in prod
  • The :latest tag of LLM land
Two kinds of model ID. The alias is convenient in a notebook and a liability on a hot path.

You already saw the failure mode in the non-determinism lesson: "silent model updates" is one of the four reasons temperature=0 still isn't deterministic. An alias that moves is a dependency upgrade you didn't review, deployed straight to production, with no diff to read. Both Anthropic and OpenAI document which of their IDs are snapshots and which are aliases; the naming convention (a date suffix) makes snapshots easy to spot.

The rule: aliases in notebooks and experiments, snapshots everywhere a request has a user or an invoice attached.

Deprecation is a lifecycle, not an event

Pinning a snapshot doesn't opt you out of change; it makes change scheduled instead of silent. Every model you pin is already on a conveyor belt:

  1. Snapshot releasedpinyou eval it, pin it, ship it
  2. Newer snapshot shipsre-evalaliases move to it; your pin holds
  3. Deprecation announcedschedulea retirement date is published, typically months out
  4. Retiredtoo latethe ID starts returning 404s
The lifecycle of a pinned model, and the move you make at each stage. As of mid-2026, the whole belt runs roughly 12-18 months for mainstream models.

Both providers publish the schedule: Anthropic's model deprecations page and OpenAI's deprecations page list every model with its shutdown date. This is public information with a deadline, which makes "our AI feature broke because the model was retired" an outage with no excuse: it's the same class of failure as letting a TLS certificate expire.

The retirement itself arrives as a 404 on a model ID that worked yesterday. The retries guide classifies that 404 correctly: never retry, alarm immediately, because a deprecation just found you in production.

The upgrade playbook: treat a new snapshot like a code change

When you do move (voluntarily to a better snapshot, or forced by a deprecation), the discipline is the one you already know from library upgrades. A new model version can change output format adherence, refusal behavior, verbosity, and tool-use habits all at once; the choosing-a-model lesson built the eval harness, and this is the second place it earns its keep:

  1. Branch: point the candidate snapshot at your eval suite, not at traffic.
  2. Gate: it ships only if the pass rate holds. A drop is a real regression, even if the new model is "better" on benchmarks; your prompts were tuned against the old one's habits.
  3. Canary: roll it to a slice of traffic with the model ID in your logs (below), so quality complaints during the rollout are attributable.
  4. Keep the old pin warm until the canary settles, exactly like keeping the previous deploy ready to roll back. Deprecations give you months; use them to run both.

The mechanical prerequisite for all four steps: the model ID must live in config, not be scattered as string literals through your services.

typescript
// models.ts: the lockfile your provider never gave you.
// One entry per feature, snapshots only, with the metadata that
// makes upgrades and cost attribution routine instead of archaeology.
export const MODELS = {
  "ticket-summary": {
    id: "claude-haiku-4-5-20251001",
    price: { in: 1 / 1e6, out: 5 / 1e6 },   // USD/token, lives next to the id
    retiresAt: "2027-03-01",                 // from the provider's deprecation page
    owner: "support-platform",
  },
  "contract-review": {
    id: "claude-sonnet-4-5-20250929",
    price: { in: 3 / 1e6, out: 15 / 1e6 },
    retiresAt: "2027-06-15",
    owner: "legal-tools",
  },
} as const;

One file to review when a deprecation email lands, one diff when a feature migrates, and per-feature price constants already sitting next to the ID for the cost log line. A 20-line CI script that compares retiresAt against today's date turns "the shared-inbox email" into a failing build 90 days out.

The operational surface you skipped in the quickstart

Three small things that cost nothing on day one and pay off in every incident:

Log the request ID and the echoed model. Every response carries a provider-side request ID (Anthropic: the request-id response header; OpenAI: x-request-id) and the response body echoes which model actually served the call. Log both. The request ID is what provider support asks for, and the echoed model field is your proof of exactly which snapshot produced a bad output, which matters precisely when you're mid-migration and two pins are live:

typescript
log.info("llm_call", {
  feature: "ticket-summary",
  model: res.model,                          // what actually served, not what you asked for
  request_id: res._request_id,               // provider-side trace handle
  // ...tokens, cost, latency, per the observability log line
});

Know that the API itself is versioned separately. Anthropic requires an anthropic-version header on every request: it pins the request/response shape, independent of which model you call. The SDK sets it for you, which is fine, until you're debugging raw HTTP and forget it exists. Model version and API version move on separate tracks.

Subscribe to the feeds. Provider status pages, changelogs, and the deprecation pages above. Ten minutes of RSS setup replaces "we found out from a Hacker News thread."

Key takeaways

  • An LLM integration has two dependencies: the SDK (your lockfile pins it) and the model (nothing pins it unless you do). The model string is the only pin you get.
  • Snapshots (-20250929 date suffixes) are frozen; aliases (gpt-5, -latest) move under you. Aliases in notebooks, snapshots in production, always.
  • Deprecation is a published lifecycle, not a surprise: as of mid-2026, mainstream models retire on a roughly 12-18 month belt, announced months ahead. A retirement you didn't calendar is a self-inflicted outage that arrives as a 404.
  • Upgrade like a library bump: eval the new snapshot against your suite, gate on pass rate, canary with the model ID in logs, keep the old pin warm.
  • Centralize model IDs in one config registry with price, owner, and retirement date; alarm on retirement dates from CI.
  • Log the provider request ID and the echoed model on every call, and remember the API shape is versioned separately from the model.
Next lesson →