# @gtrabanco/𝜋-nan-provider

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Version](https://img.shields.io/badge/version-1.5.4-blue)](https://github.com/gtrabanco/pi-nan-provider/releases)
[![Built with NaN](https://nan.builders/badge/built-with-nan-compact.svg)](https://cloud.nan.builders/r/7GK06FX8)

[NaN Builders](https://cloud.nan.builders/r/7GK06FX8) model provider + MCP nativo para [pi](https://github.com/earendil-works/pi). 

Registra el proveedor `nan` vía `pi.registerProvider()` usando la API OpenAI-compatible de NaN (`https://api.nan.builders/v1`), y registra el servidor MCP de media de la comunidad con `pi.registerMcpServer()`.

> [!NOTE]
> **Requisitos:** pi **≥ 1.4.0** (rango peer `^1.4.0`). El paquete está verificado contra **pi 1.5.1 / pi-ai 1.5.1**; el floor del rango peer solo sube con los re-aligns de menor de pi.

## ✨ Funciones

| Área | Qué obtienes |
| :--- | :--- |
| **Catálogo de modelos en dos capas** | Fallback generado en build-time (pi arranca aunque no haya red) fusionado con la lista en vivo `GET /v1/models`; después `filterModels` oculta lo que tu clave no pueda usar. |
| **Overlay de catálogo en tiempo de lectura** | Los datos de capacidad se reaplican desde el catálogo generado en cada lectura de `getModels()` / `getAllModels()`, así que un `models-store.json` escrito por una versión anterior del paquete no puede eclipsarlos ([#19](https://github.com/gtrabanco/pi-nan-provider/issues/19)) ni volver a registrar ids no-chat como modelos de chat ([#32](https://github.com/gtrabanco/pi-nan-provider/issues/32)). La store decide *qué* ids existen; el catálogo generado decide *qué pueden hacer*. |
| **Guardia de endpoints no-chat** | `NON_CHAT_MODEL_IDS` mantiene los ids de embedding / rerank / TTS / STT / imagen / vídeo fuera del selector de chat de pi. |
| **Sanitizador de payloads estricto** | Cuerpo de `/chat/completions` reescrito para cumplir [el esquema publicado de NaN](https://nan.builders/openapi.json) antes de salir de pi, sea cual sea la versión de pi-ai incluida. |
| **Rescate de desbordamiento de contexto** | El `400 Invalid request` genérico de NaN se reclasifica como desbordamiento cuando la petición se estima por encima de la ventana del modelo ([#3](https://github.com/gtrabanco/pi-nan-provider/issues/3)), y un `524` de caché fría por encima de `NAN_COLD_CACHE_CEILING_TOKENS` también ([#18](https://github.com/gtrabanco/pi-nan-provider/issues/18)) — pi compacta y reintenta en vez de quedarse colgado. |
| **Reintento de streams truncados** | `supportsFinishReason: true` convierte un stream SSE cerrado antes de `finish_reason` en un error reintentable en vez de un stall silencioso ([#2](https://github.com/gtrabanco/pi-nan-provider/issues/2)). |
| **Uso de tokens en streaming** | `stream_options.include_usage` activo por defecto, así que pi reporta recuentos reales de entrada/salida/razonamiento/caché ([#4](https://github.com/gtrabanco/pi-nan-provider/issues/4), [#7](https://github.com/gtrabanco/pi-nan-provider/issues/7)). |
| **Controles de razonamiento** | `reasoningEffortValues` + `thinkingLevelMap` por modelo ([#16](https://github.com/gtrabanco/pi-nan-provider/issues/16)), el guard de stream solo-razonamiento ([#21](https://github.com/gtrabanco/pi-nan-provider/issues/21)) y el límite provisional `max_completion_tokens` para `qwen3.8-flash` ([#34](https://github.com/gtrabanco/pi-nan-provider/issues/34)). |
| **Cabecera de atribución** | `X-Pi-Provider: gtrabanco/pi-nan-provider@<version>` en cada petición, eliminada cuando la telemetría de instalación de pi está desactivada. |
| **Modelos de imagen nativos** | `flux-2-klein` y `qwen-image-2.1` registrados como modelos de pi `type: "image"`, accesibles desde codemode y extensiones. |
| **Puente MCP nativo de media** | El servidor `nan-media` (9 herramientas) registrado con `pi.registerMcpServer()`, gestionado con `/nan-mcp`. |
| **Informe de cuotas** | `/nan-usage` fusiona `GET /v1/usage` con los topes mensuales documentados. |
| **Evidencia de anomalías de caché** | Captura de `cf-ray` sobre la firma repetida `id` + `created` del gateway ([#17](https://github.com/gtrabanco/pi-nan-provider/issues/17)) más el recolector determinista `/nan-debug` ([#35](https://github.com/gtrabanco/pi-nan-provider/issues/35)). |
| **Integración con pi** | Refresco de modelos dirigido por el runtime Models de pi, overrides por modelo en `models.json` y el changelog de la extensión (`pi.changelogPath`) que pi muestra tras una actualización. |

Comandos:

| Comando | Propósito |
| :--- | :--- |
| `/nan-mcp` | Gestionar el puente MCP de media (`status` / `enable` / `disable`) |
| `/nan-usage [1-90]` | Uso de tokens por modelo vs. los topes documentados |
| `/nan-debug [tema] [--window N] [--out ruta] [--emit]` | Paquete de evidencia reproducible y redactado para NaN |

---

### ⚡ Inicio Rápido

1. **Consigue tu API Key**: [Reclama tu API key de NaN aquí](https://cloud.nan.builders/r/7GK06FX8) (enlace de referidos).
2. **Instala**:
   ```bash
   pi install npm:@gtrabanco/pi-nan-provider
   ```
3. **Autentica**:
   ```bash
   export NAN_API_KEY="sk-tu-clave-aqui"
   ```
4. **Verifica**:
   ```bash
   pi --list-models nan
   ```

---

**Documentación en español** (este archivo) · [Docs in English](README.md)

## ⚙️ Cómo funciona

El proveedor utiliza un **catálogo de modelos en dos capas más un overlay en tiempo de lectura** para garantizar la fiabilidad:

| Capa | Fuente | Propósito |
| :--- | :--- | :--- |
| **1. Fallback generado** | `scripts/models.generated.ts` | Snapshot en tiempo de build desde [models.dev](https://models.dev) más overrides manuales con procedencia. Asegura que pi siempre pueda arrancar, incluso si la red falla. |
| **2. Fetch en vivo de `/models`** | NaN Runtime API | Obtiene los modelos que tu clave realmente puede llamar y los combina con el fallback; `filterModels` oculta lo que la lista en vivo no devuelve. |
| **3. Overlay en tiempo de lectura** | `withStaleCatalogOverlay` (`src/provider-factory.ts`) | Reaplica los datos de capacidad del catálogo generado en cada lectura de `getModels()` / `getAllModels()`, así que un `models-store.json` escrito por una versión anterior del paquete no puede eclipsarlos ([#19](https://github.com/gtrabanco/pi-nan-provider/issues/19)) ni volver a registrar ids no-chat como modelos de chat ([#32](https://github.com/gtrabanco/pi-nan-provider/issues/32)). |

> [!IMPORTANT]
> **Lista en vivo**: La lista en vivo es la autoridad. Desde el 2026-11-10 NaN ya no tiene modelos exclusivos por tier, así que todos los modelos del catálogo deben estar disponibles para cualquier clave; un modelo que `/models` en vivo no lista para tu clave sigue filtrándose.

Los ids no-chat que devuelve `/models` en vivo (embedding, rerank, TTS, STT, imagen, vídeo) **no se registran** como modelos de chat — la clasificación es el único `NON_CHAT_MODEL_IDS` exportado desde `src/fetch-models.ts` (la misma fuente que usa el generador en build-time). La fusión salta sus marcadores y los coloca en un bucket observable `nonChat`; `baselineModels` también los filtra como capa defensiva, y el overlay en tiempo de lectura elimina las entradas de tipo chat cuyo id está en esa lista — un store antiguo escrito antes de #15 (issue #32) no puede resucitarlos. Esto evita que el selector de chat de pi ofrezca endpoints que no pueden servir chat (las peticiones devolverían 404).

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.

### 🧾 Sanitizador estricto de peticiones

NaN responde HTTP 400 `Invalid request. Check your request parameters.` con cualquier cuerpo que no cumpla el esquema que publica en [nan.builders/openapi.json](https://nan.builders/openapi.json) — un esquema más estricto que lo que pi-ai emite en varios sitios. `src/openai-compat-sanitizer.ts` reescribe cada cuerpo saliente de `/chat/completions`, así que ninguna versión de pi-ai ni ninguna forma de conversación pueden producir una petición inválida:

- un array `content` de asistente que lleva una tool call se mueve al campo superior `tool_calls` (el `ContentPart` de NaN solo acepta `text` y `image_url`);
- se elimina `reasoning_details`, y las partes de contenido desconocidas (p. ej. `thinking`) se pliegan en una parte `text` para que el razonamiento no se pierda en silencio;
- se eliminan los campos que el esquema de NaN no lista: `store` siempre, un array `tools` vacío siempre, y `stream_options` salvo que `compat.supportsUsageInStreaming` del modelo diga que el gateway lo reporta;
- `max_completion_tokens` se renombra a `max_tokens` (el campo documentado) salvo que `compat.maxTokensField` del modelo lo desactive ([#34](https://github.com/gtrabanco/pi-nan-provider/issues/34), `qwen3.8-flash`).

Un hook `onPayload` suministrado por el llamante se conserva y se encadena después de la sanitización.

### 🏷️ Cabecera de atribución

Cada modelo que este paquete registra lleva una cabecera HTTP `X-Pi-Provider: gtrabanco/pi-nan-provider@<version>`, añadida sobre las cabeceras de identidad que ya envíe pi-ai (el `User-Agent` de pi nunca se modifica). Identifica las peticiones que vienen de esta extensión de proveedor — incluida la consulta de cuota de `/nan-usage` — y se omite silenciosamente si no se puede resolver la versión del paquete. Los overrides de usuario en `models.json` que reemplacen un modelo reemplazan también sus cabeceras.

La cabecera **sigue la opción de telemetría de pi**: con `enableInstallTelemetry: false` en `settings.json` la cabecera desaparece de todas las peticiones (se evalúa en vivo, así que cambiar la opción aplica en la siguiente petición sin reiniciar) — la cabecera identifica el paquete en uso, y quien haya desactivado la telemetría no debería ser identificable por ella.

### 🧠 Seguridad al cambiar de modelo

Al cambiar de modelo, versiones antiguas de pi-ai reenviaban el razonamiento del modelo anterior como texto plano de asistente **sin límite de tamaño**, lo que podía desbordar la ventana de un modelo de 262K; NaN respondía con un `400 Invalid request. Check your request parameters.` genérico que parecía un bug del proveedor (seguimiento upstream: [pi-nan-provider#3](https://github.com/gtrabanco/pi-nan-provider/issues/3)). pi-ai 1.3.0 arregló esto upstream (`transformMessages` ahora descarta los bloques de thinking de otro modelo) y este paquete requiere **pi ≥ 1.4.0** (rango peer `^1.4.0`), así que el guard extension-side que lo mitigaba fue **eliminado en la versión 1.4.0 del paquete** — no hay que hacer nada, upstream lo gestiona.

Una petición puede seguir desbordando por méritos propios — tool outputs enormes, imágenes o una ventana de destino simplemente más pequeña que el contenido — y NaN responde con el mismo 400 genérico en lugar de nombrar el desbordamiento, mientras que la auto-compactación de pi no lo reconoce, así que la sesión se quedaría atascada en el techo. Por eso el proveedor **vuelve a comprobar el tamaño de la petición a la salida**: cuando llega ese 400 genérico para una petición estimada por encima de la ventana del modelo, el error se reescribe como un mensaje de context overflow que pi reconoce, de modo que compacta y reintenta en vez de bloquearse. Un 400 genérico en una petición dentro de la ventana no se toca, así que nunca se etiquetan mal errores no relacionados.

### ⏱️ Streams truncados intermitentes (auto-retry, sin stall silencioso)

El gateway LiteLLM de NaN cierra ocasionalmente un stream SSE **antes** de emitir el chunk final `finish_reason` (observado en `glm5.3-flash`; [issue #2](https://github.com/gtrabanco/pi-nan-provider/issues/2)). El catálogo declara `supportsFinishReason: true`, así que pi-ai lo convierte en el error `Stream ended without finish_reason` — que coincide con el patrón de errores reintentables de pi y se **reintenta automáticamente**, en vez de aceptar en silencio una respuesta a medias. Si una versión del gateway nunca manda `finish_reason`, el turno ahora falla de forma visible al agotar los reintentos.

Puedes sobrescribir el `compat` de cualquier modelo en `~/.pi/agent/models.json` (docs de pi → Per-model Overrides); los overrides se componen por encima del proveedor registrado. Ejemplo (forzando el comportamiento de retry explícitamente):

```json
{
  "providers": {
    "nan": {
      "modelOverrides": {
        "glm5.3-flash": { "compat": { "supportsFinishReason": true } }
      }
    }
  }
}
```

> Poner `supportsFinishReason: false` restaura el antiguo stall silencioso — no recomendado.

**Escape hatches de compatibilidad de pi-ai 1.5.1** (`~/.pi/agent/models.json`, por modelo): `compat.requiresToolsWithToolHistory: true` restaura el envío de `tools: []` en conversaciones con historial de tools (pi-ai 1.5.1 lo omite por defecto, arreglando los rechazos `tools must not be an empty array` de vLLM 0.20+ detrás de LiteLLM); `compat.requiresNonNullAssistantContent: true` envía `""` en vez de `null` en mensajes de asistente que solo llevan tool calls; `compat.promptCacheKeyMode: "disabled"` garantiza que nunca se envíe `prompt_cache_key` al gateway (el valor por defecto `auto` ya nunca lo activa para NaN).

**Uso de tokens en streaming:** `supportsUsageInStreaming` es `true` por defecto. El esquema publicado de NaN no documenta `stream_options`, pero el gateway real lo acepta y lo aplica — medido el 2026-09-16 ([#7](https://github.com/gtrabanco/pi-nan-provider/issues/7)): dos llamadas de streaming idénticas por modelo, 0 chunks de usage sin el flag y exactamente 1 con él, en `deepseek-v4-flash`, `glm5.3-flash`, `qwen3.6`, `gemma4` y `mimo-v2.5` (ya eliminado). Por eso pi muestra tokens reales de entrada/salida/reasoning/caché en lugar de ceros. Si un modelo resulta no devolver el usage en streaming, desactívalo por modelo — el sanitizer entonces elimina `stream_options` y el payload vuelve a ser estricto:

```json
{
  "providers": {
    "nan": {
      "modelOverrides": {
        "some-model": { "compat": { "supportsUsageInStreaming": false } }
      }
    }
  }
}
```

## 🔑 Autenticación

`resolve()` comprueba primero la credencial almacenada y después recurre a la variable de entorno correspondiente.

| Método | Comando / Acción | Notas |
| :--- | :--- | :--- |
| **Var de Entorno** | `export NAN_API_KEY="..."` | Lo más rápido para desarrollo local. |
| **`/login`** | `pi > /login nan` | Persistente; se guarda en `~/.pi/agent/auth.json`. |
| **Config Manual** | Editar `~/.pi/agent/auth.json` | Manipulación directa de JSON. |

Consigue una clave en la [plataforma NaN](https://cloud.nan.builders/r/7GK06FX8) (ajustes de usuario → API Keys; enlace de referidos).

## 🔌 MCP nativo

Dado que [pi 0.99.0 incluye un cliente MCP integrado](https://github.com/earendil-works/pi/blob/main/docs/usage.md), este paquete usa MCP nativo (requiere **pi >= 1.4.0**, el floor del rango peer desde la versión 1.5.0 del paquete).

El puente de media está **activado por defecto** (por sesión, visible en `/mcp`). Usa `/nan-mcp` para gestionarlo.

> [!WARNING]
> **El servidor MCP remoto oficial de NaN ya no expone ninguna herramienta.** Hasta 1.2.0 este paquete
> incluía un puente `nan-search` apuntando a `https://api.nan.builders/mcp`, que exponía
> `web_search`. Ese servidor ahora responde a `initialize` con `capabilities.tools` vacío y a
> `tools/list` con `{"tools":[]}` — verificado con una clave válida el 2026-10-15 — y NaN eliminó toda
> mención a MCP de su documentación (la antigua página `/docs/mcp` ahora redirige a `/docs/agent-setup`).
> El puente se eliminó en **1.2.1** porque registrar un servidor que no expone nada solo produce un
> servidor sin herramientas en `/mcp`.
>
> **Trae tu propio `web_search` y fetch.** Instala un servidor MCP que los proporcione (p. ej. el par
> `fetch` + `brave-search` de los servidores MCP oficiales, o cualquier otro servidor MCP de
> búsqueda/fetch) y decláralo en tu `mcp.json` global:
>
> ```json
> {
>   "mcpServers": {
>     "fetch": { "command": "uvx", "args": ["mcp-server-fetch"] },
>     "web-search": {
>       "command": "npx",
>       "args": ["-y", "@modelcontextprotocol/server-brave-search"],
>       "env": { "BRAVE_API_KEY": "tu-clave" }
>     }
>   }
> }
> ```
>
> Vuelve a comprobar `https://api.nan.builders/mcp` periódicamente: si NaN publica herramientas otra
> vez, este paquete puede restaurar el puente.

### 🛠️ Comando de Gestión: `/nan-mcp`

| Comando | Efecto |
| :--- | :--- |
| `/nan-mcp status` | Muestra el estado del puente de media y la resolución de la clave API. |
| `/nan-mcp enable [target]` | Activa `nan-mcp-server` (persiste). |
| `/nan-mcp disable [target]` | Lo desactiva de forma persistente. |

El puente resuelve la credencial almacenada de `/login nan` (almacenada → env). Si ninguna se resuelve, el servidor no se registra y se muestra un aviso en la salida de estado.

> [!NOTE]
> El registro ocurre cuando se carga la extensión. Si ejecutas `/login nan` **después** de que la sesión haya empezado, la factoría ya se ejecutó — usa `/reload` (o `/nan-mcp enable`) para registrar los puentes sin reiniciar pi.

### 🚧 Hosts sin la extensión MCP integrada de pi (PI WEB)

pi conecta los servidores registrados desde la extensión que maneja `mcp_servers_change` — la
extensión integrada `mcp`, que además registra `/mcp`. Los hosts que construyen el resource loader
de pi sin `extensionFactories` no cargan **ninguna** extensión integrada (verificado:
`createAgentSessionServices` de PI WEB, pi 0.99.1 / pi-web 1.202609.1), así que nada conecta los
servidores y pi reporta `MCP server "nan-media" is registered, but no loaded extension connects MCP
servers`.

Ahí los puentes no pueden exponer herramientas, así que este paquete reemplaza ese error críptico
con un único aviso por sesión, mantiene los registros inactivos y deja que `/nan-mcp enable` persista
el toggle sin registrar un servidor que nunca podrá conectarse. Para tener MCP en la CLI, activa el
conector con `pi config` → Built-in extensions → `mcp`.

### Funciones de pi y modelos NaN

- **Modelos virtuales** — registra un router bajo `nan` (`pi.registerVirtualModel({ provider: "nan", ... })`);
  enruta a los modelos físicos de NaN sin configuración por parte del proveedor.
- **Codemode** — funciona con cualquier modelo NaN que soporte tool calling. Las herramientas MCP de
  este paquete usan `exposure: "direct"`, así que los scripts de codemode también pueden llamarlas.
  Requiere la extensión integrada `codemode` de pi (ver la nota de PI WEB arriba).
- **Modelos clasificadores** — no aplica: NaN no expone ninguna API de clasificación (solo chat y
  endpoints de embedding/rerank/audio/imagen), así que este proveedor no registra modelos
  `type: "classifier"`.

- **Modelos de imagen nativos de NaN** — flux-2-klein (text-to-image + image-to-image) y
  qwen-image-2.1 (text-to-image) se registran como modelos de imagen de pi (`type: "image"`,
  api `"nan-images"`). No aparecen en `/model`; se acceden desde codemode
  con `models.generateImages(model, { input })` o desde extensiones con
  `ctx.modelRegistry.generateImages()`:

  ```
  const painter = await models.getModelOfType("image", "nan", "flux-2-klein")
  const result = await models.generateImages(painter, { input: [{ type: "text", text: "A red fox in the snow, watercolor" }] })
  if (result.stopReason !== "stop") return result.errorMessage
  for (const block of result.output) if (block.type === "image") image(block)
  ```

  Ambos modelos necesitan el tier de membresía "inference" (403 de lo contrario); los endpoints
  de imagen tienen su propio límite de tasa separado del chat (20 peticiones/min, 100
  peticiones/mes compartido entre los dos modelos) y no consumen el presupuesto de tokens del
  chat. [docs de NaN](https://nan.builders/docs/models) y [OpenAPI](https://nan.builders/openapi.json)
  (comprobado 2026-10-01). El servidor MCP nan-media de la comunidad ya ofrece la misma
  generación y edición como herramientas MCP, mientras que los modelos de imagen nativos permiten
  que codemode y las extensiones los llamen directamente.

---

### Servidor MCP de Media (Comunidad)
*Conecta [`nan-mcp-server`](https://github.com/luciferfran/nan-mcp-server) mediante un cliente stdio local mínimo.*

- **Por sesión**: El servidor se conecta al registrarse (eager). Aparece en `/mcp` con fuente "extension".
- **Configuración**: Los archivos se guardan en `~/nan-mcp-output/`.

| Herramienta | Propósito |
| :--- | :--- |
| `mcp__nan_media__generate_image` | Generación de imágenes (flux-2-klein) |
| `mcp__nan_media__edit_image` | Edición imagen→imagen (flux-2-klein) |
| `mcp__nan_media__text_to_speech` | Síntesis de audio (kokoro) |
| `mcp__nan_media__list_voices` | Listar voces disponibles |
| `mcp__nan_media__speech_to_text` | Transcripción de audio (whisper) |
| `mcp__nan_media__list_models` | Listar los ids de modelos que alcanza la key |
| `mcp__nan_media__embed_text` | Embeddings de texto (qwen3-embedding) |
| `mcp__nan_media__rerank_documents` | Reordenación de documentos (Qwen3-Reranker) |
| `mcp__nan_media__get_quota` | Cuota de tokens por modelo (usado / tope / restante / reinicio) |

> pi 0.99.0–0.99.1 usaba la forma con guiones (`mcp__nan-media__*`); pi 0.99.2+ sanitiza los
> nombres de herramientas a solo `[A-Za-z0-9_]`, así que los guiones se convirtieron en guiones
> bajos. La forma con guiones bajos aplica en todas las versiones soportadas (pi >= 1.4.0).

#### 🔧 Configuración del Puente de Media

| Variable | Por defecto | Descripción |
| :--- | :--- | :--- |
| `NAN_MEDIA_MCP` | — | Override por sesión (`0` o `false` para desactivar). |
| `NAN_MEDIA_MCP_VERSION` | `1.3.0` | Versión del servidor fijada (recomendado). |
| `NAN_MEDIA_MCP_COMMAND` | — | Override del comando personalizado. |
| `NAN_MEDIA_MCP_TIMEOUT_MS` | `120000` | Timeout por llamada. |

#### 🤖 Detección Automática de Actualizaciones

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**.

```bash
bun run check-nan-mcp-server            # reporte legible
bun run check-nan-mcp-server --json     # JSON para máquinas
bun run check-nan-mcp-server --issue    # crea/refresca el issue
```

Las actualizaciones del SDK de pi (`@earendil-works/pi-ai` / `@earendil-works/pi-coding-agent`)
también están automatizadas: el workflow semanal `check-pi-sdk-versions` abre el issue
`sdk-monitor` **y** prepara un PR con el bump de devDependencies (`bun test` +
`bun run typecheck` + diff de superficie de tipos incluidos) para revisión humana — ver
`.github/workflows/check-pi-sdk-versions.yml` y `scripts/check-pi-sdk-versions.mjs`.

---

## 📊 Uso de Cuotas: `/nan-usage`

Muestra tu uso de tokens de NaN por modelo combinado con los límites mensuales documentados, los totales de la ventana, tus totales acumulados y el tiempo hasta el reinicio del ciclo de facturación.

### Cómo funciona

`/nan-usage` llama al endpoint [`GET /v1/usage`](https://nan.builders/docs/api#tag/usage) de NaN con **la misma API key que ya usas para chat** — la credencial guardada en pi (`/login nan`) o `NAN_API_KEY`. Sin login de la CLI, sin cookie de sesión, sin configuración extra.

El endpoint devuelve tu uso (filas por fecha y modelo) y los totales de la ventana solicitada — máximo 90 días inclusivos. El comando lee esos totales y los combina con los límites publicados en la [documentación de NaN](https://nan.builders/docs/models) para mostrarte qué porcentaje de cada cuota mensual has consumido. `/usage` solo reporta consumo, así que los límites siempre vienen de la tabla documentada (comprobado 2026-09-27).

Límite de tasa: 30 peticiones por minuto para `/usage`, separado del de los endpoints de modelos.

### Uso

```
/nan-usage        # mes en curso en UTC (coincide con las cuotas mensuales)
/nan-usage 7      # ventana móvil de 7 días (1-90)
/nan-usage 30     # ventana móvil de 30 días
/nan-usage help   # muestra la línea de uso
```

### Configuración

1. **Autentica el proveedor `nan` en pi** con tu clave `sk-...`:
   ```
   /login nan
   ```
   o expórtala en su lugar:
   ```bash
   export NAN_API_KEY=sk-...
   ```
2. **Úsalo en pi**:
   ```
   /nan-usage
   ```

> [!TIP]
> ¿No se resuelve ninguna API key? El comando muestra la tabla estática de cuotas de la documentación y te indica cómo autenticarte.

### Lo que ves

**Con API key** (uso real):
```
📊 NaN Usage

🗓️  Window: 2026-09-01 → 2026-09-27 UTC (27 days)

⏱️  Next billing reset: 2026-09-30 UTC (2d 14h 30m 0s)

Models with monthly caps:

DeepSeek V4 Flash:
  [████████░░░░░░░░░░░░] 40.0% of monthly cap
  Used: 1.2B / 3.0B (1.8B remaining) · 5,120 requests

MiMo V2.5:
  [███░░░░░░░░░░░░░░░░░] 12.5% of monthly cap
  Used: 125.0M / 1.0B (875.0M remaining) · 840 requests

GLM 5.3:
  [████████░░░░░░░░░░░░] 40.0% of monthly cap
  Used: 1.2B / 3.0B (1.8B remaining) · 2,400 requests
  ↳ rolling window: 400.0M / 4h (daily granularity — /usage cannot break it down)

Uncapped models:

Qwen 3.6: 890.5K used · 123 requests

Window totals: 2.6B tokens (2.0B prompt / 640.0M completion) · 18,432 requests
All time: 13.2B tokens · 41,250 requests (cached 2026-02-01)
💡 Source: GET /v1/usage · caps from https://nan.builders/docs/models
```

**Sin API key** (solo límites estáticos):
```
📊 NaN Quota Status (static limits)

⏱️  Next billing reset: 2026-09-30 UTC (2d 14h 30m 0s)

Model                        Monthly Cap
─────────────────────────────────────────────────
DeepSeek V4 Flash            3.0B
MiMo V2.5                    1.0B
MiMo V2.6 Flash              1.0B
Qwen 3.6                     uncapped
Gemma 4                      uncapped
Qwen 3.8 Flash               500.0M
GLM 5.3 Flash                2.0B
GLM 5.3                      3.0B (rolling 400.0M/4h)

💡 Set NAN_API_KEY or run `/login nan` to see real usage (GET /v1/usage).
```

Los errores siguen siendo accionables: `401` → ejecuta `/login nan` o corrige `NAN_API_KEY`, `404` → la cuenta no tiene identidad de uso que reportar, `429` → reintenta tras los segundos de `Retry-After`.

---

## 🧾 Evidencia de anomalías de caché: `/nan-debug`

La caché de respuestas del gateway de NaN puede devolver el mismo `id` y
`created` en peticiones seguidas (la firma del issue #17). NaN pidió el `model`
y el `cf-ray` de un par de llamadas si vuelve a pasar; los transcripts de pi no
guardan cabeceras de respuesta, así que el proveedor las captura.

Cuando dos streams del mismo modelo devuelven el mismo `id` + `created`, se
añade una línea JSON a `<agentDir>/nan-cache-anomalies.jsonl` — `model`, el par
`id`/`created`, los dos `cf-ray`, timestamps ISO y un SHA-256 del payload de la
petición. Es **solo local**: ni contenido de prompt/respuesta, ni credenciales,
ni rutas, ni subida remota. Un fallo de escritura nunca rompe un stream.

`/nan-debug` convierte ese fichero y tus transcripts en un paquete de evidencia
redactado y reproducible para NaN:

```text
/nan-debug                          # últimos 7 días, escribe <agentDir>/nan-debug-report/
/nan-debug cache --window 2         # etiqueta de tema + ventana de 2 días
/nan-debug --window 2 --emit        # además escribe <agentDir>/nan-debug-report.zip
/nan-debug cache --out ~/evidence --emit
```

- `--window N` (1–90 días) consulta además `GET /v1/usage` para el mismo periodo
  con tu clave guardada y escribe `usage_window.json`; sin el flag no se hace
  ninguna petición. Esa llamada en vivo consume el límite de peticiones.
- El zip se escribe **solo** con `--emit`; la ejecución por defecto escribe el
  informe local e imprime un resumen.
- `--out <ruta>` escribe el informe donde lo indiques (y, con `--emit`,
  `<ruta>.zip`); un `~/` inicial se expande a tu directorio home, así que los
  ejemplos de arriba escriben donde dicen. Un token de flag nunca se acepta
  como ruta.
- El informe contiene `README.md`, `anonymized_usage.jsonl` (totales por
  fecha/sesión/modelo, conteo de `stopReason`, tasa de acierto de caché),
  `turns.jsonl` (uso por turno, `stopReason`, clase de error, `responseId`),
  `anomalies.jsonl` (par de `cf-ray`) y `metadata.json` (versiones de paquete,
  pi-ai y pi; procedencia del catálogo).
- Las etiquetas de sesión y worktree son MD5 truncado a 12 caracteres;
  `errorMessage` se reduce a `overflow | rate_limited | invalid_request |
  transport | other`; el contenido de prompt/respuesta y las credenciales nunca
  se recogen. Entradas idénticas producen bytes de zip idénticos.

## 📊 Modelos

Catálogo base (verificado contra [docs de NaN](https://nan.builders/docs/models) y [OpenAPI](https://nan.builders/openapi.json)).

Modelos de chat:

| Modelo | Contexto | Máx. Salida | Entrada | Razonamiento |
| :--- | :--- | :--- | :--- | :---: |
| `qwen3.6` | 262,144 | 65,536 | texto, imagen | ✅ |
| `gemma4` | 262,144 | 32,768 | texto, imagen | ✅ |
| `deepseek-v4-flash` | 1,048,576 | 32,768* | texto, imagen | ✅ |
| `mimo-v2.6-flash` | 1,048,576 | 131,072 | texto, imagen | ✅ |
| `glm5.3-flash` | 1,048,576 | 131,072 | texto, imagen | ✅ |
| `qwen3.8-flash` | 1,048,576 | 131,072 | texto, imagen | ✅ |
| `glm5.3` | 1,048,576 | 131,072 | texto, imagen | ✅ |
| `kimi-k3.1-pro` | 1,048,576 | 32,768 | texto, imagen | ✅ |
| `qwen3.8-max` | 1,000,000 | 32,768 | texto, imagen | ✅ |
| `deepseek-v4-pro` | 1,048,576 | 32,768 | texto | ✅ |
| `hy4` | 1,048,576 | 32,768 | texto, imagen | ✅ |

> 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` fue solo-premium y solo-en-vivo hasta que NaN eliminó los modelos exclusivos por tier el 2026-11-10; ahora está en el catálogo estático con los valores que tenía (texto+imagen, 1,048,576 / 131,072).
>
> `kimi-k3.1-pro`, `qwen3.8-max`, `deepseek-v4-pro` y `hy4` son los modelos de proveedor externo (ZDR) que NaN lanzó el 2026-11-10. Los docs y el OpenAPI de NaN todavía no los listan, pero el `GET /v1/models` en vivo sí (comprobado 2026-11-10). Sus valores salen de la página de precios de NaN ("1M context"), los docs de NaN para pi (`maxTokens` 32,768 en todos los modelos de 1M de contexto) y las entradas de los proveedores originales en models.dev (modalidades; `qwen3.8-max` usa la ventana de 1,000,000 del proveedor); el coste es 0 y no se declara mapeo de `reasoning_effort`, porque no se ha medido cómo lo trata la pasarela con proveedores externos. Cada entrada lleva su nota de procedencia en `scripts/models.generated.ts`.
>
> `*` 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.

> [!NOTE]
> **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).

Modelos de imagen no-chat (disponibles vía `models.generateImages()` /
`getModelOfType("image", …)`, no a través de `/model`):

| Modelo | Entrada | Propósito |
| :--- | :--- | :--- |
| `flux-2-klein` | texto, imagen | Text-to-image + image-to-image |
| `qwen-image-2.1` | texto | Text-to-image |

`minimax-h3` (generación de vídeo, listado por `/models` en vivo desde el 2026-11-10) también se clasifica como no-chat: nunca se registra como modelo de chat.

> [!NOTE]  
> `mimo-v2.6-flash` está servido por NaN y desde el 2026-09-29 aparece también en models.dev (proveedor `nan`); la entrada manual que lo sostenía se conserva como fallback y su nota de procedencia sigue unida a la entrada generada.
>
> `mimo-v2.5` fue eliminado por NaN: ya no aparece ni en [los docs de NaN](https://nan.builders/docs/models) ni en la [lista de modelos del OpenAPI](https://nan.builders/openapi.json) (comprobado 2026-09-29), así que sale del catálogo — lo sustituye `mimo-v2.6-flash` (mismos límites 1,048,576 / 131,072 y cuota mensual de 1.0B). El consumo histórico de `mimo-v2.5` en `/nan-usage` sigue apareciendo en la sección de modelos no documentados.

---

## 🧠 Controles de razonamiento

El parámetro `reasoning_effort` de NaN controla cuánto piensa el modelo antes de responder — pero el grado de control varía por modelo:

| Modelo | Effort de razonamiento | Cómo funciona |
| :--- | :--- | :--- |
| `glm5.3` | `low` · `medium` · `high` · `max` | Completamente controlable — valores más altos permiten al modelo razonar más. `thinking: "off"` envía `low`, el nivel más bajo que listan los docs de NaN, vía el `thinkingLevelMap` del catálogo. `minimal` **no** está entre los valores declarados de este modelo: el issue #16 lo midió solo en `glm5.3-flash`, y `glm5.3` en sí nunca se midió |
| `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 (13,382 tokens), así que `thinking: "off"` envía `minimal`, el valor efectivo más bajo, vía el `thinkingLevelMap` del catálogo |
| `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 |
| `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) |
| `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 |
| `kimi-k3.1-pro`, `qwen3.8-max`, `deepseek-v4-pro`, `hy4` | *(sin mapeo declarado)* | Modelos de proveedor externo lanzados el 2026-11-10; no se ha medido cómo traduce la pasarela `reasoning_effort` para ellos, así que el catálogo no declara valores de effort ni `thinkingLevelMap` (`thinking: "off"` no envía ningún parámetro) |
| `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 |

> [!NOTE]  
> `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) y por NaN para `qwen3.8-flash` (issue #34, 2026-11-11 — el exceso además factura contra la cuota). Como límite provisional para `qwen3.8-flash` el catálogo envía `max_completion_tokens`, que NaN confirmó que SÍ se respeta; sobrevive porque la compat de ese modelo desactiva el renombrado a `max_tokens` del sanitizador de esquema (su OpenAPI publicado solo documenta `max_tokens`). Retira ese override cuando NaN publique el arreglo en su gateway — `test/issue-34-max-completion-tokens.test.ts` es el tripwire. Solo el razonamiento aún puede agotar el presupuesto completo y cerrar el turno con contenido vacío; ese corte lleva el marcador `reasoning_only_stream` de NaN y pi-ai reintenta el turno (issue #21).

## 🚀 Desarrollo

```bash
bun install
bun run generate-models   # Regenerar catálogo de fallback
bun test                  # Ejecutar todos los tests
bun run typecheck         # Ejecutar typechecking
bun run check-nan-mcp-server            # ¿Hay una versión nueva de nan-mcp-server?
bun run check-pi-sdk-versions           # ¿Hay una versión nueva de pi / pi-ai?
```

Cada cambio de código debe subir la versión de `package.json` en el mismo PR; los valores de
capacidad que diverjan de models.dev van en `scripts/manual-overrides.ts` con una nota de
procedencia, nunca editados a mano en el catálogo generado.

*Las versiones siguen semver estricto. CI publica automáticamente al hacer merge a `main`. Tras una
actualización, pi muestra el `CHANGELOG.md` de este paquete (`pi.changelogPath`, pi 1.5.0+).*
