@gtrabanco/pi-nan-provider 0.6.2 → 0.6.4
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 +21 -0
- package/README.es.md +94 -126
- package/README.md +92 -124
- package/package.json +1 -1
- package/scripts/models.generated.ts +2 -2
- package/src/cross-model-thinking-guard.ts +104 -0
- package/src/index.ts +33 -1
package/AGENTS.md
CHANGED
|
@@ -37,6 +37,27 @@ Regenerate the catalog after touching `scripts/generate-models.ts`:
|
|
|
37
37
|
bun run generate-models
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
## Live NaN API during diagnosis
|
|
41
|
+
|
|
42
|
+
Diagnostic calls against the real gateway are allowed — they spend the
|
|
43
|
+
maintainer's quota, so they are **permission-gated**:
|
|
44
|
+
|
|
45
|
+
- **Ask the maintainer before running any live probe, with an approximate token
|
|
46
|
+
cost (input + output).** No silent probing. If the cost is not worth it, report
|
|
47
|
+
the behavior to NaN and let them reproduce it instead of debugging it here.
|
|
48
|
+
- **Tests must never hit the network.** `bunfig.toml` preloads
|
|
49
|
+
`test/network-guard.ts`, which makes any un-injected `fetch` throw. Keep it:
|
|
50
|
+
inject `fetchImpl` / `options.fetch`, or use the local fixture. The permission
|
|
51
|
+
gate covers ad-hoc diagnosis only, never `bun test`.
|
|
52
|
+
- **Default to `qwen3.6` — it is unlimited.**
|
|
53
|
+
- **For massive/bulk probes prefer a model the maintainer uses less with a large
|
|
54
|
+
token budget, e.g. `mimo-v2.5`** (1M context).
|
|
55
|
+
- **When the model under investigation is the point** (e.g. reproducing a
|
|
56
|
+
model-specific 400), use it, but minimize tokens: smallest viable prompt,
|
|
57
|
+
lowest `max_tokens`, stop at the first decisive response.
|
|
58
|
+
- Repro commands that run a real `pi` session (`pi --fork ... -p ...`) use the
|
|
59
|
+
same key; keep them minimal and delete the forked session files afterwards.
|
|
60
|
+
|
|
40
61
|
## One shared implementation for all providers
|
|
41
62
|
|
|
42
63
|
`nan` (and any future provider, e.g. `helmcode`) must stay behind the single shared
|
package/README.es.md
CHANGED
|
@@ -1,177 +1,145 @@
|
|
|
1
1
|
# @gtrabanco/pi-nan-provider
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
|
+
[](https://github.com/gtrabanco/pi-nan-provider/releases)
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
[NaN Builders](https://nan.builders) model provider + MCP bridges para [pi](https://github.com/earendil-works/pi).
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
Registra el proveedor `nan` vía `pi.registerProvider()` usando la API OpenAI-compatible de NaN (`https://api.nan.builders/v1`), y conecta las herramientas MCP de NaN en pi con `pi.registerTool()`.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
---
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
### ⚡ Inicio Rápido
|
|
12
13
|
|
|
13
|
-
1. **
|
|
14
|
-
2. **
|
|
14
|
+
1. **Consigue tu API Key**: [Reclama tu API key de NaN aquí](https://cloud.nan.builders/r/7GK06FX8) (enlace de referidos).
|
|
15
|
+
2. **Instala**:
|
|
16
|
+
```bash
|
|
17
|
+
pi install npm:@gtrabanco/pi-nan-provider
|
|
18
|
+
```
|
|
19
|
+
3. **Autentica**:
|
|
20
|
+
```bash
|
|
21
|
+
export NAN_API_KEY="sk-tu-clave-aqui"
|
|
22
|
+
```
|
|
23
|
+
4. **Verifica**:
|
|
24
|
+
```bash
|
|
25
|
+
pi --list-models nan
|
|
26
|
+
```
|
|
15
27
|
|
|
16
|
-
|
|
28
|
+
---
|
|
17
29
|
|
|
18
|
-
|
|
30
|
+
**Documentación en español** (este archivo) · [Docs in English](README.md)
|
|
19
31
|
|
|
20
|
-
##
|
|
32
|
+
## ⚙️ Cómo funciona
|
|
21
33
|
|
|
22
|
-
|
|
23
|
-
pi install npm:@gtrabanco/pi-nan-provider
|
|
24
|
-
# o, desde git:
|
|
25
|
-
pi install git:github.com/gtrabanco/pi-nan-provider
|
|
26
|
-
# o, para probarlo sin instalar:
|
|
27
|
-
pi -e npm:@gtrabanco/pi-nan-provider
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
Después reinicia pi (o `/reload`). Verifica con:
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
pi --list-models nan
|
|
34
|
-
```
|
|
34
|
+
El proveedor utiliza un **catálogo de modelos de dos capas** para garantizar la fiabilidad:
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
| Capa | Fuente | Propósito |
|
|
37
|
+
| :--- | :--- | :--- |
|
|
38
|
+
| **1. Fallback generado** | `scripts/models.generated.ts` | Snapshot en tiempo de build desde [models.dev](https://models.dev). Asegura que pi siempre pueda arrancar, incluso si la red falla. |
|
|
39
|
+
| **2. Fetch en vivo de `/models`** | NaN Runtime API | Obtiene los modelos disponibles en tiempo real según el tier de tu API key. Se combina con los datos del fallback. |
|
|
37
40
|
|
|
38
|
-
|
|
41
|
+
> [!IMPORTANT]
|
|
42
|
+
> **Detección de Tier**: La lista en vivo es la autoridad. Si tu clave tiene acceso premium, esos modelos aparecerán automáticamente; de lo contrario, se filtran.
|
|
39
43
|
|
|
40
|
-
|
|
44
|
+
El registro es síncrono a propósito: el catálogo de fallback está disponible al instante, y el runtime de Models de pi dirige el refresco en vivo (refresco de red en el arranque interactivo y periódico, solo caché en el registro), persistiendo el overlay entre ejecuciones.
|
|
41
45
|
|
|
42
|
-
|
|
43
|
-
export NAN_API_KEY="sk-tu-clave-aqui"
|
|
44
|
-
```
|
|
46
|
+
### 🧠 Seguridad al cambiar de modelo (guard de razonamiento cross-model)
|
|
45
47
|
|
|
46
|
-
|
|
48
|
+
Al cambiar de modelo, pi-ai reenvía el razonamiento del modelo anterior como texto plano de asistente — **sin límite de tamaño**. Un único razonamiento largo o degenerado puede desbordar la ventana de un modelo de 262K, y NaN responde con un `400 Invalid request. Check your request parameters.` genérico que parece un bug del proveedor (seguimiento upstream: [pi-nan-provider#3](https://github.com/gtrabanco/pi-nan-provider/issues/3); issue abierta upstream: [pi#6167](https://github.com/earendil-works/pi/issues/6167)).
|
|
47
49
|
|
|
48
|
-
**
|
|
50
|
+
Este paquete **elimina todos los bloques de razonamiento cross-model reenviados**, de modo que cambiar de un modelo de 1M de contexto a uno de 262K (`qwen3.6`) ya no desborda la ventana. Las respuestas y los tool results de los modelos no se tocan — solo se quitan sus trazas internas de razonamiento, así que `qwen3.6` puede seguir respondiendo sobre lo que hizo otro modelo. El razonamiento del mismo modelo no se toca nunca, y el guard solo actúa sobre peticiones dirigidas a los proveedores de este paquete. Pon `NAN_THINKING_GUARD=0` para desactivarlo.
|
|
49
51
|
|
|
50
|
-
|
|
51
|
-
{
|
|
52
|
-
"nan": { "type": "api_key", "key": "sk-tu-clave-aqui" }
|
|
53
|
-
}
|
|
54
|
-
```
|
|
52
|
+
## 🔑 Autenticación
|
|
55
53
|
|
|
56
|
-
|
|
54
|
+
`resolve()` comprueba primero la credencial almacenada y después recurre a la variable de entorno correspondiente.
|
|
57
55
|
|
|
58
|
-
|
|
56
|
+
| Método | Comando / Acción | Notas |
|
|
57
|
+
| :--- | :--- | :--- |
|
|
58
|
+
| **Var de Entorno** | `export NAN_API_KEY="..."` | Lo más rápido para desarrollo local. |
|
|
59
|
+
| **`/login`** | `pi > /login nan` | Persistente; se guarda en `~/.pi/agent/auth.json`. |
|
|
60
|
+
| **Config Manual** | Editar `~/.pi/agent/auth.json` | Manipulación directa de JSON. |
|
|
59
61
|
|
|
60
|
-
|
|
62
|
+
Consigue una clave en la [plataforma NaN](https://cloud.nan.builders/r/7GK06FX8) (ajustes de usuario → API Keys; enlace de referidos).
|
|
61
63
|
|
|
62
|
-
|
|
64
|
+
## 🔌 Puentes MCP
|
|
63
65
|
|
|
64
|
-
|
|
65
|
-
|---|---|
|
|
66
|
-
| `/nan-mcp status` | Estado de ambos puentes y de dónde sale cada interruptor (env / persistido / por defecto) |
|
|
67
|
-
| `/nan-mcp enable [target]` | Activa un puente — o ambos si no das target — y lo persiste en `<agentDir>/nan-provider.json` (p. ej. `~/.pi/agent/nan-provider.json`); las herramientas se registran al instante en la sesión actual |
|
|
68
|
-
| `/nan-mcp disable [target]` | Desactiva persistentemente; pi no tiene `unregisterTool`, así que las herramientas ya registradas siguen hasta reiniciar; las sesiones futuras no las registran |
|
|
66
|
+
Dado que [pi no incluye un cliente MCP integrado](https://github.com/earendil-works/pi/blob/main/docs/usage.md), este paquete conecta los servidores MCP como **herramientas nativas de pi**.
|
|
69
67
|
|
|
70
|
-
|
|
68
|
+
Ambos puentes están **activados y son perezosos (lazy) por defecto**. Usa `/nan-mcp` para gestionarlos.
|
|
71
69
|
|
|
72
|
-
###
|
|
70
|
+
### 🛠️ Comando de Gestión: `/nan-mcp`
|
|
73
71
|
|
|
74
|
-
|
|
72
|
+
| Comando | Efecto |
|
|
73
|
+
| :--- | :--- |
|
|
74
|
+
| `/nan-mcp status` | Muestra el estado actual de ambos puentes. |
|
|
75
|
+
| `/nan-mcp enable [target]` | Activa `web-search` o `nan-mcp-server` (persiste). |
|
|
76
|
+
| `/nan-mcp disable [target]` | Desactiva un puente de forma persistente. |
|
|
75
77
|
|
|
76
|
-
|
|
78
|
+
---
|
|
77
79
|
|
|
78
|
-
|
|
80
|
+
### 1. Servidor MCP oficial de NaN
|
|
81
|
+
*Puente oficial para herramientas remotas vía [https://api.nan.builders/mcp](https://nan.builders/docs/api).*
|
|
79
82
|
|
|
80
|
-
|
|
83
|
+
- **`nan_web_search(query, ...)`**: Realiza búsquedas web a través del gateway de NaN.
|
|
81
84
|
|
|
82
|
-
|
|
85
|
+
### 2. Servidor MCP de Media (Comunidad)
|
|
86
|
+
*Conecta [`nan-mcp-server`](https://github.com/luciferfran/nan-mcp-server) mediante un cliente stdio local mínimo.*
|
|
83
87
|
|
|
84
|
-
- **
|
|
85
|
-
- **
|
|
86
|
-
- **Configuración**: `NAN_API_KEY` se reenvía automáticamente (la misma clave del proveedor); los ficheros generados van a `~/nan-mcp-output/` (por defecto del servidor, configurable con `NAN_OUTPUT_DIR`).
|
|
88
|
+
- **Carga Perezosa (Lazy)**: El proceso del servidor se lanza **solo** cuando se invoca una herramienta y se cierra inmediatamente después.
|
|
89
|
+
- **Configuración**: Los archivos se guardan en `~/nan-mcp-output/`.
|
|
87
90
|
|
|
88
91
|
| Herramienta | Propósito |
|
|
89
|
-
|
|
90
|
-
| `nan_generate_image
|
|
91
|
-
| `nan_edit_image
|
|
92
|
-
| `nan_text_to_speech
|
|
93
|
-
| `nan_list_voices
|
|
94
|
-
| `nan_speech_to_text
|
|
92
|
+
| :--- | :--- |
|
|
93
|
+
| `nan_generate_image` | Generación de imágenes (flux-2-klein) |
|
|
94
|
+
| `nan_edit_image` | Edición imagen→imagen (flux-2-klein) |
|
|
95
|
+
| `nan_text_to_speech` | Síntesis de audio (kokoro) |
|
|
96
|
+
| `nan_list_voices` | Listar voces disponibles |
|
|
97
|
+
| `nan_speech_to_text` | Transcripción de audio (whisper) |
|
|
95
98
|
|
|
96
|
-
|
|
99
|
+
#### 🔧 Configuración del Puente de Media
|
|
97
100
|
|
|
98
|
-
| Variable | Por defecto |
|
|
99
|
-
|
|
100
|
-
| `NAN_MEDIA_MCP` | — | Override por sesión
|
|
101
|
-
| `NAN_MEDIA_MCP_VERSION` | `1.0.8` | Versión del servidor fijada
|
|
102
|
-
| `NAN_MEDIA_MCP_COMMAND` | — |
|
|
101
|
+
| Variable | Por defecto | Descripción |
|
|
102
|
+
| :--- | :--- | :--- |
|
|
103
|
+
| `NAN_MEDIA_MCP` | — | Override por sesión (`0` o `false` para desactivar). |
|
|
104
|
+
| `NAN_MEDIA_MCP_VERSION` | `1.0.8` | Versión del servidor fijada (recomendado). |
|
|
105
|
+
| `NAN_MEDIA_MCP_COMMAND` | — | Override del comando personalizado. |
|
|
106
|
+
| `NAN_MEDIA_MCP_TIMEOUT_MS` | `120000` | Timeout por llamada. |
|
|
107
|
+
| `NAN_MCP_TOOLS` | — | Override para el puente oficial (`0` para desactivar). |
|
|
103
108
|
|
|
104
|
-
#### Detección
|
|
109
|
+
#### 🤖 Detección Automática de Actualizaciones
|
|
105
110
|
|
|
106
|
-
Una nueva versión de `nan-mcp-server` no desviará silenciosamente el pin de esta conexión. El
|
|
107
|
-
planificador (`.github/workflows/check-nan-mcp-server-update.yml`) ejecuta `bun run scripts/check-nan-mcp-server.ts`
|
|
108
|
-
semanalmente y, cuando el registro npm muestra una versión más reciente, abre (o refresca) un único
|
|
109
|
-
issue etiquetado `dependencies` que describe si el bump es **breaking** o **seguro**, más la lista de
|
|
110
|
-
commits aguas arriba. Puedes ejecutarlo localmente en cualquier momento:
|
|
111
|
+
Una nueva versión de `nan-mcp-server` no desviará silenciosamente el pin de esta conexión. El planificador (`.github/workflows/check-nan-mcp-server-update.yml`) se ejecuta semanalmente y, cuando encuentra una versión nueva, abre un issue indicando si el cambio es **breaking** o **seguro**.
|
|
111
112
|
|
|
112
113
|
```bash
|
|
113
|
-
bun run check-nan-mcp-server #
|
|
114
|
-
bun run check-nan-mcp-server --json # JSON para
|
|
115
|
-
bun run check-nan-mcp-server --issue # crea/refresca el issue
|
|
114
|
+
bun run check-nan-mcp-server # reporte legible
|
|
115
|
+
bun run check-nan-mcp-server --json # JSON para máquinas
|
|
116
|
+
bun run check-nan-mcp-server --issue # crea/refresca el issue
|
|
116
117
|
```
|
|
117
118
|
|
|
118
|
-
|
|
119
|
-
el último servidor sigue exponiendo todas las tools conectadas (`generate_image`, `edit_image`,
|
|
120
|
-
`text_to_speech`, `list_voices`, `speech_to_text`), el bump se reporta como **no rompente**; si elimina
|
|
121
|
-
o renombra alguna tool conectada, el issue se marca **breaking** para revisión manual antes de subir.
|
|
122
|
-
| `NAN_MEDIA_MCP_TIMEOUT_MS` | `120000` | Timeout por llamada; el proceso se mata al expirar |
|
|
123
|
-
| `NAN_MCP_TOOLS` | — | Override por sesión del puente oficial: `0`/`false`/`off` desactiva `nan_web_search`; sin definir → persistido/por defecto |
|
|
124
|
-
|
|
125
|
-
## Modelos
|
|
126
|
-
|
|
127
|
-
Catálogo base (de models.dev, proveedor `nan`, obtenido 2026-09-07 y corregido contra [los docs de NaN](https://nan.builders/docs/models) y [openapi.json](https://nan.builders/openapi.json) — límites *servidos* por NaN, no máximos teóricos):
|
|
128
|
-
|
|
129
|
-
| Modelo | Contexto | Máx. salida | Entrada | Razonamiento |
|
|
130
|
-
|---|---|---|---|---|
|
|
131
|
-
| `qwen3.6` | 262,144 | 65,536 | texto, imagen | sí |
|
|
132
|
-
| `gemma4` | 262,144 | 32,768 | texto, imagen | sí |
|
|
133
|
-
| `deepseek-v4-flash` | 1,000,000 | 384,000 | texto, imagen | sí |
|
|
134
|
-
| `mimo-v2.5` | 1,048,576 | 131,072 | texto, imagen | sí |
|
|
135
|
-
| `glm5.3-flash` | 1,000,000 | 131,072 | texto, imagen | sí |
|
|
136
|
-
| `qwen3.8-flash` | 262,144 | 131,072 | texto, imagen | sí |
|
|
137
|
-
|
|
138
|
-
Notas (grabadas por entrada en `scripts/models.generated.ts`):
|
|
139
|
-
|
|
140
|
-
- `qwen3.8-flash` sirve 262K tokens, «la ventana nativa del modelo» ([docs de NaN](https://nan.builders/docs/models), 2026-09-07). Un override previo de 1M (confirmado por el mantenedor el 2026-09-05) se retiró cuando los docs actualizados siguieron diciendo 262K; models.dev coincide en 262,144. Este tipo de divergencias se registran como `MANUAL_OVERRIDES` en tiempo de build (con procedencia) en `scripts/manual-overrides.ts` — añade una ahí en vez de editar el fichero generado.
|
|
141
|
-
- `deepseek-v4-flash` incluye entrada de imagen porque NaN sirve la variante Vision-Exp ([docs de NaN](https://nan.builders/docs/models), confirmado por los content-parts de visión en [openapi.json](https://nan.builders/openapi.json)); models.dev la lista como solo texto.
|
|
142
|
-
- `glm5.2` fue eliminado por NaN (2026-09-05). models.dev aún lo listaba el 2026-09-07, así que el generador lo excluye vía `PROVIDER_REMOVED_MODEL_IDS` con la razón registrada — una regeneración no debe resucitar modelos retirados por el proveedor.
|
|
143
|
-
- `mimo-v2.5` es omnimodal (texto/imagen/audio) en NaN, pero el tipo de modelo de pi solo representa entrada texto/imagen, así que el audio se omite en `input`.
|
|
144
|
-
- NaN factura por cuota de membresía, que models.dev reporta como coste cero por token — el coste mostrado por pi será $0.
|
|
145
|
-
- Compat (`supportsDeveloperRole: false`, `supportsReasoningEffort: true`, `supportsUsageInStreaming: true`, `maxTokensField: "max_tokens"`) coincide con la config LiteLLM probada en batalla que este paquete reemplaza; el ejemplo de los docs de NaN (`supportsDeveloperRole: true`) no está probado.
|
|
146
|
-
- **Conformidad con el esquema estricto**: NaN valida cada payload de `/chat/completions` contra su propio esquema estricto ([openapi.json](https://nan.builders/openapi.json)) y devuelve HTTP 400 `Invalid request. Check your request parameters.` para formas no permitidas — p. ej. un mensaje `assistant` reenviado con un bloque `toolCall` dentro de `content`, un campo `reasoning_details` solo de OpenAI, campos a nivel superior no documentados como `store` / `stream_options`, o un **array vacío `tools: []`**. Por eso cada petición se reescribe con un saneador en el proveedor (`src/openai-compat-sanitizer.ts`, conectado vía `onPayload` en la fábrica compartida) para que siempre sea válida según el esquema, sin importar qué versión de pi-ai esté empaquetada:
|
|
147
|
-
- **Tier/cuota**: qué modelos puedes llamar lo decide tu membresía de NaN. Con clave, el fetch en vivo refleja exactamente eso (ver *Cómo funciona* — detección de tier). El `glm5.3` de tier premium no está en el proveedor `nan` de models.dev y ninguna fuente documenta su límite de salida, así que no entra en el catálogo estático (marcado como no emitible en los metadatos); las claves premium lo reciben en vivo vía el refresh de `/models`, con límites conservadores (128K contexto / 4K salida). Solo está `glm5.3-flash` en el catálogo estático.
|
|
148
|
-
|
|
149
|
-
### Relación con `~/.pi/agent/models.json`
|
|
150
|
-
|
|
151
|
-
Este paquete reemplaza el bloque `nan` manual de `~/.pi/agent/models.json` (el [ejemplo pi](https://nan.builders/docs/examples) de los docs de NaN). Si conservas ese bloque, ten en cuenta que **models.json se compone por encima de los proveedores registrados** — el fichero estático gana sobre este paquete. Elimina la entrada `nan` de `models.json` (conserva `defaultProvider`/`defaultModel` en `settings.json` si los usas) para usar el catálogo en vivo de este paquete. Los topes de salida por petición pueden seguir configurándose ahí o vía `params` del modelo.
|
|
152
|
-
|
|
153
|
-
## Compatibilidad con versiones de pi
|
|
119
|
+
---
|
|
154
120
|
|
|
155
|
-
|
|
121
|
+
## 📊 Modelos
|
|
156
122
|
|
|
157
|
-
|
|
158
|
-
- **Fallback legacy**: si el overload nativo de Provider es rechazado (o la construcción del proveedor falla), el registro cae a la forma legacy documentada `(name, config)` con el mismo catálogo generado y auth por env `$NAN_API_KEY` (la auth por credencial almacenada es una limitación del camino legacy, no un cambio silencioso).
|
|
159
|
-
- **Puentes MCP**: se omiten por completo en runtimes sin `registerTool`; los proveedores se registran igualmente.
|
|
160
|
-
- **Entrada asíncrona**: pi espera las factorías de extensión en 0.83 y 0.84 por igual, así que la resolución de la API de streaming durante el registro es transparente.
|
|
161
|
-
- `peerDependencies` es `>=0.83.0` sin límite superior (incluidos los forks en 0.83).
|
|
162
|
-
- **Imports de pi-ai en la extensión**: solo se importa estáticamente el root `@earendil-works/pi-ai`. El loader de extensiones de pi aliasa ese especificador al entrypoint compat; los imports por subruta (p. ej. `@earendil-works/pi-ai/api/openai-completions.lazy`) reciben el alias como prefijo y no resuelven, lo que rompe la carga de toda la extensión. Protegido por `test/extension-load.test.ts`.
|
|
123
|
+
Catálogo base (verificado contra [docs de NaN](https://nan.builders/docs/models) y [OpenAPI](https://nan.builders/openapi.json)).
|
|
163
124
|
|
|
164
|
-
|
|
125
|
+
| Modelo | Contexto | Máx. Salida | Entrada | Razonamiento |
|
|
126
|
+
| :--- | :--- | :--- | :--- | :---: |
|
|
127
|
+
| `qwen3.6` | 262,144 | 65,536 | texto, imagen | ✅ |
|
|
128
|
+
| `gemma4` | 262,144 | 32,768 | texto, imagen | ✅ |
|
|
129
|
+
| `deepseek-v4-flash` | 1,000,000 | 384,000 | texto, imagen | ✅ |
|
|
130
|
+
| `mimo-v2.5` | 1,048,576 | 131,072 | texto, imagen | ✅ |
|
|
131
|
+
| `glm5.3-flash` | 1,000,000 | 131,072 | texto, imagen | ✅ |
|
|
132
|
+
| `qwen3.8-flash` | 262,144 | 131,072 | texto, imagen | ✅ |
|
|
165
133
|
|
|
166
|
-
|
|
134
|
+
---
|
|
167
135
|
|
|
168
|
-
## Desarrollo
|
|
136
|
+
## 🚀 Desarrollo
|
|
169
137
|
|
|
170
138
|
```bash
|
|
171
139
|
bun install
|
|
172
|
-
bun run generate-models #
|
|
173
|
-
bun test #
|
|
174
|
-
bun run typecheck #
|
|
140
|
+
bun run generate-models # Regenerar catálogo de fallback
|
|
141
|
+
bun test # Ejecutar todos los tests
|
|
142
|
+
bun run typecheck # Ejecutar typechecking
|
|
175
143
|
```
|
|
176
144
|
|
|
177
|
-
|
|
145
|
+
*Las versiones siguen semver estricto. CI publica automáticamente al hacer merge a `main`.*
|
package/README.md
CHANGED
|
@@ -1,177 +1,145 @@
|
|
|
1
1
|
# @gtrabanco/pi-nan-provider
|
|
2
2
|
|
|
3
|
-
[
|
|
3
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
|
+
[](https://github.com/gtrabanco/pi-nan-provider/releases)
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
[NaN Builders](https://nan.builders) model provider + MCP bridges for [pi](https://github.com/earendil-works/pi).
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
Registers the `nan` provider via `pi.registerProvider()` using NaN's OpenAI-compatible API (`https://api.nan.builders/v1`), and bridges NaN's MCP tools into pi with `pi.registerTool()`.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
---
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
### ⚡ Quick Start
|
|
12
13
|
|
|
13
|
-
1. **
|
|
14
|
-
2. **
|
|
14
|
+
1. **Get an API Key**: [Claim your NaN API key here](https://cloud.nan.builders/r/7GK06FX8) (referral link).
|
|
15
|
+
2. **Install**:
|
|
16
|
+
```bash
|
|
17
|
+
pi install npm:@gtrabanco/pi-nan-provider
|
|
18
|
+
```
|
|
19
|
+
3. **Authenticate**:
|
|
20
|
+
```bash
|
|
21
|
+
export NAN_API_KEY="sk-your-key-here"
|
|
22
|
+
```
|
|
23
|
+
4. **Verify**:
|
|
24
|
+
```bash
|
|
25
|
+
pi --list-models nan
|
|
26
|
+
```
|
|
15
27
|
|
|
16
|
-
|
|
28
|
+
---
|
|
17
29
|
|
|
18
|
-
|
|
30
|
+
**Docs in English** (this file) · [Documentación en español](README.es.md)
|
|
19
31
|
|
|
20
|
-
##
|
|
32
|
+
## ⚙️ How it works
|
|
21
33
|
|
|
22
|
-
|
|
23
|
-
pi install npm:@gtrabanco/pi-nan-provider
|
|
24
|
-
# or, from git:
|
|
25
|
-
pi install git:github.com/gtrabanco/pi-nan-provider
|
|
26
|
-
# or, to try it without installing:
|
|
27
|
-
pi -e npm:@gtrabanco/pi-nan-provider
|
|
28
|
-
```
|
|
34
|
+
The provider uses a **two-layer model catalog** to ensure reliability:
|
|
29
35
|
|
|
30
|
-
|
|
36
|
+
| Layer | Source | Purpose |
|
|
37
|
+
| :--- | :--- | :--- |
|
|
38
|
+
| **1. Generated Fallback** | `scripts/models.generated.ts` | Build-time snapshot from [models.dev](https://models.dev). Ensures pi can always start, even if the network fails. |
|
|
39
|
+
| **2. Live `/models` Fetch** | NaN Runtime API | Fetches your real-time available models based on your API key's tier. Merged with fallback data. |
|
|
31
40
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
```
|
|
41
|
+
> [!IMPORTANT]
|
|
42
|
+
> **Tier Detection**: The live list is authoritative. If your key has premium access, those models will appear automatically; otherwise, they are filtered out.
|
|
35
43
|
|
|
36
|
-
|
|
44
|
+
The registration is synchronous on purpose: the generated fallback catalog is available immediately, and pi's Models runtime drives the live refresh (network refresh at interactive startup and periodically, cache-only at registration), persisting the overlay between runs.
|
|
37
45
|
|
|
38
|
-
|
|
46
|
+
### 🧠 Model-switch safety (cross-model reasoning guard)
|
|
39
47
|
|
|
40
|
-
|
|
48
|
+
When you switch models, pi-ai replays the previous model's reasoning as plain assistant text — with **no size bound**. A single long or degenerate reasoning trace can therefore overflow a 262K-context model's window, and NaN answers with a generic `400 Invalid request. Check your request parameters.` that looks like a provider bug (upstream tracking: [pi-nan-provider#3](https://github.com/gtrabanco/pi-nan-provider/issues/3); open upstream issue: [pi#6167](https://github.com/earendil-works/pi/issues/6167)).
|
|
41
49
|
|
|
42
|
-
|
|
43
|
-
export NAN_API_KEY="sk-your-key-here"
|
|
44
|
-
```
|
|
50
|
+
This package **drops every replayed cross-model reasoning block**, so switching from a 1M-context model to a 262K one (`qwen3.6`) no longer overflows the window. The models' answers and tool results are untouched — only their internal reasoning traces are removed, so `qwen3.6` can still answer about what another model did. Same-model reasoning is never altered, and the guard only acts on requests targeting this package's providers. Set `NAN_THINKING_GUARD=0` to disable it.
|
|
45
51
|
|
|
46
|
-
|
|
52
|
+
## 🔑 Authentication
|
|
47
53
|
|
|
48
|
-
|
|
54
|
+
`resolve()` checks the stored credential first, then falls back to the matching environment variable.
|
|
49
55
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
| Method | Command / Action | Notes |
|
|
57
|
+
| :--- | :--- | :--- |
|
|
58
|
+
| **Env Var** | `export NAN_API_KEY="..."` | Fastest for local development. |
|
|
59
|
+
| **`/login`** | `pi > /login nan` | Persistent; stores in `~/.pi/agent/auth.json`. |
|
|
60
|
+
| **Manual Config** | Edit `~/.pi/agent/auth.json` | Direct JSON manipulation. |
|
|
55
61
|
|
|
56
|
-
Get a key from the [NaN platform](https://cloud.nan.builders/r/7GK06FX8) (user settings → API Keys; referral link).
|
|
62
|
+
Get a key from the [NaN platform](https://cloud.nan.builders/r/7GK06FX8) (user settings → API Keys; referral link).
|
|
57
63
|
|
|
58
|
-
## MCP
|
|
64
|
+
## 🔌 MCP Bridges
|
|
59
65
|
|
|
60
|
-
pi
|
|
66
|
+
Since [pi does not include a built-in MCP client](https://github.com/earendil-works/pi/blob/main/docs/usage.md), this package bridges MCP servers as **native pi tools**.
|
|
61
67
|
|
|
62
|
-
Both bridges are **enabled and lazy by default
|
|
68
|
+
Both bridges are **enabled and lazy by default**. Use `/nan-mcp` to manage them.
|
|
63
69
|
|
|
64
|
-
|
|
65
|
-
|---|---|
|
|
66
|
-
| `/nan-mcp status` | State of both bridges and where each toggle comes from (env / persisted / default) |
|
|
67
|
-
| `/nan-mcp enable [target]` | Enable a bridge — or both when no target is given — and persist it in `<agentDir>/nan-provider.json` (e.g. `~/.pi/agent/nan-provider.json`); tools register immediately for the current session |
|
|
68
|
-
| `/nan-mcp disable [target]` | Disable persistently; pi has no `unregisterTool`, so already-registered tools remain until restart, future sessions skip them |
|
|
70
|
+
### 🛠️ Management Command: `/nan-mcp`
|
|
69
71
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
| Command | Effect |
|
|
73
|
+
| :--- | :--- |
|
|
74
|
+
| `/nan-mcp status` | Shows current state of both bridges. |
|
|
75
|
+
| `/nan-mcp enable [target]` | Enables `web-search` or `nan-mcp-server` (persisted). |
|
|
76
|
+
| `/nan-mcp disable [target]` | Disables a bridge persistently. |
|
|
75
77
|
|
|
76
|
-
|
|
78
|
+
---
|
|
77
79
|
|
|
78
|
-
|
|
80
|
+
### 1. Official NaN MCP Server
|
|
81
|
+
*Official bridge for remote tools via [https://api.nan.builders/mcp](https://nan.builders/docs/api).*
|
|
79
82
|
|
|
80
|
-
|
|
83
|
+
- **`nan_web_search(query, ...)`**: Performs web searches through NaN's gateway.
|
|
81
84
|
|
|
82
|
-
|
|
85
|
+
### 2. Community Media MCP Server
|
|
86
|
+
*Bridges [`nan-mcp-server`](https://github.com/luciferfran/nan-mcp-server) via a minimal local stdio client.*
|
|
83
87
|
|
|
84
|
-
- **
|
|
85
|
-
- **
|
|
86
|
-
- **Config**: `NAN_API_KEY` is forwarded automatically (same key as the provider); generated files land in `~/nan-mcp-output/` (the server's default, override with `NAN_OUTPUT_DIR`).
|
|
88
|
+
- **Lazy Loading**: The server process is spawned **only** when a tool is invoked and terminated immediately after.
|
|
89
|
+
- **Config**: Files land in `~/nan-mcp-output/`.
|
|
87
90
|
|
|
88
91
|
| Tool | Purpose |
|
|
89
|
-
|
|
90
|
-
| `nan_generate_image
|
|
91
|
-
| `nan_edit_image
|
|
92
|
-
| `nan_text_to_speech
|
|
93
|
-
| `nan_list_voices
|
|
94
|
-
| `nan_speech_to_text
|
|
92
|
+
| :--- | :--- |
|
|
93
|
+
| `nan_generate_image` | Image generation (flux-2-klein) |
|
|
94
|
+
| `nan_edit_image` | Image-to-image editing (flux-2-klein) |
|
|
95
|
+
| `nan_text_to_speech` | Audio synthesis (kokoro) |
|
|
96
|
+
| `nan_list_voices` | List available voices |
|
|
97
|
+
| `nan_speech_to_text` | Audio transcription (whisper) |
|
|
95
98
|
|
|
96
|
-
|
|
99
|
+
#### 🔧 Media Bridge Configuration
|
|
97
100
|
|
|
98
|
-
| Variable | Default |
|
|
99
|
-
|
|
100
|
-
| `NAN_MEDIA_MCP` | — | Per-session override
|
|
101
|
-
| `NAN_MEDIA_MCP_VERSION` | `1.0.8` | Pinned server version
|
|
102
|
-
| `NAN_MEDIA_MCP_COMMAND` | — |
|
|
101
|
+
| Variable | Default | Description |
|
|
102
|
+
| :--- | :--- | :--- |
|
|
103
|
+
| `NAN_MEDIA_MCP` | — | Per-session override (`0` or `false` to disable). |
|
|
104
|
+
| `NAN_MEDIA_MCP_VERSION` | `1.0.8` | Pinned server version (recommended). |
|
|
105
|
+
| `NAN_MEDIA_MCP_COMMAND` | — | Custom command override. |
|
|
106
|
+
| `NAN_MEDIA_MCP_TIMEOUT_MS` | `120000` | Per-call timeout. |
|
|
107
|
+
| `NAN_MCP_TOOLS` | — | Override for the official bridge (`0` to disable). |
|
|
103
108
|
|
|
104
|
-
#### Automated
|
|
109
|
+
#### 🤖 Automated Update Detection
|
|
105
110
|
|
|
106
|
-
A newer `nan-mcp-server` release won't silently drift this bridge's pin. The scheduler
|
|
107
|
-
(`.github/workflows/check-nan-mcp-server-update.yml`) runs `bun run scripts/check-nan-mcp-server.ts`
|
|
108
|
-
weekly and, when the npm registry shows a newer version, opens (or refreshes) one
|
|
109
|
-
`dependencies`-labelled issue describing whether the bump is **breaking** or **safe**, plus the
|
|
110
|
-
upstream commit list. Run it locally any time:
|
|
111
|
+
A newer `nan-mcp-server` release won't silently drift this bridge's pin. The scheduler (`.github/workflows/check-nan-mcp-server-update.yml`) runs weekly and, when a newer version is found, opens an issue describing if the bump is **breaking** or **safe**.
|
|
111
112
|
|
|
112
113
|
```bash
|
|
113
114
|
bun run check-nan-mcp-server # human-readable report
|
|
114
115
|
bun run check-nan-mcp-server --json # machine-readable JSON
|
|
115
|
-
bun run check-nan-mcp-server --issue # create/refresh the issue
|
|
116
|
+
bun run check-nan-mcp-server --issue # create/refresh the issue
|
|
116
117
|
```
|
|
117
118
|
|
|
118
|
-
|
|
119
|
-
latest server still exposes every bridged tool (`generate_image`, `edit_image`, `text_to_speech`,
|
|
120
|
-
`list_voices`, `speech_to_text`), the bump is reported as **non-breaking**; if it drops or renames a
|
|
121
|
-
bridged tool, the issue is flagged **breaking** for manual review before bumping.
|
|
122
|
-
| `NAN_MEDIA_MCP_TIMEOUT_MS` | `120000` | Per-call timeout; the process is killed after it |
|
|
123
|
-
| `NAN_MCP_TOOLS` | — | Per-session override for the official bridge: `0`/`false`/`off` disables `nan_web_search`; unset → persisted/default |
|
|
124
|
-
|
|
125
|
-
## Models
|
|
126
|
-
|
|
127
|
-
Baseline catalog (from models.dev, provider `nan`, fetched 2026-09-07 and corrected against [NaN's docs](https://nan.builders/docs/models) and [openapi.json](https://nan.builders/openapi.json) — NaN's *served* limits, not raw model maxima):
|
|
128
|
-
|
|
129
|
-
| Model | Context | Max output | Input | Reasoning |
|
|
130
|
-
|---|---|---|---|---|
|
|
131
|
-
| `qwen3.6` | 262,144 | 65,536 | text, image | yes |
|
|
132
|
-
| `gemma4` | 262,144 | 32,768 | text, image | yes |
|
|
133
|
-
| `deepseek-v4-flash` | 1,000,000 | 384,000 | text, image | yes |
|
|
134
|
-
| `mimo-v2.5` | 1,048,576 | 131,072 | text, image | yes |
|
|
135
|
-
| `glm5.3-flash` | 1,000,000 | 131,072 | text, image | yes |
|
|
136
|
-
| `qwen3.8-flash` | 262,144 | 131,072 | text, image | yes |
|
|
137
|
-
|
|
138
|
-
Notes (recorded per entry in `scripts/models.generated.ts`):
|
|
139
|
-
|
|
140
|
-
- `qwen3.8-flash` serves 262K tokens, "the model's native window" ([NaN docs](https://nan.builders/docs/models), 2026-09-07). An earlier 1M override (maintainer-confirmed 2026-09-05) was withdrawn once the updated docs still said 262K; models.dev agrees at 262,144. Divergences like this are recorded as build-time `MANUAL_OVERRIDES` (with provenance) in `scripts/manual-overrides.ts` — apply one instead of editing the generated file.
|
|
141
|
-
- `deepseek-v4-flash` includes image input because NaN serves the Vision-Exp variant ([NaN docs](https://nan.builders/docs/models), confirmed by the vision content-parts in [openapi.json](https://nan.builders/openapi.json)); models.dev lists text only.
|
|
142
|
-
- `glm5.2` was removed by NaN (2026-09-05). models.dev still listed it on 2026-09-07, so the generator excludes it via `PROVIDER_REMOVED_MODEL_IDS` with a recorded reason — a regeneration must not resurrect provider-removed models.
|
|
143
|
-
- `mimo-v2.5` is omnimodal (text/image/audio) on NaN, but pi's model type only represents text/image input, so audio is dropped from `input`.
|
|
144
|
-
- NaN bills via membership quota, which models.dev reports as zero per-token cost — pi's cost display will read $0.
|
|
145
|
-
- Compat (`supportsDeveloperRole: false`, `supportsReasoningEffort: true`, `supportsUsageInStreaming: true`, `maxTokensField: "max_tokens"`) matches the battle-tested LiteLLM config this package replaces; NaN's docs example (`supportsDeveloperRole: true`) is not battle-tested.
|
|
146
|
-
- **Strict schema conformance**: NaN validates every `/chat/completions` payload against its own strict schema ([openapi.json](https://nan.builders/openapi.json)) and returns HTTP 400 `Invalid request. Check your request parameters.` for disallowed shapes — e.g. a replayed `assistant` message with a `toolCall` block inside `content`, an OpenAI-only `reasoning_details` field, undocumented top-level fields like `store` / `stream_options`, or an **empty `tools: []` array**. Every request is therefore rewritten by a provider-side sanitizer (`src/openai-compat-sanitizer.ts`, wired through the shared factory's `onPayload`) so it is always schema-valid no matter which pi-ai version is bundled:
|
|
147
|
-
- **Tier/quota**: which models you can call is decided by your NaN membership. With a key, the live fetch reflects exactly that (see *How it works* — tier detection). The premium-tier `glm5.3` is absent from the models.dev `nan` provider and no source documents its max output tokens, so it is not in the static catalog (flagged as unemittable in the catalog metadata); premium keys still get it live via the `/models` refresh with conservative placeholder limits (128K context / 4K output). Only `glm5.3-flash` is in the static catalog.
|
|
148
|
-
|
|
149
|
-
### Relationship to `~/.pi/agent/models.json`
|
|
150
|
-
|
|
151
|
-
This package replaces the hand-written `nan` block in `~/.pi/agent/models.json` (the NaN docs [pi example](https://nan.builders/docs/examples)). If you keep that block, be aware that **models.json overrides compose above registered providers** — the static file wins over this package. Remove the `nan` entry from `models.json` (keep `defaultProvider`/`defaultModel` in `settings.json` if you use them) to use the live catalog from this package. Per-request output caps can still be set there or via model `params`.
|
|
152
|
-
|
|
153
|
-
## pi version compatibility
|
|
119
|
+
---
|
|
154
120
|
|
|
155
|
-
|
|
121
|
+
## 📊 Models
|
|
156
122
|
|
|
157
|
-
|
|
158
|
-
- **Legacy fallback**: if the native Provider overload is rejected (or provider construction fails), registration falls back to the documented legacy `(name, config)` form with the same generated catalog and `$NAN_API_KEY` env auth (stored-credential auth is a limitation of the legacy path, not a silent behavior change).
|
|
159
|
-
- **MCP bridges**: skipped entirely on runtimes without `registerTool`; providers still register.
|
|
160
|
-
- **Async entrypoint**: pi awaits extension factories on 0.83 and 0.84 alike, so the streaming-API resolution at registration is transparent.
|
|
161
|
-
- `peerDependencies` is `>=0.83.0` with no upper bound (0.83 forks included).
|
|
162
|
-
- **Extension-side pi-ai imports**: only the bare `@earendil-works/pi-ai` root is imported statically. pi's extension loader aliases that specifier to the compat entrypoint; subpath imports (e.g. `@earendil-works/pi-ai/api/openai-completions.lazy`) get the alias applied as a prefix and fail to resolve, which is a whole-extension load failure. Guarded by `test/extension-load.test.ts`.
|
|
123
|
+
Baseline catalog (verified against [NaN docs](https://nan.builders/docs/models) and [OpenAPI](https://nan.builders/openapi.json)).
|
|
163
124
|
|
|
164
|
-
|
|
125
|
+
| Model | Context | Max Output | Input | Reasoning |
|
|
126
|
+
| :--- | :--- | :--- | :--- | :---: |
|
|
127
|
+
| `qwen3.6` | 262,144 | 65,536 | text, image | ✅ |
|
|
128
|
+
| `gemma4` | 262,144 | 32,768 | text, image | ✅ |
|
|
129
|
+
| `deepseek-v4-flash` | 1,000,000 | 384,000 | text, image | ✅ |
|
|
130
|
+
| `mimo-v2.5` | 1,048,576 | 131,072 | text, image | ✅ |
|
|
131
|
+
| `glm5.3-flash` | 1,000,000 | 131,072 | text, image | ✅ |
|
|
132
|
+
| `qwen3.8-flash` | 262,144 | 131,072 | text, image | ✅ |
|
|
165
133
|
|
|
166
|
-
|
|
134
|
+
---
|
|
167
135
|
|
|
168
|
-
## Development
|
|
136
|
+
## 🚀 Development
|
|
169
137
|
|
|
170
138
|
```bash
|
|
171
139
|
bun install
|
|
172
|
-
bun run generate-models #
|
|
173
|
-
bun test #
|
|
174
|
-
bun run typecheck #
|
|
140
|
+
bun run generate-models # Regenerate fallback catalog
|
|
141
|
+
bun test # Run all tests
|
|
142
|
+
bun run typecheck # Run typechecking
|
|
175
143
|
```
|
|
176
144
|
|
|
177
|
-
|
|
145
|
+
*Releases follow strict semver. CI publishes automatically on merge to `main`.*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gtrabanco/pi-nan-provider",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
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",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// This file is auto-generated by scripts/generate-models.ts
|
|
2
2
|
// Do not edit manually — run `bun run generate-models` to update.
|
|
3
3
|
//
|
|
4
|
-
// Source: https://models.dev/api.json (provider "nan"), fetched 2026-09-
|
|
4
|
+
// Source: https://models.dev/api.json (provider "nan"), fetched 2026-09-11T11:47:14.840Z
|
|
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.
|
|
@@ -381,7 +381,7 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
|
|
|
381
381
|
export const GENERATED_CATALOG_META = {
|
|
382
382
|
source: "https://models.dev/api.json",
|
|
383
383
|
modelsDevProvider: "nan",
|
|
384
|
-
fetchedAt: "2026-09-
|
|
384
|
+
fetchedAt: "2026-09-11T11:47:14.840Z",
|
|
385
385
|
modelCount: 6,
|
|
386
386
|
models: ["deepseek-v4-flash","gemma4","glm5.3-flash","mimo-v2.5","qwen3.6","qwen3.8-flash"],
|
|
387
387
|
notes: [
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-model thinking guard.
|
|
3
|
+
*
|
|
4
|
+
* Root cause this guards against (pi-ai, still present on 0.85.1 / main): when
|
|
5
|
+
* history is replayed into a DIFFERENT model, `transformMessages` downgrades
|
|
6
|
+
* every non-redacted `thinking` block to a plain `text` block verbatim
|
|
7
|
+
* (`packages/ai/src/api/transform-messages.ts`), and `openai-completions`
|
|
8
|
+
* serializes that into the assistant `content` string. Nothing bounds
|
|
9
|
+
* `block.thinking`, and nothing bounds the SUM across messages.
|
|
10
|
+
*
|
|
11
|
+
* Measured on real sessions: replayed reasoning is 30–60% of the whole context
|
|
12
|
+
* (e.g. 356,723 of 903,464 chars in one session; 349,882 of 749,525 in another).
|
|
13
|
+
* Switching from a 1M-context model (glm5.3-flash, deepseek-v4-flash) to a
|
|
14
|
+
* 262K-context one (`qwen3.6`) then overflows the window, and NaN's gateway
|
|
15
|
+
* answers a generic `400 Invalid request. Check your request parameters.`
|
|
16
|
+
*
|
|
17
|
+
* A per-block cap was tried first and is NOT enough: many medium blocks sum to
|
|
18
|
+
* hundreds of thousands of tokens. This guard therefore DROPS every replayed
|
|
19
|
+
* cross-model `thinking` block outright — it is the reasoning trace that
|
|
20
|
+
* pi-ai would have replayed as plain text, not the model's answers or tool
|
|
21
|
+
* results, so qwen can still answer questions about what glm/deepseek did.
|
|
22
|
+
* Same-model reasoning is never touched (signatures and continuity depend on
|
|
23
|
+
* it), and the guard only acts on requests targeting this package's providers.
|
|
24
|
+
*
|
|
25
|
+
* The extension runs in pi's `context` hook, which fires BEFORE pi-ai's
|
|
26
|
+
* `transformMessages` (pi-agent-core `transformContext` → `convertToLlm` →
|
|
27
|
+
* provider stream). It therefore sees the original `thinking` blocks and can
|
|
28
|
+
* remove them before they become text.
|
|
29
|
+
*
|
|
30
|
+
* This is a bounded mitigation, not the fix: the unbounded conversion (and pi
|
|
31
|
+
* not re-checking the context size on a model switch) belongs upstream. See
|
|
32
|
+
* https://github.com/gtrabanco/pi-nan-provider/issues/3.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/** Env var that opts out of the guard (`0`, `false`, `no` or `off`). Default: enabled. */
|
|
36
|
+
export const NAN_THINKING_GUARD_ENV = "NAN_THINKING_GUARD";
|
|
37
|
+
|
|
38
|
+
export interface CrossModelThinkingGuardOptions {
|
|
39
|
+
/** Provider ids this guard applies to (the caller's registered providers). */
|
|
40
|
+
providerIds: ReadonlySet<string>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface GuardTarget {
|
|
44
|
+
provider?: string;
|
|
45
|
+
api?: string;
|
|
46
|
+
id?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface ThinkingBlock {
|
|
50
|
+
type?: string;
|
|
51
|
+
[k: string]: unknown;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface GuardMessage {
|
|
55
|
+
role?: string;
|
|
56
|
+
provider?: string;
|
|
57
|
+
api?: string;
|
|
58
|
+
model?: string;
|
|
59
|
+
content?: unknown;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Explicit opt-out only: anything other than a known falsy value keeps the guard on. */
|
|
63
|
+
export function crossModelThinkingGuardEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
64
|
+
const value = env[NAN_THINKING_GUARD_ENV]?.trim().toLowerCase();
|
|
65
|
+
return value !== "0" && value !== "false" && value !== "no" && value !== "off";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isGuardMessage(value: unknown): value is GuardMessage {
|
|
69
|
+
return typeof value === "object" && value !== null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isThinkingBlock(value: unknown): value is ThinkingBlock {
|
|
73
|
+
return typeof value === "object" && value !== null && (value as ThinkingBlock).type === "thinking";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Remove every `thinking` block from assistant messages produced by a model
|
|
78
|
+
* other than the target. Returns the SAME array reference when nothing changed,
|
|
79
|
+
* so callers can skip cloning on the common path. Never mutates the input.
|
|
80
|
+
*/
|
|
81
|
+
export function stripCrossModelThinking<T>(
|
|
82
|
+
messages: readonly T[],
|
|
83
|
+
target: GuardTarget | undefined,
|
|
84
|
+
options: CrossModelThinkingGuardOptions,
|
|
85
|
+
): readonly T[] {
|
|
86
|
+
if (!target?.provider || !options.providerIds.has(target.provider)) return messages;
|
|
87
|
+
|
|
88
|
+
let changed = false;
|
|
89
|
+
|
|
90
|
+
const next = messages.map((raw) => {
|
|
91
|
+
if (!isGuardMessage(raw) || raw.role !== "assistant" || !Array.isArray(raw.content)) return raw;
|
|
92
|
+
|
|
93
|
+
// Same-model replay keeps reasoning intact: signatures and continuity depend on it.
|
|
94
|
+
const isSameModel = raw.provider === target.provider && raw.api === target.api && raw.model === target.id;
|
|
95
|
+
if (isSameModel) return raw;
|
|
96
|
+
|
|
97
|
+
const content = (raw.content as unknown[]).filter((block) => !isThinkingBlock(block));
|
|
98
|
+
if (content.length === (raw.content as unknown[]).length) return raw;
|
|
99
|
+
changed = true;
|
|
100
|
+
return { ...raw, content };
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return changed ? next : messages;
|
|
104
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -25,9 +25,13 @@
|
|
|
25
25
|
* audio/image/transcription is actually invoked.
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
-
import type { ExtensionAPI, ProviderConfig } from "@earendil-works/pi-coding-agent";
|
|
28
|
+
import type { ContextEvent, ExtensionAPI, ProviderConfig } from "@earendil-works/pi-coding-agent";
|
|
29
29
|
import type { Provider } from "@earendil-works/pi-ai";
|
|
30
30
|
import { registerNanMcpCommand } from "./commands.ts";
|
|
31
|
+
import {
|
|
32
|
+
crossModelThinkingGuardEnabled,
|
|
33
|
+
stripCrossModelThinking,
|
|
34
|
+
} from "./cross-model-thinking-guard.ts";
|
|
31
35
|
import { baselineModels } from "./fetch-models.ts";
|
|
32
36
|
import { createNanWebSearchTool, webSearchBridgeEnabled, NAN_API_KEY_ENV } from "./mcp/nan-search.ts";
|
|
33
37
|
import { createNanMediaTools, mediaMcpEnabled } from "./mcp/nan-media.ts";
|
|
@@ -108,7 +112,35 @@ function registerMcpToolsCompat(pi: ExtensionAPI): void {
|
|
|
108
112
|
}
|
|
109
113
|
}
|
|
110
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Drop the reasoning pi-ai replays across a model switch.
|
|
117
|
+
*
|
|
118
|
+
* pi-ai's `transformMessages` downgrades a previous model's `thinking` blocks
|
|
119
|
+
* to plain text with no size bound (still true on 0.85.1 / main), and nothing
|
|
120
|
+
* bounds the sum across messages — measured at 30–60% of the whole context on
|
|
121
|
+
* real sessions. Switching from a 1M-context model to a 262K one (`qwen3.6`)
|
|
122
|
+
* then overflows the window, and NaN's gateway answers a generic
|
|
123
|
+
* `400 Invalid request. Check your request parameters.` This hook runs before
|
|
124
|
+
* pi-ai converts the blocks, so removing the cross-model reasoning here keeps
|
|
125
|
+
* the replayed context small. Answers and tool results are untouched. See
|
|
126
|
+
* src/cross-model-thinking-guard.ts.
|
|
127
|
+
*
|
|
128
|
+
* Scope: only requests targeting this package's providers are touched, and
|
|
129
|
+
* only messages from a DIFFERENT model — same-model reasoning is never removed.
|
|
130
|
+
*/
|
|
131
|
+
export function registerCrossModelThinkingGuard(pi: ExtensionAPI): void {
|
|
132
|
+
if (typeof pi.on !== "function") return; // old pi without the context hook
|
|
133
|
+
const providerIds = new Set(PROVIDERS.map((provider) => provider.id));
|
|
134
|
+
pi.on("context", (event, ctx) => {
|
|
135
|
+
if (!crossModelThinkingGuardEnabled()) return;
|
|
136
|
+
const guarded = stripCrossModelThinking(event.messages, ctx.model, { providerIds });
|
|
137
|
+
if (guarded === event.messages) return;
|
|
138
|
+
return { messages: guarded as ContextEvent["messages"] };
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
111
142
|
export default async function nanProviderExtension(pi: ExtensionAPI): Promise<void> {
|
|
143
|
+
registerCrossModelThinkingGuard(pi);
|
|
112
144
|
for (const config of PROVIDERS) {
|
|
113
145
|
await registerProviderCompat(pi, config);
|
|
114
146
|
}
|