@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.
@@ -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 { MANUAL_OVERRIDES } from "./manual-overrides.ts";
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
- 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.",
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
  };