Skip to content

Repository files navigation

driftcite: find the API calls in your code that already stopped working
Apache 2.0 · 35 providers · 579 artifacts · zero dependencies · 230 tests passing · Node 18+



Why · Rules · Checks · Fix · CI · Alerts · Precision · Coverage


$ npx driftcite .
feed: live | 0 dependencies resolved

4 provider findings (4 breaking)

[BREAKING] openai/model_id/text-davinci-003 -- retired (DIED 945 days ago, 2024-01-04)
  Shut down. Requests fail.
  use instead: gpt-5.6-terra
  evidence: https://developers.openai.com/api/docs/deprecations
    models.py:6  LEGACY = "text-davinci-003"

[BREAKING] openai/model_id/gpt-4-vision-preview -- retired (DIED 608 days ago, 2024-12-06)
  Shut down. Requests fail.
  use instead: gpt-4o
  evidence: https://developers.openai.com/api/docs/deprecations
    models.py:8  VISION = "gpt-4-vision-preview"

[BREAKING] openai/model_id/gpt-4-turbo -- deprecated (breaks in 78 days, 2026-10-23)
  Shutdown announced for 2026-10-23.
  use instead: gpt-5.6-sol
  evidence: https://developers.openai.com/api/docs/deprecations
    models.py:7  CHAT_MODEL = "gpt-4-turbo"

[BREAKING] stripe/endpoint//v1/invoices/upcoming -- removed
  GET /v1/invoices/upcoming existed in 2024-06-20 and is gone in 2026-06-24.
  evidence: https://github.com/stripe/openapi/compare/v1200...v2345
    billing.js:4  return stripe.request("GET", "/v1/invoices/upcoming", { customer });

That is a whole run, on 2026-08-06. The only line removed is the one naming the directory it was pointed at; no finding was dropped to make the output look tidier. The day counts are the part that moves: they are computed against the day the command runs, so they will not read 945 and 78 when you run it.

Runs on your machine. Nothing is uploaded. No account.



Why this exists

Your lockfile does not move when a provider removes an endpoint, drops a request field, or shuts down a model. Nothing in your dependency tree changes. Every dependency tool reads manifests, so every dependency tool is blind to this by construction.

Reading the changelog is not enough either. CircleCI went down for 1h50m in March 2022 after an upstream endpoint moved, and their postmortem says it better than we could:

We realized during the incident that although we had been notified several times about this change, we had not realized that it would affect us.

They were told. Repeatedly. Over two years. Somebody still had to map a prose announcement onto their own call sites, and nobody did.


Two rules

Every fact cites the provider

A manifest asserts, the scanner locates, and no language model sits anywhere in the detection path.

When we say Stripe removed /v1/invoices/upcoming, the finding links to Stripe's own git compare. Verify it in one click.

You should not have to trust us.

Severity is computed against today

An entry reading "deprecated, retires 2026-10-23" is a countdown, not a footnote, and becomes retired the moment that date passes.

Findings sort by how long you have left: breaks in 78 days before, DIED 945 days ago after.

No dependency tool has a field for time remaining.


What it checks

Hosted API drift

Generated by diffing two versions of a provider's own published OpenAPI spec. Removed endpoints, dropped parameters, retired enum values. Nothing hand-written, nothing inferred.

python3 scanner/openapi_diff.py \
  --provider stripe --from v1200 --to v2345

Maintainer-flagged versions

npm carries a per-version deprecated string, PyPI carries yanked_reason. Written by the maintainer, public, and unseen after install time.

Lockfiles read: package-lock.json, pnpm-lock.yaml, yarn.lock (classic and berry), pinned requirements.txt, Cargo.lock, Gemfile.lock, and go.sum — Go's retract directives and // Deprecated: module comments are read from the latest go.mod the proxy serves, which is where Go keeps them. A format it cannot read yet is named in the output instead of silently skipped.

RubyGems publishes no yanked flag — a yanked version simply stops being served — so that finding says exactly that and never claims the maintainer deprecated anything.

urllib3@1.25
  "Broken release"

basic-ftp@4.6.6
  "Security vulnerability fixed in 5.2.1, please upgrade"

Sweep every repository you own at once:

python3 scanner/sweep.py --parent ~/code --match '*'

Fixing it

npx driftcite . --fix           # show the swaps
npx driftcite . --fix --write   # apply them
  models.py:8
- VISION = "gpt-4-vision-preview"
+ VISION = "gpt-4o"
  https://developers.openai.com/api/docs/deprecations

There is no model in this path either. It swaps a string the provider retired for the string the provider named, inside the quoting your code already uses.

When the replacement is prose rather than a drop-in token, it refuses and says so:

1 finding(s) need a human
  stripe/endpoint//v1/invoices/upcoming
    the provider named no replacement

Important

Comment lines are never edited, only the lines it reported may change, and if an edit would touch anything else the file is left alone entirely.


Adopting it in a codebase that already has drift

Most codebases will have findings on day one, and a build that fails forever is a build people delete the check from. Two ways to accept what is already there without going blind to what arrives next.

A baseline. Accept today's findings, fail only on new ones:

npx driftcite . --write-baseline

This writes .driftcite-baseline.json. Everything already broken is accepted; anything new still fails. Delete an entry once you fix it.

An ignore file for permanent decisions, .driftciteignore:

legacy/*                                  # a path
openai/model_id/text-davinci-003          # an artifact, anywhere
examples/* :: google/model_id/gemini-2.0-flash   # that artifact, only there

Suppressed findings are counted and reported as suppressed, never silently dropped. A tool that hides things is worse than one that annoys.

In CI

- uses: nilaypatell/driftcite@v0.2.1

Fails the build on breaking drift and writes the findings, with evidence links, into the job summary. Inputs: path, fail-on-breaking, check-dependencies, offline, slack-webhook.

Not on GitHub? The Action is a thin wrapper; the CLI is the product and runs in any CI. GitLab:

driftcite:
  image: node:22
  script: [npx driftcite .]

The same one-liner works in Bitbucket Pipelines, Jenkins, or a plain cron job on a server. Exit 1 on breaking drift is the whole interface.

Hearing about it without running anything

Slack. --slack posts breaking findings to a Slack incoming webhook, with the evidence links and the provider-named replacements in the message. The URL is read from DRIFTCITE_SLACK_WEBHOOK — environment, not argv, so it never lands in shell history. Only breaking findings post; a clean scan says nothing, because a daily "all clear" is how a channel gets muted. Schedule it with a baseline committed and the channel only ever hears about drift that is new:

- uses: nilaypatell/driftcite@v0.2.1
  with:
    slack-webhook: ${{ secrets.DRIFTCITE_SLACK_WEBHOOK }}
$ DRIFTCITE_SLACK_WEBHOOK=https://hooks.slack.com/… npx driftcite . --slack

Calendar. Every dated shutdown in the feed, as an all-day event. Subscribe once, in any calendar app:

https://raw.githubusercontent.com/nilaypatell/driftcite/main/feed/calendar.ics

RSS. Every artifact that appears, every status that moves, and — because providers really do this — every time a shutdown date itself is moved:

https://raw.githubusercontent.com/nilaypatell/driftcite/main/feed/changes.xml

All three carry the same evidence links as the scan output. Nothing in an alert is a guess.

Getting a pull request instead of a report

Two ways, one outcome: a PR arrives with the fix already applied and the provider's own page cited.

Install the GitHub App: github.com/apps/driftcite. Nothing to copy into your repo. The App holds exactly two permissions (contents and pull requests), stores only the artifact IDs your code matched — never your code — and opens one PR per repository when a provider retires something you call. Here is a real one it opened.

Or keep it entirely in your own CI: copy driftcite-autofix.yml into your own .github/workflows/. On a schedule it scans, applies only the fixes the provider itself named, and opens the same pull request from inside your repo.

- const MODEL  = "text-davinci-003";
+ const MODEL  = "gpt-5.6-terra";
- const VISION = "gpt-4-vision-preview";
+ const VISION = "gpt-4o";

It fixes what it is certain about and refuses the rest out loud:

1 finding(s) need a human
  stripe/endpoint//v1/invoices/upcoming
    the provider named no replacement

It only ever runs against the repository it is installed in, and it will not reopen a pull request you have left sitting. Automatic pull requests are welcome when you asked for them and spam when you did not.

How it runs

Where it runs What you get
npx driftcite . your machine what is broken right now
GitHub Action your CI the same, every PR, exit 1 on breaking
Autofix workflow your CI a pull request with the fix already applied
The App our sweep a PR in every installed repo when a provider moves

The first two answer what is broken now. The third closes the loop yourself. The fourth closes it for every repository at once, without a workflow file in any of them, and runs even while your laptop is asleep.

Note

Your source code never reaches us at any tier. The hosted watch stores only the artifact IDs your code matched, a few hundred strings, never files.


Precision is the product

The first version reported 86 findings on a real 1,200-dependency repository and roughly a quarter were real. The parameter refund was matching inside "refunded" and inside a sentence about refunds. A retired enum value like hosted is an ordinary English word.

Each artifact kind now matches only in the shape it actually takes when sent to a provider:

Kind Matches as
enum_value a quoted string literal
request_param a quoted literal or an object key
model_id · endpoint a quoted literal or a bounded token

Kinds other than model IDs also require the file to reference that provider at all. Comment lines are skipped. Vendored manifests are ignored.

86 findings at ~25% true positives → 5 at 100%

A tool that is wrong three times out of four gets muted, then deleted. Coverage has gone from 29 artifacts to 235 since then without loosening one match shape, and 24 of the 179 tests exist for no other purpose than to stop it being loosened: refund inside "refunded", hosted as an ordinary English word, a literal quoted inside a comment. Every one of them was a finding this scanner produced before it was a finding this scanner refuses. Coverage is worthless if it arrives with noise.


Coverage

Provider Source Artifacts
OpenAI openai/openai-openapi, deprecations page, Responses migration guide and the v1 SDK migration 120
Mistral model docs and SDK migration guide, curated 45
Square square/connect-api-specification 41
Azure Foundry model retirement schedule, curated 38
Datadog DataDog/datadog-api-client-python 36
Cloudflare cloudflare/api-schemas 34
Kubernetes kubernetes/kubernetes API spec diffs 27
GitHub github/rest-api-description 26
Google Gemini deprecations, changelog and SDK migration guide 23
Anthropic model deprecations and migration guide, curated 21
AWS Lambda runtime schedule and the SDK v2 end-of-support notice, curated 21
Cohere deprecations page and v1-to-v2 guide, curated 19
Bedrock AWS model lifecycle page, curated 17
Algolia algolia/api-clients-automation 21
Google Cloud Cloud Functions runtime schedule, curated 11
Perplexity · Together · xAI deprecation pages and changelogs, curated 24
Stability · ElevenLabs · AssemblyAI · Fireworks · DeepSeek deprecation pages and changelogs, curated 19
Stripe stripe/openapi · tags v1200 through v2375 8
Meta · Salesforce · Google Ads · Shopify API version sunset schedules, curated 14
Twilio · Heroku · Firebase product EOLs, stack retirements, compat deprecation 6
DigitalOcean · Groq · Adyen · Plaid specs and deprecation pages 8
Discord · SendGrid · Intercom · DocuSign · PagerDuty · ngrok · Okta · Elastic · Netlify specs tracked, no drift observed yet 0
Asana · Box tracked, currently no drift 0
npm · PyPI · crates.io · RubyGems every package, no per-provider work live

Thirty-five providers in the published feed, 579 artifacts, every one carrying the provider's own evidence URL. Forty-six are tracked in total. Of the eleven spec repositories added on 2026-08-07, two produced artifacts on their very first historical diff — Kubernetes and Algolia — and the other nine are polled and quiet, which is what a fresh spec under watch looks like.

Coverage grows where findings fire, not where rows are cheap. Measured across 467 real repositories, model IDs and SDK renames produce almost all true findings, so the 2026-08-07 expansion went there first: eight AI providers' model retirements, the API-version sunset schedules of Shopify, Meta, Salesforce and Google Ads, the Lambda and Cloud Functions runtime retirement calendars, and the SDK breaks no lockfile tool can see (openai-python v0, aws-sdk v2, Firebase compat). Where a provider retires by redirect or fall-forward — xAI, Shopify, Meta — the artifact says so and carries no deadline, because nothing hard-fails on the date.

Azure's rows are in the feed again. They were withheld while the published client predated require_context, because a reseller's retirement date reported to somebody calling the vendor directly is a wrong answer with a citation attached. 0.2.1 reads the gate, so the data can follow it.

Two of them resell other people's models on their own clock. Azure retires gpt-4o and claude-sonnet-4-5 on dates their original vendors do not recognise, and AWS states the same thing outright: "only the dates on this page apply." The two are handled differently because they name models differently. Bedrock's IDs carry the reseller in the literal — anthropic.claude-3-5-sonnet-20240620-v1:0 cannot be a direct Anthropic call — so those 17 report anywhere. Azure serves the bare vendor ID, so every one of its artifacts sets require_context and is reported only in files that name Azure. A right answer delivered to the wrong caller is a wrong answer. The same gate guards Together's artifacts, whose HuggingFace-style ids are also how vLLM configs and relays spell models, and the runtime artifacts, because "python3.9" is only a finding in a file that names its platform.

The registries are the cheapest coverage in software: one cursor covers every npm package, one header covers every PyPI project.

Adding a provider

Providers live in providers.yaml, not in code, so adding one is a data change rather than a patch:

  stripe:
    repo: stripe/openapi
    path: openapi/spec3.json
    markers: [stripe, STRIPE_SECRET, STRIPE_API]

markers matter more than they look. They are what stops a retired parameter named refund from flagging every codebase that has ever mentioned a refund.

Full guide in CONTRIBUTING.md. Pull requests welcome.

Tracking an API we don't — including your own

npx driftcite . --init-local

This writes .driftcite-local.json at your repo root. Put feed-shaped artifacts in it — same fields, same rules — and the next scan reports them alongside the public feed. It exists for the APIs the public feed cannot carry: your internal services, a vendor too small for the feed, or a provider whose pull request here hasn't merged yet.

The rules are not relaxed because the file is yours. Every artifact needs an evidence URL (your wiki counts), distinctive literals, and file_markers for any kind other than a model ID; an artifact that fails validation is skipped and named, and a literal the public feed already carries loses to the feed — two death dates on one line of code is the bug, whoever writes the second one. If the API is public, upstream it: a spec repo is four lines in providers.yaml, a deprecations page is a curated manifest.


The feed

Manifests live in manifests/, are regenerated daily by a scheduled workflow in this repository, and are committed here in the open. The git history is the record of what was observed, and when.

The clock does more than diff specs. Every documentation page a curated manifest cites is watched by content hash, so a provider editing a deprecations page becomes a work order instead of silent rot. Every provider's live model list is probed daily where a key is configured: a retirement is then recorded because the ID stopped being served, not only because a page said so. And the endpoints themselves are asked whether they are dying, by HEAD, for the Sunset and Deprecation headers of RFC 8594 and RFC 9745 — twelve paths across GitHub, Stripe and Cohere on 2026-08-06, none of which sent either one. All three records are committed here, in the open, like everything else.

Every fact is public and free to read. You can vendor the whole thing. The hard part was never obtaining it, it is maintaining it, every day, forever, across every provider, because it decays the moment anyone stops.


License

Component License
Scanner · differ · manifest schema Apache 2.0. Embed it, fork it, ship it inside your own tooling
Published manifests Data license. Free to use, redistribute and research; not for repackaging as a competing feed

This split is deliberate. The scanner is permissive because we want it embedded everywhere, including in other people's pipelines. The work that has to be redone every single day is the feed, so that is where the one restriction sits.

The underlying facts are not ours and never could be, which is exactly why every artifact cites the provider who published it.


Documentation


Every number in this README was measured on 2026-08-06, not estimated. The counts are what scanner/build_feed.py printed; the transcripts are whole runs, untrimmed; the day counts inside them are computed against the day the command runs and will differ from yours.

About

Find the API calls in your code that already stopped working. Every fact cites the provider who published it.

Topics

Resources

Contributing

Security policy

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages