@gtrabanco/pi-nan-provider 0.7.0 → 0.10.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/AGENTS.md +70 -23
- package/README.es.md +67 -37
- package/README.md +66 -40
- package/package.json +5 -5
- package/scripts/check-nan-mcp-server.ts +11 -3
- package/scripts/check-pi-sdk-versions.mjs +392 -0
- package/scripts/generate-models.ts +64 -12
- package/scripts/manual-overrides.ts +20 -8
- package/scripts/models.generated.ts +55 -52
- package/src/commands.ts +132 -52
- package/src/context-overflow-classifier.ts +1 -1
- package/src/cross-model-thinking-guard.ts +1 -1
- package/src/index.ts +81 -41
- package/src/mcp/api-key.ts +76 -0
- package/src/mcp/media-server.ts +56 -0
- package/src/provider-factory.ts +7 -2
- package/src/usage.ts +352 -175
- package/src/mcp/nan-media.ts +0 -236
- package/src/mcp/nan-search.ts +0 -241
- package/src/mcp/stdio-client.ts +0 -222
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Monitor PI SDK versions and NaN-related dependencies for pi-nan-provider.
|
|
4
|
+
*
|
|
5
|
+
* This script:
|
|
6
|
+
* 1. Reads current versions from package.json (devDependencies and peerDependencies).
|
|
7
|
+
* 2. Queries npm for latest versions.
|
|
8
|
+
* 3. Flags version bumps and checks compatibility.
|
|
9
|
+
*
|
|
10
|
+
* Exit 0 = nothing to do.
|
|
11
|
+
* Exit 1 = updates available.
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* node scripts/check-pi-sdk-versions.mjs --report
|
|
15
|
+
* node scripts/check-pi-sdk-versions.mjs --fail
|
|
16
|
+
* node scripts/check-pi-sdk-versions.mjs (JSON output for CI)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import https from "node:https";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
import { readFileSync } from "node:fs";
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Config
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
const ROOT = fileURLToPath(new URL("../", import.meta.url));
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Packages that ship from the `@earendil-works` scope and carry Pi SDK code.
|
|
31
|
+
* pi-nan-provider depends on these for AI model operations.
|
|
32
|
+
*/
|
|
33
|
+
const SDK_PACKAGES = [
|
|
34
|
+
"@earendil-works/pi-ai",
|
|
35
|
+
"@earendil-works/pi-coding-agent",
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Package.json reader
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
function readPackageJson() {
|
|
43
|
+
const raw = readFileSync(new URL("../package.json", import.meta.url), "utf-8");
|
|
44
|
+
return JSON.parse(raw);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get the currently constrained version for a package from
|
|
49
|
+
* devDependencies and peerDependencies.
|
|
50
|
+
*/
|
|
51
|
+
function getConstrainedVersion(pkg, packageName) {
|
|
52
|
+
const sources = ["devDependencies", "peerDependencies"];
|
|
53
|
+
for (const src of sources) {
|
|
54
|
+
const ver = pkg[src]?.[packageName];
|
|
55
|
+
if (ver) return { version: ver, source: src };
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// npm registry client (no dependencies)
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
function npmGet(packageName) {
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
const req = https.get(
|
|
67
|
+
`https://registry.npmjs.org/${encodeURIComponent(packageName)}`,
|
|
68
|
+
{ headers: { accept: "application/json" } },
|
|
69
|
+
(res) => {
|
|
70
|
+
if (res.statusCode !== 200) {
|
|
71
|
+
reject(new Error(`npm registry returned ${res.statusCode} for ${packageName}`));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
let data = "";
|
|
75
|
+
res.on("data", (chunk) => (data += chunk));
|
|
76
|
+
res.on("end", () => resolve(JSON.parse(data)));
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
req.on("error", reject);
|
|
80
|
+
req.setTimeout(15_000, () => {
|
|
81
|
+
req.destroy();
|
|
82
|
+
reject(new Error(`Timeout fetching ${packageName} from npm`));
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function npmLatest(packageName) {
|
|
88
|
+
return npmGet(packageName).then((data) => {
|
|
89
|
+
const distTags = data["dist-tags"] ?? {};
|
|
90
|
+
const latestVersion = distTags["latest"];
|
|
91
|
+
if (!latestVersion) {
|
|
92
|
+
throw new Error(`No "latest" dist-tag for ${packageName}`);
|
|
93
|
+
}
|
|
94
|
+
const versions = Object.keys(data.versions ?? {}).sort((a, b) => {
|
|
95
|
+
const pa = a.split(".").map(Number);
|
|
96
|
+
const pb = b.split(".").map(Number);
|
|
97
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
98
|
+
const va = pa[i] ?? 0;
|
|
99
|
+
const vb = pb[i] ?? 0;
|
|
100
|
+
if (va < vb) return -1;
|
|
101
|
+
if (va > vb) return 1;
|
|
102
|
+
}
|
|
103
|
+
return 0;
|
|
104
|
+
});
|
|
105
|
+
return { latestVersion, latestTag: "latest", allVersions: versions, data };
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// Semver helpers
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
function parseSemver(version) {
|
|
114
|
+
// Handle versions like "1" (major only) or "1.2" (major.minor)
|
|
115
|
+
let normalized = version;
|
|
116
|
+
while (normalized.split(".").length < 3) {
|
|
117
|
+
normalized += ".0";
|
|
118
|
+
}
|
|
119
|
+
const m = normalized.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
120
|
+
if (!m) return null;
|
|
121
|
+
return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]), raw: version };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function bumpLevel(current, target) {
|
|
125
|
+
const c = parseSemver(current);
|
|
126
|
+
const t = parseSemver(target);
|
|
127
|
+
if (!c || !t) return "unknown";
|
|
128
|
+
if (t.major > c.major) return "major";
|
|
129
|
+
if (t.minor > c.minor) return "minor";
|
|
130
|
+
if (t.patch > c.patch) return "patch";
|
|
131
|
+
return "none";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Check if a version satisfies a semver range.
|
|
136
|
+
* Handles caret, tilde, comparison operators, and ranges.
|
|
137
|
+
*/
|
|
138
|
+
function checkSemverSatisfies(version, range) {
|
|
139
|
+
const cVer = parseSemver(version);
|
|
140
|
+
if (!cVer) return false;
|
|
141
|
+
|
|
142
|
+
// Handle disjunction (OR): ">=1.0.0 <2.0.0 || >=3.0.0"
|
|
143
|
+
if (range.includes("||")) {
|
|
144
|
+
const parts = range.split("||").map((p) => p.trim());
|
|
145
|
+
return parts.some((part) => checkSemverSatisfies(version, part));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Handle conjunction (AND via spaces): ">=1.0.0 <2.0.0"
|
|
149
|
+
const parts = [];
|
|
150
|
+
let current = "";
|
|
151
|
+
const tokens = range.split(/\s+/);
|
|
152
|
+
for (const token of tokens) {
|
|
153
|
+
if (token.startsWith(">=") || token.startsWith("<=") ||
|
|
154
|
+
token.startsWith(">") || token.startsWith("<") || token.startsWith("=") ||
|
|
155
|
+
token.startsWith("^") || token.startsWith("~")) {
|
|
156
|
+
if (current && !current.startsWith("^") && !current.startsWith("~")) {
|
|
157
|
+
parts.push(current);
|
|
158
|
+
}
|
|
159
|
+
current = token;
|
|
160
|
+
} else if (current) {
|
|
161
|
+
current = current + " " + token;
|
|
162
|
+
} else {
|
|
163
|
+
current = token;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (current) parts.push(current);
|
|
167
|
+
|
|
168
|
+
if (parts.length > 1) {
|
|
169
|
+
// Check each part
|
|
170
|
+
return parts.every((part) => checkSemverSatisfies(version, part));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Single constraint
|
|
174
|
+
const constraint = range.trim();
|
|
175
|
+
|
|
176
|
+
// Handle caret: ^X.Y.Z
|
|
177
|
+
if (constraint.startsWith("^")) {
|
|
178
|
+
const target = parseSemver(constraint.slice(1));
|
|
179
|
+
if (!target) return false;
|
|
180
|
+
// ^0.x: same major and minor >= target.minor
|
|
181
|
+
// ^1.x+: same major and minor >= target.minor
|
|
182
|
+
return cVer.major === target.major && cVer.minor >= target.minor;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Handle tilde: ~X.Y.Z
|
|
186
|
+
if (constraint.startsWith("~")) {
|
|
187
|
+
const target = parseSemver(constraint.slice(1));
|
|
188
|
+
if (!target) return false;
|
|
189
|
+
return cVer.major === target.major && cVer.minor === target.minor && cVer.patch >= target.patch;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Handle comparison: >=X.Y.Z, <X.Y.Z, =X.Y.Z, etc.
|
|
193
|
+
let op = "";
|
|
194
|
+
let verStr = constraint;
|
|
195
|
+
if (constraint.startsWith(">=")) {
|
|
196
|
+
op = ">=";
|
|
197
|
+
verStr = constraint.slice(2);
|
|
198
|
+
} else if (constraint.startsWith("<=")) {
|
|
199
|
+
op = "<=";
|
|
200
|
+
verStr = constraint.slice(2);
|
|
201
|
+
} else if (constraint.startsWith(">")) {
|
|
202
|
+
op = ">";
|
|
203
|
+
verStr = constraint.slice(1);
|
|
204
|
+
} else if (constraint.startsWith("<")) {
|
|
205
|
+
op = "<";
|
|
206
|
+
verStr = constraint.slice(1);
|
|
207
|
+
} else if (constraint.startsWith("=")) {
|
|
208
|
+
op = "=";
|
|
209
|
+
verStr = constraint.slice(1);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const target = parseSemver(verStr);
|
|
213
|
+
if (!target) return false;
|
|
214
|
+
|
|
215
|
+
const cmp = semverCompare(cVer, target);
|
|
216
|
+
|
|
217
|
+
switch (op) {
|
|
218
|
+
case ">=": return cmp >= 0;
|
|
219
|
+
case "<=": return cmp <= 0;
|
|
220
|
+
case ">": return cmp > 0;
|
|
221
|
+
case "<": return cmp < 0;
|
|
222
|
+
case "=": return cmp === 0;
|
|
223
|
+
default: return false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function semverCompare(a, b) {
|
|
228
|
+
if (a.major !== b.major) return a.major - b.major;
|
|
229
|
+
if (a.minor !== b.minor) return a.minor - b.minor;
|
|
230
|
+
return (a.patch ?? 0) - (b.patch ?? 0);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// Main
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
async function main() {
|
|
238
|
+
const args = process.argv.slice(2);
|
|
239
|
+
const flags = {
|
|
240
|
+
report: args.includes("--report"),
|
|
241
|
+
fail: args.includes("--fail"),
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const pkg = readPackageJson();
|
|
245
|
+
|
|
246
|
+
const results = [];
|
|
247
|
+
let exitCode = 0;
|
|
248
|
+
|
|
249
|
+
for (const packageName of SDK_PACKAGES) {
|
|
250
|
+
try {
|
|
251
|
+
const npmInfo = await npmLatest(packageName);
|
|
252
|
+
const constrained = getConstrainedVersion(pkg, packageName);
|
|
253
|
+
|
|
254
|
+
if (!constrained) {
|
|
255
|
+
results.push({
|
|
256
|
+
packageName,
|
|
257
|
+
status: "not-found",
|
|
258
|
+
message: "Not found in package.json dependencies",
|
|
259
|
+
});
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Extract numeric version from constraint
|
|
264
|
+
let constraintVer = constrained.version;
|
|
265
|
+
// Remove version range operators to get the base version
|
|
266
|
+
if (constraintVer.startsWith("^")) {
|
|
267
|
+
constraintVer = constraintVer.slice(1);
|
|
268
|
+
} else if (constraintVer.startsWith("~")) {
|
|
269
|
+
constraintVer = constraintVer.slice(1);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const bump = bumpLevel(constraintVer, npmInfo.latestVersion);
|
|
273
|
+
|
|
274
|
+
// Check if latest satisfies peer dependencies
|
|
275
|
+
const peerRange = pkg.peerDependencies?.[packageName] ?? null;
|
|
276
|
+
const latestSatisfiesPeer = peerRange
|
|
277
|
+
? checkSemverSatisfies(npmInfo.latestVersion, peerRange)
|
|
278
|
+
: true;
|
|
279
|
+
|
|
280
|
+
// Check if current satisfies peer dependencies
|
|
281
|
+
const currentSatisfiesPeer = peerRange
|
|
282
|
+
? checkSemverSatisfies(constraintVer, peerRange)
|
|
283
|
+
: true;
|
|
284
|
+
|
|
285
|
+
const result = {
|
|
286
|
+
packageName,
|
|
287
|
+
source: constrained.source,
|
|
288
|
+
constrainedVersion: constrained.version,
|
|
289
|
+
constraintVer,
|
|
290
|
+
latestVersion: npmInfo.latestVersion,
|
|
291
|
+
latestTag: npmInfo.latestTag,
|
|
292
|
+
bump,
|
|
293
|
+
peerRange,
|
|
294
|
+
latestSatisfiesPeer,
|
|
295
|
+
currentSatisfiesPeer,
|
|
296
|
+
totalVersions: npmInfo.allVersions.length,
|
|
297
|
+
allVersions: npmInfo.allVersions,
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// Determine status
|
|
301
|
+
if (!currentSatisfiesPeer) {
|
|
302
|
+
result.status = "WARNING";
|
|
303
|
+
result.message = `Current version ${constraintVer} does NOT satisfy peer dependency range ${peerRange}`;
|
|
304
|
+
exitCode = 1;
|
|
305
|
+
} else if (bump === "major" && npmInfo.latestVersion !== constraintVer) {
|
|
306
|
+
result.status = "BREAKING";
|
|
307
|
+
result.message = `Major version bump: ${constraintVer} → ${npmInfo.latestVersion}. Review changelog before upgrading.`;
|
|
308
|
+
if (latestSatisfiesPeer) {
|
|
309
|
+
result.message += ` Latest version satisfies peer dependency range.`;
|
|
310
|
+
} else {
|
|
311
|
+
result.message += ` ⚠️ Latest version does NOT satisfy peer dependency range!`;
|
|
312
|
+
}
|
|
313
|
+
exitCode = 1;
|
|
314
|
+
} else if (bump === "minor" && npmInfo.latestVersion !== constraintVer) {
|
|
315
|
+
result.status = "minor";
|
|
316
|
+
result.message = `Minor version bump: ${constraintVer} → ${npmInfo.latestVersion}. Check for API changes.`;
|
|
317
|
+
exitCode = 1;
|
|
318
|
+
} else if (bump === "patch" && npmInfo.latestVersion !== constraintVer) {
|
|
319
|
+
result.status = "patch";
|
|
320
|
+
result.message = `Patch version bump: ${constraintVer} → ${npmInfo.latestVersion}. May be safe to update.`;
|
|
321
|
+
} else {
|
|
322
|
+
result.status = "up-to-date";
|
|
323
|
+
result.message = `Already on latest version.`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (latestSatisfiesPeer && bump !== "none") {
|
|
327
|
+
result.message += ` Latest satisfies peer dependency range.`;
|
|
328
|
+
} else if (!latestSatisfiesPeer) {
|
|
329
|
+
result.message += ` Latest does NOT satisfy peer dependency range!`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
results.push(result);
|
|
333
|
+
} catch (err) {
|
|
334
|
+
results.push({
|
|
335
|
+
packageName,
|
|
336
|
+
status: "error",
|
|
337
|
+
message: err.message,
|
|
338
|
+
});
|
|
339
|
+
exitCode = 1;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Print report
|
|
344
|
+
if (flags.report) {
|
|
345
|
+
console.log("\n=== PI SDK Version Monitor (pi-nan-provider) ===\n");
|
|
346
|
+
|
|
347
|
+
for (const r of results) {
|
|
348
|
+
if (r.status === "not-found") {
|
|
349
|
+
console.log(` ❌ ${r.packageName}: ${r.message}`);
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (r.status === "error") {
|
|
353
|
+
console.log(` ⚠️ ${r.packageName}: ${r.message}`);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (r.status === "up-to-date") {
|
|
357
|
+
console.log(` ✅ ${r.packageName}: ${r.message} (${r.latestVersion})`);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (r.status === "WARNING") {
|
|
361
|
+
console.log(` ❗ ${r.packageName}: ${r.message} (current: ${r.constrainedVersion})`);
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
const icon = r.status === "BREAKING" ? "🔴" : r.status === "minor" ? "🟡" : "🟢";
|
|
365
|
+
console.log(` ${icon} ${r.packageName}: ${r.message} (${r.source}: ${r.constrainedVersion})`);
|
|
366
|
+
console.log(` → Latest available: ${r.latestVersion} (tag: ${r.latestTag})`);
|
|
367
|
+
if (r.peerRange) {
|
|
368
|
+
console.log(` → Peer dependency range: ${r.peerRange}`);
|
|
369
|
+
console.log(` → Latest satisfies peer range: ${r.latestSatisfiesPeer ? "✅" : "❌"}`);
|
|
370
|
+
}
|
|
371
|
+
if (r.bump === "major") {
|
|
372
|
+
console.log(` → Total versions published: ${r.totalVersions}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
console.log();
|
|
377
|
+
} else {
|
|
378
|
+
// JSON output for CI consumption
|
|
379
|
+
console.log(JSON.stringify(results, null, 2));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (flags.fail && exitCode === 1) {
|
|
383
|
+
process.exit(1);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
process.exit(exitCode);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
main().catch((err) => {
|
|
390
|
+
console.error("Fatal:", err.message);
|
|
391
|
+
process.exit(2);
|
|
392
|
+
});
|
|
@@ -8,16 +8,28 @@
|
|
|
8
8
|
* package. Run before publishing (`bun run generate-models`, wired into
|
|
9
9
|
* `prepublishOnly`).
|
|
10
10
|
*
|
|
11
|
+
* Sources of truth (maintainer instruction, reaffirmed 2026-09-29):
|
|
12
|
+
* - https://nan.builders/docs/models decides WHICH models exist — which ids
|
|
13
|
+
* belong in the chat catalog, which are premium/live-only, which were
|
|
14
|
+
* removed by the provider, which are not chat models at all. Every
|
|
15
|
+
* exclusion cites it.
|
|
16
|
+
* - models.dev (provider "nan") supplies the numeric limits for the ids it
|
|
17
|
+
* documents; a divergence from the docs is recorded in MANUAL_OVERRIDES
|
|
18
|
+
* with a source note.
|
|
19
|
+
*
|
|
11
20
|
* Provenance rules (enforced, not decorative):
|
|
12
21
|
* - Every emitted number must come from models.dev or an explicit
|
|
13
22
|
* MANUAL_OVERRIDES note recording where it was confirmed. Nothing invented.
|
|
14
|
-
* - The
|
|
15
|
-
*
|
|
23
|
+
* - The official community chat models (qwen3.6, gemma4, deepseek-v4-flash,
|
|
24
|
+
* mimo-v2.6-flash) MUST reach the catalog — from models.dev or from a
|
|
25
|
+
* MANUAL_ONLY entry — or this script exits non-zero.
|
|
16
26
|
* - Any other models.dev entry missing `limit.context`/`limit.output` is
|
|
17
|
-
* skipped and flagged "needs manual verification" — never guessed.
|
|
27
|
+
* skipped and flagged "needs manual verification" — never guessed. Entries
|
|
28
|
+
* that are not chat models are excluded via NON_CHAT_MODEL_IDS instead.
|
|
18
29
|
* - models.dev modalities are intersected with pi's supported input set
|
|
19
|
-
* ("text" | "image"); e.g. mimo-v2.
|
|
20
|
-
* pi's Model type and is dropped from `input` (noted on
|
|
30
|
+
* ("text" | "image"); e.g. mimo-v2.6-flash's audio input is not
|
|
31
|
+
* representable in pi's Model type and is dropped from `input` (noted on
|
|
32
|
+
* the entry).
|
|
21
33
|
*/
|
|
22
34
|
|
|
23
35
|
import type { GeneratedModelEntry } from "../src/fetch-models.ts";
|
|
@@ -39,17 +51,23 @@ const REASONING_EFFORT_VALUES_FROM_DOCS: Record<string, string[]> = {
|
|
|
39
51
|
...REASONING_EFFORT_VALUES,
|
|
40
52
|
// deepseek-v4-flash: any value (no effect) — model decides per request
|
|
41
53
|
"deepseek-v4-flash": [],
|
|
42
|
-
// qwen3.8-flash, mimo-v2.
|
|
54
|
+
// qwen3.8-flash, mimo-v2.6-flash: accepted, depth not adjustable
|
|
43
55
|
"qwen3.8-flash": [],
|
|
44
|
-
"mimo-v2.
|
|
56
|
+
"mimo-v2.6-flash": [],
|
|
45
57
|
};
|
|
46
58
|
|
|
47
59
|
const MODELS_DEV_API_URL = "https://models.dev/api.json";
|
|
48
60
|
const SOURCE_PROVIDER_ID = "nan";
|
|
49
61
|
const FETCH_TIMEOUT_MS = 15_000;
|
|
50
62
|
|
|
51
|
-
/**
|
|
52
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Official community chat models per https://nan.builders/docs/models
|
|
65
|
+
* (checked 2026-09-29; glm5.3 is premium-tier and deliberately live-only).
|
|
66
|
+
* Each MUST reach the catalog — from models.dev or from a MANUAL_ONLY entry —
|
|
67
|
+
* or generation fails. The docs decide this set, not models.dev: models.dev
|
|
68
|
+
* dropped mimo-v2.5 together with the docs in 2026-09.
|
|
69
|
+
*/
|
|
70
|
+
const REQUIRED_MODEL_IDS = ["qwen3.6", "gemma4", "deepseek-v4-flash", "mimo-v2.6-flash"] as const;
|
|
53
71
|
|
|
54
72
|
/** pi's Model.input only supports these values. */
|
|
55
73
|
const PI_SUPPORTED_INPUT = new Set(["text", "image"]);
|
|
@@ -75,6 +93,20 @@ const MANUAL_NOTES: Record<string, string> = {
|
|
|
75
93
|
const PROVIDER_REMOVED_MODEL_IDS: Record<string, string> = {
|
|
76
94
|
"glm5.2":
|
|
77
95
|
"removed by NaN (2026-09-05); absent from the official chat model list in https://nan.builders/openapi.json and https://nan.builders/docs/models (checked 2026-09-07) while models.dev provider nan still listed it — excluded so regeneration does not resurrect it",
|
|
96
|
+
"mimo-v2.5":
|
|
97
|
+
"removed by NaN (checked 2026-09-29): zero mentions in https://nan.builders/docs/models and in the model param description of https://nan.builders/openapi.json, and models.dev provider nan dropped it as well — superseded by mimo-v2.6-flash; excluded so regeneration does not resurrect it",
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* models.dev entries that are not chat models. They can never join a chat
|
|
102
|
+
* catalog (their output is an image, and models.dev reports limit.output 0),
|
|
103
|
+
* so they are excluded with the docs as the reason instead of being flagged
|
|
104
|
+
* "needs manual verification" forever — that flag would suggest limits worth
|
|
105
|
+
* confirming for a model this package must not serve as chat.
|
|
106
|
+
*/
|
|
107
|
+
const NON_CHAT_MODEL_IDS: Record<string, string> = {
|
|
108
|
+
"qwen-image-2.1":
|
|
109
|
+
"image-generation model (text→image; output modality image) documented in https://nan.builders/docs/models and https://nan.builders/openapi.json (checked 2026-09-29) — not a chat model, out of the static chat catalog (models.dev reports limit.output 0)",
|
|
78
110
|
};
|
|
79
111
|
|
|
80
112
|
/**
|
|
@@ -221,6 +253,10 @@ function convertModel(modelId: string, m: ModelsDevModel): GeneratedModel | { sk
|
|
|
221
253
|
if (removedReason) {
|
|
222
254
|
return { skip: `provider-removed: "${modelId}" excluded from the catalog (${removedReason})` };
|
|
223
255
|
}
|
|
256
|
+
const nonChatReason = NON_CHAT_MODEL_IDS[modelId];
|
|
257
|
+
if (nonChatReason) {
|
|
258
|
+
return { skip: `non-chat: "${modelId}" excluded from the chat catalog (${nonChatReason})` };
|
|
259
|
+
}
|
|
224
260
|
const liveOnlyReason = LIVE_ONLY_MODEL_IDS[modelId];
|
|
225
261
|
if (liveOnlyReason) {
|
|
226
262
|
return { skip: `live-only: "${modelId}" kept out of the static catalog (${liveOnlyReason})` };
|
|
@@ -301,8 +337,20 @@ async function main(): Promise<void> {
|
|
|
301
337
|
}
|
|
302
338
|
}
|
|
303
339
|
|
|
304
|
-
// Add manual-only models (not
|
|
340
|
+
// Add manual-only models (models.dev provider "nan" does not list them).
|
|
341
|
+
// When models.dev starts listing one, the manual entry is NOT emitted —
|
|
342
|
+
// otherwise the id would reach the catalog twice — and the reason plus the
|
|
343
|
+
// original manual provenance are attached to the models.dev-derived entry,
|
|
344
|
+
// so the manual data never disappears silently.
|
|
345
|
+
const entryById = new Map(entries.map((entry) => [entry.id, entry]));
|
|
305
346
|
for (const [modelId, detail] of Object.entries(MANUAL_ONLY_MODEL_IDS)) {
|
|
347
|
+
const existing = entryById.get(modelId);
|
|
348
|
+
if (existing) {
|
|
349
|
+
const note = `manual-only: "${modelId}" skipped: models.dev provider "${SOURCE_PROVIDER_ID}" now lists it, so the manual-only entry is not emitted and the models.dev values are used (${detail})`;
|
|
350
|
+
existing.notes = [...(existing.notes ?? []), note];
|
|
351
|
+
console.warn(`generate-models: skipped ${note}`);
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
306
354
|
const override = MANUAL_OVERRIDES[modelId];
|
|
307
355
|
if (override) {
|
|
308
356
|
console.log(`generate-models: manual override for "${modelId}" (${Object.keys(override).filter((k) => k !== "note").join(", ")})`);
|
|
@@ -318,8 +366,9 @@ async function main(): Promise<void> {
|
|
|
318
366
|
const missingRequired = REQUIRED_MODEL_IDS.filter((id) => !entryIds.has(id));
|
|
319
367
|
if (missingRequired.length > 0) {
|
|
320
368
|
fail(
|
|
321
|
-
`required models missing from
|
|
322
|
-
"do NOT invent their data; confirm
|
|
369
|
+
`required models missing from the catalog: ${missingRequired.join(", ")} (official community chat models per ` +
|
|
370
|
+
"https://nan.builders/docs/models) — do NOT invent their data; confirm them in the docs and add them " +
|
|
371
|
+
"via MANUAL_OVERRIDES (models.dev lists them) or MANUAL_ONLY_MODEL_IDS (models.dev does not), with a source note.",
|
|
323
372
|
);
|
|
324
373
|
}
|
|
325
374
|
|
|
@@ -333,6 +382,9 @@ async function main(): Promise<void> {
|
|
|
333
382
|
...Object.entries(PROVIDER_REMOVED_MODEL_IDS).map(
|
|
334
383
|
([id, reason]) => `provider-removed: "${id}" excluded from the catalog (${reason})`,
|
|
335
384
|
),
|
|
385
|
+
...Object.entries(NON_CHAT_MODEL_IDS).map(
|
|
386
|
+
([id, reason]) => `non-chat: "${id}" excluded from the chat catalog (${reason})`,
|
|
387
|
+
),
|
|
336
388
|
...Object.entries(LIVE_ONLY_MODEL_IDS).map(
|
|
337
389
|
([id, reason]) => `live-only: "${id}" kept out of the static catalog (${reason})`,
|
|
338
390
|
),
|
|
@@ -43,7 +43,7 @@ export interface ManualModelOverride {
|
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
45
|
* Reasoning effort values sourced from the NaN docs (https://nan.builders/docs/models
|
|
46
|
-
* #controlling-reasoning, checked 2026-09-
|
|
46
|
+
* #controlling-reasoning, checked 2026-09-29).
|
|
47
47
|
*
|
|
48
48
|
* models.dev has NO `reasoning_effort_values` field — only `reasoning_options`
|
|
49
49
|
* (which is [{type:"toggle"}] or []), so this mapping must be hand-maintained
|
|
@@ -55,7 +55,10 @@ export interface ManualModelOverride {
|
|
|
55
55
|
* - glm5.3, glm5.3-flash: fully controllable (low/medium/high/max)
|
|
56
56
|
* - qwen3.6, gemma4: none/minimal skip reasoning, others cap depth
|
|
57
57
|
* - deepseek-v4-flash: any value accepted but model decides per-request
|
|
58
|
-
* - qwen3.8-flash, mimo-v2.
|
|
58
|
+
* - qwen3.8-flash, mimo-v2.6-flash: accepted, depth not adjustable
|
|
59
|
+
*
|
|
60
|
+
* (mimo-v2.5, which used to share that last row, was removed by NaN — absent
|
|
61
|
+
* from the docs and from openapi.json, checked 2026-09-29.)
|
|
59
62
|
*
|
|
60
63
|
* A model with reasoning_effort_values=[] means the parameter is accepted but
|
|
61
64
|
* the model manages its own reasoning depth — it is never an error.
|
|
@@ -68,13 +71,22 @@ export const REASONING_EFFORT_VALUES: Record<string, string[]> = {
|
|
|
68
71
|
};
|
|
69
72
|
|
|
70
73
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
+
* Provenance for the manual-only mimo-v2.6-flash entry, shared by
|
|
75
|
+
* MANUAL_ONLY_MODEL_IDS (generator) and MANUAL_ONLY_MODELS (tests) so both
|
|
76
|
+
* sides assert the exact same string.
|
|
77
|
+
*/
|
|
78
|
+
const MANUAL_ONLY_NOTE =
|
|
79
|
+
"omnimodal model (text, image, audio input) served by NaN; limits and modalities from https://nan.builders/docs/models#mimo-v2-6-flash (checked 2026-09-25, quotas re-checked 2026-09-29): 1,048,576 context / 131,072 max output / 1.0B monthly quota per member. Reasoning: accepted, depth not adjustable (docs table, checked 2026-09-29). Tool calling: yes. Streaming: yes. Input modalities (pi-representable): text, image (audio not representable in pi's Model type). Fallback entry: emitted only while models.dev provider nan does not list the model — models.dev started listing it on 2026-09-29, so the generator then keeps the models.dev values and attaches this note to that entry instead of emitting a second one.";
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Models NaN serves that models.dev provider nan may not list. Mirrors the
|
|
83
|
+
* generator's MANUAL_ONLY_MODEL_IDS — kept in sync so the generator and the
|
|
84
|
+
* tests share the same source. An entry stops being emitted as soon as
|
|
85
|
+
* models.dev lists the model; the note above then rides on the models.dev
|
|
86
|
+
* entry, so the manual data never vanishes silently.
|
|
74
87
|
*/
|
|
75
88
|
export const MANUAL_ONLY_MODEL_IDS: Record<string, string> = {
|
|
76
|
-
"mimo-v2.6-flash":
|
|
77
|
-
"omnimodal model (text, image, audio input) served by NaN, not yet listed on models.dev provider nan (checked 2026-09-25); included so the live /models refresh does not hand it UNKNOWN_MODEL_LIMITS. Same limits as mimo-v2.5: 1,048,576 / 131,072 / 1.0B monthly quota per member. Reasoning: accepted, depth not adjustable. Tool calling: yes. Streaming: yes. Input modalities (pi-representable): text, image (audio not representable in pi's Model type).",
|
|
89
|
+
"mimo-v2.6-flash": MANUAL_ONLY_NOTE,
|
|
78
90
|
};
|
|
79
91
|
|
|
80
92
|
/**
|
|
@@ -89,7 +101,7 @@ export const MANUAL_ONLY_MODELS: Record<string, ManualModelOverride> = {
|
|
|
89
101
|
reasoningEffortValues: [],
|
|
90
102
|
contextWindow: 1_048_576,
|
|
91
103
|
maxTokens: 131_072,
|
|
92
|
-
note:
|
|
104
|
+
note: MANUAL_ONLY_NOTE,
|
|
93
105
|
},
|
|
94
106
|
};
|
|
95
107
|
|