@aidriftline/driftline 0.1.17 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -103,6 +103,10 @@ Needs Node 20+. Free, no key.
103
103
 
104
104
  Every model also has its own page with the date, replacement and source: [aidriftline.com/models](https://aidriftline.com/models/).
105
105
 
106
+ ## Versions
107
+
108
+ Releases are numbered `0.x.y`. A new `x` means new features; a new `y` means fixes or updated retirement data, which can ship several times a week. `uses: AI-Driftline/driftline@v0` always runs the newest release, and the Action reads the live retirement list at aidriftline.com either way, so a provider's change reaches you without a new version.
109
+
106
110
  ## Development
107
111
 
108
112
  ```sh
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aidriftline/driftline",
3
- "version": "0.1.17",
3
+ "version": "0.2.1",
4
4
  "description": "Finds OpenAI, Anthropic and Google model IDs in your code and warns before they are retired. CLI, GitHub Action and MCP server.",
5
5
  "mcpName": "io.github.AI-Driftline/driftline",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "checked": "2026-11-13",
3
- "note": "Hand-maintained from the official deprecation pages. Re-check before each release. Entries with a 'kind' other than text are scanned and reported but never drift-tested. gpt-4-1106-preview is listed on OpenAI's page under both 2026-03-26 and 2026-10-23; the later date is used so builds are not failed on a conflicting record. Entries with routes: true keep answering after the date, but the provider sends the requests to the replacement model (Google calls this automatic routing); the 'retires' date is the day routing starts. gemini-3.5-flash and gemini-3.7-flash already route to newer models with no date given, so they are left out.",
3
+ "note": "Hand-maintained from the official deprecation pages. Re-check before each release. Entries with a 'kind' other than text are scanned and reported but never drift-tested. gpt-4-1106-preview is listed on OpenAI's page under both 2026-03-26 and 2026-10-23; the later date is used so builds are not failed on a conflicting record. Entries with routes: true keep answering after the date, but the provider sends the requests to the replacement model (Google calls this automatic routing); the 'retires' date is the day routing starts. gemini-3.5-flash and gemini-3.7-flash already route to newer models with no date given, so they are left out. gemini-3.1-flash-live-preview was removed on 2026-11-14: Google now lists it with no shutdown date.",
4
4
  "sources": {
5
5
  "openai": "https://developers.openai.com/api/docs/deprecations",
6
6
  "anthropic": "https://platform.claude.com/docs/en/about-claude/model-deprecations",
@@ -1709,18 +1709,10 @@
1709
1709
  "replacement": "gemini-3.8-live",
1710
1710
  "kind": "audio"
1711
1711
  },
1712
- {
1713
- "id": "gemini-3.1-flash-live-preview",
1714
- "provider": "google",
1715
- "retires": "2026-11-17",
1716
- "qualifier": "earliest",
1717
- "replacement": "gemini-3.8-live",
1718
- "kind": "audio"
1719
- },
1720
1712
  {
1721
1713
  "id": "gemini-2.5-flash-native-audio-preview-12-2025",
1722
1714
  "provider": "google",
1723
- "retires": "2026-11-17",
1715
+ "retires": "2027-03-31",
1724
1716
  "qualifier": "earliest",
1725
1717
  "replacement": "gemini-3.8-live",
1726
1718
  "kind": "audio"
@@ -1728,25 +1720,28 @@
1728
1720
  {
1729
1721
  "id": "gemini-3.1-flash-tts-preview",
1730
1722
  "provider": "google",
1731
- "retires": "2026-11-17",
1723
+ "retires": "2026-12-18",
1732
1724
  "qualifier": "earliest",
1733
1725
  "replacement": "gemini-3.8-flash-tts",
1726
+ "replacement_alt": "gemini-3.8-flash-lite-tts",
1734
1727
  "kind": "audio"
1735
1728
  },
1736
1729
  {
1737
1730
  "id": "gemini-2.5-flash-preview-tts",
1738
1731
  "provider": "google",
1739
- "retires": "2026-11-17",
1732
+ "retires": "2026-12-18",
1740
1733
  "qualifier": "earliest",
1741
1734
  "replacement": "gemini-3.8-flash-tts",
1735
+ "replacement_alt": "gemini-3.8-flash-lite-tts",
1742
1736
  "kind": "audio"
1743
1737
  },
1744
1738
  {
1745
1739
  "id": "gemini-2.5-pro-preview-tts",
1746
1740
  "provider": "google",
1747
- "retires": "2026-11-17",
1741
+ "retires": "2026-12-18",
1748
1742
  "qualifier": "earliest",
1749
1743
  "replacement": "gemini-3.8-flash-tts",
1744
+ "replacement_alt": "gemini-3.8-flash-lite-tts",
1750
1745
  "kind": "audio"
1751
1746
  },
1752
1747
  {
package/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // GitHub Action entry point.
3
- import { replacementText, replacementChange, loadBestRegistry } from "./registry.js";
3
+ import { replacementText, replacementChange, loadBestRegistry, reviewText } from "./registry.js";
4
4
  import { migrationFor } from "./migration.js";
5
5
  import { resolve } from "node:path";
6
6
  import { statSync } from "node:fs";
@@ -65,7 +65,8 @@ async function main() {
65
65
  ? `${f.id} is past its earliest shutdown date (${f.entry.retires}) and may stop working at any time.`
66
66
  : `${f.id} ${STATUS_TEXT[f.status]} (${f.entry.retires}).`;
67
67
  const routedMore = f.entry.routes && migrationFor(f.entry)?.change === "params" ? ` Before then, apply the request changes the provider lists: ${migrationFor(f.entry).note}` : "";
68
- annotate(level, text + repl + more + routedMore, f.file, f.line);
68
+ const review = f.entry.under_review ? ` ${reviewText(f.entry)}` : "";
69
+ annotate(level, text + repl + more + routedMore + review, f.file, f.line);
69
70
  }
70
71
 
71
72
  // The report contains model answers; stop GitHub from reading any "::command::" lines inside it.
package/src/mcp.js CHANGED
@@ -6,7 +6,7 @@
6
6
  import { readFileSync, statSync } from "node:fs";
7
7
  import { resolve } from "node:path";
8
8
  import { createInterface } from "node:readline";
9
- import { loadBestRegistry, classify, normalizeId, replacementChange, replacementText } from "./registry.js";
9
+ import { loadBestRegistry, classify, normalizeId, replacementChange, replacementText, reviewText } from "./registry.js";
10
10
  import { migrationFor } from "./migration.js";
11
11
  import { scanDirectory } from "./scan.js";
12
12
 
@@ -47,7 +47,8 @@ function describe(rawId, reg, today) {
47
47
  ...(m.previous_replacement ? { previous_replacement: m.previous_replacement, replacement_changed: m.replacement_changed } : {}), routes: !!m.routes, ...(m.aliasOf ? { alias_of: m.aliasOf } : {}), page: page(m.aliasOf || m.id),
48
48
  } : {}),
49
49
  ...(m && migrationFor(m) ? { migration: migrationFor(m) } : {}),
50
- advice: advice(c, today),
50
+ ...(m?.under_review ? { under_review: m.under_review } : {}),
51
+ advice: (m?.under_review ? reviewText(m) + " " : "") + advice(c, today),
51
52
  };
52
53
  }
53
54
 
@@ -62,6 +63,7 @@ const MODEL_RESULT = {
62
63
  qualifier: { type: "string", enum: ["exact", "earliest", "not-before"], description: "exact date, earliest possible shutdown (Google), or not before this date (Anthropic)" },
63
64
  days_left: { type: "number", description: "Days until the date; 0 or negative when it has passed" },
64
65
  replacement: { type: ["string", "null"] },
66
+ under_review: { type: "object", description: "Present when the provider's page no longer matches this entry and Driftline is updating it: since (date), what (date, replacement or listing), page (source to check)", properties: { since: { type: "string" }, what: { type: "string" }, page: { type: "string" } } },
65
67
  replacement_alt: { type: "string", description: "A second replacement the provider names (\"X or Y\"); which one fits depends on how the model is used" },
66
68
  previous_replacement: { type: "string", description: "The replacement the provider recommended before it changed its advice" },
67
69
  replacement_changed: { type: "string", description: "YYYY-MM-DD: when the change was seen on the provider's page" },
package/src/registry.js CHANGED
@@ -103,6 +103,15 @@ export const isTextModel = (entry) => !entry?.kind || entry.kind === "text";
103
103
  export const replacementText = (entry) => entry?.replacement
104
104
  ? entry.replacement + (entry.replacement_alt ? ` or ${entry.replacement_alt}` : "") + (entry.replacement_note ? ` (${entry.replacement_note})` : "") : "";
105
105
 
106
+ // The live list marks an entry under_review when the provider's page no longer matches it (found by the page check
107
+ // every 6 hours) and the registry hasn't been fixed yet. Say so instead of presenting the stored date as fact.
108
+ const WHO_NAME = { openai: "OpenAI", anthropic: "Anthropic", google: "Google" };
109
+ export function reviewText(entry) {
110
+ const r = entry?.under_review;
111
+ if (!r) return "";
112
+ return `Under review: ${WHO_NAME[entry.provider] || entry.provider}'s page no longer matches the ${r.what || "entry"} shown here (since ${r.since}); Driftline is updating it. Check ${r.page || "the provider's deprecation page"} before acting on this ${r.what === "replacement" ? "replacement" : "date"}.`;
113
+ }
114
+
106
115
  // Providers sometimes change the recommended replacement after announcing a retirement. The registry keeps
107
116
  // the old one and the date we saw the change, so reports can say so instead of silently switching targets.
108
117
  export const RECENT_CHANGE_DAYS = 120;
package/src/report.js CHANGED
@@ -1,4 +1,4 @@
1
- import { replacementChange } from "./registry.js";
1
+ import { replacementChange, reviewText } from "./registry.js";
2
2
  import { migrationFor, MIGRATION_LABEL } from "./migration.js";
3
3
 
4
4
  const STATUS_LABEL = {
@@ -68,6 +68,7 @@ export function renderReport({ findings, drift, registry, notices = [], answers
68
68
  : `Found ${byModel.size} model ID${byModel.size === 1 ? "" : "s"}. None are scheduled for retirement.`);
69
69
  lines.push("", "| Model | Status | Retirement | Replacement | Change needed | Where |", "|---|---|---|---|---|---|");
70
70
  const migNotes = new Map(); // note → { n, ids, guide } for the list under the table
71
+ const underReview = []; // rows whose provider page changed and the registry isn't fixed yet
71
72
  const order = ["retired", "at-risk", "deprecated", "active", "unknown"];
72
73
  const all = [...byModel.values()].sort((a, b) => order.indexOf(a.status) - order.indexOf(b.status));
73
74
  const rows = all.slice(0, MAX_ROWS); // flagged models sort first, so only unknown/active rows get cut
@@ -83,9 +84,14 @@ export function renderReport({ findings, drift, registry, notices = [], answers
83
84
  }
84
85
  const places = r.places.slice(0, 3).join(", ") + (r.places.length > 3 ? ` +${r.places.length - 3} more` : "");
85
86
  const docsOnly = r.places.length && byModel.get(r.id).docs === r.places.length ? " · docs only" : "";
86
- lines.push(`| ${code(r.id)}${r.entry?.aliasOf ? " · alias of " + code(r.entry.aliasOf) : ""}${r.entry?.kind && r.entry.kind !== "text" ? " · " + r.entry.kind : ""} | ${label(r)}${docsOnly} | ${esc(when(r))} | ${r.entry?.replacement ? code(r.entry.replacement) + (r.entry.replacement_alt ? " or " + code(r.entry.replacement_alt) : "") + (r.entry.replacement_note ? " (" + esc(r.entry.replacement_note) + ")" : "") + changed(r.entry, today) : ""} | ${changeCell} | ${places} |`);
87
+ if (r.entry?.under_review) underReview.push(r);
88
+ lines.push(`| ${code(r.id)}${r.entry?.aliasOf ? " · alias of " + code(r.entry.aliasOf) : ""}${r.entry?.kind && r.entry.kind !== "text" ? " · " + r.entry.kind : ""} | ${label(r)}${docsOnly}${r.entry?.under_review ? " · 🔍 under review" : ""} | ${esc(when(r))} | ${r.entry?.replacement ? code(r.entry.replacement) + (r.entry.replacement_alt ? " or " + code(r.entry.replacement_alt) : "") + (r.entry.replacement_note ? " (" + esc(r.entry.replacement_note) + ")" : "") + changed(r.entry, today) : ""} | ${changeCell} | ${places} |`);
87
89
  }
88
90
  if (all.length > rows.length) lines.push("", `_${all.length - rows.length} more model ID(s) with no retirement listed are not shown._`);
91
+ if (underReview.length) {
92
+ lines.push("", "**🔍 Under review:**", "");
93
+ for (const r of underReview) lines.push(`- ${code(r.id)}: ${esc(reviewText(r.entry))}`);
94
+ }
89
95
  if (migNotes.size) {
90
96
  lines.push("", "**More than a rename:**", "");
91
97
  for (const [note, m] of migNotes) lines.push(`${m.n}. ${m.ids.map(code).join(", ")}: ${esc(note)}${m.guide ? ` ([provider guide](${m.guide}))` : ""}`);