@modelstatus/cli 0.1.75 → 0.1.76

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelstatus/cli",
3
- "version": "0.1.75",
3
+ "version": "0.1.76",
4
4
  "description": "Track which AI models you use, where, and never get surprised by a retirement. Free offline model-health for any repo (mm status), browser sign-in for cloud inventory + alerts.",
5
5
  "keywords": [
6
6
  "llm",
package/src/index.js CHANGED
@@ -373,9 +373,11 @@ async function ciReport(dir, flags, res) {
373
373
  const uniq = [...new Set(res.candidates.map((c) => c.model_string))];
374
374
  const resolved = uniq.length ? (await client.resolve(uniq)).data || [] : [];
375
375
  const byStr = new Map(resolved.map((r) => [r.input.toLowerCase(), r]));
376
+ const { dropResolvedFragments } = await import("./registry/local.js");
377
+ const uploadable = dropResolvedFragments(res.candidates, (c) => !!byStr.get(c.model_string.toLowerCase())?.model_id);
376
378
  const seen = new Set();
377
379
  const usages = [];
378
- for (const c of res.candidates) {
380
+ for (const c of uploadable) {
379
381
  const r = byStr.get(c.model_string.toLowerCase());
380
382
  const k = `${r?.model_id ?? "custom:" + c.model_string}|${c.location_label}`;
381
383
  if (seen.has(k)) continue;
@@ -685,12 +687,14 @@ function cmdIntegrations(positional, flags) {
685
687
  async function cmdStatus(positional, flags) {
686
688
  const dir = path.resolve(positional[1] || flags.dir || ".");
687
689
  const { getRegistry } = await import("./registry/fetch.js");
688
- const { resolveLocal, computeHealth } = await import("./registry/local.js");
690
+ const { resolveLocal, computeHealth, dropResolvedFragments } = await import("./registry/local.js");
689
691
  const snapshot = await getRegistry({ offline: !!flags.offline, log: (m) => process.stderr.write(`! ${m}\n`) });
690
692
 
691
- const candidates = await collectFrom(parseSources(flags), scanOpts(flags, dir), snapshot.detection, explicitSources(flags));
693
+ let candidates = await collectFrom(parseSources(flags), scanOpts(flags, dir), snapshot.detection, explicitSources(flags));
692
694
  const resolved = resolveLocal(snapshot, [...new Set(candidates.map((c) => c.model_string))]);
693
695
  const byStr = new Map(resolved.map((r) => [r.input.toLowerCase(), r]));
696
+ // Suppress detector fragments of resolved aliases (see dropResolvedFragments).
697
+ candidates = dropResolvedFragments(candidates, (c) => !!byStr.get(c.model_string.toLowerCase())?.model_slug);
694
698
 
695
699
  // Aggregate locations per known model / per custom string.
696
700
  const known = new Map(); // slug -> { model, count }
@@ -51,3 +51,34 @@ export function needsAttention(snapshot, retiringWindowDays = 90, today = new Da
51
51
  .filter((m) => m.health !== "ok")
52
52
  .sort((a, b) => String(a.retires_date || "9999-99-99").localeCompare(String(b.retires_date || "9999-99-99")));
53
53
  }
54
+
55
+ /**
56
+ * Drop unresolved (custom) candidates that NEST with a resolved match at the
57
+ * same location (either direction of containment). The detector often emits
58
+ * overlapping spans for one id — e.g. from the Bedrock ARN
59
+ * "anthropic.claude-3-sonnet-20240229-v1:0" it emits both
60
+ * "claude-3-sonnet-20240229" (resolves → claude-3-sonnet) and the longer
61
+ * fragment "claude-3-sonnet-20240229-v1" (doesn't resolve) — which would
62
+ * double-count the line as a known model AND a phantom custom id. Both
63
+ * directions are intentional: for variants like "ft:gpt-4:acme", the resolved
64
+ * base model carries the lifecycle signal (a fine-tune dies with its base), so
65
+ * the custom row is noise there too. Customs that share a line with a resolved
66
+ * model WITHOUT string overlap (two different ids on one line) are kept.
67
+ */
68
+ export function dropResolvedFragments(candidates, isResolved) {
69
+ const locOf = (c) => (c.source_path || c.location_label || "") + ":" + (c.source_line || "");
70
+ const resolvedByLoc = new Map(); // loc → [lowercased resolved strings]
71
+ for (const c of candidates) {
72
+ if (!isResolved(c)) continue;
73
+ const k = locOf(c);
74
+ if (!resolvedByLoc.has(k)) resolvedByLoc.set(k, []);
75
+ resolvedByLoc.get(k).push(c.model_string.toLowerCase());
76
+ }
77
+ return candidates.filter((c) => {
78
+ if (isResolved(c)) return true;
79
+ const here = resolvedByLoc.get(locOf(c));
80
+ if (!here) return true;
81
+ const s = c.model_string.toLowerCase();
82
+ return !here.some((rs) => rs !== s && (rs.includes(s) || s.includes(rs)));
83
+ });
84
+ }
@@ -11,7 +11,7 @@ import fs from "node:fs";
11
11
  import os from "node:os";
12
12
  import path from "node:path";
13
13
  import { getRegistry } from "../registry/fetch.js";
14
- import { resolveLocal, computeHealth } from "../registry/local.js";
14
+ import { resolveLocal, computeHealth, dropResolvedFragments } from "../registry/local.js";
15
15
  import { scanFilesystemStreaming } from "../sources/filesystem.js";
16
16
  import { compilePatterns } from "../detect/core.js";
17
17
 
@@ -76,6 +76,9 @@ export function loadRegistry(log = () => {}) {
76
76
  export function summarize(snapshot, candidates) {
77
77
  const resolved = resolveLocal(snapshot, [...new Set(candidates.map((c) => c.model_string))]);
78
78
  const byStr = new Map(resolved.map((r) => [r.input.toLowerCase(), r]));
79
+ // A custom id that's a fragment of a resolved alias on the same line is a
80
+ // detector artifact, not a finding (e.g. the inner piece of a Bedrock ARN).
81
+ candidates = dropResolvedFragments(candidates, (c) => !!byStr.get(c.model_string.toLowerCase())?.model_slug);
79
82
  const known = new Map(); // slug → { model, count }
80
83
  const custom = new Map(); // string → count
81
84
  const refsBySlug = new Map(); // slug → [candidate]