@gtrabanco/pi-nan-provider
NaN Builders (api.nan.builders) model provider for pi - OpenAI-compatible registration with a models.dev-generated fallback, tier-aware live catalog, and an optional community media MCP bridge
Package details
Install @gtrabanco/pi-nan-provider from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@gtrabanco/pi-nan-provider- Package
@gtrabanco/pi-nan-provider- Version
1.5.4- Published
- Nov 12, 2026
- Downloads
- 6,122/mo · 1,547/wk
- Author
- gtrabanco
- License
- MIT
- Types
- extension
- Size
- 442.3 KB
- Dependencies
- 0 dependencies · 2 peers
Pi manifest JSON
{
"extensions": [
"./src/index.ts"
],
"changelogPath": "CHANGELOG.md"
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
@gtrabanco/𝜋-nan-provider
NaN Builders model provider + a native MCP server for pi.
Registers the nan provider via pi.registerProvider() using NaN's OpenAI-compatible API (https://api.nan.builders/v1), and registers the community media MCP server via pi's native pi.registerMcpServer().
[!NOTE] Requirements: pi ≥ 1.4.0 (peer range
^1.4.0). The package is verified against pi 1.5.1 / pi-ai 1.5.1; the peer floor only rises on pi minor realigns.
✨ Features
| Area | What you get |
|---|---|
| Two-layer model catalog | Build-time generated fallback (pi always starts, even offline) merged with the live GET /v1/models list; filterModels then hides whatever your key cannot call. |
| Read-time catalog overlay | Capability data is re-applied from the generated catalog on every getModels() / getAllModels() read, so a models-store.json written by an older package version can neither shadow it (#19) nor re-register non-chat ids as chat models (#32). The store decides which ids exist; the generated catalog decides what they can do. |
| Non-chat endpoint guard | NON_CHAT_MODEL_IDS keeps embedding / rerank / TTS / STT / image / video ids out of pi's chat picker. |
| Strict payload sanitizer | Every /chat/completions body is rewritten to match NaN's published schema before it leaves pi, whatever pi-ai version is bundled. |
| Context-overflow rescue | NaN's generic 400 Invalid request is reclassified as an overflow when the request is estimated over the model's window (#3), and a cold-cache 524 over NAN_COLD_CACHE_CEILING_TOKENS too (#18) — pi compacts and retries instead of wedging. |
| Truncated-stream retry | supportsFinishReason: true turns an SSE stream closed before finish_reason into a retryable error instead of a silent stall (#2). |
| Streaming token usage | stream_options.include_usage is on by default, so pi reports real input/output/reasoning/cache counts (#4, #7). |
| Reasoning controls | Per-model reasoningEffortValues + thinkingLevelMap (#16), the reasoning-only stream guard (#21) and the max_completion_tokens interim bound for qwen3.8-flash (#34). |
| Attribution header | X-Pi-Provider: gtrabanco/pi-nan-provider@<version> on every request, stripped when pi's install telemetry is disabled. |
| Native image models | flux-2-klein and qwen-image-2.1 registered as pi type: "image" models, reachable from codemode and extensions. |
| Native MCP media bridge | The nan-media server (9 tools) registered through pi.registerMcpServer(), managed with /nan-mcp. |
| Quota reporting | /nan-usage merges GET /v1/usage with the documented monthly caps. |
| Cache-anomaly evidence | cf-ray capture on the repeated id + created gateway signature (#17) plus the deterministic /nan-debug collector (#35). |
| pi integration | Model refresh driven by pi's Models runtime, per-model models.json overrides, and the extension changelog (pi.changelogPath) that pi shows after an update. |
Commands:
| Command | Purpose |
|---|---|
/nan-mcp |
Manage the media MCP bridge (status / enable / disable) |
/nan-usage [1-90] |
Token usage per model vs. the documented caps |
/nan-debug [topic] [--window N] [--out path] [--emit] |
Redacted, reproducible evidence package for NaN |
⚡ Quick Start
- Get an API Key: Claim your NaN API key here (referral link).
- Install:
pi install npm:@gtrabanco/pi-nan-provider - Authenticate:
export NAN_API_KEY="sk-your-key-here" - Verify:
pi --list-models nan
Docs in English (this file) · Documentación en español
⚙️ How it works
The provider uses a two-layer model catalog plus a read-time overlay to ensure reliability:
| Layer | Source | Purpose |
|---|---|---|
| 1. Generated Fallback | scripts/models.generated.ts |
Build-time snapshot from models.dev plus manual overrides with provenance. Ensures pi can always start, even if the network fails. |
2. Live /models Fetch |
NaN Runtime API | Fetches the models your key can actually call and merges them with the fallback; filterModels hides anything the live list does not return. |
| 3. Read-time Overlay | withStaleCatalogOverlay (src/provider-factory.ts) |
Re-applies generated capability data on every getModels() / getAllModels() read, so a models-store.json written by an older package version cannot shadow it (#19) nor re-register non-chat ids as chat models (#32). |
[!IMPORTANT] Live list: The live list is authoritative. Since 2026-11-10 NaN no longer has tier-exclusive models, so every catalog model is meant to be available to every key; a model the live
/modelsresponse does not list for your key is still filtered out.
NaN's live /models also returns non-chat endpoint ids (embedding, rerank, TTS, STT, image, video). These are not registered as chat models — the classification is the single exported NON_CHAT_MODEL_IDS from src/fetch-models.ts (the same source used by the build-time generator). The merge skips their placeholders and places them in an observable nonChat bucket; baselineModels also filters them defensively, and the read-time overlay drops chat-type entries whose id is in that list — a stale store written before #15 (issue #32) cannot resurrect them. This prevents pi's chat picker from offering endpoints that cannot serve chat (requests would 404).
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.
🧾 Strict request sanitizer
NaN answers HTTP 400 Invalid request. Check your request parameters. for any body that does not match the schema it publishes at nan.builders/openapi.json — a schema stricter than what pi-ai emits in several places. src/openai-compat-sanitizer.ts rewrites every outgoing /chat/completions payload on the way out, so no pi-ai version or conversation shape can produce an invalid request:
- an assistant
contentarray carrying a tool call is moved into the top-leveltool_callsfield (NaN'sContentPartaccepts onlytextandimage_url); reasoning_detailsis stripped, and unknown content parts (e.g.thinking) are folded into atextpart so reasoning is not silently lost;- fields NaN's schema does not list are removed:
storealways, an emptytoolsarray always, andstream_optionsunless the model'scompat.supportsUsageInStreamingsays the gateway reports it; max_completion_tokensis renamed tomax_tokens(the documented field) unless the model'scompat.maxTokensFieldopts out (#34,qwen3.8-flash).
A caller-supplied onPayload hook is preserved and chained after sanitization.
🏷️ Attribution header
Every model this package registers carries an X-Pi-Provider: gtrabanco/pi-nan-provider@<version> HTTP header, added on top of whatever identity headers pi-ai sends (pi's own User-Agent is never modified). It identifies the requests coming from this provider extension — including the /nan-usage quota fetch — and it is omitted silently when the package version cannot be resolved. User models.json overrides that replace a model entry replace its headers too.
The header follows pi's telemetry setting: with enableInstallTelemetry: false in settings.json the header disappears from every request (evaluated live, so toggling the setting applies on the next request without a restart) — the header identifies the package in use, and users who opted out of telemetry should not be identifiable by it.
🧠 Model-switch safety
When you switch models, older pi-ai versions replayed the previous model's reasoning as plain assistant text with no size bound, which could overflow a 262K-context model's window; NaN answered with a generic 400 Invalid request. Check your request parameters. that looked like a provider bug (upstream tracking: pi-nan-provider#3). pi-ai 1.3.0 fixed this upstream (transformMessages now drops thinking blocks from a different model) and this package requires pi ≥ 1.4.0 (peer range ^1.4.0), so the extension-side guard that used to mitigate it was removed in package 1.4.0 — no action is needed, upstream handles it.
A request can still overflow on its own merits — oversized tool outputs, images, or a destination window simply smaller than the content — and NaN answers the same generic 400 instead of naming the overflow, while pi's auto-compaction does not recognize it, so the session would wedge at the ceiling. The provider therefore re-checks the request size on the way out: when that generic 400 arrives for a request estimated over the model's window, the error is rewritten into a context-overflow message pi recognizes, so it compacts and retries instead of stalling. A generic 400 on a within-window request is left untouched, so unrelated errors are never mislabelled.
⏱️ Intermittent truncated streams (auto-retry, no silent stall)
NaN's LiteLLM gateway occasionally closes an SSE stream before emitting the final finish_reason chunk (observed on glm5.3-flash; issue #2). The catalog declares supportsFinishReason: true, so pi-ai turns that into the error Stream ended without finish_reason — which matches pi's retryable-provider pattern and is retried automatically, instead of silently accepting a half-finished answer. If a gateway version never sends finish_reason, the turn now fails visibly once the retry budget is exhausted.
You can override any model's compat per-model in ~/.pi/agent/models.json (pi's docs/models.md → Per-model Overrides); overrides compose above the registered provider. Example (forcing the retry behavior explicitly):
{
"providers": {
"nan": {
"modelOverrides": {
"glm5.3-flash": { "compat": { "supportsFinishReason": true } }
}
}
}
}
Setting
supportsFinishReason: falserestores the old silent-stall behavior — not recommended.
pi-ai 1.5.1 compat escape hatches (~/.pi/agent/models.json, per model): compat.requiresToolsWithToolHistory: true restores sending tools: [] in conversations with tool history (pi-ai 1.5.1 omits it by default, fixing vLLM 0.20+ tools must not be an empty array rejections behind LiteLLM); compat.requiresNonNullAssistantContent: true sends "" instead of null in assistant messages that only carry tool calls; compat.promptCacheKeyMode: "disabled" guarantees prompt_cache_key is never sent to the gateway (the default auto already never enables it for NaN).
Streaming token usage: supportsUsageInStreaming is true by default. NaN's published schema does not document stream_options, but the live gateway honors it — measured 2026-09-16 (#7): two identical streaming calls per model, 0 usage chunks without the flag and exactly 1 with it, on deepseek-v4-flash, glm5.3-flash, qwen3.6, gemma4 and mimo-v2.5 (since removed). pi therefore reports real input/output/reasoning/cache token counts instead of zeros. If a model turns out not to report streaming usage, opt out per model — the request sanitizer then strips stream_options and the payload stays strict:
{
"providers": {
"nan": {
"modelOverrides": {
"some-model": { "compat": { "supportsUsageInStreaming": false } }
}
}
}
}
🔑 Authentication
resolve() checks the stored credential first, then falls back to the matching environment variable.
| Method | Command / Action | Notes |
|---|---|---|
| Env Var | export NAN_API_KEY="..." |
Fastest for local development. |
/login |
pi > /login nan |
Persistent; stores in ~/.pi/agent/auth.json. |
| Manual Config | Edit ~/.pi/agent/auth.json |
Direct JSON manipulation. |
Get a key from the NaN platform (user settings → API Keys; referral link).
🔌 Native MCP Server
Since pi 0.99.0 ships a built-in MCP client, this package bridges to native MCP (requires pi >= 1.4.0, the peer range floor since package 1.5.0).
The media bridge is enabled by default (session-scoped, visible in /mcp). Use /nan-mcp to manage it.
[!WARNING] NaN's official remote MCP server no longer exposes any tool. Until 1.2.0 this package shipped a
nan-searchbridge pointing athttps://api.nan.builders/mcp, which exposedweb_search. That server now answersinitializewith an emptycapabilities.toolsandtools/listwith{"tools":[]}— verified with a valid key on 2026-10-15 — and NaN removed every MCP mention from its documentation (the old/docs/mcppage now redirects to/docs/agent-setup). The bridge was removed in 1.2.1 because registering a server that exposes nothing only produces a tool-less server in/mcp.Bring your own
web_searchand fetch. Install an MCP server that provides them (e.g. thefetch+brave-searchpair for the official MCP servers, or any other search/fetch MCP server) and declare it in your globalmcp.json:{ "mcpServers": { "fetch": { "command": "uvx", "args": ["mcp-server-fetch"] }, "web-search": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-brave-search"], "env": { "BRAVE_API_KEY": "your-key" } } } }Re-check
https://api.nan.builders/mcpperiodically: if NaN publishes tools again, this package can restore the bridge.
🛠️ Management Command: /nan-mcp
| Command | Effect |
|---|---|
/nan-mcp status |
Shows the media bridge state and API key resolution. |
/nan-mcp enable [target] |
Enables nan-mcp-server (persisted). |
/nan-mcp disable [target] |
Disables it persistently. |
The bridge resolves the stored /login nan credential (stored → env). If neither resolves, the server does not register and a warning is shown in the status output.
[!NOTE] Registration happens when the extension loads. If you run
/login nanafter the session started, the factory has already run — run/reload(or/nan-mcp enable) to register the bridges without restarting pi.
🚧 Hosts without pi's built-in MCP extension (PI WEB)
pi connects registered servers from the extension that handles mcp_servers_change — the built-in
mcp extension, which also registers /mcp. Hosts that build pi's resource loader without
extensionFactories load no built-in extension at all (verified: PI WEB's
createAgentSessionServices, pi 0.99.1 / pi-web 1.202609.1), so nothing connects the servers and pi
reports MCP server "nan-media" is registered, but no loaded extension connects MCP servers.
There the bridges cannot expose tools, so this package replaces that cryptic error with one warning
per session, keeps the registrations idle, and lets /nan-mcp enable persist the toggle without
registering a server that can never connect. To get MCP in the CLI, enable the connector with
pi config → Built-in extensions → mcp.
pi features and NaN models
Virtual models — register a router under
nan(pi.registerVirtualModel({ provider: "nan", ... })); it routes to the physical NaN models with no provider-side configuration.Codemode — works with any tool-calling NaN model. This package's MCP tools use
exposure: "direct", so codemode scripts can call them as well. Requires pi's built-incodemodeextension (see the PI WEB note above).Classifier models — not applicable: NaN exposes no classifier API (chat plus embedding/rerank/audio/image endpoints only), so this provider registers no
type: "classifier"models.Native NaN image models — flux-2-klein (text-to-image + image-to-image) and qwen-image-2.1 (text-to-image) are registered as pi image models (
type: "image", api"nan-images"). They do not appear in/model; they are reached through codemode withmodels.generateImages(model, { input })or from extensions withctx.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)Both models need the "inference" membership tier (403 otherwise); the image endpoints are rate-limited separately from chat (20 requests/min, 100 requests/month shared between the two models) and do not consume the chat token budget. NaN docs and OpenAPI (checked 2026-10-01). The community nan-media MCP server already offers the same generation and editing as MCP tools, while the native image models let codemode and extensions call them directly.
Community Media MCP Server
Bridges nan-mcp-server via a minimal local stdio client.
- Session-scoped: The server connects at registration time (eager). Appears in
/mcpwith source "extension". - Config: Files land in
~/nan-mcp-output/.
| Tool | Purpose |
|---|---|
mcp__nan_media__generate_image |
Image generation (flux-2-klein) |
mcp__nan_media__edit_image |
Image-to-image editing (flux-2-klein) |
mcp__nan_media__text_to_speech |
Audio synthesis (kokoro) |
mcp__nan_media__list_voices |
List available voices |
mcp__nan_media__speech_to_text |
Audio transcription (whisper) |
mcp__nan_media__list_models |
List the NaN model ids the key can reach |
mcp__nan_media__embed_text |
Text embeddings (qwen3-embedding) |
mcp__nan_media__rerank_documents |
Document reranking (Qwen3-Reranker) |
mcp__nan_media__get_quota |
Per-model token quota (used / cap / remaining / reset) |
pi 0.99.0–0.99.1 used the hyphenated form (
mcp__nan-media__*); pi 0.99.2+ sanitizes tool names to[A-Za-z0-9_]only, so hyphens became underscores. The underscore form applies on every supported version (pi >= 1.4.0).
🔧 Media Bridge Configuration
| Variable | Default | Description |
|---|---|---|
NAN_MEDIA_MCP |
— | Per-session override (0 or false to disable). |
NAN_MEDIA_MCP_VERSION |
1.3.0 |
Pinned server version (recommended). |
NAN_MEDIA_MCP_COMMAND |
— | Custom command override. |
NAN_MEDIA_MCP_TIMEOUT_MS |
120000 |
Per-call timeout. |
🤖 Automated Update Detection
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.
bun run check-nan-mcp-server # human-readable report
bun run check-nan-mcp-server --json # machine-readable JSON
bun run check-nan-mcp-server --issue # create/refresh the issue
PI SDK updates (@earendil-works/pi-ai / @earendil-works/pi-coding-agent) are automated
too: the weekly check-pi-sdk-versions workflow files the sdk-monitor issue and
prepares a devDependencies-bump PR (bun test + bun run typecheck + type-surface diff
included) for human review — see .github/workflows/check-pi-sdk-versions.yml and scripts/check-pi-sdk-versions.mjs.
📊 Quota Usage: /nan-usage
Shows your NaN token usage per model merged with the documented monthly caps, the window totals, your all-time totals, and the time until the billing cycle resets.
How it works
/nan-usage calls NaN's GET /v1/usage endpoint with the same API key you already use for chat — pi's stored credential (/login nan) or NAN_API_KEY. No CLI login, no session cookie, no extra setup.
The endpoint returns your usage (rows per date and model) plus totals covering the requested window — at most 90 inclusive days. The command reads those totals and merges them with the caps published in the NaN docs to show how much of each monthly cap you have consumed. /usage reports consumption only, so the caps themselves always come from the documented table (checked 2026-09-27).
Rate limit: 30 requests per minute for /usage, separate from the model endpoints.
Usage
/nan-usage # current UTC month (matches the monthly caps)
/nan-usage 7 # rolling 7-day window (1-90)
/nan-usage 30 # rolling 30-day window
/nan-usage help # show the usage line
Setup
- Authenticate the
nanprovider in pi with yoursk-...key:or export it instead:/login nanexport NAN_API_KEY=sk-... - Use in pi:
/nan-usage
[!TIP] No API key resolves? The command falls back to the static quota table from the docs and tells you how to authenticate.
What you see
With an API key (real usage):
📊 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
Without an API key (static limits only):
📊 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).
Failures stay actionable: 401 → run /login nan or fix NAN_API_KEY, 404 → the account has no usage identity to report, 429 → retry after the Retry-After seconds.
🧾 Cache anomaly evidence: /nan-debug
NaN's gateway response cache can replay the same completion id and created
timestamp for back-to-back requests (the issue #17 signature). NaN asked for the
model and the cf-ray of a couple of calls when it recurs; pi transcripts do
not keep response headers, so the provider captures them itself.
When two streams for the same model return the same id + created, one JSON
line is appended to <agentDir>/nan-cache-anomalies.jsonl —
model, the id/created pair, both cf-ray values, ISO timestamps and a
SHA-256 of the request payload. It is local-only: no prompt or response
content, no credentials, no paths, no upload. A write failure never breaks a
stream.
/nan-debug turns that file plus your session transcripts into a redacted,
reproducible evidence package for NaN:
/nan-debug # last 7 days, writes <agentDir>/nan-debug-report/
/nan-debug cache --window 2 # topic label + a 2-day window
/nan-debug --window 2 --emit # also writes <agentDir>/nan-debug-report.zip
/nan-debug cache --out ~/evidence --emit
--window N(1–90 days) also fetchesGET /v1/usagefor the same period with your stored key and writesusage_window.json; without the flag nothing is fetched. That live call counts against your quota's request rate.- The zip is written only with
--emit; the default run writes the local report and prints a preview. --out <path>writes the report anywhere you name it (and, with--emit,<path>.zip); a leading~/expands to your home directory, so the examples above write where they say. A flag token is never accepted as the path.- The report contains
README.md,anonymized_usage.jsonl(per date/session/ model totals, stop-reason counts, cache-hit rate),turns.jsonl(per-turn usage, stop reason, error class, response id),anomalies.jsonl(cf-raypair) andmetadata.json(package/pi-ai/pi versions, catalog provenance). - Session and worktree labels are MD5-truncated to 12 chars;
errorMessageis reduced tooverflow | rate_limited | invalid_request | transport | other; prompt/response content and credentials are never collected. Identical inputs produce byte-identical zip bytes.
📊 Models
Baseline catalog (verified against NaN docs and OpenAPI).
Chat models:
| Model | Context | Max Output | Input | Reasoning |
|---|---|---|---|---|
qwen3.6 |
262,144 | 65,536 | text, image | ✅ |
gemma4 |
262,144 | 32,768 | text, image | ✅ |
deepseek-v4-flash |
1,048,576 | 32,768* | text, image | ✅ |
mimo-v2.6-flash |
1,048,576 | 131,072 | text, image | ✅ |
glm5.3-flash |
1,048,576 | 131,072 | text, image | ✅ |
qwen3.8-flash |
1,048,576 | 131,072 | text, image | ✅ |
glm5.3 |
1,048,576 | 131,072 | text, image | ✅ |
kimi-k3.1-pro |
1,048,576 | 32,768 | text, image | ✅ |
qwen3.8-max |
1,000,000 | 32,768 | text, image | ✅ |
deepseek-v4-pro |
1,048,576 | 32,768 | text | ✅ |
hy4 |
1,048,576 | 32,768 | text, image | ✅ |
[!NOTE] Context windows above are the documented windows from the NaN docs (checked 2026-10-14), which are the source of truth; models.dev is a downstream mirror a human updates later and may lag.
glm5.3was premium-only and live-only until NaN removed tier-exclusive models on 2026-11-10; it now ships in the static catalog with the values it carried (text+image, 1,048,576 / 131,072).
kimi-k3.1-pro,qwen3.8-max,deepseek-v4-proandhy4are NaN's external-provider (ZDR) models launched on 2026-11-10. NaN's docs and OpenAPI do not list them yet, but the liveGET /v1/modelsdoes (checked 2026-11-10). Their values come from NaN's pricing page ("1M context"), the NaN pi docs (maxTokens32,768 for every 1M-context model) and the first-party vendor entries on models.dev (modalities;qwen3.8-maxuses the vendor's 1,000,000 window); cost is 0 and noreasoning_effortmapping is declared, because how the gateway handles it for external providers is unmeasured. Each entry carries its provenance note inscripts/models.generated.ts.
*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 rejectsmax_tokensabove ~256k with 403.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_timeoutat ~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).
Non-chat image models (available through models.generateImages() /
getModelOfType("image", …), not through /model):
| Model | Input | Purpose |
|---|---|---|
flux-2-klein |
text, image | Text-to-image + image-to-image |
qwen-image-2.1 |
text | Text-to-image |
minimax-h3 (video generation, listed by the live /models since 2026-11-10) is classified as non-chat too: it is never registered as a chat model.
[!NOTE]
mimo-v2.6-flashis served by NaN and has been listed by models.dev providernansince 2026-09-29. Before that it entered the catalog through a manual-only entry; that entry is kept as a fallback (re-emitted automatically if models.dev drops the model again) and its provenance note stays attached to the generated entry.
mimo-v2.5was removed by NaN: it no longer appears in the NaN docs nor in the OpenAPI model list (checked 2026-09-29), so it is out of the catalog —mimo-v2.6-flashreplaces it (same 1,048,576 / 131,072 limits, 1.0B monthly quota). Historical/nan-usageconsumption formimo-v2.5still shows under the undocumented-models section.
🧠 Reasoning controls
NaN's reasoning_effort parameter controls how much the model thinks before answering — but the degree of control varies by model:
| Model | Reasoning effort | How it works |
|---|---|---|
glm5.3 |
low · medium · high · max |
Fully controllable — higher values let the model reason longer. thinking: "off" sends low, the shallowest level the NaN docs list, via the catalog's thinkingLevelMap. minimal is not in this model's declared values: issue #16 measured it on glm5.3-flash only, and glm5.3 itself was never measured |
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 (13,382 tokens), so thinking: "off" sends minimal, the shallowest effective value, via the catalog's thinkingLevelMap |
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 |
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) |
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 |
kimi-k3.1-pro, qwen3.8-max, deepseek-v4-pro, hy4 |
(no mapping declared) | External-provider models launched 2026-11-10; how the gateway translates reasoning_effort for them is unmeasured, so the catalog declares no effort values and no thinkingLevelMap (thinking: "off" sends no parameter) |
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 |
[!NOTE]
maxTokensdoes 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 ofmaxTokens— measured:max_tokens=512on 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) and by NaN forqwen3.8-flash(issue #34, 2026-11-11 — the excess also bills against the quota). As an interim bound forqwen3.8-flashthe catalog sendsmax_completion_tokensinstead, which NaN confirmed IS respected; it survives because that model's compat opts out of the schema sanitizer's rename tomax_tokens(its published OpenAPI schema still documents onlymax_tokens). Remove that override once NaN ships their gateway fix —test/issue-34-max-completion-tokens.test.tsis the tripwire. Reasoning alone can still consume the whole budget and end the turn with empty content; that closure carries NaN'sreasoning_only_streammarker and pi-ai retries the turn (issue #21).
🚀 Development
bun install
bun run generate-models # Regenerate fallback catalog
bun test # Run all tests
bun run typecheck # Run typechecking
bun run check-nan-mcp-server # Newer nan-mcp-server release?
bun run check-pi-sdk-versions # Newer pi / pi-ai release?
Every code change must bump package.json in the same PR; capability values that diverge from
models.dev belong in scripts/manual-overrides.ts with a provenance note, never hand-edited into
the generated catalog.
Releases follow strict semver. CI publishes automatically on merge to main. After an update, pi
shows this package's CHANGELOG.md (pi.changelogPath, pi 1.5.0+).