modelalive 0.5.0 → 1.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/dist/alive.d.ts +2 -0
- package/dist/alive.js +54 -0
- package/dist/expiring.d.ts +6 -0
- package/dist/expiring.js +26 -0
- package/dist/index.d.ts +19 -10
- package/dist/index.js +97 -67
- package/dist/normalize.d.ts +1 -0
- package/dist/normalize.js +17 -0
- package/dist/parity.test.d.ts +1 -0
- package/dist/parity.test.js +42 -0
- package/dist/scan.d.ts +14 -0
- package/dist/scan.js +111 -0
- package/dist/settings.d.ts +5 -0
- package/dist/settings.js +20 -0
- package/dist/smoke.test.js +8 -1
- package/dist/types.d.ts +14 -0
- package/dist/types.js +16 -0
- package/package.json +2 -2
- package/registry.json +4207 -3
package/dist/alive.d.ts
ADDED
package/dist/alive.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { daysUntil, effectiveStatus, loadRegistry, resolveAlias, } from "./registry.js";
|
|
2
|
+
import { normalizeModel } from "./normalize.js";
|
|
3
|
+
export function alive(model, today = new Date()) {
|
|
4
|
+
const registry = loadRegistry();
|
|
5
|
+
const queried = normalizeModel(model);
|
|
6
|
+
const [canonical, aliased] = resolveAlias(queried, registry);
|
|
7
|
+
const entry = registry.models[canonical];
|
|
8
|
+
if (!entry) {
|
|
9
|
+
return {
|
|
10
|
+
model: canonical,
|
|
11
|
+
queried_model: queried,
|
|
12
|
+
canonical_model: canonical,
|
|
13
|
+
aliased,
|
|
14
|
+
alive: true,
|
|
15
|
+
status: "unknown",
|
|
16
|
+
confidence: "unknown",
|
|
17
|
+
message: "Model not in registry — assumed alive.",
|
|
18
|
+
registry_version: registry.version,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const status = effectiveStatus(entry, today);
|
|
22
|
+
const sourceKey = entry.source ?? "";
|
|
23
|
+
const sourceMeta = registry.sources[sourceKey];
|
|
24
|
+
const daysLeft = daysUntil(entry.retired_at, today);
|
|
25
|
+
const base = {
|
|
26
|
+
model: canonical,
|
|
27
|
+
queried_model: queried,
|
|
28
|
+
canonical_model: canonical,
|
|
29
|
+
aliased,
|
|
30
|
+
provider: entry.provider,
|
|
31
|
+
deprecated_at: entry.deprecated_at,
|
|
32
|
+
retired_at: entry.retired_at,
|
|
33
|
+
replacement: entry.replacement ?? null,
|
|
34
|
+
breaking_changes: entry.breaking_changes ?? [],
|
|
35
|
+
migrate_url: entry.migrate_url,
|
|
36
|
+
days_until_retirement: daysLeft,
|
|
37
|
+
registry_version: registry.version,
|
|
38
|
+
source_url: sourceMeta?.url,
|
|
39
|
+
source_checked_at: sourceMeta?.checked_at,
|
|
40
|
+
confidence: entry.source ? "verified" : "unknown",
|
|
41
|
+
alive: status !== "retired",
|
|
42
|
+
status: status,
|
|
43
|
+
};
|
|
44
|
+
if (status === "retired") {
|
|
45
|
+
const repl = entry.replacement ? ` Use '${entry.replacement}' instead.` : "";
|
|
46
|
+
base.message = `Model '${canonical}' was retired.${repl}`;
|
|
47
|
+
base.alive = false;
|
|
48
|
+
}
|
|
49
|
+
else if (status === "deprecated") {
|
|
50
|
+
base.message = `Model '${canonical}' is deprecated.`;
|
|
51
|
+
base.alive = true;
|
|
52
|
+
}
|
|
53
|
+
return base;
|
|
54
|
+
}
|
package/dist/expiring.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { alive } from "./alive.js";
|
|
2
|
+
import { effectiveStatus, loadRegistry, parseDate } from "./registry.js";
|
|
3
|
+
export function listExpiring(opts = {}) {
|
|
4
|
+
const withinDays = opts.withinDays ?? 30;
|
|
5
|
+
const today = opts.today ?? new Date();
|
|
6
|
+
const registry = loadRegistry();
|
|
7
|
+
const results = [];
|
|
8
|
+
for (const [modelId, entry] of Object.entries(registry.models)) {
|
|
9
|
+
if (opts.provider && entry.provider !== opts.provider)
|
|
10
|
+
continue;
|
|
11
|
+
const status = effectiveStatus(entry, today);
|
|
12
|
+
if (status !== "deprecated" && status !== "retired")
|
|
13
|
+
continue;
|
|
14
|
+
const retiredAt = parseDate(entry.retired_at);
|
|
15
|
+
if (!retiredAt)
|
|
16
|
+
continue;
|
|
17
|
+
const ms = retiredAt.getTime() - today.getTime();
|
|
18
|
+
const daysLeft = Math.floor(ms / (1000 * 60 * 60 * 24));
|
|
19
|
+
if (status === "retired" || daysLeft < 0)
|
|
20
|
+
continue;
|
|
21
|
+
if (daysLeft <= withinDays) {
|
|
22
|
+
results.push(alive(modelId, today));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return results.sort((a, b) => (a.days_until_retirement ?? 9999) - (b.days_until_retirement ?? 9999));
|
|
26
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
|
-
import type { AliveResult } from "./types.js";
|
|
2
|
-
export
|
|
3
|
-
export declare function check(model: string, opts?:
|
|
4
|
-
|
|
5
|
-
today?: Date;
|
|
6
|
-
}): AliveResult;
|
|
1
|
+
import type { AliveResult, CheckOptions } from "./types.js";
|
|
2
|
+
export { alive } from "./alive.js";
|
|
3
|
+
export declare function check(model: string, opts?: CheckOptions): AliveResult;
|
|
4
|
+
export declare function checkMany(models: string[], today?: Date): AliveResult[];
|
|
7
5
|
export declare function resolve(model: string, today?: Date): string;
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
export interface ResolveDetail {
|
|
7
|
+
queried_model: string;
|
|
8
|
+
resolved: string;
|
|
9
|
+
chain: string[];
|
|
10
|
+
breaking_changes: string[];
|
|
11
|
+
}
|
|
12
|
+
export declare function resolveDetail(model: string, today?: Date): ResolveDetail;
|
|
13
|
+
export declare function ensure(model: string, opts?: CheckOptions): string;
|
|
14
|
+
export declare function requireAlive(model: string, opts?: CheckOptions): string;
|
|
15
|
+
export declare function gate<T>(model: string, fn: (safeModel: string) => T, opts?: CheckOptions): T;
|
|
12
16
|
export * from "./types.js";
|
|
17
|
+
export { normalizeModel } from "./normalize.js";
|
|
18
|
+
export { listExpiring } from "./expiring.js";
|
|
19
|
+
export { scanPath } from "./scan.js";
|
|
20
|
+
export type { ScanFinding, ScanReport } from "./scan.js";
|
|
21
|
+
export { defaultStrictUnknown, defaultWarnDays, defaultWarnDeprecated, } from "./settings.js";
|
package/dist/index.js
CHANGED
|
@@ -1,95 +1,125 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const entry = registry.models[canonical];
|
|
8
|
-
if (!entry) {
|
|
9
|
-
return {
|
|
10
|
-
model: canonical,
|
|
11
|
-
queried_model: queried,
|
|
12
|
-
canonical_model: canonical,
|
|
13
|
-
aliased,
|
|
14
|
-
alive: true,
|
|
15
|
-
status: "unknown",
|
|
16
|
-
confidence: "unknown",
|
|
17
|
-
message: "Model not in registry — assumed alive.",
|
|
18
|
-
registry_version: registry.version,
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
const status = effectiveStatus(entry, today);
|
|
22
|
-
const sourceKey = entry.source ?? "";
|
|
23
|
-
const sourceMeta = registry.sources[sourceKey];
|
|
24
|
-
const daysLeft = daysUntil(entry.retired_at, today);
|
|
25
|
-
const base = {
|
|
26
|
-
model: canonical,
|
|
27
|
-
queried_model: queried,
|
|
28
|
-
canonical_model: canonical,
|
|
29
|
-
aliased,
|
|
30
|
-
provider: entry.provider,
|
|
31
|
-
deprecated_at: entry.deprecated_at,
|
|
32
|
-
retired_at: entry.retired_at,
|
|
33
|
-
replacement: entry.replacement ?? null,
|
|
34
|
-
breaking_changes: entry.breaking_changes ?? [],
|
|
35
|
-
migrate_url: entry.migrate_url,
|
|
36
|
-
days_until_retirement: daysLeft,
|
|
37
|
-
registry_version: registry.version,
|
|
38
|
-
source_url: sourceMeta?.url,
|
|
39
|
-
source_checked_at: sourceMeta?.checked_at,
|
|
40
|
-
confidence: entry.source ? "verified" : "unknown",
|
|
41
|
-
alive: status !== "retired",
|
|
42
|
-
status: status,
|
|
43
|
-
};
|
|
44
|
-
if (status === "retired") {
|
|
45
|
-
const repl = entry.replacement ? ` Use '${entry.replacement}' instead.` : "";
|
|
46
|
-
base.message = `Model '${canonical}' was retired.${repl}`;
|
|
47
|
-
base.alive = false;
|
|
48
|
-
}
|
|
49
|
-
else if (status === "deprecated") {
|
|
50
|
-
base.message = `Model '${canonical}' is deprecated.`;
|
|
51
|
-
base.alive = true;
|
|
52
|
-
}
|
|
53
|
-
return base;
|
|
54
|
-
}
|
|
1
|
+
import { alive } from "./alive.js";
|
|
2
|
+
import { normalizeModel } from "./normalize.js";
|
|
3
|
+
import { defaultStrictUnknown, defaultWarnDays, defaultWarnDeprecated, } from "./settings.js";
|
|
4
|
+
import { ModelDeprecatedError, ModelExpiringSoonError, ModelRetiredError, ModelUnknownError, } from "./types.js";
|
|
5
|
+
const MAX_DEPTH = 12;
|
|
6
|
+
export { alive } from "./alive.js";
|
|
55
7
|
export function check(model, opts = {}) {
|
|
56
|
-
const
|
|
57
|
-
|
|
8
|
+
const strictUnknown = opts.strictUnknown ?? defaultStrictUnknown();
|
|
9
|
+
const warnDeprecated = opts.warnDeprecated ?? defaultWarnDeprecated();
|
|
10
|
+
const warnDays = opts.warnDays ?? defaultWarnDays();
|
|
11
|
+
const today = opts.today;
|
|
12
|
+
const result = alive(model, today);
|
|
13
|
+
if (strictUnknown && result.status === "unknown") {
|
|
58
14
|
throw new ModelUnknownError(result);
|
|
59
15
|
}
|
|
60
16
|
if (result.status === "retired") {
|
|
61
17
|
throw new ModelRetiredError(result);
|
|
62
18
|
}
|
|
19
|
+
if (warnDeprecated && result.status === "deprecated") {
|
|
20
|
+
throw new ModelDeprecatedError(result);
|
|
21
|
+
}
|
|
22
|
+
if (warnDays != null &&
|
|
23
|
+
result.status === "deprecated" &&
|
|
24
|
+
result.days_until_retirement != null &&
|
|
25
|
+
result.days_until_retirement >= 0 &&
|
|
26
|
+
result.days_until_retirement <= warnDays) {
|
|
27
|
+
throw new ModelExpiringSoonError(result);
|
|
28
|
+
}
|
|
63
29
|
return result;
|
|
64
30
|
}
|
|
31
|
+
export function checkMany(models, today = new Date()) {
|
|
32
|
+
return models.map((m) => alive(m, today));
|
|
33
|
+
}
|
|
65
34
|
export function resolve(model, today = new Date()) {
|
|
66
|
-
|
|
35
|
+
return resolveDetail(model, today).resolved;
|
|
36
|
+
}
|
|
37
|
+
export function resolveDetail(model, today = new Date()) {
|
|
38
|
+
let current = normalizeModel(model);
|
|
67
39
|
const visited = new Set();
|
|
68
|
-
|
|
40
|
+
const chain = [];
|
|
41
|
+
const breakingChanges = [];
|
|
42
|
+
for (let i = 0; i < MAX_DEPTH; i++) {
|
|
69
43
|
if (visited.has(current))
|
|
70
44
|
break;
|
|
71
45
|
visited.add(current);
|
|
46
|
+
chain.push(current);
|
|
72
47
|
const result = alive(current, today);
|
|
48
|
+
for (const change of result.breaking_changes ?? []) {
|
|
49
|
+
if (!breakingChanges.includes(change))
|
|
50
|
+
breakingChanges.push(change);
|
|
51
|
+
}
|
|
73
52
|
if (result.status === "active" || result.status === "unknown") {
|
|
74
|
-
return
|
|
53
|
+
return {
|
|
54
|
+
queried_model: model,
|
|
55
|
+
resolved: result.canonical_model ?? current,
|
|
56
|
+
chain,
|
|
57
|
+
breaking_changes: breakingChanges,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (!result.replacement) {
|
|
61
|
+
return {
|
|
62
|
+
queried_model: model,
|
|
63
|
+
resolved: result.canonical_model ?? current,
|
|
64
|
+
chain,
|
|
65
|
+
breaking_changes: breakingChanges,
|
|
66
|
+
};
|
|
75
67
|
}
|
|
76
|
-
if (!result.replacement)
|
|
77
|
-
return result.canonical_model ?? current;
|
|
78
68
|
const repl = alive(result.replacement, today);
|
|
79
|
-
if (repl.status === "active")
|
|
80
|
-
|
|
69
|
+
if (repl.status === "active") {
|
|
70
|
+
for (const change of repl.breaking_changes ?? []) {
|
|
71
|
+
if (!breakingChanges.includes(change))
|
|
72
|
+
breakingChanges.push(change);
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
queried_model: model,
|
|
76
|
+
resolved: result.replacement,
|
|
77
|
+
chain: [...chain, result.replacement],
|
|
78
|
+
breaking_changes: breakingChanges,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
81
|
current = result.replacement;
|
|
82
82
|
}
|
|
83
|
-
return
|
|
83
|
+
return {
|
|
84
|
+
queried_model: model,
|
|
85
|
+
resolved: current,
|
|
86
|
+
chain,
|
|
87
|
+
breaking_changes: breakingChanges,
|
|
88
|
+
};
|
|
84
89
|
}
|
|
85
90
|
export function ensure(model, opts = {}) {
|
|
86
|
-
const
|
|
87
|
-
|
|
91
|
+
const strictUnknown = opts.strictUnknown ?? defaultStrictUnknown();
|
|
92
|
+
const warnDeprecated = opts.warnDeprecated ?? defaultWarnDeprecated();
|
|
93
|
+
const warnDays = opts.warnDays ?? defaultWarnDays();
|
|
94
|
+
const today = opts.today;
|
|
95
|
+
const result = alive(model, today);
|
|
96
|
+
if (strictUnknown && result.status === "unknown") {
|
|
88
97
|
throw new ModelUnknownError(result);
|
|
89
98
|
}
|
|
90
99
|
if (result.status === "retired" && !result.replacement) {
|
|
91
100
|
throw new ModelRetiredError(result);
|
|
92
101
|
}
|
|
93
|
-
|
|
102
|
+
if (warnDeprecated && result.status === "deprecated") {
|
|
103
|
+
throw new ModelDeprecatedError(result);
|
|
104
|
+
}
|
|
105
|
+
if (warnDays != null &&
|
|
106
|
+
result.status === "deprecated" &&
|
|
107
|
+
result.days_until_retirement != null &&
|
|
108
|
+
result.days_until_retirement >= 0 &&
|
|
109
|
+
result.days_until_retirement <= warnDays) {
|
|
110
|
+
throw new ModelExpiringSoonError(result);
|
|
111
|
+
}
|
|
112
|
+
return resolve(model, today);
|
|
113
|
+
}
|
|
114
|
+
export function requireAlive(model, opts = {}) {
|
|
115
|
+
check(model, opts);
|
|
116
|
+
return model;
|
|
117
|
+
}
|
|
118
|
+
export function gate(model, fn, opts = {}) {
|
|
119
|
+
return fn(ensure(model, opts));
|
|
94
120
|
}
|
|
95
121
|
export * from "./types.js";
|
|
122
|
+
export { normalizeModel } from "./normalize.js";
|
|
123
|
+
export { listExpiring } from "./expiring.js";
|
|
124
|
+
export { scanPath } from "./scan.js";
|
|
125
|
+
export { defaultStrictUnknown, defaultWarnDays, defaultWarnDeprecated, } from "./settings.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function normalizeModel(model: string): string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const MODEL_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._:/-]{0,200}$/;
|
|
2
|
+
const FINE_TUNED_PREFIX = /^ft:([^:]+(?::[^:]+)*)/;
|
|
3
|
+
export function normalizeModel(model) {
|
|
4
|
+
let cleaned = model.trim();
|
|
5
|
+
if (!cleaned)
|
|
6
|
+
throw new Error("Model ID cannot be empty");
|
|
7
|
+
const ftMatch = FINE_TUNED_PREFIX.exec(cleaned);
|
|
8
|
+
if (ftMatch) {
|
|
9
|
+
const base = ftMatch[1].split(":")[0];
|
|
10
|
+
if (base)
|
|
11
|
+
cleaned = base;
|
|
12
|
+
}
|
|
13
|
+
if (!MODEL_ID_PATTERN.test(cleaned)) {
|
|
14
|
+
throw new Error(`Invalid model ID format: ${model}`);
|
|
15
|
+
}
|
|
16
|
+
return cleaned;
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { alive, check, checkMany, defaultStrictUnknown, ensure, gate, listExpiring, normalizeModel, resolve, resolveDetail, scanPath, } from "./index.js";
|
|
3
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { ModelRetiredError, ModelUnknownError, } from "./types.js";
|
|
7
|
+
// Core lifecycle
|
|
8
|
+
assert.equal(alive("claude-sonnet-4-20250514").status, "retired");
|
|
9
|
+
assert.equal(resolve("claude-sonnet-4-20250514"), "claude-sonnet-4-6");
|
|
10
|
+
assert.equal(ensure("claude-sonnet-4-20250514"), "claude-sonnet-4-6");
|
|
11
|
+
assert.equal(alive("claude-sonnet-4-6").status, "active");
|
|
12
|
+
const detail = resolveDetail("claude-opus-4-20250514");
|
|
13
|
+
assert.equal(detail.resolved, "claude-opus-4-8");
|
|
14
|
+
assert.ok(detail.breaking_changes.length >= 1);
|
|
15
|
+
assert.equal(normalizeModel("ft:gpt-4o-mini:org:foo:bar"), "gpt-4o-mini");
|
|
16
|
+
// Bedrock host entry
|
|
17
|
+
const bedrock = alive("anthropic.claude-sonnet-4-6-v1:0");
|
|
18
|
+
assert.equal(bedrock.provider, "bedrock");
|
|
19
|
+
assert.equal(bedrock.status, "active");
|
|
20
|
+
// Azure host entry
|
|
21
|
+
const azure = alive("azure/gpt-4o");
|
|
22
|
+
assert.equal(azure.provider, "azure");
|
|
23
|
+
// OpenRouter crosswalk
|
|
24
|
+
assert.equal(resolve("anthropic/claude-sonnet-4-6"), "claude-sonnet-4-6");
|
|
25
|
+
const gated = gate("gemini-2.0-flash", (safe) => safe);
|
|
26
|
+
assert.equal(gated, "gemini-3.5-flash");
|
|
27
|
+
// checkMany
|
|
28
|
+
assert.equal(checkMany(["gpt-4o", "claude-sonnet-4-6"]).length, 2);
|
|
29
|
+
// listExpiring
|
|
30
|
+
const expiring = listExpiring({ withinDays: 365, provider: "anthropic" });
|
|
31
|
+
assert.ok(Array.isArray(expiring));
|
|
32
|
+
// scanPath
|
|
33
|
+
const dir = mkdtempSync(join(tmpdir(), "modelalive-"));
|
|
34
|
+
writeFileSync(join(dir, "app.py"), 'MODEL = "claude-sonnet-4-20250514"\n');
|
|
35
|
+
const scan = scanPath(dir);
|
|
36
|
+
assert.ok(scan.findings.length >= 1);
|
|
37
|
+
// Errors
|
|
38
|
+
assert.throws(() => check("totally-unknown-model-xyz", { strictUnknown: true }), ModelUnknownError);
|
|
39
|
+
assert.throws(() => check("claude-sonnet-4-20250514"), ModelRetiredError);
|
|
40
|
+
// env defaults (undefined when unset in test env)
|
|
41
|
+
assert.equal(typeof defaultStrictUnknown(), "boolean");
|
|
42
|
+
console.log("modelalive js parity: ok");
|
package/dist/scan.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface ScanFinding {
|
|
2
|
+
path: string;
|
|
3
|
+
line: number;
|
|
4
|
+
model: string;
|
|
5
|
+
status: string;
|
|
6
|
+
replacement?: string | null;
|
|
7
|
+
alive: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface ScanReport {
|
|
10
|
+
root: string;
|
|
11
|
+
scannedFiles: number;
|
|
12
|
+
findings: ScanFinding[];
|
|
13
|
+
}
|
|
14
|
+
export declare function scanPath(root: string, today?: Date): ScanReport;
|
package/dist/scan.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { alive } from "./alive.js";
|
|
2
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
const SKIP_DIRS = new Set([
|
|
5
|
+
".git",
|
|
6
|
+
".venv",
|
|
7
|
+
"venv",
|
|
8
|
+
"node_modules",
|
|
9
|
+
"__pycache__",
|
|
10
|
+
".pytest_cache",
|
|
11
|
+
"dist",
|
|
12
|
+
"build",
|
|
13
|
+
".eggs",
|
|
14
|
+
]);
|
|
15
|
+
const EXTENSIONS = new Set([
|
|
16
|
+
".py",
|
|
17
|
+
".ts",
|
|
18
|
+
".tsx",
|
|
19
|
+
".js",
|
|
20
|
+
".jsx",
|
|
21
|
+
".json",
|
|
22
|
+
".yaml",
|
|
23
|
+
".yml",
|
|
24
|
+
".toml",
|
|
25
|
+
".env",
|
|
26
|
+
".md",
|
|
27
|
+
".sh",
|
|
28
|
+
]);
|
|
29
|
+
const PATTERNS = [
|
|
30
|
+
/['"](claude-[a-zA-Z0-9._-]{3,128})['"]/g,
|
|
31
|
+
/['"](gpt-[a-zA-Z0-9._-]{3,128})['"]/g,
|
|
32
|
+
/['"](gemini-[a-zA-Z0-9._-]{3,128})['"]/g,
|
|
33
|
+
/['"](o[0-9]-[a-zA-Z0-9._-]{3,128})['"]/g,
|
|
34
|
+
/['"](llama-[a-zA-Z0-9._-]{3,128})['"]/g,
|
|
35
|
+
/['"](grok-[a-zA-Z0-9._-]{3,128})['"]/g,
|
|
36
|
+
/['"](deepseek-[a-zA-Z0-9._-]{3,128})['"]/g,
|
|
37
|
+
/['"](mistral-[a-zA-Z0-9._-]{3,128})['"]/g,
|
|
38
|
+
/['"](qwen[a-zA-Z0-9._-]{0,128})['"]/g,
|
|
39
|
+
/['"](anthropic\.claude-[a-zA-Z0-9._:-]{3,128})['"]/g,
|
|
40
|
+
/['"]((?:anthropic|openai|google|meta-llama|qwen|deepseek|mistral|x-ai)\/[a-zA-Z0-9._:-]{3,128})['"]/g,
|
|
41
|
+
/model\s*[=:]\s*['"]([a-zA-Z0-9][a-zA-Z0-9._:/-]{2,127})['"]/g,
|
|
42
|
+
];
|
|
43
|
+
function walk(dir, files = []) {
|
|
44
|
+
for (const name of readdirSync(dir)) {
|
|
45
|
+
const path = join(dir, name);
|
|
46
|
+
const parts = path.split(/[/\\]/);
|
|
47
|
+
if (parts.some((p) => SKIP_DIRS.has(p)))
|
|
48
|
+
continue;
|
|
49
|
+
const st = statSync(path);
|
|
50
|
+
if (st.isDirectory())
|
|
51
|
+
walk(path, files);
|
|
52
|
+
else
|
|
53
|
+
files.push(path);
|
|
54
|
+
}
|
|
55
|
+
return files;
|
|
56
|
+
}
|
|
57
|
+
function shouldScan(path) {
|
|
58
|
+
if (path.endsWith(".min.js") || path.endsWith(".map"))
|
|
59
|
+
return false;
|
|
60
|
+
const ext = path.slice(path.lastIndexOf("."));
|
|
61
|
+
return EXTENSIONS.has(ext) || path.endsWith("Dockerfile") || path.endsWith(".env");
|
|
62
|
+
}
|
|
63
|
+
export function scanPath(root, today = new Date()) {
|
|
64
|
+
const report = { root, scannedFiles: 0, findings: [] };
|
|
65
|
+
const seen = new Set();
|
|
66
|
+
for (const path of walk(root)) {
|
|
67
|
+
if (!shouldScan(path))
|
|
68
|
+
continue;
|
|
69
|
+
let text;
|
|
70
|
+
try {
|
|
71
|
+
text = readFileSync(path, "utf-8");
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
report.scannedFiles += 1;
|
|
77
|
+
const lines = text.split(/\r?\n/);
|
|
78
|
+
for (let i = 0; i < lines.length; i++) {
|
|
79
|
+
const line = lines[i];
|
|
80
|
+
for (const pattern of PATTERNS) {
|
|
81
|
+
pattern.lastIndex = 0;
|
|
82
|
+
let match;
|
|
83
|
+
while ((match = pattern.exec(line)) !== null) {
|
|
84
|
+
const model = match[1];
|
|
85
|
+
const key = `${path}:${i + 1}:${model}`;
|
|
86
|
+
if (seen.has(key))
|
|
87
|
+
continue;
|
|
88
|
+
seen.add(key);
|
|
89
|
+
let result;
|
|
90
|
+
try {
|
|
91
|
+
result = alive(model, today);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (result.status === "active")
|
|
97
|
+
continue;
|
|
98
|
+
report.findings.push({
|
|
99
|
+
path: path.startsWith(root) ? path.slice(root.length + 1) : path,
|
|
100
|
+
line: i + 1,
|
|
101
|
+
model,
|
|
102
|
+
status: result.status,
|
|
103
|
+
replacement: result.replacement,
|
|
104
|
+
alive: result.alive,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return report;
|
|
111
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Environment-driven defaults (parity with Python MODELALIVE_*). */
|
|
2
|
+
export declare function envFlag(name: string, defaultValue?: boolean): boolean;
|
|
3
|
+
export declare function defaultStrictUnknown(): boolean;
|
|
4
|
+
export declare function defaultWarnDeprecated(): boolean;
|
|
5
|
+
export declare function defaultWarnDays(): number | undefined;
|
package/dist/settings.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Environment-driven defaults (parity with Python MODELALIVE_*). */
|
|
2
|
+
export function envFlag(name, defaultValue = false) {
|
|
3
|
+
const raw = (process.env[name] ?? "").trim().toLowerCase();
|
|
4
|
+
if (!raw)
|
|
5
|
+
return defaultValue;
|
|
6
|
+
return ["1", "true", "yes", "on"].includes(raw);
|
|
7
|
+
}
|
|
8
|
+
export function defaultStrictUnknown() {
|
|
9
|
+
return envFlag("MODELALIVE_STRICT");
|
|
10
|
+
}
|
|
11
|
+
export function defaultWarnDeprecated() {
|
|
12
|
+
return envFlag("MODELALIVE_WARN_DEPRECATED");
|
|
13
|
+
}
|
|
14
|
+
export function defaultWarnDays() {
|
|
15
|
+
const raw = (process.env["MODELALIVE_WARN_DAYS"] ?? "").trim();
|
|
16
|
+
if (!raw)
|
|
17
|
+
return undefined;
|
|
18
|
+
const n = Number.parseInt(raw, 10);
|
|
19
|
+
return Number.isFinite(n) && n >= 0 ? n : undefined;
|
|
20
|
+
}
|
package/dist/smoke.test.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { alive, ensure, resolve } from "./index.js";
|
|
2
|
+
import { alive, ensure, gate, normalizeModel, resolve, resolveDetail, } from "./index.js";
|
|
3
3
|
assert.equal(alive("claude-sonnet-4-20250514").status, "retired");
|
|
4
4
|
assert.equal(resolve("claude-sonnet-4-20250514"), "claude-sonnet-4-6");
|
|
5
5
|
assert.equal(ensure("claude-sonnet-4-20250514"), "claude-sonnet-4-6");
|
|
6
6
|
assert.equal(alive("claude-sonnet-4-6").status, "active");
|
|
7
|
+
const detail = resolveDetail("claude-opus-4-20250514");
|
|
8
|
+
assert.equal(detail.resolved, "claude-opus-4-8");
|
|
9
|
+
assert.ok(detail.breaking_changes.length >= 1);
|
|
10
|
+
assert.equal(normalizeModel("ft:gpt-4o-mini:org:foo:bar"), "gpt-4o-mini");
|
|
11
|
+
assert.equal(alive("anthropic.claude-sonnet-4-6-v1:0").provider, "bedrock");
|
|
12
|
+
const gated = gate("gemini-2.0-flash", (safe) => safe);
|
|
13
|
+
assert.equal(gated, "gemini-3.5-flash");
|
|
7
14
|
console.log("modelalive js: ok");
|
package/dist/types.d.ts
CHANGED
|
@@ -46,3 +46,17 @@ export declare class ModelUnknownError extends Error {
|
|
|
46
46
|
readonly result: AliveResult;
|
|
47
47
|
constructor(result: AliveResult);
|
|
48
48
|
}
|
|
49
|
+
export declare class ModelDeprecatedError extends Error {
|
|
50
|
+
readonly result: AliveResult;
|
|
51
|
+
constructor(result: AliveResult);
|
|
52
|
+
}
|
|
53
|
+
export declare class ModelExpiringSoonError extends Error {
|
|
54
|
+
readonly result: AliveResult;
|
|
55
|
+
constructor(result: AliveResult);
|
|
56
|
+
}
|
|
57
|
+
export interface CheckOptions {
|
|
58
|
+
strictUnknown?: boolean | null;
|
|
59
|
+
warnDeprecated?: boolean | null;
|
|
60
|
+
warnDays?: number | null;
|
|
61
|
+
today?: Date;
|
|
62
|
+
}
|
package/dist/types.js
CHANGED
|
@@ -14,3 +14,19 @@ export class ModelUnknownError extends Error {
|
|
|
14
14
|
this.name = "ModelUnknownError";
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
+
export class ModelDeprecatedError extends Error {
|
|
18
|
+
result;
|
|
19
|
+
constructor(result) {
|
|
20
|
+
super(result.message ?? `Model ${result.model} is deprecated`);
|
|
21
|
+
this.result = result;
|
|
22
|
+
this.name = "ModelDeprecatedError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export class ModelExpiringSoonError extends Error {
|
|
26
|
+
result;
|
|
27
|
+
constructor(result) {
|
|
28
|
+
super(result.message ?? `Model ${result.model} retires soon`);
|
|
29
|
+
this.result = result;
|
|
30
|
+
this.name = "ModelExpiringSoonError";
|
|
31
|
+
}
|
|
32
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "modelalive",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Pre-flight check: is this LLM model ID still alive?",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"scripts": {
|
|
16
16
|
"build": "tsc && cp ../registry/models.json registry.json",
|
|
17
17
|
"prebuild": "cp ../registry/models.json registry.json",
|
|
18
|
-
"test": "
|
|
18
|
+
"test": "npm run build && node --test dist/smoke.test.js dist/parity.test.js"
|
|
19
19
|
},
|
|
20
20
|
"keywords": ["llm", "anthropic", "openai", "gemini", "deprecation", "model-id"],
|
|
21
21
|
"license": "MIT",
|