modelalive 0.5.0 → 1.0.2

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/index.d.ts CHANGED
@@ -5,8 +5,20 @@ export declare function check(model: string, opts?: {
5
5
  today?: Date;
6
6
  }): AliveResult;
7
7
  export declare function resolve(model: string, today?: Date): string;
8
+ export interface ResolveDetail {
9
+ queried_model: string;
10
+ resolved: string;
11
+ chain: string[];
12
+ breaking_changes: string[];
13
+ }
14
+ export declare function resolveDetail(model: string, today?: Date): ResolveDetail;
8
15
  export declare function ensure(model: string, opts?: {
9
16
  strictUnknown?: boolean;
10
17
  today?: Date;
11
18
  }): string;
19
+ export declare function gate<T>(model: string, fn: (safeModel: string) => T, opts?: {
20
+ strictUnknown?: boolean;
21
+ today?: Date;
22
+ }): T;
12
23
  export * from "./types.js";
24
+ export { normalizeModel } from "./normalize.js";
package/dist/index.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { daysUntil, effectiveStatus, loadRegistry, resolveAlias, } from "./registry.js";
2
+ import { normalizeModel } from "./normalize.js";
2
3
  import { ModelRetiredError, ModelUnknownError } from "./types.js";
4
+ const MAX_DEPTH = 12;
3
5
  export function alive(model, today = new Date()) {
4
6
  const registry = loadRegistry();
5
- const queried = model.trim();
7
+ const queried = normalizeModel(model);
6
8
  const [canonical, aliased] = resolveAlias(queried, registry);
7
9
  const entry = registry.models[canonical];
8
10
  if (!entry) {
@@ -63,24 +65,60 @@ export function check(model, opts = {}) {
63
65
  return result;
64
66
  }
65
67
  export function resolve(model, today = new Date()) {
66
- let current = model.trim();
68
+ return resolveDetail(model, today).resolved;
69
+ }
70
+ export function resolveDetail(model, today = new Date()) {
71
+ let current = normalizeModel(model);
67
72
  const visited = new Set();
68
- for (let i = 0; i < 12; i++) {
73
+ const chain = [];
74
+ const breakingChanges = [];
75
+ for (let i = 0; i < MAX_DEPTH; i++) {
69
76
  if (visited.has(current))
70
77
  break;
71
78
  visited.add(current);
79
+ chain.push(current);
72
80
  const result = alive(current, today);
81
+ for (const change of result.breaking_changes ?? []) {
82
+ if (!breakingChanges.includes(change))
83
+ breakingChanges.push(change);
84
+ }
73
85
  if (result.status === "active" || result.status === "unknown") {
74
- return result.canonical_model ?? current;
86
+ return {
87
+ queried_model: model,
88
+ resolved: result.canonical_model ?? current,
89
+ chain,
90
+ breaking_changes: breakingChanges,
91
+ };
92
+ }
93
+ if (!result.replacement) {
94
+ return {
95
+ queried_model: model,
96
+ resolved: result.canonical_model ?? current,
97
+ chain,
98
+ breaking_changes: breakingChanges,
99
+ };
75
100
  }
76
- if (!result.replacement)
77
- return result.canonical_model ?? current;
78
101
  const repl = alive(result.replacement, today);
79
- if (repl.status === "active")
80
- return result.replacement;
102
+ if (repl.status === "active") {
103
+ for (const change of repl.breaking_changes ?? []) {
104
+ if (!breakingChanges.includes(change))
105
+ breakingChanges.push(change);
106
+ }
107
+ return {
108
+ queried_model: model,
109
+ resolved: result.replacement,
110
+ chain: [...chain, result.replacement],
111
+ breaking_changes: breakingChanges,
112
+ };
113
+ }
81
114
  current = result.replacement;
82
115
  }
83
- return current;
116
+ return {
117
+ queried_model: model,
118
+ resolved: current,
119
+ chain,
120
+ breaking_changes: breakingChanges,
121
+ };
84
122
  }
85
123
  export function ensure(model, opts = {}) {
86
124
  const result = alive(model, opts.today);
@@ -92,4 +130,8 @@ export function ensure(model, opts = {}) {
92
130
  }
93
131
  return resolve(model, opts.today);
94
132
  }
133
+ export function gate(model, fn, opts = {}) {
134
+ return fn(ensure(model, opts));
135
+ }
95
136
  export * from "./types.js";
137
+ export { normalizeModel } from "./normalize.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
+ }
@@ -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").canonical_model, "claude-sonnet-4-6");
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modelalive",
3
- "version": "0.5.0",
3
+ "version": "1.0.2",
4
4
  "description": "Pre-flight check: is this LLM model ID still alive?",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",