@gtrabanco/pi-nan-provider 0.6.11 → 0.8.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.es.md +49 -24
- package/README.md +65 -24
- package/package.json +3 -2
- package/scripts/check-pi-sdk-versions.mjs +392 -0
- package/scripts/generate-models.ts +78 -1
- package/scripts/manual-overrides.ts +57 -1
- package/scripts/models.generated.ts +61 -6
- package/src/fetch-models.ts +7 -0
- package/src/usage.ts +347 -174
|
@@ -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
|
+
});
|
|
@@ -21,7 +21,28 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import type { GeneratedModelEntry } from "../src/fetch-models.ts";
|
|
24
|
-
import {
|
|
24
|
+
import type { ManualModelOverride } from "./manual-overrides.ts";
|
|
25
|
+
import {
|
|
26
|
+
MANUAL_OVERRIDES,
|
|
27
|
+
MANUAL_ONLY_MODEL_IDS,
|
|
28
|
+
REASONING_EFFORT_VALUES,
|
|
29
|
+
} from "./manual-overrides.ts";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Reasoning effort values sourced from the NaN docs (https://nan.builders/docs/models
|
|
33
|
+
* #controlling-reasoning, checked 2026-09-25). models.dev has no reasoning_effort_values
|
|
34
|
+
* field — only reasoning_options which is [{type:"toggle"}] or [] — so the actual
|
|
35
|
+
* effort levels must be hand-maintained from the docs. An empty array means the
|
|
36
|
+
* parameter is accepted but depth is not adjustable by the user.
|
|
37
|
+
*/
|
|
38
|
+
const REASONING_EFFORT_VALUES_FROM_DOCS: Record<string, string[]> = {
|
|
39
|
+
...REASONING_EFFORT_VALUES,
|
|
40
|
+
// deepseek-v4-flash: any value (no effect) — model decides per request
|
|
41
|
+
"deepseek-v4-flash": [],
|
|
42
|
+
// qwen3.8-flash, mimo-v2.5: accepted, depth not adjustable
|
|
43
|
+
"qwen3.8-flash": [],
|
|
44
|
+
"mimo-v2.5": [],
|
|
45
|
+
};
|
|
25
46
|
|
|
26
47
|
const MODELS_DEV_API_URL = "https://models.dev/api.json";
|
|
27
48
|
const SOURCE_PROVIDER_ID = "nan";
|
|
@@ -158,6 +179,43 @@ function normalizeInput(modalitiesInput: string[] | undefined, modelId: string):
|
|
|
158
179
|
return input;
|
|
159
180
|
}
|
|
160
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Build a GeneratedModelEntry for a model that is not yet on models.dev
|
|
184
|
+
* (MANUAL_ONLY_MODEL_IDS). Uses conservative but accurate defaults from the
|
|
185
|
+
* NaN docs so that the live /models refresh does not hand them
|
|
186
|
+
* UNKNOWN_MODEL_LIMITS.
|
|
187
|
+
*/
|
|
188
|
+
function buildManualOnlyModelEntry(
|
|
189
|
+
modelId: string,
|
|
190
|
+
detail: string,
|
|
191
|
+
override: ManualModelOverride | undefined,
|
|
192
|
+
): GeneratedModel {
|
|
193
|
+
const reasoningEffortValues =
|
|
194
|
+
override?.reasoningEffortValues ??
|
|
195
|
+
REASONING_EFFORT_VALUES_FROM_DOCS[modelId] ??
|
|
196
|
+
undefined;
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
entry: {
|
|
200
|
+
id: modelId,
|
|
201
|
+
name: override?.name ?? modelId,
|
|
202
|
+
reasoning: override?.reasoning ?? true,
|
|
203
|
+
input: override?.input ?? ["text", "image"],
|
|
204
|
+
cost: override?.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
205
|
+
contextWindow: override?.contextWindow ?? 1_048_576,
|
|
206
|
+
maxTokens: override?.maxTokens ?? 131_072,
|
|
207
|
+
reasoningEffortValues,
|
|
208
|
+
compat: { ...NAN_COMPAT },
|
|
209
|
+
notes: [
|
|
210
|
+
NAN_COMPAT_NOTE,
|
|
211
|
+
`manual-only: "${modelId}" not yet on models.dev provider nan (${detail}).`,
|
|
212
|
+
...(override ? [override.note] : []),
|
|
213
|
+
],
|
|
214
|
+
extras: {},
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
161
219
|
function convertModel(modelId: string, m: ModelsDevModel): GeneratedModel | { skip: string } {
|
|
162
220
|
const removedReason = PROVIDER_REMOVED_MODEL_IDS[modelId];
|
|
163
221
|
if (removedReason) {
|
|
@@ -184,6 +242,13 @@ function convertModel(modelId: string, m: ModelsDevModel): GeneratedModel | { sk
|
|
|
184
242
|
console.log(`generate-models: manual override for "${modelId}" (${overridden.join(", ")})`);
|
|
185
243
|
}
|
|
186
244
|
|
|
245
|
+
// Reasoning effort values sourced from the NaN docs (models.dev does not
|
|
246
|
+
// expose them — only reasoning_options: [{type:"toggle"}] or []).
|
|
247
|
+
// An empty array means the parameter is accepted but depth is not
|
|
248
|
+
// adjustable by the user; the model manages its own reasoning depth.
|
|
249
|
+
const reasoningEffortValues =
|
|
250
|
+
override?.reasoningEffortValues ?? REASONING_EFFORT_VALUES_FROM_DOCS[modelId] ?? undefined;
|
|
251
|
+
|
|
187
252
|
return {
|
|
188
253
|
entry: {
|
|
189
254
|
id: modelId,
|
|
@@ -198,6 +263,7 @@ function convertModel(modelId: string, m: ModelsDevModel): GeneratedModel | { sk
|
|
|
198
263
|
},
|
|
199
264
|
contextWindow: override?.contextWindow ?? contextWindow,
|
|
200
265
|
maxTokens: override?.maxTokens ?? maxTokens,
|
|
266
|
+
reasoningEffortValues,
|
|
201
267
|
compat: { ...NAN_COMPAT },
|
|
202
268
|
notes: [
|
|
203
269
|
NAN_COMPAT_NOTE,
|
|
@@ -235,6 +301,17 @@ async function main(): Promise<void> {
|
|
|
235
301
|
}
|
|
236
302
|
}
|
|
237
303
|
|
|
304
|
+
// Add manual-only models (not yet on models.dev).
|
|
305
|
+
for (const [modelId, detail] of Object.entries(MANUAL_ONLY_MODEL_IDS)) {
|
|
306
|
+
const override = MANUAL_OVERRIDES[modelId];
|
|
307
|
+
if (override) {
|
|
308
|
+
console.log(`generate-models: manual override for "${modelId}" (${Object.keys(override).filter((k) => k !== "note").join(", ")})`);
|
|
309
|
+
}
|
|
310
|
+
const result = buildManualOnlyModelEntry(modelId, detail, MANUAL_OVERRIDES[modelId]);
|
|
311
|
+
entries.push(result.entry);
|
|
312
|
+
console.log(`generate-models: added manual-only model "${modelId}"`);
|
|
313
|
+
}
|
|
314
|
+
|
|
238
315
|
entries.sort((a, b) => a.id.localeCompare(b.id));
|
|
239
316
|
|
|
240
317
|
const entryIds = new Set(entries.map((entry) => entry.id));
|
|
@@ -35,13 +35,69 @@ export interface ManualModelOverride {
|
|
|
35
35
|
contextWindow?: number;
|
|
36
36
|
/** Max output tokens override. */
|
|
37
37
|
maxTokens?: number;
|
|
38
|
+
/** Reasoning effort values as declared by NaN docs. An empty array means the parameter is accepted but depth is model-managed (not adjustable by the user). */
|
|
39
|
+
reasoningEffortValues?: string[];
|
|
38
40
|
/** Required provenance note; emitted verbatim onto the generated entry. */
|
|
39
41
|
note: string;
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Reasoning effort values sourced from the NaN docs (https://nan.builders/docs/models
|
|
46
|
+
* #controlling-reasoning, checked 2026-09-25).
|
|
47
|
+
*
|
|
48
|
+
* models.dev has NO `reasoning_effort_values` field — only `reasoning_options`
|
|
49
|
+
* (which is [{type:"toggle"}] or []), so this mapping must be hand-maintained
|
|
50
|
+
* from the docs. The values flow through MANUAL_OVERRIDES.reasoningEffortValues
|
|
51
|
+
* and appear in GeneratedModelEntry.reasoningEffortValues, where the pi
|
|
52
|
+
* model-selector can read them to show the correct granularity.
|
|
53
|
+
*
|
|
54
|
+
* Per-model contract from the docs:
|
|
55
|
+
* - glm5.3, glm5.3-flash: fully controllable (low/medium/high/max)
|
|
56
|
+
* - qwen3.6, gemma4: none/minimal skip reasoning, others cap depth
|
|
57
|
+
* - deepseek-v4-flash: any value accepted but model decides per-request
|
|
58
|
+
* - qwen3.8-flash, mimo-v2.5, mimo-v2.6-flash: accepted, depth not adjustable
|
|
59
|
+
*
|
|
60
|
+
* A model with reasoning_effort_values=[] means the parameter is accepted but
|
|
61
|
+
* the model manages its own reasoning depth — it is never an error.
|
|
62
|
+
*/
|
|
63
|
+
export const REASONING_EFFORT_VALUES: Record<string, string[]> = {
|
|
64
|
+
"glm5.3": ["low", "medium", "high", "max"],
|
|
65
|
+
"glm5.3-flash": ["low", "medium", "high", "max"],
|
|
66
|
+
"qwen3.6": ["none", "minimal", "low", "medium", "high", "max"],
|
|
67
|
+
"gemma4": ["none", "minimal", "low", "medium", "high", "max"],
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Models that NaN serves but models.dev provider nan hasn't listed yet.
|
|
72
|
+
* Mirrors the generator's MANUAL_ONLY_MODEL_IDS — kept in sync so the
|
|
73
|
+
* generator and the tests share the same source.
|
|
74
|
+
*/
|
|
75
|
+
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).",
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Manual-only model entries with the full override shape for test assertions.
|
|
82
|
+
* Mirrors MANUAL_ONLY_MODEL_IDS so the generator and the tests share the
|
|
83
|
+
* same set of models.
|
|
84
|
+
*/
|
|
85
|
+
export const MANUAL_ONLY_MODELS: Record<string, ManualModelOverride> = {
|
|
86
|
+
"mimo-v2.6-flash": {
|
|
87
|
+
input: ["text", "image"],
|
|
88
|
+
reasoning: true,
|
|
89
|
+
reasoningEffortValues: [],
|
|
90
|
+
contextWindow: 1_048_576,
|
|
91
|
+
maxTokens: 131_072,
|
|
92
|
+
note: "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).",
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
42
96
|
export const MANUAL_OVERRIDES: Record<string, ManualModelOverride> = {
|
|
43
97
|
"deepseek-v4-flash": {
|
|
44
98
|
input: ["text", "image"],
|
|
45
|
-
|
|
99
|
+
// Any value accepted but model decides per-request (no effect).
|
|
100
|
+
reasoningEffortValues: [],
|
|
101
|
+
note: "input includes image: NaN serves the Vision-Exp variant ('takes images as input', https://nan.builders/docs/models; the image_url content-parts in https://nan.builders/openapi.json list deepseek-v4-flash among the vision models). models.dev provider nan also lists text+image now (DeepSeek V4.1 Flash entry, checked 2026-09-13; its 2026-09-07 snapshot listed text only), so this override is kept as a pin for the vision capability rather than as a divergence. reasoning_effort_values=[]: the model decides per-request how much to reason (https://nan.builders/docs/models, checked 2026-09-25).",
|
|
46
102
|
},
|
|
47
103
|
};
|