@gtrabanco/pi-nan-provider 0.8.0 → 0.10.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.
@@ -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 founding models (qwen3.6, gemma4, deepseek-v4-flash, mimo-v2.5) MUST
15
- * exist on models.dev with complete limits, or this script exits non-zero.
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.5's audio input is not representable in
20
- * pi's Model type and is dropped from `input` (noted on the entry).
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.5: accepted, depth not adjustable
54
+ // qwen3.8-flash, mimo-v2.6-flash: accepted, depth not adjustable
43
55
  "qwen3.8-flash": [],
44
- "mimo-v2.5": [],
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
- /** Models the founding prompt requires in the catalog; absence is fatal. */
52
- const REQUIRED_MODEL_IDS = ["qwen3.6", "gemma4", "deepseek-v4-flash", "mimo-v2.5"] as const;
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
  /**
@@ -205,6 +237,7 @@ function buildManualOnlyModelEntry(
205
237
  contextWindow: override?.contextWindow ?? 1_048_576,
206
238
  maxTokens: override?.maxTokens ?? 131_072,
207
239
  reasoningEffortValues,
240
+ ...(override?.thinkingLevelMap ? { thinkingLevelMap: { ...override.thinkingLevelMap } } : {}),
208
241
  compat: { ...NAN_COMPAT },
209
242
  notes: [
210
243
  NAN_COMPAT_NOTE,
@@ -221,6 +254,10 @@ function convertModel(modelId: string, m: ModelsDevModel): GeneratedModel | { sk
221
254
  if (removedReason) {
222
255
  return { skip: `provider-removed: "${modelId}" excluded from the catalog (${removedReason})` };
223
256
  }
257
+ const nonChatReason = NON_CHAT_MODEL_IDS[modelId];
258
+ if (nonChatReason) {
259
+ return { skip: `non-chat: "${modelId}" excluded from the chat catalog (${nonChatReason})` };
260
+ }
224
261
  const liveOnlyReason = LIVE_ONLY_MODEL_IDS[modelId];
225
262
  if (liveOnlyReason) {
226
263
  return { skip: `live-only: "${modelId}" kept out of the static catalog (${liveOnlyReason})` };
@@ -264,6 +301,7 @@ function convertModel(modelId: string, m: ModelsDevModel): GeneratedModel | { sk
264
301
  contextWindow: override?.contextWindow ?? contextWindow,
265
302
  maxTokens: override?.maxTokens ?? maxTokens,
266
303
  reasoningEffortValues,
304
+ ...(override?.thinkingLevelMap ? { thinkingLevelMap: { ...override.thinkingLevelMap } } : {}),
267
305
  compat: { ...NAN_COMPAT },
268
306
  notes: [
269
307
  NAN_COMPAT_NOTE,
@@ -301,8 +339,20 @@ async function main(): Promise<void> {
301
339
  }
302
340
  }
303
341
 
304
- // Add manual-only models (not yet on models.dev).
342
+ // Add manual-only models (models.dev provider "nan" does not list them).
343
+ // When models.dev starts listing one, the manual entry is NOT emitted —
344
+ // otherwise the id would reach the catalog twice — and the reason plus the
345
+ // original manual provenance are attached to the models.dev-derived entry,
346
+ // so the manual data never disappears silently.
347
+ const entryById = new Map(entries.map((entry) => [entry.id, entry]));
305
348
  for (const [modelId, detail] of Object.entries(MANUAL_ONLY_MODEL_IDS)) {
349
+ const existing = entryById.get(modelId);
350
+ if (existing) {
351
+ 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})`;
352
+ existing.notes = [...(existing.notes ?? []), note];
353
+ console.warn(`generate-models: skipped ${note}`);
354
+ continue;
355
+ }
306
356
  const override = MANUAL_OVERRIDES[modelId];
307
357
  if (override) {
308
358
  console.log(`generate-models: manual override for "${modelId}" (${Object.keys(override).filter((k) => k !== "note").join(", ")})`);
@@ -318,8 +368,9 @@ async function main(): Promise<void> {
318
368
  const missingRequired = REQUIRED_MODEL_IDS.filter((id) => !entryIds.has(id));
319
369
  if (missingRequired.length > 0) {
320
370
  fail(
321
- `required models missing from models.dev provider "${SOURCE_PROVIDER_ID}": ${missingRequired.join(", ")} — ` +
322
- "do NOT invent their data; confirm and add them via MANUAL_OVERRIDES with a source note.",
371
+ `required models missing from the catalog: ${missingRequired.join(", ")} (official community chat models per ` +
372
+ "https://nan.builders/docs/models) — do NOT invent their data; confirm them in the docs and add them " +
373
+ "via MANUAL_OVERRIDES (models.dev lists them) or MANUAL_ONLY_MODEL_IDS (models.dev does not), with a source note.",
323
374
  );
324
375
  }
325
376
 
@@ -333,6 +384,9 @@ async function main(): Promise<void> {
333
384
  ...Object.entries(PROVIDER_REMOVED_MODEL_IDS).map(
334
385
  ([id, reason]) => `provider-removed: "${id}" excluded from the catalog (${reason})`,
335
386
  ),
387
+ ...Object.entries(NON_CHAT_MODEL_IDS).map(
388
+ ([id, reason]) => `non-chat: "${id}" excluded from the chat catalog (${reason})`,
389
+ ),
336
390
  ...Object.entries(LIVE_ONLY_MODEL_IDS).map(
337
391
  ([id, reason]) => `live-only: "${id}" kept out of the static catalog (${reason})`,
338
392
  ),
@@ -37,13 +37,20 @@ export interface ManualModelOverride {
37
37
  maxTokens?: number;
38
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
39
  reasoningEffortValues?: string[];
40
+ /**
41
+ * Mapping from pi-ai's user-facing thinking levels to model-specific effort
42
+ * strings. Only the "off" key is needed to suppress runaway reasoning.
43
+ * When `reasoning: "off"` maps to undefined in pi-ai, thinkingLevelMap.off
44
+ * supplies the effort value that NaN understands (e.g. "none" or "minimal").
45
+ */
46
+ thinkingLevelMap?: Partial<Record<string, string | null>>;
40
47
  /** Required provenance note; emitted verbatim onto the generated entry. */
41
48
  note: string;
42
49
  }
43
50
 
44
51
  /**
45
- * Reasoning effort values sourced from the NaN docs (https://nan.builders/docs/models
46
- * #controlling-reasoning, checked 2026-09-25).
52
+ * Manual reasoning effort values sourced from the NaN docs
53
+ * (https://nan.builders/docs/models #controlling-reasoning, checked 2026-09-29).
47
54
  *
48
55
  * models.dev has NO `reasoning_effort_values` field — only `reasoning_options`
49
56
  * (which is [{type:"toggle"}] or []), so this mapping must be hand-maintained
@@ -53,28 +60,48 @@ export interface ManualModelOverride {
53
60
  *
54
61
  * Per-model contract from the docs:
55
62
  * - glm5.3, glm5.3-flash: fully controllable (low/medium/high/max)
63
+ * + minimal also accepted (issue #16, measured 2026-09-27): minimal
64
+ * yields 38 reasoning tokens vs 2,288 for high — the docs simply
65
+ * omitted it because it was not originally advertised, but it IS
66
+ * accepted and effective by the gateway.
56
67
  * - 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
68
+ * - deepseek-v4-flash: any value accepted but only "none" deterministically
69
+ * disables reasoning (issue #16, measured 2026-09-27); every other value
70
+ * (minimal/low/medium/high/omitted) leaves 13-14K reasoning tokens with
71
+ * runaway reasoning and finish_reason:"length" with ZERO answer. Since only
72
+ * "none" has a measurable effect, the catalog declares ["none"].
73
+ * - qwen3.8-flash, mimo-v2.6-flash: accepted, depth not adjustable
74
+ *
75
+ * (mimo-v2.5, which used to share that last row, was removed by NaN — absent
76
+ * from the docs and from openapi.json, checked 2026-09-29.)
59
77
  *
60
78
  * A model with reasoning_effort_values=[] means the parameter is accepted but
61
79
  * the model manages its own reasoning depth — it is never an error.
62
80
  */
63
81
  export const REASONING_EFFORT_VALUES: Record<string, string[]> = {
64
82
  "glm5.3": ["low", "medium", "high", "max"],
65
- "glm5.3-flash": ["low", "medium", "high", "max"],
83
+ "glm5.3-flash": ["minimal", "low", "medium", "high", "max"],
66
84
  "qwen3.6": ["none", "minimal", "low", "medium", "high", "max"],
67
85
  "gemma4": ["none", "minimal", "low", "medium", "high", "max"],
68
86
  };
69
87
 
70
88
  /**
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.
89
+ * Provenance for the manual-only mimo-v2.6-flash entry, shared by
90
+ * MANUAL_ONLY_MODEL_IDS (generator) and MANUAL_ONLY_MODELS (tests) so both
91
+ * sides assert the exact same string.
92
+ */
93
+ const MANUAL_ONLY_NOTE =
94
+ "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.";
95
+
96
+ /**
97
+ * Models NaN serves that models.dev provider nan may not list. Mirrors the
98
+ * generator's MANUAL_ONLY_MODEL_IDS — kept in sync so the generator and the
99
+ * tests share the same source. An entry stops being emitted as soon as
100
+ * models.dev lists the model; the note above then rides on the models.dev
101
+ * entry, so the manual data never vanishes silently.
74
102
  */
75
103
  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).",
104
+ "mimo-v2.6-flash": MANUAL_ONLY_NOTE,
78
105
  };
79
106
 
80
107
  /**
@@ -89,15 +116,52 @@ export const MANUAL_ONLY_MODELS: Record<string, ManualModelOverride> = {
89
116
  reasoningEffortValues: [],
90
117
  contextWindow: 1_048_576,
91
118
  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).",
119
+ note: MANUAL_ONLY_NOTE,
93
120
  },
94
121
  };
95
122
 
123
+ /**
124
+ * Manual overrides keyed by model id. Fields override the models.dev-derived
125
+ * entry one-for-one. Every override carries a provenance note.
126
+ *
127
+ * Issue #16 (measured 2026-09-27 against live gateway, repro included):
128
+ * - deepseek-v4-flash: reasoning_effort_values=["none"] (only "none" suppresses);
129
+ * thinkingLevelMap={off:"none"} (supplies reasoning_effort when pi maps
130
+ * reasoning:"off" → undefined).
131
+ * - glm5.3-flash: reasoningEffortValues includes "minimal" (not documented but
132
+ * effective); thinkingLevelMap={off:"minimal"} (supplies reasoning_effort
133
+ * when pi maps reasoning:"off" → undefined).
134
+ * - qwen3.6, gemma4: the NaN docs already document `none` as "skip the
135
+ * reasoning phase entirely" and state that with no parameter the model
136
+ * defaults to reasoning ON (16,384-token budget), so off must send
137
+ * `none` explicitly — thinkingLevelMap={off:"none"} (NaN docs
138
+ * #controlling-reasoning, checked 2026-10-01). No measurement needed: the
139
+ * docs are explicit about both the default and the `none` behavior.
140
+ */
96
141
  export const MANUAL_OVERRIDES: Record<string, ManualModelOverride> = {
97
142
  "deepseek-v4-flash": {
98
143
  input: ["text", "image"],
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).",
144
+ reasoningEffortValues: ["none"],
145
+ thinkingLevelMap: { off: "none" },
146
+ note:
147
+ "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. " +
148
+ "reasoning_effort_values=[\"none\"] (issue #16, measured 2026-09-27 against live gateway, repro included in the issue): only \"none\" deterministically disables reasoning (0 reasoning tokens, answer produced). Every other value leaves 13-14K reasoning tokens and causes runaway reasoning (finish_reason:\"length\" with ZERO answer text). The catalog declares only [\"none\"] because the remaining values are useless noise. " +
149
+ "thinkingLevelMap { off: \"none\" } (issue #16, measured 2026-09-27): when pi maps reasoning:\"off\" to reasoningEffort:undefined, the thinkingLevelMap.off=\"none\" supplies the effort so the gateway receives reasoning_effort:\"none\" and does NOT enter runaway reasoning mode.",
150
+ },
151
+ "glm5.3-flash": {
152
+ thinkingLevelMap: { off: "minimal" },
153
+ note:
154
+ "thinkingLevelMap { off: \"minimal\" } (issue #16, measured 2026-09-27 against live gateway, repro included in the issue): when pi maps reasoning:\"off\" to reasoningEffort:undefined, the thinkingLevelMap.off=\"minimal\" supplies the effort so the gateway receives reasoning_effort:\"minimal\" (38 reasoning tokens + answer). Note: \"none\" does NOT suppress reasoning on glm5.3-flash (13,382 tokens) — only \"minimal\" was measured to be effective. " +
155
+ "reasoning_effort_values includes \"minimal\" (not documented by NaN docs but accepted and effective by the gateway, issue #16).",
156
+ },
157
+ "qwen3.6": {
158
+ thinkingLevelMap: { off: "none" },
159
+ note:
160
+ "thinkingLevelMap { off: \"none\" } (issue #16): NaN docs state that with no reasoning_effort parameter every model uses its own default and that default is reasoning ON for qwen3.6 (16,384-token budget), while `none` and `minimal` skip the reasoning phase entirely (https://nan.builders/docs/models#controlling-reasoning, checked 2026-10-01). pi maps reasoning:\"off\" to reasoningEffort:undefined, so without this map `thinking: off` sent no parameter and the model reasoned anyway; the map sends reasoning_effort:\"none\" and actually disables reasoning.",
161
+ },
162
+ "gemma4": {
163
+ thinkingLevelMap: { off: "none" },
164
+ note:
165
+ "thinkingLevelMap { off: \"none\" } (issue #16): NaN docs state that with no reasoning_effort parameter gemma4 defaults to reasoning ON (16,384-token budget), while `none`/`minimal` skip reasoning entirely (https://nan.builders/docs/models#controlling-reasoning, checked 2026-10-01). pi maps reasoning:\"off\" to reasoningEffort:undefined, so without this map `thinking: off` sent no parameter and the model reasoned anyway; the map sends reasoning_effort:\"none\" and actually disables reasoning.",
102
166
  },
103
167
  };
@@ -1,7 +1,7 @@
1
1
  // This file is auto-generated by scripts/generate-models.ts
2
2
  // Do not edit manually — run `bun run generate-models` to update.
3
3
  //
4
- // Source: https://models.dev/api.json (provider "nan"), fetched 2026-09-27T21:25:24.946Z
4
+ // Source: https://models.dev/api.json (provider "nan"), fetched 2026-10-01T10:18:48.654Z
5
5
  // Provenance: every contextWindow/maxTokens/input/cost value traces to
6
6
  // models.dev or to the per-entry notes below. Nothing is invented; entries
7
7
  // models.dev documents incompletely are omitted and flagged instead.
@@ -28,7 +28,12 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
28
28
  },
29
29
  "contextWindow": 1000000,
30
30
  "maxTokens": 384000,
31
- "reasoningEffortValues": [],
31
+ "reasoningEffortValues": [
32
+ "none"
33
+ ],
34
+ "thinkingLevelMap": {
35
+ "off": "none"
36
+ },
32
37
  "compat": {
33
38
  "supportsDeveloperRole": false,
34
39
  "supportsReasoningEffort": true,
@@ -38,7 +43,7 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
38
43
  },
39
44
  "notes": [
40
45
  "compat matches the maintainer's working ~/.pi/agent/models.json LiteLLM config for api.nan.builders (2026-09-04): supportsDeveloperRole false, supportsReasoningEffort true, maxTokensField max_tokens. NaN's docs example sets only supportsDeveloperRole: true and is not battle-tested. supportsFinishReason true (2026-09-13, issue #2): the LiteLLM gateway intermittently closes SSE streams before emitting finish_reason; with true pi-ai raises 'Stream ended without finish_reason', which matches pi-ai's retryable-provider pattern ('ended without') and is retried automatically, whereas false silently synthesized stop/toolUse and stalled the turn mid-answer. supportsUsageInStreaming true (2026-09-16, issue #7): NaN's published schema is silent about stream_options, but the live gateway honors it — two identical streaming calls per model, differing only in stream_options: { include_usage: true }, returned 0 usage chunks without it and exactly 1 with it (prompt/completion/reasoning/cached token counts) on deepseek-v4-flash, glm5.3-flash, qwen3.6, mimo-v2.5 and gemma4, and a real pi session then recorded token counts where it recorded zeros. pi-ai only sends stream_options when this is not false, and the sanitizer forwards it when the model declares true, so chat models opt in by default and usage is reported (issue #4). A model that does not report streaming usage can still opt out per model with a models.json compat override (supportsUsageInStreaming: false); the sanitizer then strips stream_options and the payload stays strict.",
41
- "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
+ "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=[\"none\"] (issue #16, measured 2026-09-27 against live gateway, repro included in the issue): only \"none\" deterministically disables reasoning (0 reasoning tokens, answer produced). Every other value leaves 13-14K reasoning tokens and causes runaway reasoning (finish_reason:\"length\" with ZERO answer text). The catalog declares only [\"none\"] because the remaining values are useless noise. thinkingLevelMap { off: \"none\" } (issue #16, measured 2026-09-27): when pi maps reasoning:\"off\" to reasoningEffort:undefined, the thinkingLevelMap.off=\"none\" supplies the effort so the gateway receives reasoning_effort:\"none\" and does NOT enter runaway reasoning mode."
42
47
  ],
43
48
  "extras": {
44
49
  "id": "deepseek-v4-flash",
@@ -71,7 +76,8 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
71
76
  "cost": {
72
77
  "input": 0,
73
78
  "output": 0
74
- }
79
+ },
80
+ "canonical_model_id": "deepseek/deepseek-v4.1-flash"
75
81
  }
76
82
  },
77
83
  {
@@ -98,6 +104,9 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
98
104
  "high",
99
105
  "max"
100
106
  ],
107
+ "thinkingLevelMap": {
108
+ "off": "none"
109
+ },
101
110
  "compat": {
102
111
  "supportsDeveloperRole": false,
103
112
  "supportsReasoningEffort": true,
@@ -106,7 +115,8 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
106
115
  "maxTokensField": "max_tokens"
107
116
  },
108
117
  "notes": [
109
- "compat matches the maintainer's working ~/.pi/agent/models.json LiteLLM config for api.nan.builders (2026-09-04): supportsDeveloperRole false, supportsReasoningEffort true, maxTokensField max_tokens. NaN's docs example sets only supportsDeveloperRole: true and is not battle-tested. supportsFinishReason true (2026-09-13, issue #2): the LiteLLM gateway intermittently closes SSE streams before emitting finish_reason; with true pi-ai raises 'Stream ended without finish_reason', which matches pi-ai's retryable-provider pattern ('ended without') and is retried automatically, whereas false silently synthesized stop/toolUse and stalled the turn mid-answer. supportsUsageInStreaming true (2026-09-16, issue #7): NaN's published schema is silent about stream_options, but the live gateway honors it — two identical streaming calls per model, differing only in stream_options: { include_usage: true }, returned 0 usage chunks without it and exactly 1 with it (prompt/completion/reasoning/cached token counts) on deepseek-v4-flash, glm5.3-flash, qwen3.6, mimo-v2.5 and gemma4, and a real pi session then recorded token counts where it recorded zeros. pi-ai only sends stream_options when this is not false, and the sanitizer forwards it when the model declares true, so chat models opt in by default and usage is reported (issue #4). A model that does not report streaming usage can still opt out per model with a models.json compat override (supportsUsageInStreaming: false); the sanitizer then strips stream_options and the payload stays strict."
118
+ "compat matches the maintainer's working ~/.pi/agent/models.json LiteLLM config for api.nan.builders (2026-09-04): supportsDeveloperRole false, supportsReasoningEffort true, maxTokensField max_tokens. NaN's docs example sets only supportsDeveloperRole: true and is not battle-tested. supportsFinishReason true (2026-09-13, issue #2): the LiteLLM gateway intermittently closes SSE streams before emitting finish_reason; with true pi-ai raises 'Stream ended without finish_reason', which matches pi-ai's retryable-provider pattern ('ended without') and is retried automatically, whereas false silently synthesized stop/toolUse and stalled the turn mid-answer. supportsUsageInStreaming true (2026-09-16, issue #7): NaN's published schema is silent about stream_options, but the live gateway honors it — two identical streaming calls per model, differing only in stream_options: { include_usage: true }, returned 0 usage chunks without it and exactly 1 with it (prompt/completion/reasoning/cached token counts) on deepseek-v4-flash, glm5.3-flash, qwen3.6, mimo-v2.5 and gemma4, and a real pi session then recorded token counts where it recorded zeros. pi-ai only sends stream_options when this is not false, and the sanitizer forwards it when the model declares true, so chat models opt in by default and usage is reported (issue #4). A model that does not report streaming usage can still opt out per model with a models.json compat override (supportsUsageInStreaming: false); the sanitizer then strips stream_options and the payload stays strict.",
119
+ "thinkingLevelMap { off: \"none\" } (issue #16): NaN docs state that with no reasoning_effort parameter gemma4 defaults to reasoning ON (16,384-token budget), while `none`/`minimal` skip reasoning entirely (https://nan.builders/docs/models#controlling-reasoning, checked 2026-10-01). pi maps reasoning:\"off\" to reasoningEffort:undefined, so without this map `thinking: off` sent no parameter and the model reasoned anyway; the map sends reasoning_effort:\"none\" and actually disables reasoning."
110
120
  ],
111
121
  "extras": {
112
122
  "id": "gemma4",
@@ -117,7 +127,14 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
117
127
  "reasoning": true,
118
128
  "reasoning_options": [
119
129
  {
120
- "type": "toggle"
130
+ "type": "effort",
131
+ "values": [
132
+ "none",
133
+ "low",
134
+ "medium",
135
+ "high",
136
+ "max"
137
+ ]
121
138
  }
122
139
  ],
123
140
  "tool_call": true,
@@ -142,7 +159,8 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
142
159
  "cost": {
143
160
  "input": 0,
144
161
  "output": 0
145
- }
162
+ },
163
+ "canonical_model_id": "google/gemma-4-26b-a4b-it"
146
164
  }
147
165
  },
148
166
  {
@@ -162,11 +180,15 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
162
180
  "contextWindow": 1000000,
163
181
  "maxTokens": 131072,
164
182
  "reasoningEffortValues": [
183
+ "minimal",
165
184
  "low",
166
185
  "medium",
167
186
  "high",
168
187
  "max"
169
188
  ],
189
+ "thinkingLevelMap": {
190
+ "off": "minimal"
191
+ },
170
192
  "compat": {
171
193
  "supportsDeveloperRole": false,
172
194
  "supportsReasoningEffort": true,
@@ -175,7 +197,8 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
175
197
  "maxTokensField": "max_tokens"
176
198
  },
177
199
  "notes": [
178
- "compat matches the maintainer's working ~/.pi/agent/models.json LiteLLM config for api.nan.builders (2026-09-04): supportsDeveloperRole false, supportsReasoningEffort true, maxTokensField max_tokens. NaN's docs example sets only supportsDeveloperRole: true and is not battle-tested. supportsFinishReason true (2026-09-13, issue #2): the LiteLLM gateway intermittently closes SSE streams before emitting finish_reason; with true pi-ai raises 'Stream ended without finish_reason', which matches pi-ai's retryable-provider pattern ('ended without') and is retried automatically, whereas false silently synthesized stop/toolUse and stalled the turn mid-answer. supportsUsageInStreaming true (2026-09-16, issue #7): NaN's published schema is silent about stream_options, but the live gateway honors it — two identical streaming calls per model, differing only in stream_options: { include_usage: true }, returned 0 usage chunks without it and exactly 1 with it (prompt/completion/reasoning/cached token counts) on deepseek-v4-flash, glm5.3-flash, qwen3.6, mimo-v2.5 and gemma4, and a real pi session then recorded token counts where it recorded zeros. pi-ai only sends stream_options when this is not false, and the sanitizer forwards it when the model declares true, so chat models opt in by default and usage is reported (issue #4). A model that does not report streaming usage can still opt out per model with a models.json compat override (supportsUsageInStreaming: false); the sanitizer then strips stream_options and the payload stays strict."
200
+ "compat matches the maintainer's working ~/.pi/agent/models.json LiteLLM config for api.nan.builders (2026-09-04): supportsDeveloperRole false, supportsReasoningEffort true, maxTokensField max_tokens. NaN's docs example sets only supportsDeveloperRole: true and is not battle-tested. supportsFinishReason true (2026-09-13, issue #2): the LiteLLM gateway intermittently closes SSE streams before emitting finish_reason; with true pi-ai raises 'Stream ended without finish_reason', which matches pi-ai's retryable-provider pattern ('ended without') and is retried automatically, whereas false silently synthesized stop/toolUse and stalled the turn mid-answer. supportsUsageInStreaming true (2026-09-16, issue #7): NaN's published schema is silent about stream_options, but the live gateway honors it — two identical streaming calls per model, differing only in stream_options: { include_usage: true }, returned 0 usage chunks without it and exactly 1 with it (prompt/completion/reasoning/cached token counts) on deepseek-v4-flash, glm5.3-flash, qwen3.6, mimo-v2.5 and gemma4, and a real pi session then recorded token counts where it recorded zeros. pi-ai only sends stream_options when this is not false, and the sanitizer forwards it when the model declares true, so chat models opt in by default and usage is reported (issue #4). A model that does not report streaming usage can still opt out per model with a models.json compat override (supportsUsageInStreaming: false); the sanitizer then strips stream_options and the payload stays strict.",
201
+ "thinkingLevelMap { off: \"minimal\" } (issue #16, measured 2026-09-27 against live gateway, repro included in the issue): when pi maps reasoning:\"off\" to reasoningEffort:undefined, the thinkingLevelMap.off=\"minimal\" supplies the effort so the gateway receives reasoning_effort:\"minimal\" (38 reasoning tokens + answer). Note: \"none\" does NOT suppress reasoning on glm5.3-flash (13,382 tokens) — only \"minimal\" was measured to be effective. reasoning_effort_values includes \"minimal\" (not documented by NaN docs but accepted and effective by the gateway, issue #16)."
179
202
  ],
180
203
  "extras": {
181
204
  "id": "glm5.3-flash",
@@ -184,7 +207,17 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
184
207
  "family": "glm-flash",
185
208
  "attachment": true,
186
209
  "reasoning": true,
187
- "reasoning_options": [],
210
+ "reasoning_options": [
211
+ {
212
+ "type": "effort",
213
+ "values": [
214
+ "low",
215
+ "medium",
216
+ "high",
217
+ "max"
218
+ ]
219
+ }
220
+ ],
188
221
  "tool_call": true,
189
222
  "structured_output": true,
190
223
  "temperature": true,
@@ -207,12 +240,13 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
207
240
  "cost": {
208
241
  "input": 0,
209
242
  "output": 0
210
- }
243
+ },
244
+ "canonical_model_id": "zhipuai/glm-5.3-flash"
211
245
  }
212
246
  },
213
247
  {
214
- "id": "mimo-v2.5",
215
- "name": "MiMo-V2.5",
248
+ "id": "mimo-v2.6-flash",
249
+ "name": "MiMo-V2.6-Flash",
216
250
  "reasoning": true,
217
251
  "input": [
218
252
  "text",
@@ -235,21 +269,21 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
235
269
  "maxTokensField": "max_tokens"
236
270
  },
237
271
  "notes": [
238
- "compat matches the maintainer's working ~/.pi/agent/models.json LiteLLM config for api.nan.builders (2026-09-04): supportsDeveloperRole false, supportsReasoningEffort true, maxTokensField max_tokens. NaN's docs example sets only supportsDeveloperRole: true and is not battle-tested. supportsFinishReason true (2026-09-13, issue #2): the LiteLLM gateway intermittently closes SSE streams before emitting finish_reason; with true pi-ai raises 'Stream ended without finish_reason', which matches pi-ai's retryable-provider pattern ('ended without') and is retried automatically, whereas false silently synthesized stop/toolUse and stalled the turn mid-answer. supportsUsageInStreaming true (2026-09-16, issue #7): NaN's published schema is silent about stream_options, but the live gateway honors it — two identical streaming calls per model, differing only in stream_options: { include_usage: true }, returned 0 usage chunks without it and exactly 1 with it (prompt/completion/reasoning/cached token counts) on deepseek-v4-flash, glm5.3-flash, qwen3.6, mimo-v2.5 and gemma4, and a real pi session then recorded token counts where it recorded zeros. pi-ai only sends stream_options when this is not false, and the sanitizer forwards it when the model declares true, so chat models opt in by default and usage is reported (issue #4). A model that does not report streaming usage can still opt out per model with a models.json compat override (supportsUsageInStreaming: false); the sanitizer then strips stream_options and the payload stays strict."
272
+ "compat matches the maintainer's working ~/.pi/agent/models.json LiteLLM config for api.nan.builders (2026-09-04): supportsDeveloperRole false, supportsReasoningEffort true, maxTokensField max_tokens. NaN's docs example sets only supportsDeveloperRole: true and is not battle-tested. supportsFinishReason true (2026-09-13, issue #2): the LiteLLM gateway intermittently closes SSE streams before emitting finish_reason; with true pi-ai raises 'Stream ended without finish_reason', which matches pi-ai's retryable-provider pattern ('ended without') and is retried automatically, whereas false silently synthesized stop/toolUse and stalled the turn mid-answer. supportsUsageInStreaming true (2026-09-16, issue #7): NaN's published schema is silent about stream_options, but the live gateway honors it — two identical streaming calls per model, differing only in stream_options: { include_usage: true }, returned 0 usage chunks without it and exactly 1 with it (prompt/completion/reasoning/cached token counts) on deepseek-v4-flash, glm5.3-flash, qwen3.6, mimo-v2.5 and gemma4, and a real pi session then recorded token counts where it recorded zeros. pi-ai only sends stream_options when this is not false, and the sanitizer forwards it when the model declares true, so chat models opt in by default and usage is reported (issue #4). A model that does not report streaming usage can still opt out per model with a models.json compat override (supportsUsageInStreaming: false); the sanitizer then strips stream_options and the payload stays strict.",
273
+ "manual-only: \"mimo-v2.6-flash\" skipped: models.dev provider \"nan\" now lists it, so the manual-only entry is not emitted and the models.dev values are used (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.)"
239
274
  ],
240
275
  "extras": {
241
- "id": "mimo-v2.5",
242
- "name": "MiMo-V2.5",
243
- "description": "Open MiMo model for multimodal coding agents and long-context automation",
276
+ "id": "mimo-v2.6-flash",
277
+ "name": "MiMo-V2.6-Flash",
278
+ "description": "MiMo Flash model for multimodal coding agents and long-context automation",
244
279
  "family": "mimo",
245
280
  "attachment": true,
246
281
  "reasoning": true,
247
282
  "reasoning_options": [],
248
283
  "tool_call": true,
249
284
  "temperature": true,
250
- "knowledge": "2024-12",
251
- "release_date": "2026-04-22",
252
- "last_updated": "2026-04-22",
285
+ "release_date": "2026-09-22",
286
+ "last_updated": "2026-09-22",
253
287
  "modalities": {
254
288
  "input": [
255
289
  "text",
@@ -268,38 +302,10 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
268
302
  "cost": {
269
303
  "input": 0,
270
304
  "output": 0
271
- }
305
+ },
306
+ "canonical_model_id": "xiaomi/mimo-v2.6-flash"
272
307
  }
273
308
  },
274
- {
275
- "id": "mimo-v2.6-flash",
276
- "name": "mimo-v2.6-flash",
277
- "reasoning": true,
278
- "input": [
279
- "text",
280
- "image"
281
- ],
282
- "cost": {
283
- "input": 0,
284
- "output": 0,
285
- "cacheRead": 0,
286
- "cacheWrite": 0
287
- },
288
- "contextWindow": 1048576,
289
- "maxTokens": 131072,
290
- "compat": {
291
- "supportsDeveloperRole": false,
292
- "supportsReasoningEffort": true,
293
- "supportsUsageInStreaming": true,
294
- "supportsFinishReason": true,
295
- "maxTokensField": "max_tokens"
296
- },
297
- "notes": [
298
- "compat matches the maintainer's working ~/.pi/agent/models.json LiteLLM config for api.nan.builders (2026-09-04): supportsDeveloperRole false, supportsReasoningEffort true, maxTokensField max_tokens. NaN's docs example sets only supportsDeveloperRole: true and is not battle-tested. supportsFinishReason true (2026-09-13, issue #2): the LiteLLM gateway intermittently closes SSE streams before emitting finish_reason; with true pi-ai raises 'Stream ended without finish_reason', which matches pi-ai's retryable-provider pattern ('ended without') and is retried automatically, whereas false silently synthesized stop/toolUse and stalled the turn mid-answer. supportsUsageInStreaming true (2026-09-16, issue #7): NaN's published schema is silent about stream_options, but the live gateway honors it — two identical streaming calls per model, differing only in stream_options: { include_usage: true }, returned 0 usage chunks without it and exactly 1 with it (prompt/completion/reasoning/cached token counts) on deepseek-v4-flash, glm5.3-flash, qwen3.6, mimo-v2.5 and gemma4, and a real pi session then recorded token counts where it recorded zeros. pi-ai only sends stream_options when this is not false, and the sanitizer forwards it when the model declares true, so chat models opt in by default and usage is reported (issue #4). A model that does not report streaming usage can still opt out per model with a models.json compat override (supportsUsageInStreaming: false); the sanitizer then strips stream_options and the payload stays strict.",
299
- "manual-only: \"mimo-v2.6-flash\" not yet on models.dev provider nan (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).)."
300
- ],
301
- "extras": {}
302
- },
303
309
  {
304
310
  "id": "qwen3.6",
305
311
  "name": "Qwen3.6 35B-A3B",
@@ -324,6 +330,9 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
324
330
  "high",
325
331
  "max"
326
332
  ],
333
+ "thinkingLevelMap": {
334
+ "off": "none"
335
+ },
327
336
  "compat": {
328
337
  "supportsDeveloperRole": false,
329
338
  "supportsReasoningEffort": true,
@@ -332,7 +341,8 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
332
341
  "maxTokensField": "max_tokens"
333
342
  },
334
343
  "notes": [
335
- "compat matches the maintainer's working ~/.pi/agent/models.json LiteLLM config for api.nan.builders (2026-09-04): supportsDeveloperRole false, supportsReasoningEffort true, maxTokensField max_tokens. NaN's docs example sets only supportsDeveloperRole: true and is not battle-tested. supportsFinishReason true (2026-09-13, issue #2): the LiteLLM gateway intermittently closes SSE streams before emitting finish_reason; with true pi-ai raises 'Stream ended without finish_reason', which matches pi-ai's retryable-provider pattern ('ended without') and is retried automatically, whereas false silently synthesized stop/toolUse and stalled the turn mid-answer. supportsUsageInStreaming true (2026-09-16, issue #7): NaN's published schema is silent about stream_options, but the live gateway honors it — two identical streaming calls per model, differing only in stream_options: { include_usage: true }, returned 0 usage chunks without it and exactly 1 with it (prompt/completion/reasoning/cached token counts) on deepseek-v4-flash, glm5.3-flash, qwen3.6, mimo-v2.5 and gemma4, and a real pi session then recorded token counts where it recorded zeros. pi-ai only sends stream_options when this is not false, and the sanitizer forwards it when the model declares true, so chat models opt in by default and usage is reported (issue #4). A model that does not report streaming usage can still opt out per model with a models.json compat override (supportsUsageInStreaming: false); the sanitizer then strips stream_options and the payload stays strict."
344
+ "compat matches the maintainer's working ~/.pi/agent/models.json LiteLLM config for api.nan.builders (2026-09-04): supportsDeveloperRole false, supportsReasoningEffort true, maxTokensField max_tokens. NaN's docs example sets only supportsDeveloperRole: true and is not battle-tested. supportsFinishReason true (2026-09-13, issue #2): the LiteLLM gateway intermittently closes SSE streams before emitting finish_reason; with true pi-ai raises 'Stream ended without finish_reason', which matches pi-ai's retryable-provider pattern ('ended without') and is retried automatically, whereas false silently synthesized stop/toolUse and stalled the turn mid-answer. supportsUsageInStreaming true (2026-09-16, issue #7): NaN's published schema is silent about stream_options, but the live gateway honors it — two identical streaming calls per model, differing only in stream_options: { include_usage: true }, returned 0 usage chunks without it and exactly 1 with it (prompt/completion/reasoning/cached token counts) on deepseek-v4-flash, glm5.3-flash, qwen3.6, mimo-v2.5 and gemma4, and a real pi session then recorded token counts where it recorded zeros. pi-ai only sends stream_options when this is not false, and the sanitizer forwards it when the model declares true, so chat models opt in by default and usage is reported (issue #4). A model that does not report streaming usage can still opt out per model with a models.json compat override (supportsUsageInStreaming: false); the sanitizer then strips stream_options and the payload stays strict.",
345
+ "thinkingLevelMap { off: \"none\" } (issue #16): NaN docs state that with no reasoning_effort parameter every model uses its own default and that default is reasoning ON for qwen3.6 (16,384-token budget), while `none` and `minimal` skip the reasoning phase entirely (https://nan.builders/docs/models#controlling-reasoning, checked 2026-10-01). pi maps reasoning:\"off\" to reasoningEffort:undefined, so without this map `thinking: off` sent no parameter and the model reasoned anyway; the map sends reasoning_effort:\"none\" and actually disables reasoning."
336
346
  ],
337
347
  "extras": {
338
348
  "id": "qwen3.6",
@@ -343,7 +353,14 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
343
353
  "reasoning": true,
344
354
  "reasoning_options": [
345
355
  {
346
- "type": "toggle"
356
+ "type": "effort",
357
+ "values": [
358
+ "none",
359
+ "low",
360
+ "medium",
361
+ "high",
362
+ "max"
363
+ ]
347
364
  }
348
365
  ],
349
366
  "tool_call": true,
@@ -368,7 +385,8 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
368
385
  "cost": {
369
386
  "input": 0,
370
387
  "output": 0
371
- }
388
+ },
389
+ "canonical_model_id": "alibaba/qwen3.6-35b-a3b"
372
390
  }
373
391
  },
374
392
  {
@@ -428,7 +446,8 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
428
446
  "cost": {
429
447
  "input": 0,
430
448
  "output": 0
431
- }
449
+ },
450
+ "canonical_model_id": "alibaba/qwen3.8-flash"
432
451
  }
433
452
  }
434
453
  ];
@@ -436,15 +455,20 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
436
455
  export const GENERATED_CATALOG_META = {
437
456
  source: "https://models.dev/api.json",
438
457
  modelsDevProvider: "nan",
439
- fetchedAt: "2026-09-27T21:25:24.946Z",
440
- modelCount: 7,
441
- models: ["deepseek-v4-flash","gemma4","glm5.3-flash","mimo-v2.5","mimo-v2.6-flash","qwen3.6","qwen3.8-flash"],
458
+ fetchedAt: "2026-10-01T10:18:48.654Z",
459
+ modelCount: 6,
460
+ models: ["deepseek-v4-flash","gemma4","glm5.3-flash","mimo-v2.6-flash","qwen3.6","qwen3.8-flash"],
442
461
  notes: [
462
+ "non-chat: \"qwen-image-2.1\" excluded from the chat catalog (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))",
443
463
  "live-only: \"glm5.3\" kept out of the static catalog (premium-tier model (models.dev now documents it with 1M context / 131,072 max output; NaN docs https://nan.builders/docs/models + https://nan.builders/openapi.json, checked 2026-09-13) kept live-only so a non-premium key never sees a model it cannot call when the live /models fetch is unavailable; premium keys still get it via the /models refresh with conservative placeholder limits)",
444
464
  "provider-removed: \"glm5.2\" excluded from the catalog (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)",
465
+ "provider-removed: \"mimo-v2.5\" excluded from the catalog (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)",
445
466
  "compat matches the maintainer's working ~/.pi/agent/models.json LiteLLM config for api.nan.builders (2026-09-04): supportsDeveloperRole false, supportsReasoningEffort true, maxTokensField max_tokens. NaN's docs example sets only supportsDeveloperRole: true and is not battle-tested. supportsFinishReason true (2026-09-13, issue #2): the LiteLLM gateway intermittently closes SSE streams before emitting finish_reason; with true pi-ai raises 'Stream ended without finish_reason', which matches pi-ai's retryable-provider pattern ('ended without') and is retried automatically, whereas false silently synthesized stop/toolUse and stalled the turn mid-answer. supportsUsageInStreaming true (2026-09-16, issue #7): NaN's published schema is silent about stream_options, but the live gateway honors it — two identical streaming calls per model, differing only in stream_options: { include_usage: true }, returned 0 usage chunks without it and exactly 1 with it (prompt/completion/reasoning/cached token counts) on deepseek-v4-flash, glm5.3-flash, qwen3.6, mimo-v2.5 and gemma4, and a real pi session then recorded token counts where it recorded zeros. pi-ai only sends stream_options when this is not false, and the sanitizer forwards it when the model declares true, so chat models opt in by default and usage is reported (issue #4). A model that does not report streaming usage can still opt out per model with a models.json compat override (supportsUsageInStreaming: false); the sanitizer then strips stream_options and the payload stays strict.",
446
- "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).",
447
- "manual-only: \"mimo-v2.6-flash\" not yet on models.dev provider nan (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).).",
467
+ "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=[\"none\"] (issue #16, measured 2026-09-27 against live gateway, repro included in the issue): only \"none\" deterministically disables reasoning (0 reasoning tokens, answer produced). Every other value leaves 13-14K reasoning tokens and causes runaway reasoning (finish_reason:\"length\" with ZERO answer text). The catalog declares only [\"none\"] because the remaining values are useless noise. thinkingLevelMap { off: \"none\" } (issue #16, measured 2026-09-27): when pi maps reasoning:\"off\" to reasoningEffort:undefined, the thinkingLevelMap.off=\"none\" supplies the effort so the gateway receives reasoning_effort:\"none\" and does NOT enter runaway reasoning mode.",
468
+ "thinkingLevelMap { off: \"none\" } (issue #16): NaN docs state that with no reasoning_effort parameter gemma4 defaults to reasoning ON (16,384-token budget), while `none`/`minimal` skip reasoning entirely (https://nan.builders/docs/models#controlling-reasoning, checked 2026-10-01). pi maps reasoning:\"off\" to reasoningEffort:undefined, so without this map `thinking: off` sent no parameter and the model reasoned anyway; the map sends reasoning_effort:\"none\" and actually disables reasoning.",
469
+ "thinkingLevelMap { off: \"minimal\" } (issue #16, measured 2026-09-27 against live gateway, repro included in the issue): when pi maps reasoning:\"off\" to reasoningEffort:undefined, the thinkingLevelMap.off=\"minimal\" supplies the effort so the gateway receives reasoning_effort:\"minimal\" (38 reasoning tokens + answer). Note: \"none\" does NOT suppress reasoning on glm5.3-flash (13,382 tokens) — only \"minimal\" was measured to be effective. reasoning_effort_values includes \"minimal\" (not documented by NaN docs but accepted and effective by the gateway, issue #16).",
470
+ "manual-only: \"mimo-v2.6-flash\" skipped: models.dev provider \"nan\" now lists it, so the manual-only entry is not emitted and the models.dev values are used (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.)",
471
+ "thinkingLevelMap { off: \"none\" } (issue #16): NaN docs state that with no reasoning_effort parameter every model uses its own default and that default is reasoning ON for qwen3.6 (16,384-token budget), while `none` and `minimal` skip the reasoning phase entirely (https://nan.builders/docs/models#controlling-reasoning, checked 2026-10-01). pi maps reasoning:\"off\" to reasoningEffort:undefined, so without this map `thinking: off` sent no parameter and the model reasoned anyway; the map sends reasoning_effort:\"none\" and actually disables reasoning.",
448
472
  "contextWindow 262,144: the earlier 1,000,000 override (maintainer-confirmed 2026-09-05) was withdrawn 2026-09-07 — the updated https://nan.builders/docs/models still states '262K token context, the model's native window' and models.dev agrees at 262,144; NaN docs are treated as the most reliable source (maintainer instruction, 2026-09-07)."
449
473
  ],
450
474
  } as const;