@gtrabanco/pi-nan-provider 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -172,20 +172,43 @@ Since 1.0.0 the package version **aligns its MAJOR.MINOR with the pi release lin
172
172
  override (maintainer-confirmed 2026-09-05) was withdrawn 2026-09-07 — the
173
173
  updated NaN docs still say 262K "the model's native window" and models.dev
174
174
  agrees at 262,144.
175
+ - **Catalog authority order** (maintainer instruction, 2026-10-14):
176
+ 1. a **manual override**, as long as the docs have not changed since it was
177
+ written; 2. **https://nan.builders/docs/models** — the source of truth;
178
+ 3. **models.dev** (provider `nan`). When the docs change and an override
179
+ predates the change, the docs win, **but the override must be re-verified
180
+ against the live gateway first** — a measured override usually encodes a
181
+ real failure mode the docs may not describe. **When the live probe
182
+ disagrees with the docs, the measurement wins** (maintainer instruction,
183
+ 2026-10-14): the docs' `deepseek-v4-flash` "any value (no effect)" claim
184
+ was disproven by a re-probe, so the measured `none` behavior stayed.
185
+ models.dev is a
186
+ downstream mirror a human updates later than the docs or a Discord
187
+ announcement, so a divergence is never by itself proof the docs are
188
+ wrong. Every divergence or exclusion is recorded with provenance.
175
189
  - **Which models exist**: https://nan.builders/docs/models is the source of
176
190
  truth (maintainer instruction, reaffirmed 2026-09-29), cross-checked against
177
- the `model` param description in https://nan.builders/openapi.json; models.dev
178
- (provider `nan`) supplies the numeric limits for the ids it documents, and
179
- every divergence or exclusion is recorded with provenance.
180
- Official chat models (checked 2026-09-29): community `deepseek-v4-flash`,
191
+ the `model` param description in https://nan.builders/openapi.json. All seven
192
+ chat models declare 1M-token windows where the docs say so —
193
+ `deepseek-v4-flash`, `glm5.3-flash`, `qwen3.8-flash` and `mimo-v2.6-flash` at
194
+ **1,048,576**, `qwen3.6` and `gemma4` at **262,144** — and the first three
195
+ carry a `MANUAL_OVERRIDES.contextWindow` because models.dev lagged (rounded
196
+ 1,000,000 for the first two, stale 262,144 for `qwen3.8-flash`; docs checked
197
+ 2026-10-14).
198
+ Official chat models (checked 2026-10-14): community `deepseek-v4-flash`,
181
199
  `mimo-v2.6-flash`, `qwen3.8-flash`, `glm5.3-flash`, `qwen3.6`, `gemma4`
182
200
  (all text+image vision) + premium-tier `glm5.3`
183
- (~753B MoE, text-only input, 1M context, 400M tokens/rolling 4h window).
184
- glm5.3 is now documented by models.dev too (1M context / 131,072 max output,
185
- checked 2026-09-13) but stays out of the static catalog via
186
- `LIVE_ONLY_MODEL_IDS` (premium tier) so a non-premium key never sees a
187
- model it cannot call when the live `/models` fetch is unavailable; premium
188
- keys still receive it live with conservative placeholder limits.
201
+ (~753B MoE, multimodal text+image+audio+video input, 1M context, 400M
202
+ tokens/rolling 4h window).
203
+ glm5.3 stays out of the static catalog via `LIVE_ONLY_MODEL_IDS` (premium
204
+ tier) so a non-premium key never sees a model it cannot call when the live
205
+ `/models` fetch is unavailable. Premium keys receive it live with its
206
+ DOCUMENTED capabilities, not a blind placeholder: `LIVE_ONLY_MODEL_DATA` in
207
+ `src/fetch-models.ts` (1,048,576 / 131,072, text+image, `thinkingLevelMap
208
+ { off: "low" }`) is applied both by `mergeLiveWithGenerated` and by
209
+ `buildOverlayMap`, so a stored `glm5.3` entry is also corrected. The store
210
+ still decides which ids exist; the table only decides their capabilities
211
+ (issue #19's invariant is intact).
189
212
  Removed by the provider: `glm5.2` (2026-09-05) and `mimo-v2.5` (checked
190
213
  2026-09-29 — zero mentions in the docs and in openapi.json, and models.dev
191
214
  dropped it too; superseded by `mimo-v2.6-flash`). models.dev may re-list a
@@ -251,8 +274,8 @@ Since 1.0.0 the package version **aligns its MAJOR.MINOR with the pi release lin
251
274
  Server names and tool IDs:
252
275
  - `nan-search` → tools: `mcp__nan_search__web_search`
253
276
  (official remote, HTTP, `Authorization: Bearer <key>` header)
254
- - `nan-media` → tools: `mcp__nan_media__generate_image/edit_image/text_to_speech/list_voices/speech_to_text/list_models/embed_text/rerank_documents`
255
- (community stdio, `npx -y nan-mcp-server@1.1.2`, 120s timeout; 8 tools as of 1.1.2)
277
+ - `nan-media` → tools: `mcp__nan_media__generate_image/edit_image/text_to_speech/list_voices/speech_to_text/list_models/embed_text/rerank_documents/get_quota`
278
+ (community stdio, `npx -y nan-mcp-server@1.3.0`, 120s timeout; 9 tools as of 1.3.0)
256
279
  - `exposure: "direct"` on both — tools are declared to the model.
257
280
  - User `mcp.json` entries with the same name take precedence.
258
281
  - `/nan-mcp enable/disable` calls `pi.registerMcpServer()` / `pi.unregisterMcpServer()` directly.
@@ -271,10 +294,10 @@ Since 1.0.0 the package version **aligns its MAJOR.MINOR with the pi release lin
271
294
  key, shared rate limit/quota). Spec: https://nan.builders/openapi.json
272
295
  (tag "MCP"). Currently exposes `web_search`.
273
296
  - Community `nan-mcp-server` (https://github.com/luciferfran/nan-mcp-server):
274
- stdio MCP server, version-pinned via NAN_MEDIA_MCP_VERSION (default 1.1.2)
275
- or full command override via NAN_MEDIA_MCP_COMMAND. Tools (1.1.2): generate_image,
297
+ stdio MCP server, version-pinned via NAN_MEDIA_MCP_VERSION (default 1.3.0)
298
+ or full command override via NAN_MEDIA_MCP_COMMAND. Tools (1.3.0): generate_image,
276
299
  edit_image, text_to_speech, list_voices, speech_to_text, list_models, embed_text,
277
- rerank_documents.
300
+ rerank_documents, get_quota.
278
301
  - Automated check (scripts/check-nan-mcp-server.ts) compares the npm registry
279
302
  against the pin weekly and files a `dependencies` issue when a newer release
280
303
  exists, with a breaking/safe verdict from the live server tool surface.
@@ -293,11 +316,48 @@ Since 1.0.0 the package version **aligns its MAJOR.MINOR with the pi release lin
293
316
  `mcp_servers_change` so pi's report stays silent, and warns once per session;
294
317
  `/nan-mcp enable` persists the toggle without registering. Regression tests:
295
318
  `test/mcp-host-connector.test.ts`.
296
- - **pi 1.0.0 / pi-ai 1.0.0 compat** (verified 2026-10-01): the peer range is now
297
- `">=0.99.0 <2"` (widened from `">=0.99.0 <1"`); the pi-ai 1.0.0 index/compat/types
298
- type surfaces are identical to 0.99.1 (the release only adds a `./models` subpath,
299
- Anthropic federation env consts, and a z.ai CN overflow pattern); the package's
300
- tests and typecheck pass against 1.0.0 unchanged.
319
+ - **`deepseek-v4-flash` reasoning_effort: the docs are stale, the measurement
320
+ wins** (re-probed live 2026-10-14). The docs claim the parameter is "any value
321
+ (no effect)"; a three-model probe disproves it — `none` gave **0** reasoning
322
+ tokens with a full answer on `deepseek-v4-flash`, `mimo-v2.6-flash` and
323
+ `qwen3.6`, while `low` gave 57 / 22 / 951 and omitting the parameter 63 / 17 /
324
+ 905 (`minimal` on deepseek gave 109). So `none` disables reasoning and every
325
+ other value leaves it on. The catalog keeps `reasoningEffortValues: ["none"]`
326
+ and `thinkingLevelMap { off: "none" }`, and `mimo-v2.6-flash` — which the docs
327
+ called "depth not adjustable" — gains `thinkingLevelMap { off: "none" }` for
328
+ the same reason. `qwen3.8-flash` stays unmapped: only `none` was sampled for
329
+ it, so no other level is mapped. Probe: short prompt, `max_tokens: 2048`,
330
+ streaming with `include_usage`; 10 requests, under 50k tokens.
331
+ - **Reasoning-only stream limit** (https://nan.builders/docs/models
332
+ #reasoning-only-stream-limit, checked 2026-10-14): NaN closes a turn that
333
+ produced only reasoning once it passes 45,000 reasoning characters, or after
334
+ 420 s without visible output (600 s once the turn has demonstrably exceeded
335
+ that character count); the character budget also scales with prompt size
336
+ (~150,000 headroom + half the prompt, capped at 300,000). The cut is
337
+ `finish_reason: "length"` on an empty delta plus a final usage chunk marked
338
+ `estimated: true`, `billed: false` and carrying
339
+ `nan_truncation: { reason: "reasoning_only_stream" }`.
340
+ `src/reasoning-only-stream-guard.ts` (issue #21) keys on that marker;
341
+ earlier comment/message text said "60,000 characters" and was corrected to
342
+ the documented figures.
343
+ - **pi 1.2.0 / pi-ai 1.2.0 compat** (verified 2026-10-14, the current verified
344
+ baseline): the peer range stays `">=0.99.0 <2"` (1.2.0 is inside it). The pi-ai
345
+ 1.2.0 index/compat type surface is byte-identical to 1.0.0 (`index.d.ts` diff is
346
+ empty); the only additions are `KnownProvider` gaining `"azure"` (renamed from
347
+ `"azure-openai-responses"` in pi 1.0.3 — not used here), a new
348
+ `"openai-decisions"` classifier API, `SamplingParams`/`SamplingParamsByThinkingLevel`
349
+ type aliases (fields pi-ai already honored; `models.json`-only), and a new
350
+ optional `RefreshModelsContext.baseUrl`. `ThinkingLevelMap`, `ImageModel`,
351
+ `ImageApi`, `OpenAICompletionsCompat`, `createProvider`, `envApiKeyAuth`,
352
+ `registerMcpServer`/`unregisterMcpServer` and `registerVirtualModel` are all
353
+ unchanged, and the `thinkingLevelMap.off` branch in `buildRequest` still fires
354
+ at `api/openai-completions.js` ~line 719. The package's tests (328) and
355
+ typecheck pass against 1.2.0 unchanged; the upgrade is devDependency-only.
356
+ - **pi 1.0.0 / pi-ai 1.0.0 compat** (verified 2026-10-01): the peer range was
357
+ widened to `">=0.99.0 <2"` (from `">=0.99.0 <1"`); the pi-ai 1.0.0
358
+ index/compat/types type surfaces are identical to 0.99.1 (the release only adds
359
+ a `./models` subpath, Anthropic federation env consts, and a z.ai CN overflow
360
+ pattern); the package's tests and typecheck pass against 1.0.0 unchanged.
301
361
  - **Native image models** (verified 2026-10-01): pi 1.0 lets a provider register
302
362
  `ImageModel` entries (`type: "image"`) plus a `Provider.images` implementation map
303
363
  keyed by a custom `ImageApi` id (`"nan-images"`); codemode reaches them with
package/README.es.md CHANGED
@@ -195,6 +195,7 @@ conector con `pi config` → Built-in extensions → `mcp`.
195
195
  | `mcp__nan_media__list_models` | Listar los ids de modelos que alcanza la key |
196
196
  | `mcp__nan_media__embed_text` | Embeddings de texto (qwen3-embedding) |
197
197
  | `mcp__nan_media__rerank_documents` | Reordenación de documentos (Qwen3-Reranker) |
198
+ | `mcp__nan_media__get_quota` | Cuota de tokens por modelo (usado / tope / restante / reinicio) |
198
199
 
199
200
  > pi 0.99.0–0.99.1 usaba las formas con guiones (`mcp__nan-search__web_search`,
200
201
  > `mcp__nan-media__*`); pi 0.99.2+ sanitiza los nombres de herramientas a solo
@@ -206,7 +207,7 @@ conector con `pi config` → Built-in extensions → `mcp`.
206
207
  | Variable | Por defecto | Descripción |
207
208
  | :--- | :--- | :--- |
208
209
  | `NAN_MEDIA_MCP` | — | Override por sesión (`0` o `false` para desactivar). |
209
- | `NAN_MEDIA_MCP_VERSION` | `1.1.2` | Versión del servidor fijada (recomendado). |
210
+ | `NAN_MEDIA_MCP_VERSION` | `1.3.0` | Versión del servidor fijada (recomendado). |
210
211
  | `NAN_MEDIA_MCP_COMMAND` | — | Override del comando personalizado. |
211
212
  | `NAN_MEDIA_MCP_TIMEOUT_MS` | `120000` | Timeout por llamada. |
212
213
  | `NAN_MCP_TOOLS` | — | Override para el puente oficial (`0` para desactivar). |
@@ -330,10 +331,14 @@ Modelos de chat:
330
331
  | :--- | :--- | :--- | :--- | :---: |
331
332
  | `qwen3.6` | 262,144 | 65,536 | texto, imagen | ✅ |
332
333
  | `gemma4` | 262,144 | 32,768 | texto, imagen | ✅ |
333
- | `deepseek-v4-flash` | 1,000,000 | 384,000 | texto, imagen | ✅ |
334
+ | `deepseek-v4-flash` | 1,048,576 | 32,768* | texto, imagen | ✅ |
334
335
  | `mimo-v2.6-flash` | 1,048,576 | 131,072 | texto, imagen | ✅ |
335
- | `glm5.3-flash` | 1,000,000 | 131,072 | texto, imagen | ✅ |
336
- | `qwen3.8-flash` | 262,144 | 131,072 | texto, imagen | ✅ |
336
+ | `glm5.3-flash` | 1,048,576 | 131,072 | texto, imagen | ✅ |
337
+ | `qwen3.8-flash` | 1,048,576 | 131,072 | texto, imagen | ✅ |
338
+
339
+ > Las ventanas de la tabla son las **documentadas** en [los docs de NaN](https://nan.builders/docs/models) (comprobado 2026-10-14), que son la fuente de verdad; models.dev es un espejo que un humano actualiza más tarde y puede ir por detrás. `glm5.3` (tier premium, fuera de la tabla estática) es 1,048,576 / 131,072 con entrada texto+imagen cuando una clave premium lo descubre en vivo.
340
+ >
341
+ > `*` El límite de salida de 32,768 en `deepseek-v4-flash` es el techo de los docs oficiales de NaN para pi (https://nan.builders/docs/pi, "the same set the NaN CLI writes", issue #20, 2026-10-02); models.dev sigue listando 384,000, pero la pasarela rechaza `max_tokens` por encima de ~256k con 403.
337
342
 
338
343
  > [!NOTE]
339
344
  > **Techo práctico con caché de prompt fría (issue #18).** Las ventanas de contexto de la tabla son las ventanas reales de los modelos, pero el borde de api.nan.builders (Cloudflare) aborta las respuestas que superan su Proxy Read Timeout de 120 s. Medido con peticiones frías directas (relleno único, sin caché de prompt; 2026-10-01): 208.036 tokens (~960 KB de cuerpo) respondió en 12,9 s mientras que ~220k y ~286k tokens devolvieron **HTTP 524 `origin_response_timeout`** a los ~126 s; los mismos tamaños con caché cálida responden en ~10 s. El techo práctico para peticiones frías queda por tanto muy por debajo de las ventanas declaradas. Desde 1.0.0 el provider reclasifica un 524 en una petición estimada por encima de 200.000 tokens (`NAN_COLD_CACHE_CEILING_TOKENS`) como desbordamiento de contexto para que pi compacte y reintente en vez de quedarse colgado; un 524 en una petición menor se deja intacto (timeout transitorio genuino).
@@ -363,11 +368,12 @@ El parámetro `reasoning_effort` de NaN controla cuánto piensa el modelo antes
363
368
  | :--- | :--- | :--- |
364
369
  | `glm5.3`, `glm5.3-flash` | `minimal` · `low` · `medium` · `high` · `max` | Completamente controlable — valores más altos permiten al modelo razonar más; `minimal` produce ~38 tokens de razonamiento (issue #16, medido 2026-09-27); `none` NO suprime el razonamiento en glm5.3-flash (13,382 tokens) |
365
370
  | `qwen3.6`, `gemma4` | `none` · `minimal` · `low` · `medium` · `high` · `max` | `none`/`minimal` omiten el razonamiento por completo; otros valores limitan a 2K / 8K / 16K / 32K tokens. `thinking: "off"` envía `reasoning_effort: "none"` automáticamente vía el `thinkingLevelMap` del catálogo — los docs de NaN dicen que sin parámetro estos modelos razonan por defecto (presupuesto de 16,384 tokens), así que `off` debe ser explícito |
366
- | `deepseek-v4-flash` | `none` · `minimal` · `low` · `medium` · `high` · `max` (el catálogo declara ["none"]) | `none` deshabilita el razonamiento de forma fiable (0 tokens de razonamiento, medido 2026-09-27); cualquier otro valor deja 13-14K tokens de razonamiento y causa razonamiento descontrolado (finish_reason:"length" con CERO texto de respuesta) — el catálogo solo declara `none` porque el resto es ruido. `thinking: "off"` ahora envía `reasoning_effort: "none"` automáticamente vía el `thinkingLevelMap` del catálogo |
367
- | `qwen3.8-flash`, `mimo-v2.6-flash` | *(aceptado pero no ajustable)* | El parámetro es aceptado y nunca rechazado, pero el modelo gestiona su propia profundidad de razonamiento — nunca es un error enviar un valor que estos modelos no ajustan |
371
+ | `deepseek-v4-flash` | `none` (el catálogo declara ["none"]) | `none` deshabilita el razonamiento (0 tokens de razonamiento, respuesta completa); cualquier otro valor — incluido `minimal` y omitir el parámetro — lo deja ENCENDIDO (57 / 63 / 109 tokens de razonamiento), así que el catálogo solo declara `none`. Re-sondado en vivo 2026-10-14: los docs de NaN afirman "any value (no effect)", pero gana la medición. `thinking: "off"` envía `reasoning_effort: "none"` automáticamente vía el `thinkingLevelMap` del catálogo — sin él el modelo se descontrola hasta `finish_reason: "length"` sin respuesta (issue #16) |
372
+ | `qwen3.8-flash` | *(aceptado pero no ajustable)* | El parámetro es aceptado y nunca rechazado, pero el modelo gestiona su propia profundidad de razonamiento — nunca es un error enviar un valor que este modelo no ajusta |
373
+ | `mimo-v2.6-flash` | `none` | Los docs lo llamaban "profundidad no ajustable", pero el gateway en vivo (2026-10-14) muestra que `none` da 0 tokens de razonamiento con respuesta completa mientras `low` da 22 y omitirlo 17 — así que `thinking: "off"` envía `reasoning_effort: "none"` vía el `thinkingLevelMap` del catálogo |
368
374
 
369
375
  > [!NOTE]
370
- > `maxTokens` NO limita la fase de razonamiento en los modelos de NaN. La fase de razonamiento corre hasta completarse (o se trunca a ~60,000 caracteres para deepseek-v4-flash) independientemente de `maxTokens` — medido: `max_tokens=512` en deepseek-v4-flash aún consumió 13,376 tokens de completación, todos de razonamiento (26x el techo). Este es un comportamiento del gateway confirmado por el issue #16 (medido 2026-09-27).
376
+ > `maxTokens` NO limita la fase de razonamiento en los modelos de NaN. La fase de razonamiento corre hasta completarse (o se corta en el límite de stream solo-razonamiento de NaN: 45.000 caracteres de razonamiento, o 420 s sin salida visible) independientemente de `maxTokens` — medido: `max_tokens=512` en deepseek-v4-flash aún consumió 13,376 tokens de completación, todos de razonamiento (26x el techo). Este es un comportamiento del gateway confirmado por el issue #16 (medido 2026-09-27).
371
377
 
372
378
  ## 🚀 Desarrollo
373
379
 
package/README.md CHANGED
@@ -192,6 +192,7 @@ registering a server that can never connect. To get MCP in the CLI, enable the c
192
192
  | `mcp__nan_media__list_models` | List the NaN model ids the key can reach |
193
193
  | `mcp__nan_media__embed_text` | Text embeddings (qwen3-embedding) |
194
194
  | `mcp__nan_media__rerank_documents` | Document reranking (Qwen3-Reranker) |
195
+ | `mcp__nan_media__get_quota` | Per-model token quota (used / cap / remaining / reset) |
195
196
 
196
197
  > pi 0.99.0–0.99.1 used the hyphenated forms (`mcp__nan-search__web_search`,
197
198
  > `mcp__nan-media__*`); pi 0.99.2+ sanitizes tool names to `[A-Za-z0-9_]` only,
@@ -203,7 +204,7 @@ registering a server that can never connect. To get MCP in the CLI, enable the c
203
204
  | Variable | Default | Description |
204
205
  | :--- | :--- | :--- |
205
206
  | `NAN_MEDIA_MCP` | — | Per-session override (`0` or `false` to disable). |
206
- | `NAN_MEDIA_MCP_VERSION` | `1.1.2` | Pinned server version (recommended). |
207
+ | `NAN_MEDIA_MCP_VERSION` | `1.3.0` | Pinned server version (recommended). |
207
208
  | `NAN_MEDIA_MCP_COMMAND` | — | Custom command override. |
208
209
  | `NAN_MEDIA_MCP_TIMEOUT_MS` | `120000` | Per-call timeout. |
209
210
  | `NAN_MCP_TOOLS` | — | Override for the official bridge (`0` to disable). |
@@ -327,12 +328,14 @@ Chat models:
327
328
  | :--- | :--- | :--- | :--- | :---: |
328
329
  | `qwen3.6` | 262,144 | 65,536 | text, image | ✅ |
329
330
  | `gemma4` | 262,144 | 32,768 | text, image | ✅ |
330
- | `deepseek-v4-flash` | 1,000,000 | 32,768* | text, image | ✅ |
331
+ | `deepseek-v4-flash` | 1,048,576 | 32,768* | text, image | ✅ |
331
332
  | `mimo-v2.6-flash` | 1,048,576 | 131,072 | text, image | ✅ |
332
- | `glm5.3-flash` | 1,000,000 | 131,072 | text, image | ✅ |
333
- | `qwen3.8-flash` | 262,144 | 131,072 | text, image | ✅ |
333
+ | `glm5.3-flash` | 1,048,576 | 131,072 | text, image | ✅ |
334
+ | `qwen3.8-flash` | 1,048,576 | 131,072 | text, image | ✅ |
334
335
 
335
336
  > [!NOTE]
337
+ > Context windows above are the **documented** windows from [the NaN docs](https://nan.builders/docs/models) (checked 2026-10-14), which are the source of truth; models.dev is a downstream mirror a human updates later and may lag. `glm5.3` (premium tier, not in the static table above) is 1,048,576 / 131,072 with text+image input when a premium key discovers it live.
338
+ >
336
339
  > `*` deepseek-v4-flash max output 32,768 is the ceiling from the official NaN pi docs (https://nan.builders/docs/pi, "the same set the NaN CLI writes", issue #20, 2026-10-02); models.dev still lists 384,000 but the gateway rejects `max_tokens` above ~256k with 403.
337
340
  >
338
341
  > **Cold prompt cache practical ceiling (issue #18).** The context windows above are the models' real windows, but the api.nan.builders edge (Cloudflare) aborts responses past its 120 s Proxy Read Timeout. Measured with direct cold requests (unique filler, no prompt cache; 2026-10-01): 208,036 tokens (~960 KB body) answered in 12.9 s while ~220k and ~286k tokens returned **HTTP 524 `origin_response_timeout`** at ~126 s; the same sizes with a warm prompt cache answer in ~10 s. The practical ceiling for cold requests is therefore well below the declared windows. Since 1.0.0 the provider reclassifies a 524 on an estimated over-200,000-token request (`NAN_COLD_CACHE_CEILING_TOKENS`) as a context overflow so pi compacts and retries instead of wedging; a 524 on a smaller request is left alone (genuine transient timeout).
@@ -360,10 +363,11 @@ NaN's `reasoning_effort` parameter controls how much the model thinks before ans
360
363
  | :--- | :--- | :--- |
361
364
  | `glm5.3`, `glm5.3-flash` | `minimal` · `low` · `medium` · `high` · `max` | Fully controllable — higher values let the model reason longer; `minimal` yields ~38 reasoning tokens (issue #16, measured 2026-09-27); `none` does NOT suppress reasoning on glm5.3-flash (13,382 tokens) |
362
365
  | `qwen3.6`, `gemma4` | `none` · `minimal` · `low` · `medium` · `high` · `max` | `none`/`minimal` skip reasoning entirely; others cap at 2K / 8K / 16K / 32K tokens. `thinking: "off"` sends `reasoning_effort: "none"` automatically via the catalog's `thinkingLevelMap` — the NaN docs state that with no parameter these models reason by default (16,384-token budget), so off must be explicit |
363
- | `deepseek-v4-flash` | `none` · `minimal` · `low` · `medium` · `high` · `max` (catalog declares ["none"]) | `none` reliably disables reasoning (0 reasoning tokens, measured 2026-09-27); every other value leaves 13-14K reasoning tokens and causes runaway reasoning (finish_reason:"length" with ZERO answer text) — the catalog only declares `none` because the rest are useless. `thinking: "off"` now sends `reasoning_effort: "none"` automatically via the catalog's `thinkingLevelMap` |
364
- | `qwen3.8-flash`, `mimo-v2.6-flash` | *(accepted but not adjustable)* | The parameter is accepted and never rejected, but the model manages its own reasoning depth — it is never an error to send a value these models don't adjust |
366
+ | `deepseek-v4-flash` | `none` (catalog declares ["none"]) | `none` disables reasoning (0 reasoning tokens, a full answer); every other value — including `minimal` and omitting the parameter — leaves it ON (57 / 63 / 109 reasoning tokens), so the catalog declares only `none`. Re-probed live 2026-10-14: the NaN docs claim "any value (no effect)", but the measurement wins. `thinking: "off"` sends `reasoning_effort: "none"` automatically via the catalog's `thinkingLevelMap` — without it the model runs away to `finish_reason: "length"` with no answer (issue #16) |
367
+ | `qwen3.8-flash` | *(accepted but not adjustable)* | The parameter is accepted and never rejected, but the model manages its own reasoning depth — it is never an error to send a value this model doesn't adjust |
368
+ | `mimo-v2.6-flash` | `none` | The docs called this "depth not adjustable", but the live gateway (2026-10-14) shows `none` gives 0 reasoning tokens with a full answer while `low` gives 22 and omitting the parameter 17 — so `thinking: "off"` sends `reasoning_effort: "none"` via the catalog's `thinkingLevelMap` |
365
369
  > [!NOTE]
366
- > `maxTokens` does NOT bound the reasoning phase on NaN models. The reasoning phase runs to completion (or truncates at ~60,000 chars for deepseek-v4-flash) regardless of `maxTokens` — measured: `max_tokens=512` on deepseek-v4-flash still consumed 13,376 completion tokens, all reasoning (26x the ceiling). This is gateway behavior confirmed by issue #16 (measured 2026-09-27).
370
+ > `maxTokens` does NOT bound the reasoning phase on NaN models. The reasoning phase runs to completion (or is cut at NaN's reasoning-only stream limit: 45,000 reasoning characters, or 420 s without visible output) regardless of `maxTokens` — measured: `max_tokens=512` on deepseek-v4-flash still consumed 13,376 completion tokens, all reasoning (26x the ceiling). This is gateway behavior confirmed by issue #16 (measured 2026-09-27).
367
371
 
368
372
 
369
373
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gtrabanco/pi-nan-provider",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "description": "NaN Builders (api.nan.builders) model provider for pi - OpenAI-compatible registration with a models.dev-generated fallback, tier-aware live catalog, and MCP bridges (official web search + optional community media server)",
5
5
  "keywords": [
6
6
  "pi",
@@ -46,8 +46,8 @@
46
46
  "@earendil-works/pi-coding-agent": ">=0.99.0 <2"
47
47
  },
48
48
  "devDependencies": {
49
- "@earendil-works/pi-ai": "1.0.0",
50
- "@earendil-works/pi-coding-agent": "1.0.0",
49
+ "@earendil-works/pi-ai": "1.2.0",
50
+ "@earendil-works/pi-coding-agent": "1.2.0",
51
51
  "@types/bun": "1.4",
52
52
  "typescript": "7"
53
53
  },
@@ -318,7 +318,7 @@ export function renderIssueBody(report: NanMcpServerCheckReport): string {
318
318
  }
319
319
  lines.push("### To bump");
320
320
  lines.push(
321
- `1. Edit \`DEFAULT_NAN_MEDIA_MCP_VERSION\` in \`src/mcp/nan-media.ts\` from \`${report.pinnedVersion}\` to \`${report.latestVersion}\`.`,
321
+ `1. Edit \`DEFAULT_NAN_MEDIA_MCP_VERSION\` in \`src/mcp/media-server.ts\` from \`${report.pinnedVersion}\` to \`${report.latestVersion}\`.`,
322
322
  );
323
323
  lines.push("2. Run \`bun test && bun run typecheck\`.");
324
324
  lines.push(
@@ -79,12 +79,10 @@ const PI_SUPPORTED_INPUT = new Set(["text", "image"]);
79
79
  * (shared with test/generated-catalog.test.ts, which pins that every
80
80
  * override lands on the generated entry with its provenance note).
81
81
  * Capability divergences from models.dev are recorded there, never here.
82
- * MANUAL_NOTES adds provenance-only notes to otherwise-untouched entries.
82
+ * MANUAL_NOTES adds provenance-only notes to otherwise-untouched entries
83
+ * (currently empty; per-model corrections live in MANUAL_OVERRIDES).
83
84
  */
84
- const MANUAL_NOTES: Record<string, string> = {
85
- "qwen3.8-flash":
86
- "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).",
87
- };
85
+ const MANUAL_NOTES: Record<string, string> = {};
88
86
 
89
87
  /**
90
88
  * Models the provider has removed but that models.dev may still list.
@@ -58,19 +58,29 @@ export interface ManualModelOverride {
58
58
  * and appear in GeneratedModelEntry.reasoningEffortValues, where the pi
59
59
  * model-selector can read them to show the correct granularity.
60
60
  *
61
- * Per-model contract from the docs:
61
+ * Per-model contract from the docs, corrected by live measurement where the
62
+ * two disagree (maintainer instruction: the measurement wins over the docs):
62
63
  * - glm5.3, glm5.3-flash: fully controllable (low/medium/high/max)
63
64
  * + minimal also accepted (issue #16, measured 2026-09-27): minimal
64
65
  * yields 38 reasoning tokens vs 2,288 for high — the docs simply
65
66
  * omitted it because it was not originally advertised, but it IS
66
67
  * accepted and effective by the gateway.
67
68
  * - qwen3.6, gemma4: none/minimal skip reasoning, others cap depth
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
69
+ * (2,048 / 8,192 / 16,384 / 32,768).
70
+ * - deepseek-v4-flash: `none` reproducibly disables reasoning (0 reasoning
71
+ * tokens, full answer); every other value — `minimal`, `low`, `high` and
72
+ * omitting the parameter — leaves it ON (issue #16, 2026-09-27,
73
+ * re-confirmed live 2026-10-14: none 0 / low 57 / omitted 63 reasoning
74
+ * tokens). The docs say "any value (no effect)"; the measurement wins, so
75
+ * the catalog declares only ["none"].
76
+ * - mimo-v2.6-flash: the docs list it as "accepted, depth not adjustable",
77
+ * but the live re-probe (2026-10-14) shows `none` → 0 reasoning tokens
78
+ * while `low` → 22 and omission → 17. Adjustable at the off end, so it
79
+ * carries `thinkingLevelMap { off: "none" }`.
80
+ * - qwen3.8-flash: the docs say "accepted, depth not adjustable"; the live
81
+ * probe measured 0 reasoning tokens for `none` (2026-10-14) but no other
82
+ * value was sampled, so no `thinkingLevelMap` is declared — the docs state
83
+ * its default is reasoning ON, so `off` sends nothing and the model decides.
74
84
  *
75
85
  * (mimo-v2.5, which used to share that last row, was removed by NaN — absent
76
86
  * from the docs and from openapi.json, checked 2026-09-29.)
@@ -91,7 +101,7 @@ export const REASONING_EFFORT_VALUES: Record<string, string[]> = {
91
101
  * sides assert the exact same string.
92
102
  */
93
103
  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.";
104
+ "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. 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
105
 
96
106
  /**
97
107
  * Models NaN serves that models.dev provider nan may not list. Mirrors the
@@ -139,26 +149,40 @@ export const MANUAL_ONLY_MODELS: Record<string, ManualModelOverride> = {
139
149
  * docs are explicit about both the default and the `none` behavior.
140
150
  */
141
151
  export const MANUAL_OVERRIDES: Record<string, ManualModelOverride> = {
152
+ "qwen3.8-flash": {
153
+ contextWindow: 1_048_576,
154
+ note:
155
+ "contextWindow 1,048,576: NaN docs state '1M token context (1,048,576 tokens)' and 'Max answer 131K tokens' (https://nan.builders/docs/models, checked 2026-10-14) — models.dev still lists 262,144. The earlier override was withdrawn 2026-09-07 when the docs read '262K token context, the model's native window'; the docs now state 1M and they are the source of truth (maintainer instruction: docs win over models.dev, which a user updates later). maxTokens stays 131,072, which models.dev already matches and the docs confirm as the 131K max answer.",
156
+ },
142
157
  "deepseek-v4-flash": {
143
158
  input: ["text", "image"],
159
+ contextWindow: 1_048_576,
144
160
  maxTokens: 32_768,
145
161
  reasoningEffortValues: ["none"],
146
162
  thinkingLevelMap: { off: "none" },
147
163
  note:
148
- "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. " +
164
+ "contextWindow 1,048,576: NaN docs state '1M token context' for this model (https://nan.builders/docs/models, checked 2026-10-14). models.dev lists 1,000,000, a rounded form of the same window; the docs value is authoritative (maintainer instruction: docs win over models.dev, which a user updates later). " +
165
+ "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), so this stays a pin for the vision capability rather than a divergence. " +
149
166
  "maxTokens 32768 per the official NaN pi docs (https://nan.builders/docs/pi, 'the same set the NaN CLI writes', checked 2026-10-02) closing issue #20 — the gateway now rejects max_tokens above ~256k for this model with 403 permission_error (measured 2026-10-02: 256,000 → 200; 262,143 → 403), and the previous models.dev-derived 384000 made every pi request fail since pi-ai sends max_tokens = model.maxTokens when no cap is set. models.dev still lists 384000, so this override pins the documented value. " +
150
167
  "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. " +
168
+ "docs vs measurement (re-probed live 2026-10-14): https://nan.builders/docs/models#controlling-reasoning documents the parameter as 'any value (no effect) — the model decides per request', but the live gateway contradicts that: `none` gave 0 reasoning tokens / 0 reasoning chars / a 134-char answer, while `low` gave 57 reasoning tokens and omitting the parameter gave 63 (`minimal` gave 109). The docs are stale here; the measurement wins (maintainer instruction), so the measured behavior is kept. " +
151
169
  "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.",
152
170
  },
153
171
  "glm5.3-flash": {
172
+ contextWindow: 1_048_576,
154
173
  thinkingLevelMap: { off: "minimal" },
155
174
  note:
175
+ "contextWindow 1,048,576: NaN docs state '1M token context' for this model (https://nan.builders/docs/models, checked 2026-10-14). models.dev lists 1,000,000, a rounded form of the same window; the docs value is authoritative (maintainer instruction: docs win over models.dev, which a user updates later). " +
156
176
  "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. " +
157
- "reasoning_effort_values includes \"minimal\" (not documented by NaN docs but accepted and effective by the gateway, issue #16).",
177
+ "reasoning_effort_values includes \"minimal\" (the docs' glm row lists low/medium/high/max, but the 2026-09-27 measurement shows minimal is accepted and effective, issue #16).",
158
178
  },
159
- "qwen3.6": {
179
+ "mimo-v2.6-flash": {
160
180
  thinkingLevelMap: { off: "none" },
161
181
  note:
182
+ "thinkingLevelMap { off: \"none\" } (live re-probe 2026-10-14): the docs list this model as 'accepted, depth not adjustable', but the gateway makes the parameter effective at the off end — `none` gave 0 reasoning tokens / a full answer, while `low` gave 22 reasoning tokens and omitting the parameter gave 17. Measured behavior wins over the docs, so `thinking: off` now sends reasoning_effort:\"none\" and actually disables reasoning. No other level was sampled, so only `off` is mapped.",
183
+ },
184
+ "qwen3.6": {
185
+ thinkingLevelMap: { off: "none" }, note:
162
186
  "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.",
163
187
  },
164
188
  "gemma4": {
@@ -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-10-02T22:53:40.291Z
4
+ // Source: https://models.dev/api.json (provider "nan"), fetched 2026-10-14T15:59:01.497Z
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.
@@ -26,7 +26,7 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
26
26
  "cacheRead": 0,
27
27
  "cacheWrite": 0
28
28
  },
29
- "contextWindow": 1000000,
29
+ "contextWindow": 1048576,
30
30
  "maxTokens": 32768,
31
31
  "reasoningEffortValues": [
32
32
  "none"
@@ -43,7 +43,7 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
43
43
  },
44
44
  "notes": [
45
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.",
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. maxTokens 32768 per the official NaN pi docs (https://nan.builders/docs/pi, 'the same set the NaN CLI writes', checked 2026-10-02) closing issue #20 — the gateway now rejects max_tokens above ~256k for this model with 403 permission_error (measured 2026-10-02: 256,000 → 200; 262,143 → 403), and the previous models.dev-derived 384000 made every pi request fail since pi-ai sends max_tokens = model.maxTokens when no cap is set. models.dev still lists 384000, so this override pins the documented value. 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."
46
+ "contextWindow 1,048,576: NaN docs state '1M token context' for this model (https://nan.builders/docs/models, checked 2026-10-14). models.dev lists 1,000,000, a rounded form of the same window; the docs value is authoritative (maintainer instruction: docs win over models.dev, which a user updates later). 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), so this stays a pin for the vision capability rather than a divergence. maxTokens 32768 per the official NaN pi docs (https://nan.builders/docs/pi, 'the same set the NaN CLI writes', checked 2026-10-02) closing issue #20 — the gateway now rejects max_tokens above ~256k for this model with 403 permission_error (measured 2026-10-02: 256,000 → 200; 262,143 → 403), and the previous models.dev-derived 384000 made every pi request fail since pi-ai sends max_tokens = model.maxTokens when no cap is set. models.dev still lists 384000, so this override pins the documented value. 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. docs vs measurement (re-probed live 2026-10-14): https://nan.builders/docs/models#controlling-reasoning documents the parameter as 'any value (no effect) — the model decides per request', but the live gateway contradicts that: `none` gave 0 reasoning tokens / 0 reasoning chars / a 134-char answer, while `low` gave 57 reasoning tokens and omitting the parameter gave 63 (`minimal` gave 109). The docs are stale here; the measurement wins (maintainer instruction), so the measured behavior is kept. 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."
47
47
  ],
48
48
  "extras": {
49
49
  "id": "deepseek-v4-flash",
@@ -177,7 +177,7 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
177
177
  "cacheRead": 0,
178
178
  "cacheWrite": 0
179
179
  },
180
- "contextWindow": 1000000,
180
+ "contextWindow": 1048576,
181
181
  "maxTokens": 131072,
182
182
  "reasoningEffortValues": [
183
183
  "minimal",
@@ -198,7 +198,7 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
198
198
  },
199
199
  "notes": [
200
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)."
201
+ "contextWindow 1,048,576: NaN docs state '1M token context' for this model (https://nan.builders/docs/models, checked 2026-10-14). models.dev lists 1,000,000, a rounded form of the same window; the docs value is authoritative (maintainer instruction: docs win over models.dev, which a user updates later). 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\" (the docs' glm row lists low/medium/high/max, but the 2026-09-27 measurement shows minimal is accepted and effective, issue #16)."
202
202
  ],
203
203
  "extras": {
204
204
  "id": "glm5.3-flash",
@@ -261,6 +261,9 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
261
261
  "contextWindow": 1048576,
262
262
  "maxTokens": 131072,
263
263
  "reasoningEffortValues": [],
264
+ "thinkingLevelMap": {
265
+ "off": "none"
266
+ },
264
267
  "compat": {
265
268
  "supportsDeveloperRole": false,
266
269
  "supportsReasoningEffort": true,
@@ -270,7 +273,8 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
270
273
  },
271
274
  "notes": [
272
275
  "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.)"
276
+ "thinkingLevelMap { off: \"none\" } (live re-probe 2026-10-14): the docs list this model as 'accepted, depth not adjustable', but the gateway makes the parameter effective at the off end — `none` gave 0 reasoning tokens / a full answer, while `low` gave 22 reasoning tokens and omitting the parameter gave 17. Measured behavior wins over the docs, so `thinking: off` now sends reasoning_effort:\"none\" and actually disables reasoning. No other level was sampled, so only `off` is mapped.",
277
+ "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. 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.)"
274
278
  ],
275
279
  "extras": {
276
280
  "id": "mimo-v2.6-flash",
@@ -403,7 +407,7 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
403
407
  "cacheRead": 0,
404
408
  "cacheWrite": 0
405
409
  },
406
- "contextWindow": 262144,
410
+ "contextWindow": 1048576,
407
411
  "maxTokens": 131072,
408
412
  "reasoningEffortValues": [],
409
413
  "compat": {
@@ -415,7 +419,7 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
415
419
  },
416
420
  "notes": [
417
421
  "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.",
418
- "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)."
422
+ "contextWindow 1,048,576: NaN docs state '1M token context (1,048,576 tokens)' and 'Max answer 131K tokens' (https://nan.builders/docs/models, checked 2026-10-14) — models.dev still lists 262,144. The earlier override was withdrawn 2026-09-07 when the docs read '262K token context, the model's native window'; the docs now state 1M and they are the source of truth (maintainer instruction: docs win over models.dev, which a user updates later). maxTokens stays 131,072, which models.dev already matches and the docs confirm as the 131K max answer."
419
423
  ],
420
424
  "extras": {
421
425
  "id": "qwen3.8-flash",
@@ -455,7 +459,7 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
455
459
  export const GENERATED_CATALOG_META = {
456
460
  source: "https://models.dev/api.json",
457
461
  modelsDevProvider: "nan",
458
- fetchedAt: "2026-10-02T22:53:40.291Z",
462
+ fetchedAt: "2026-10-14T15:59:01.497Z",
459
463
  modelCount: 6,
460
464
  models: ["deepseek-v4-flash","gemma4","glm5.3-flash","mimo-v2.6-flash","qwen3.6","qwen3.8-flash"],
461
465
  notes: [
@@ -469,11 +473,12 @@ export const GENERATED_CATALOG_META = {
469
473
  "non-chat: \"whisper\" excluded from the chat catalog (speech-to-text (STT) endpoint (https://nan.builders/docs/models, checked 2026-10-01) — not a chat model)",
470
474
  "non-chat: \"flux-2-klein\" excluded from the chat catalog (image generation endpoint (https://nan.builders/docs/models, checked 2026-10-01) — not a chat model)",
471
475
  "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.",
472
- "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. maxTokens 32768 per the official NaN pi docs (https://nan.builders/docs/pi, 'the same set the NaN CLI writes', checked 2026-10-02) closing issue #20 — the gateway now rejects max_tokens above ~256k for this model with 403 permission_error (measured 2026-10-02: 256,000 → 200; 262,143 → 403), and the previous models.dev-derived 384000 made every pi request fail since pi-ai sends max_tokens = model.maxTokens when no cap is set. models.dev still lists 384000, so this override pins the documented value. 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.",
476
+ "contextWindow 1,048,576: NaN docs state '1M token context' for this model (https://nan.builders/docs/models, checked 2026-10-14). models.dev lists 1,000,000, a rounded form of the same window; the docs value is authoritative (maintainer instruction: docs win over models.dev, which a user updates later). 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), so this stays a pin for the vision capability rather than a divergence. maxTokens 32768 per the official NaN pi docs (https://nan.builders/docs/pi, 'the same set the NaN CLI writes', checked 2026-10-02) closing issue #20 — the gateway now rejects max_tokens above ~256k for this model with 403 permission_error (measured 2026-10-02: 256,000 → 200; 262,143 → 403), and the previous models.dev-derived 384000 made every pi request fail since pi-ai sends max_tokens = model.maxTokens when no cap is set. models.dev still lists 384000, so this override pins the documented value. 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. docs vs measurement (re-probed live 2026-10-14): https://nan.builders/docs/models#controlling-reasoning documents the parameter as 'any value (no effect) — the model decides per request', but the live gateway contradicts that: `none` gave 0 reasoning tokens / 0 reasoning chars / a 134-char answer, while `low` gave 57 reasoning tokens and omitting the parameter gave 63 (`minimal` gave 109). The docs are stale here; the measurement wins (maintainer instruction), so the measured behavior is kept. 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.",
473
477
  "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.",
474
- "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).",
475
- "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.)",
478
+ "contextWindow 1,048,576: NaN docs state '1M token context' for this model (https://nan.builders/docs/models, checked 2026-10-14). models.dev lists 1,000,000, a rounded form of the same window; the docs value is authoritative (maintainer instruction: docs win over models.dev, which a user updates later). 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\" (the docs' glm row lists low/medium/high/max, but the 2026-09-27 measurement shows minimal is accepted and effective, issue #16).",
479
+ "thinkingLevelMap { off: \"none\" } (live re-probe 2026-10-14): the docs list this model as 'accepted, depth not adjustable', but the gateway makes the parameter effective at the off end — `none` gave 0 reasoning tokens / a full answer, while `low` gave 22 reasoning tokens and omitting the parameter gave 17. Measured behavior wins over the docs, so `thinking: off` now sends reasoning_effort:\"none\" and actually disables reasoning. No other level was sampled, so only `off` is mapped.",
480
+ "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. 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.)",
476
481
  "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.",
477
- "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)."
482
+ "contextWindow 1,048,576: NaN docs state '1M token context (1,048,576 tokens)' and 'Max answer 131K tokens' (https://nan.builders/docs/models, checked 2026-10-14) — models.dev still lists 262,144. The earlier override was withdrawn 2026-09-07 when the docs read '262K token context, the model's native window'; the docs now state 1M and they are the source of truth (maintainer instruction: docs win over models.dev, which a user updates later). maxTokens stays 131,072, which models.dev already matches and the docs confirm as the 131K max answer."
478
483
  ],
479
484
  } as const;
@@ -85,6 +85,41 @@ export const UNKNOWN_MODEL_LIMITS = { contextWindow: 128_000, maxTokens: 4_096 }
85
85
  /** Timeout for the live /models fetch; matches the pi-synthetic-provider precedent (~3s). */
86
86
  export const DEFAULT_MODELS_TIMEOUT_MS = 3_000;
87
87
 
88
+ /**
89
+ * Capability data for models the docs serve but the static baseline must NOT
90
+ * carry (tier-gated), so the live `/models` path registers them with real
91
+ * documented values instead of the blind `UNKNOWN_MODEL_LIMITS` placeholder.
92
+ *
93
+ * `glm5.3` is premium-tier: a non-premium key must never see it in the static
94
+ * baseline (it cannot call it), but a premium key that discovers it live must
95
+ * get its real capabilities. Every value traces to
96
+ * https://nan.builders/docs/models (checked 2026-10-14; docs are the source of
97
+ * truth — models.dev lags):
98
+ * - Context 1M tokens, multimodal input text·image·audio·video (pi-representable
99
+ * subset: text, image; pi's Model.input has no audio/video), output text,
100
+ * tool calling, reasoning_effort low·medium·high·max.
101
+ * - Max output 131,072: models.dev documents that value for glm5.3; the docs
102
+ * give the same 131K figure for the sibling glm5.3-flash, and do not state a
103
+ * lower bound for glm5.3. Kept as the confirmed maximum rather than invented.
104
+ * - thinkingLevelMap { off: "low" }: the docs list low as glm5.3's shallowest
105
+ * level, so `thinking: off` sends the shallowest supported effort. glm5.3 was
106
+ * never measured (issue #16 covered glm5.3-flash, whose shallowest effective
107
+ * value is "minimal"); "low" is the documented floor.
108
+ */
109
+ export const LIVE_ONLY_MODEL_DATA: Readonly<Record<string, Omit<GeneratedModelEntry, "compat">>> = {
110
+ "glm5.3": {
111
+ id: "glm5.3",
112
+ name: "GLM 5.3",
113
+ reasoning: true,
114
+ input: ["text", "image"],
115
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
116
+ contextWindow: 1_048_576,
117
+ maxTokens: 131_072,
118
+ reasoningEffortValues: ["low", "medium", "high", "max"],
119
+ thinkingLevelMap: { off: "low" },
120
+ },
121
+ };
122
+
88
123
  /**
89
124
  * Live endpoint IDs classified as non-chat and excluded from the registered
90
125
  * model set. Keys are model ids; values are provenance reasons citing the
@@ -218,6 +253,10 @@ export function mergeLiveWithGenerated(
218
253
  generated: readonly GeneratedModelEntry[] = NAN_GENERATED_MODELS,
219
254
  ): MergedCatalog {
220
255
  const byId = new Map(generated.map((entry) => [entry.id, entry]));
256
+ // Every generated entry shares the same NaN/LiteLLM compat block; borrow it
257
+ // for the live-only documented entries so the gateway-behavior flags never
258
+ // drift between the static catalog and the live overlay.
259
+ const catalogCompat = generated.find((entry) => entry.compat)?.compat;
221
260
  const models: Model<"openai-completions">[] = [];
222
261
  const matched: string[] = [];
223
262
  const unknown: string[] = [];
@@ -235,6 +274,16 @@ export function mergeLiveWithGenerated(
235
274
  models.push(toModel(entry, source));
236
275
  matched.push(id);
237
276
  } else {
277
+ // Documented data for tier-gated live-only ids (glm5.3): the static
278
+ // baseline excludes them so a non-premium key never sees a model it
279
+ // cannot call, but a key that discovers one live gets its real
280
+ // capabilities instead of the blind placeholder below.
281
+ const liveOnlyEntry = LIVE_ONLY_MODEL_DATA[id];
282
+ if (liveOnlyEntry) {
283
+ models.push(toModel({ ...liveOnlyEntry, compat: catalogCompat }, source));
284
+ matched.push(id);
285
+ continue;
286
+ }
238
287
  // Placeholder for uncatalogued live ids (e.g. new or undocumented):
239
288
  // limits are the documented safe envelope and capabilities stay
240
289
  // "unknown". The compat keys are the same gateway-behavior flags
@@ -443,6 +492,15 @@ export function buildOverlayMap(
443
492
  for (const entry of generated) {
444
493
  overlay.set(entry.id, { chat: toModel(entry, source) });
445
494
  }
495
+ // Tier-gated live-only chat models with documented capabilities (glm5.3):
496
+ // absent from the static baseline so a non-premium key never sees them, but
497
+ // present here so a stored entry for one is corrected to the documented
498
+ // values instead of passing through stale. The store still decides which ids
499
+ // exist (issue #19); this map only decides their capabilities.
500
+ const catalogCompat = generated.find((entry) => entry.compat)?.compat;
501
+ for (const entry of Object.values(LIVE_ONLY_MODEL_DATA)) {
502
+ overlay.set(entry.id, { chat: toModel({ ...entry, compat: catalogCompat }, source) });
503
+ }
446
504
  // Image models from the baseline (these live under a different api id
447
505
  // so they cannot coexist as chat entries — ids are unique across types).
448
506
  if (imageModels) {
@@ -27,7 +27,7 @@ export function mediaMcpTimeoutSec(): number {
27
27
 
28
28
  export const NAN_MEDIA_MCP_ENV = "NAN_MEDIA_MCP";
29
29
 
30
- export const DEFAULT_NAN_MEDIA_MCP_VERSION = "1.1.2";
30
+ export const DEFAULT_NAN_MEDIA_MCP_VERSION = "1.3.0";
31
31
  export const DEFAULT_MEDIA_MCP_TIMEOUT_MS = 120_000;
32
32
  export const NAN_MEDIA_MCP_VERSION_ENV = "NAN_MEDIA_MCP_VERSION";
33
33
  export const NAN_MEDIA_MCP_COMMAND_ENV = "NAN_MEDIA_MCP_COMMAND";
@@ -2,8 +2,13 @@
2
2
  * NaN reasoning-only stream guard (issue #21).
3
3
  *
4
4
  * Problem: NaN's gateway closes a streaming turn that produced ONLY reasoning
5
- * (no content, no tool calls) at 60,000 reasoning chars or 420 seconds:
6
- * `finish_reason: "length"` on an empty delta, followed by a final usage chunk
5
+ * (no content, no tool calls) once it has produced more than 45,000 reasoning
6
+ * characters, or 420 seconds without visible output (600 seconds once the turn
7
+ * has demonstrably exceeded that character count). The reasoning-character
8
+ * budget also scales with prompt size: ~150,000 characters of headroom plus
9
+ * half the prompt's size, capped at 300,000
10
+ * (https://nan.builders/docs/models#reasoning-only-stream-limit,
11
+ * checked 2026-10-14). The cut is:
7
12
  * (estimated, unbilled) carrying `nan_truncation: { reason: "reasoning_only_stream" }`.
8
13
  * Through pi-ai this maps to `stopReason: "length"` with no text — indistinguishable
9
14
  * from a real output-limit cut, never retried, and the turn is silently wasted.
@@ -72,7 +77,7 @@ export function isReasoningOnlyStreamChunk(chunk: unknown): boolean {
72
77
  const REASONING_ONLY_REWRITE: Partial<AssistantMessage> = {
73
78
  stopReason: "error" as const,
74
79
  errorMessage:
75
- "NaN timed out the stream before any answer content (reasoning-only stream guard: 420 s / 60,000-character limit; the turn was not billed). Original finish_reason: length.",
80
+ "NaN timed out the stream before any answer content (reasoning-only stream guard: the 420 s / 45,000-reasoning-character limit; the turn was not billed). Original finish_reason: length.",
76
81
  };
77
82
 
78
83
  /**