@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 +80 -20
- package/README.es.md +13 -7
- package/README.md +11 -7
- package/package.json +3 -3
- package/scripts/check-nan-mcp-server.ts +1 -1
- package/scripts/generate-models.ts +3 -5
- package/scripts/manual-overrides.ts +35 -11
- package/scripts/models.generated.ts +18 -13
- package/src/fetch-models.ts +58 -0
- package/src/mcp/media-server.ts +1 -1
- package/src/reasoning-only-stream-guard.ts +8 -3
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
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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.
|
|
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.
|
|
275
|
-
or full command override via NAN_MEDIA_MCP_COMMAND. Tools (1.
|
|
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
|
-
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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.
|
|
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,
|
|
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,
|
|
336
|
-
| `qwen3.8-flash` |
|
|
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`
|
|
367
|
-
| `qwen3.8-flash
|
|
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
|
|
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.
|
|
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,
|
|
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,
|
|
333
|
-
| `qwen3.8-flash` |
|
|
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`
|
|
364
|
-
| `qwen3.8-flash
|
|
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
|
|
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
|
|
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.
|
|
50
|
-
"@earendil-works/pi-coding-agent": "1.
|
|
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/
|
|
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
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
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.
|
|
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
|
-
"
|
|
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\" (
|
|
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
|
-
"
|
|
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-
|
|
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":
|
|
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
|
|
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":
|
|
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\" (
|
|
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
|
-
"
|
|
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":
|
|
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
|
|
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-
|
|
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
|
|
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\" (
|
|
475
|
-
"
|
|
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
|
|
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;
|
package/src/fetch-models.ts
CHANGED
|
@@ -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) {
|
package/src/mcp/media-server.ts
CHANGED
|
@@ -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.
|
|
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)
|
|
6
|
-
*
|
|
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 /
|
|
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
|
/**
|