@aidriftline/driftline 0.1.16 → 0.2.0
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 +4 -0
- package/package.json +1 -1
- package/registry/models.json +21 -14
- package/src/cli.js +1 -1
- package/src/mcp.js +4 -3
- package/src/registry.js +9 -3
- package/src/report.js +1 -1
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.
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|
package/registry/models.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"checked": "2026-11-
|
|
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.",
|
|
2
|
+
"checked": "2026-11-14",
|
|
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",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"retires": "2027-02-10",
|
|
14
14
|
"qualifier": "exact",
|
|
15
15
|
"replacement": "gpt-image-2.6-sunburst",
|
|
16
|
+
"replacement_alt": "gpt-image-2.6-flare",
|
|
16
17
|
"kind": "image",
|
|
17
18
|
"announced": "2026-11-10"
|
|
18
19
|
},
|
|
@@ -22,6 +23,7 @@
|
|
|
22
23
|
"retires": "2027-02-10",
|
|
23
24
|
"qualifier": "exact",
|
|
24
25
|
"replacement": "gpt-image-2.6-sunburst",
|
|
26
|
+
"replacement_alt": "gpt-image-2.6-flare",
|
|
25
27
|
"kind": "image",
|
|
26
28
|
"announced": "2026-11-10"
|
|
27
29
|
},
|
|
@@ -1072,6 +1074,7 @@
|
|
|
1072
1074
|
"retires": "2027-02-26",
|
|
1073
1075
|
"qualifier": "exact",
|
|
1074
1076
|
"replacement": "gpt-live-transcribe",
|
|
1077
|
+
"replacement_alt": "gpt-transcribe",
|
|
1075
1078
|
"kind": "audio"
|
|
1076
1079
|
},
|
|
1077
1080
|
{
|
|
@@ -1080,6 +1083,7 @@
|
|
|
1080
1083
|
"retires": "2027-02-26",
|
|
1081
1084
|
"qualifier": "exact",
|
|
1082
1085
|
"replacement": "gpt-live-transcribe",
|
|
1086
|
+
"replacement_alt": "gpt-transcribe",
|
|
1083
1087
|
"kind": "audio"
|
|
1084
1088
|
},
|
|
1085
1089
|
{
|
|
@@ -1088,6 +1092,7 @@
|
|
|
1088
1092
|
"retires": "2027-02-26",
|
|
1089
1093
|
"qualifier": "exact",
|
|
1090
1094
|
"replacement": "gpt-live-transcribe",
|
|
1095
|
+
"replacement_alt": "gpt-transcribe",
|
|
1091
1096
|
"kind": "audio"
|
|
1092
1097
|
},
|
|
1093
1098
|
{
|
|
@@ -1096,6 +1101,7 @@
|
|
|
1096
1101
|
"retires": "2027-02-26",
|
|
1097
1102
|
"qualifier": "exact",
|
|
1098
1103
|
"replacement": "gpt-live-transcribe",
|
|
1104
|
+
"replacement_alt": "gpt-transcribe",
|
|
1099
1105
|
"kind": "audio"
|
|
1100
1106
|
},
|
|
1101
1107
|
{
|
|
@@ -1104,6 +1110,7 @@
|
|
|
1104
1110
|
"retires": "2027-02-26",
|
|
1105
1111
|
"qualifier": "exact",
|
|
1106
1112
|
"replacement": "gpt-live-transcribe",
|
|
1113
|
+
"replacement_alt": "gpt-transcribe",
|
|
1107
1114
|
"kind": "audio"
|
|
1108
1115
|
},
|
|
1109
1116
|
{
|
|
@@ -1312,6 +1319,7 @@
|
|
|
1312
1319
|
"retires": "2026-10-23",
|
|
1313
1320
|
"qualifier": "exact",
|
|
1314
1321
|
"replacement": "gpt-image-2.5-sunburst",
|
|
1322
|
+
"replacement_alt": "gpt-image-2.5-flare",
|
|
1315
1323
|
"kind": "image"
|
|
1316
1324
|
},
|
|
1317
1325
|
{
|
|
@@ -1320,6 +1328,7 @@
|
|
|
1320
1328
|
"retires": "2026-12-01",
|
|
1321
1329
|
"qualifier": "exact",
|
|
1322
1330
|
"replacement": "gpt-image-2.6-sunburst",
|
|
1331
|
+
"replacement_alt": "gpt-image-2.6-flare",
|
|
1323
1332
|
"kind": "image"
|
|
1324
1333
|
},
|
|
1325
1334
|
{
|
|
@@ -1328,6 +1337,7 @@
|
|
|
1328
1337
|
"retires": "2026-12-01",
|
|
1329
1338
|
"qualifier": "exact",
|
|
1330
1339
|
"replacement": "gpt-image-2.6-sunburst",
|
|
1340
|
+
"replacement_alt": "gpt-image-2.6-flare",
|
|
1331
1341
|
"kind": "image"
|
|
1332
1342
|
},
|
|
1333
1343
|
{
|
|
@@ -1336,6 +1346,7 @@
|
|
|
1336
1346
|
"retires": "2026-12-01",
|
|
1337
1347
|
"qualifier": "exact",
|
|
1338
1348
|
"replacement": "gpt-image-2.6-sunburst",
|
|
1349
|
+
"replacement_alt": "gpt-image-2.6-flare",
|
|
1339
1350
|
"kind": "image"
|
|
1340
1351
|
},
|
|
1341
1352
|
{
|
|
@@ -1344,6 +1355,7 @@
|
|
|
1344
1355
|
"retires": "2026-12-01",
|
|
1345
1356
|
"qualifier": "exact",
|
|
1346
1357
|
"replacement": "gpt-image-2.6-sunburst",
|
|
1358
|
+
"replacement_alt": "gpt-image-2.6-flare",
|
|
1347
1359
|
"kind": "image"
|
|
1348
1360
|
},
|
|
1349
1361
|
{
|
|
@@ -1697,18 +1709,10 @@
|
|
|
1697
1709
|
"replacement": "gemini-3.8-live",
|
|
1698
1710
|
"kind": "audio"
|
|
1699
1711
|
},
|
|
1700
|
-
{
|
|
1701
|
-
"id": "gemini-3.1-flash-live-preview",
|
|
1702
|
-
"provider": "google",
|
|
1703
|
-
"retires": "2026-11-17",
|
|
1704
|
-
"qualifier": "earliest",
|
|
1705
|
-
"replacement": "gemini-3.8-live",
|
|
1706
|
-
"kind": "audio"
|
|
1707
|
-
},
|
|
1708
1712
|
{
|
|
1709
1713
|
"id": "gemini-2.5-flash-native-audio-preview-12-2025",
|
|
1710
1714
|
"provider": "google",
|
|
1711
|
-
"retires": "
|
|
1715
|
+
"retires": "2027-03-31",
|
|
1712
1716
|
"qualifier": "earliest",
|
|
1713
1717
|
"replacement": "gemini-3.8-live",
|
|
1714
1718
|
"kind": "audio"
|
|
@@ -1716,25 +1720,28 @@
|
|
|
1716
1720
|
{
|
|
1717
1721
|
"id": "gemini-3.1-flash-tts-preview",
|
|
1718
1722
|
"provider": "google",
|
|
1719
|
-
"retires": "2026-
|
|
1723
|
+
"retires": "2026-12-18",
|
|
1720
1724
|
"qualifier": "earliest",
|
|
1721
1725
|
"replacement": "gemini-3.8-flash-tts",
|
|
1726
|
+
"replacement_alt": "gemini-3.8-flash-lite-tts",
|
|
1722
1727
|
"kind": "audio"
|
|
1723
1728
|
},
|
|
1724
1729
|
{
|
|
1725
1730
|
"id": "gemini-2.5-flash-preview-tts",
|
|
1726
1731
|
"provider": "google",
|
|
1727
|
-
"retires": "2026-
|
|
1732
|
+
"retires": "2026-12-18",
|
|
1728
1733
|
"qualifier": "earliest",
|
|
1729
1734
|
"replacement": "gemini-3.8-flash-tts",
|
|
1735
|
+
"replacement_alt": "gemini-3.8-flash-lite-tts",
|
|
1730
1736
|
"kind": "audio"
|
|
1731
1737
|
},
|
|
1732
1738
|
{
|
|
1733
1739
|
"id": "gemini-2.5-pro-preview-tts",
|
|
1734
1740
|
"provider": "google",
|
|
1735
|
-
"retires": "2026-
|
|
1741
|
+
"retires": "2026-12-18",
|
|
1736
1742
|
"qualifier": "earliest",
|
|
1737
1743
|
"replacement": "gemini-3.8-flash-tts",
|
|
1744
|
+
"replacement_alt": "gemini-3.8-flash-lite-tts",
|
|
1738
1745
|
"kind": "audio"
|
|
1739
1746
|
},
|
|
1740
1747
|
{
|
package/src/cli.js
CHANGED
|
@@ -81,7 +81,7 @@ if (cmd === "-h" || cmd === "--help" || cmd === "help") {
|
|
|
81
81
|
const pick = (f) => ({
|
|
82
82
|
id: f.id, file: f.file, line: f.line, status: f.status, docOnly: !!f.doc,
|
|
83
83
|
provider: f.entry?.provider, retires: f.entry?.retires, qualifier: f.entry?.qualifier,
|
|
84
|
-
replacement: f.entry?.replacement, migration: migrationFor(f.entry) || undefined, routes: !!f.entry?.routes, aliasOf: f.entry?.aliasOf,
|
|
84
|
+
replacement: f.entry?.replacement, replacement_alt: f.entry?.replacement_alt, migration: migrationFor(f.entry) || undefined, routes: !!f.entry?.routes, aliasOf: f.entry?.aliasOf,
|
|
85
85
|
});
|
|
86
86
|
console.log(JSON.stringify({ findings: result.findings.map(pick), drift: result.drift || [] }, null, 2));
|
|
87
87
|
} else console.log(result.markdown);
|
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 } from "./registry.js";
|
|
9
|
+
import { loadBestRegistry, classify, normalizeId, replacementChange, replacementText } from "./registry.js";
|
|
10
10
|
import { migrationFor } from "./migration.js";
|
|
11
11
|
import { scanDirectory } from "./scan.js";
|
|
12
12
|
|
|
@@ -24,7 +24,7 @@ function advice(c, today = new Date()) {
|
|
|
24
24
|
const chg = replacementChange(m, today);
|
|
25
25
|
const mig = migrationFor(m);
|
|
26
26
|
const migText = mig && !mig.drop_in && mig.change !== "settings" ? ` More than a rename: ${mig.note}` : mig?.change === "choose" ? ` ${mig.note}` : "";
|
|
27
|
-
const repl = m.replacement ? ` Replacement: ${
|
|
27
|
+
const repl = m.replacement ? ` Replacement: ${replacementText(m)}.${chg ? ` ${who} changed this recommendation on ${chg.on}; before it was ${chg.was}.` : ""}` : "";
|
|
28
28
|
if (m.qualifier === "not-before") return `Active. ${who} says it will not be retired before ${m.retires}.`;
|
|
29
29
|
if (m.routes) return c.days <= 0
|
|
30
30
|
? `Since ${m.retires}, ${who} answers requests for it with ${m.replacement}. No error, but answers come from a different model.${migText}`
|
|
@@ -43,7 +43,7 @@ function describe(rawId, reg, today) {
|
|
|
43
43
|
id: rawId, ...(id !== rawId ? { normalized: id } : {}),
|
|
44
44
|
status: c.status, ...(m ? {
|
|
45
45
|
provider: m.provider, retires: m.retires, qualifier: m.qualifier, days_left: c.days,
|
|
46
|
-
replacement: m.replacement || null, ...(m.replacement_note ? { replacement_note: m.replacement_note } : {}),
|
|
46
|
+
replacement: m.replacement || null, ...(m.replacement_alt ? { replacement_alt: m.replacement_alt } : {}), ...(m.replacement_note ? { replacement_note: m.replacement_note } : {}),
|
|
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) } : {}),
|
|
@@ -62,6 +62,7 @@ const MODEL_RESULT = {
|
|
|
62
62
|
qualifier: { type: "string", enum: ["exact", "earliest", "not-before"], description: "exact date, earliest possible shutdown (Google), or not before this date (Anthropic)" },
|
|
63
63
|
days_left: { type: "number", description: "Days until the date; 0 or negative when it has passed" },
|
|
64
64
|
replacement: { type: ["string", "null"] },
|
|
65
|
+
replacement_alt: { type: "string", description: "A second replacement the provider names (\"X or Y\"); which one fits depends on how the model is used" },
|
|
65
66
|
previous_replacement: { type: "string", description: "The replacement the provider recommended before it changed its advice" },
|
|
66
67
|
replacement_changed: { type: "string", description: "YYYY-MM-DD: when the change was seen on the provider's page" },
|
|
67
68
|
migration: { type: "object", description: "Whether moving to the replacement is just a new model name (drop_in true) or needs more: change = rename | settings | params | api | re-embed | choose, with a note and the provider's guide", properties: { change: { type: "string" }, drop_in: { type: "boolean" }, note: { type: "string" }, guide: { type: ["string", "null"] } } },
|
package/src/registry.js
CHANGED
|
@@ -40,12 +40,18 @@ export function buildRegistry(data) {
|
|
|
40
40
|
if (!m.replacement) continue;
|
|
41
41
|
const seen = new Set([m.id]);
|
|
42
42
|
let next = m.replacement;
|
|
43
|
+
let hop = m; // the entry whose replacement is `next`: its second option and note belong to that target
|
|
43
44
|
seen.add(next);
|
|
44
45
|
while (byId.get(next)?.replacement && byId.get(next).qualifier !== "not-before" && !seen.has(byId.get(next).replacement)) {
|
|
45
|
-
|
|
46
|
+
hop = byId.get(next);
|
|
47
|
+
next = hop.replacement;
|
|
46
48
|
seen.add(next);
|
|
47
49
|
}
|
|
48
|
-
if (next !== m.replacement && next !== m.id) {
|
|
50
|
+
if (next !== m.replacement && next !== m.id) {
|
|
51
|
+
m.via = m.replacement; m.replacement = next;
|
|
52
|
+
if (hop.replacement_alt) m.replacement_alt = hop.replacement_alt; else delete m.replacement_alt;
|
|
53
|
+
if (hop.replacement_note) m.replacement_note = hop.replacement_note; else delete m.replacement_note;
|
|
54
|
+
}
|
|
49
55
|
}
|
|
50
56
|
// Anthropic short names point at a dated snapshot: "claude-sonnet-4-5" and "-latest" -> claude-sonnet-4-5-20250929,
|
|
51
57
|
// "claude-sonnet-4-0" -> claude-sonnet-4-20250514. The deprecation page lists only snapshots, so a short name
|
|
@@ -95,7 +101,7 @@ export const isTextModel = (entry) => !entry?.kind || entry.kind === "text";
|
|
|
95
101
|
// The replacement as people should read it: the model, plus the provider's extra instruction when there is one,
|
|
96
102
|
// e.g. "gpt-6.1-sol (reasoning.mode: pro, in the Responses API)". Without the note, a rename alone is not the move.
|
|
97
103
|
export const replacementText = (entry) => entry?.replacement
|
|
98
|
-
? entry.replacement + (entry.replacement_note ? ` (${entry.replacement_note})` : "") : "";
|
|
104
|
+
? entry.replacement + (entry.replacement_alt ? ` or ${entry.replacement_alt}` : "") + (entry.replacement_note ? ` (${entry.replacement_note})` : "") : "";
|
|
99
105
|
|
|
100
106
|
// Providers sometimes change the recommended replacement after announcing a retirement. The registry keeps
|
|
101
107
|
// the old one and the date we saw the change, so reports can say so instead of silently switching targets.
|
package/src/report.js
CHANGED
|
@@ -83,7 +83,7 @@ export function renderReport({ findings, drift, registry, notices = [], answers
|
|
|
83
83
|
}
|
|
84
84
|
const places = r.places.slice(0, 3).join(", ") + (r.places.length > 3 ? ` +${r.places.length - 3} more` : "");
|
|
85
85
|
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_note ? " (" + esc(r.entry.replacement_note) + ")" : "") + changed(r.entry, today) : ""} | ${changeCell} | ${places} |`);
|
|
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
87
|
}
|
|
88
88
|
if (all.length > rows.length) lines.push("", `_${all.length - rows.length} more model ID(s) with no retirement listed are not shown._`);
|
|
89
89
|
if (migNotes.size) {
|