@gtrabanco/pi-nan-provider 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +31 -6
- package/README.es.md +9 -5
- package/README.md +8 -5
- package/package.json +9 -7
- package/scripts/generate-models.ts +17 -21
- package/scripts/manual-overrides.ts +47 -0
- package/scripts/models.generated.ts +7 -5
- package/src/index.ts +17 -7
- package/src/mcp/nan-media.ts +3 -2
- package/src/provider-factory.ts +54 -15
package/AGENTS.md
CHANGED
|
@@ -56,17 +56,42 @@ Every PR that changes code MUST bump `package.json` version in the same PR; CI p
|
|
|
56
56
|
|
|
57
57
|
## Verified API facts (do not re-derive from stale docs)
|
|
58
58
|
|
|
59
|
-
- pi
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
- **Extension-side pi-ai imports (v0.5.0, verified on pi-ai 0.83.0 AND 0.84.4):**
|
|
60
|
+
statically import ONLY the bare `@earendil-works/pi-ai` root from `src/`. pi's
|
|
61
|
+
extension loader maps that specifier to the compat entrypoint in every loading
|
|
62
|
+
mode (bundled CLI interception, Node-mode jiti aliases, compiled-binary
|
|
63
|
+
virtualModules), and the compat entrypoint re-exports every lazy API factory —
|
|
64
|
+
including `openAICompletionsApi`. A static SUBPATH import
|
|
65
|
+
(`@earendil-works/pi-ai/api/...`) gets the alias applied as a prefix and
|
|
66
|
+
resolves to `<compat.js>/api/...`, which does not exist: the whole extension
|
|
67
|
+
fails to load (the v0.4.x load failure). Type-only subpath imports are erased
|
|
68
|
+
before resolution and are safe; a DYNAMIC subpath `import()` is the sanctioned
|
|
69
|
+
plain-node fallback and never runs under pi because the root (compat) exports
|
|
70
|
+
the factory. Guarded by `test/extension-load.test.ts`.
|
|
71
|
+
- The REAL pi-ai root (plain node/bun, outside pi) does not export
|
|
72
|
+
`openAICompletionsApi`; `createProvider` and `envApiKeyAuth(name, envVars)` are
|
|
73
|
+
on the root. `envApiKeyAuth` implements exactly: stored credential key wins →
|
|
74
|
+
first set env var → unconfigured; `login()` prompts with `{ type: "secret" }`.
|
|
75
|
+
- pi awaits extension factories (`await factory(api)`) on 0.83.0 and 0.84.4
|
|
76
|
+
alike, so the extension entrypoint may be async (v0.5.0: streaming-API
|
|
77
|
+
resolution needs it).
|
|
78
|
+
- pi-ai 0.83.0 runtime surface verified identical for this package's needs:
|
|
79
|
+
compat re-exports `index.js` (`createProvider`, `envApiKeyAuth`) and
|
|
80
|
+
`api/openai-completions.lazy.js`; `createProvider` options (`auth`, `models`,
|
|
81
|
+
`fetchModels(context)`, `filterModels(models, credential)`, `api`) and
|
|
82
|
+
`RefreshModelsContext.credential` match 0.84.4; `registerProvider` has both
|
|
83
|
+
the full-`Provider` and `(name, config)` overloads in 0.83's ExtensionAPI.
|
|
65
84
|
- `pi.registerProvider(provider)` accepts a complete pi-ai `Provider`; pi's Models
|
|
66
85
|
runtime then drives `fetchModels` refreshes (network refresh at interactive
|
|
67
86
|
startup and periodically, cache-only at registration) and persists the overlay.
|
|
68
87
|
A `fetchModels` rejection never blocks startup.
|
|
69
88
|
- models.json overrides compose **above** registered native providers.
|
|
89
|
+
- Capability values that diverge from models.dev are recorded as build-time
|
|
90
|
+
`MANUAL_OVERRIDES` (mandatory provenance note) in `scripts/manual-overrides.ts`,
|
|
91
|
+
applied by `scripts/generate-models.ts` — never hand-edited into
|
|
92
|
+
`scripts/models.generated.ts` and never invented. e.g. qwen3.8-flash
|
|
93
|
+
contextWindow 1,000,000 (maintainer-confirmed 2026-09-05; models.dev and NaN
|
|
94
|
+
docs still listed 262,144 that day).
|
|
70
95
|
- Relative imports inside this package use `.ts` extensions (pi's official
|
|
71
96
|
extension examples do the same; pi transpiles extension sources).
|
|
72
97
|
- pi intentionally has NO built-in MCP client (docs/usage.md). MCP integration
|
package/README.es.md
CHANGED
|
@@ -115,10 +115,12 @@ Catálogo base (de models.dev, proveedor `nan`, obtenido 2026-09-04 — límites
|
|
|
115
115
|
| `mimo-v2.5` | 1,048,576 | 131,072 | texto, imagen | sí |
|
|
116
116
|
| `glm5.2` | 500,000 | 131,072 | texto | sí |
|
|
117
117
|
| `glm5.3-flash` | 1,000,000 | 131,072 | texto, imagen | sí |
|
|
118
|
-
| `qwen3.8-flash` |
|
|
118
|
+
| `qwen3.8-flash` | 1,000,000 | 131,072 | texto, imagen | sí |
|
|
119
119
|
|
|
120
120
|
Notas (grabadas por entrada en `scripts/models.generated.ts`):
|
|
121
121
|
|
|
122
|
+
- La ventana de contexto de `qwen3.8-flash` está confirmada por el mantenedor en 1M (2026-09-05); models.dev y los docs de NaN aún listaban 262,144 en esa fecha. 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.
|
|
123
|
+
|
|
122
124
|
- `deepseek-v4-flash` incluye entrada de imagen porque NaN sirve la variante Vision-Exp ([docs de NaN](https://nan.builders/docs/models)); models.dev la lista como solo texto.
|
|
123
125
|
- `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`.
|
|
124
126
|
- NaN factura por cuota de membresía, que models.dev reporta como coste cero por token — el coste mostrado por pi será $0.
|
|
@@ -131,12 +133,14 @@ Este paquete reemplaza el bloque `nan` manual de `~/.pi/agent/models.json` (el [
|
|
|
131
133
|
|
|
132
134
|
## Compatibilidad con versiones de pi
|
|
133
135
|
|
|
134
|
-
Verificado contra pi **0.84.4** y la línea 0.85 (`registerProvider(provider)`, `registerProvider(name, config)`, `registerTool` y `modelRegistry.getApiKeyForProvider` presentes en ambas). La extensión degrada con elegancia entre versiones:
|
|
136
|
+
Verificado contra pi **0.83.0**, **0.84.4** y la línea 0.85 (`registerProvider(provider)`, `registerProvider(name, config)`, `registerTool` y `modelRegistry.getApiKeyForProvider` presentes en ambas; el entrypoint compat de pi-ai reexporta la fábrica de la API openai-completions en 0.83 y 0.84 por igual). La extensión degrada con elegancia entre versiones:
|
|
135
137
|
|
|
136
|
-
- **Ruta nativa
|
|
137
|
-
- **Fallback legacy**: si el overload nativo de Provider es rechazado, 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).
|
|
138
|
+
- **Ruta nativa**: Provider completo con auth credencial-almacenada-primero-then-env, overlay de catálogo en vivo y filtrado por tier.
|
|
139
|
+
- **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).
|
|
138
140
|
- **Puentes MCP**: se omiten por completo en runtimes sin `registerTool`; los proveedores se registran igualmente.
|
|
139
|
-
-
|
|
141
|
+
- **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.
|
|
142
|
+
- `peerDependencies` es `>=0.83.0` sin límite superior (incluidos los forks en 0.83).
|
|
143
|
+
- **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`.
|
|
140
144
|
|
|
141
145
|
## helmcode
|
|
142
146
|
|
package/README.md
CHANGED
|
@@ -115,10 +115,11 @@ Baseline catalog (from models.dev, provider `nan`, fetched 2026-09-04 — NaN's
|
|
|
115
115
|
| `mimo-v2.5` | 1,048,576 | 131,072 | text, image | yes |
|
|
116
116
|
| `glm5.2` | 500,000 | 131,072 | text | yes |
|
|
117
117
|
| `glm5.3-flash` | 1,000,000 | 131,072 | text, image | yes |
|
|
118
|
-
| `qwen3.8-flash` |
|
|
118
|
+
| `qwen3.8-flash` | 1,000,000 | 131,072 | text, image | yes |
|
|
119
119
|
|
|
120
120
|
Notes (recorded per entry in `scripts/models.generated.ts`):
|
|
121
121
|
|
|
122
|
+
- `qwen3.8-flash` context window is maintainer-confirmed at 1M (2026-09-05); models.dev and NaN's docs still listed 262,144 at that date. 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.
|
|
122
123
|
- `deepseek-v4-flash` includes image input because NaN serves the Vision-Exp variant ([NaN docs](https://nan.builders/docs/models)); models.dev lists text only.
|
|
123
124
|
- `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`.
|
|
124
125
|
- NaN bills via membership quota, which models.dev reports as zero per-token cost — pi's cost display will read $0.
|
|
@@ -131,12 +132,14 @@ This package replaces the hand-written `nan` block in `~/.pi/agent/models.json`
|
|
|
131
132
|
|
|
132
133
|
## pi version compatibility
|
|
133
134
|
|
|
134
|
-
Verified against pi **0.84.4
|
|
135
|
+
Verified against pi **0.83.0**, **0.84.4**, and the 0.85 line (`registerProvider(provider)`, `registerProvider(name, config)`, `registerTool`, and `modelRegistry.getApiKeyForProvider` all present in both; pi-ai's compat entrypoint re-exports the openai-completions API factory on 0.83 and 0.84 alike). The extension degrades gracefully across versions:
|
|
135
136
|
|
|
136
|
-
- **Native path
|
|
137
|
-
- **Legacy fallback**: if the native Provider overload is rejected, 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).
|
|
137
|
+
- **Native path**: full Provider with stored-credential-then-env auth, live catalog overlay, and tier filtering.
|
|
138
|
+
- **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).
|
|
138
139
|
- **MCP bridges**: skipped entirely on runtimes without `registerTool`; providers still register.
|
|
139
|
-
-
|
|
140
|
+
- **Async entrypoint**: pi awaits extension factories on 0.83 and 0.84 alike, so the streaming-API resolution at registration is transparent.
|
|
141
|
+
- `peerDependencies` is `>=0.83.0` with no upper bound (0.83 forks included).
|
|
142
|
+
- **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`.
|
|
140
143
|
|
|
141
144
|
## helmcode
|
|
142
145
|
|
package/package.json
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gtrabanco/pi-nan-provider",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "NaN Builders (api.nan.builders) model provider for pi - OpenAI-compatible registration with a models.dev-generated fallback, tier-aware live catalog, and MCP bridges (official web search + optional community media server)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi",
|
|
7
7
|
"pi-package",
|
|
8
8
|
"pi-extension",
|
|
9
|
-
"
|
|
9
|
+
"provider",
|
|
10
10
|
"ai",
|
|
11
11
|
"ai-provider",
|
|
12
|
+
"llm",
|
|
12
13
|
"nan",
|
|
13
14
|
"nan-builders",
|
|
15
|
+
"api-nan-builders",
|
|
14
16
|
"openai-compatible",
|
|
15
|
-
"
|
|
17
|
+
"ai-model"
|
|
16
18
|
],
|
|
17
19
|
"type": "module",
|
|
18
20
|
"main": "./src/index.ts",
|
|
@@ -38,14 +40,14 @@
|
|
|
38
40
|
"typecheck": "bunx tsc --noEmit"
|
|
39
41
|
},
|
|
40
42
|
"peerDependencies": {
|
|
41
|
-
"@earendil-works/pi-ai": ">=0.
|
|
42
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
43
|
+
"@earendil-works/pi-ai": ">=0.83.0 <1",
|
|
44
|
+
"@earendil-works/pi-coding-agent": ">=0.83.0 <1"
|
|
43
45
|
},
|
|
44
46
|
"devDependencies": {
|
|
45
47
|
"@earendil-works/pi-ai": "0.84.4",
|
|
46
48
|
"@earendil-works/pi-coding-agent": "0.84.4",
|
|
47
|
-
"@types/bun": "
|
|
48
|
-
"typescript": "
|
|
49
|
+
"@types/bun": "1.4",
|
|
50
|
+
"typescript": "7"
|
|
49
51
|
},
|
|
50
52
|
"license": "MIT",
|
|
51
53
|
"repository": {
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import type { GeneratedModelEntry } from "../src/fetch-models.ts";
|
|
24
|
+
import { MANUAL_OVERRIDES } from "./manual-overrides.ts";
|
|
24
25
|
|
|
25
26
|
const MODELS_DEV_API_URL = "https://models.dev/api.json";
|
|
26
27
|
const SOURCE_PROVIDER_ID = "nan";
|
|
@@ -33,22 +34,10 @@ const REQUIRED_MODEL_IDS = ["qwen3.6", "gemma4", "deepseek-v4-flash", "mimo-v2.5
|
|
|
33
34
|
const PI_SUPPORTED_INPUT = new Set(["text", "image"]);
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
|
-
* Manual corrections over models.dev
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
|
|
40
|
-
const MANUAL_OVERRIDES: Record<string, { input?: ("text" | "image")[]; note: string }> = {
|
|
41
|
-
"deepseek-v4-flash": {
|
|
42
|
-
input: ["text", "image"],
|
|
43
|
-
note: "input includes image: NaN serves the Vision-Exp variant (confirmed at https://nan.builders/docs/models, 'takes images as input'); models.dev provider nan lists text only.",
|
|
44
|
-
},
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Per-model provenance notes sourced from NaN's own documentation (manual —
|
|
49
|
-
* models.dev has no structured tier/quota data), each stating its source.
|
|
50
|
-
* Empty today: no model in the models.dev `nan` provider is tier-gated
|
|
51
|
-
* (the premium-tier GLM 5.3 is not listed there; only glm5.3-flash is).
|
|
37
|
+
* Manual corrections over models.dev live in scripts/manual-overrides.ts
|
|
38
|
+
* (shared with test/generated-catalog.test.ts, which pins that every
|
|
39
|
+
* override lands on the generated entry with its provenance note).
|
|
40
|
+
* Capabilities diverging from models.dev are recorded there, never here.
|
|
52
41
|
*/
|
|
53
42
|
const MANUAL_NOTES: Record<string, string> = {};
|
|
54
43
|
|
|
@@ -136,20 +125,25 @@ function convertModel(modelId: string, m: ModelsDevModel): GeneratedModel | { sk
|
|
|
136
125
|
const override = MANUAL_OVERRIDES[modelId];
|
|
137
126
|
const input = override?.input ?? normalizeInput(m.modalities?.input, modelId);
|
|
138
127
|
|
|
128
|
+
if (override) {
|
|
129
|
+
const overridden = Object.keys(override).filter((key) => key !== "note");
|
|
130
|
+
console.log(`generate-models: manual override for "${modelId}" (${overridden.join(", ")})`);
|
|
131
|
+
}
|
|
132
|
+
|
|
139
133
|
return {
|
|
140
134
|
entry: {
|
|
141
135
|
id: modelId,
|
|
142
|
-
name: m.name ?? modelId,
|
|
143
|
-
reasoning: m.reasoning === true,
|
|
136
|
+
name: override?.name ?? m.name ?? modelId,
|
|
137
|
+
reasoning: override?.reasoning ?? m.reasoning === true,
|
|
144
138
|
input,
|
|
145
|
-
cost: {
|
|
139
|
+
cost: override?.cost ?? {
|
|
146
140
|
input: m.cost?.input ?? 0,
|
|
147
141
|
output: m.cost?.output ?? 0,
|
|
148
142
|
cacheRead: m.cost?.cache_read ?? 0,
|
|
149
143
|
cacheWrite: m.cost?.cache_write ?? 0,
|
|
150
144
|
},
|
|
151
|
-
contextWindow,
|
|
152
|
-
maxTokens,
|
|
145
|
+
contextWindow: override?.contextWindow ?? contextWindow,
|
|
146
|
+
maxTokens: override?.maxTokens ?? maxTokens,
|
|
153
147
|
compat: { ...NAN_COMPAT },
|
|
154
148
|
notes: [
|
|
155
149
|
NAN_COMPAT_NOTE,
|
|
@@ -158,6 +152,8 @@ function convertModel(modelId: string, m: ModelsDevModel): GeneratedModel | { sk
|
|
|
158
152
|
],
|
|
159
153
|
// Preserve every property models.dev documents for this model verbatim
|
|
160
154
|
// (tier, quotas, release dates, reasoning options, attachments...).
|
|
155
|
+
// Extras keep the UN-overridden models.dev data: they are the provenance
|
|
156
|
+
// record of what models.dev says, not a capability claim.
|
|
161
157
|
extras: m as unknown as Record<string, unknown>,
|
|
162
158
|
},
|
|
163
159
|
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MANUAL_OVERRIDES — build-time corrections applied over the models.dev data
|
|
3
|
+
* by scripts/generate-models.ts before emitting scripts/models.generated.ts.
|
|
4
|
+
*
|
|
5
|
+
* models.dev lags behind gateway-side capability changes (context windows,
|
|
6
|
+
* max output tokens, modalities). When a served value diverges from
|
|
7
|
+
* models.dev, record it here instead of hand-editing the generated file.
|
|
8
|
+
*
|
|
9
|
+
* Provenance rules (mirrors AGENTS.md):
|
|
10
|
+
* - `note` is REQUIRED and must state where the value was confirmed
|
|
11
|
+
* (URL and/or who confirmed it, plus the date) — and, when the override
|
|
12
|
+
* contradicts a public source, that source and its value, so the entry
|
|
13
|
+
* can be re-verified later.
|
|
14
|
+
* - Never guess a number. If no confirmation source exists, leave the
|
|
15
|
+
* models.dev value in place.
|
|
16
|
+
*
|
|
17
|
+
* Fields override the models.dev-derived entry one-for-one:
|
|
18
|
+
* name, reasoning, input, cost, contextWindow, maxTokens.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export interface ManualModelOverride {
|
|
22
|
+
/** Display name override. */
|
|
23
|
+
name?: string;
|
|
24
|
+
/** Reasoning support override. */
|
|
25
|
+
reasoning?: boolean;
|
|
26
|
+
/** pi-representable input modalities override. */
|
|
27
|
+
input?: ("text" | "image")[];
|
|
28
|
+
/** Per-token cost override (USD/Mtok; NaN is membership-quota based, so 0 unless confirmed otherwise). */
|
|
29
|
+
cost?: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
30
|
+
/** Context window override (tokens). */
|
|
31
|
+
contextWindow?: number;
|
|
32
|
+
/** Max output tokens override. */
|
|
33
|
+
maxTokens?: number;
|
|
34
|
+
/** Required provenance note; emitted verbatim onto the generated entry. */
|
|
35
|
+
note: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const MANUAL_OVERRIDES: Record<string, ManualModelOverride> = {
|
|
39
|
+
"deepseek-v4-flash": {
|
|
40
|
+
input: ["text", "image"],
|
|
41
|
+
note: "input includes image: NaN serves the Vision-Exp variant (confirmed at https://nan.builders/docs/models, 'takes images as input'); models.dev provider nan lists text only.",
|
|
42
|
+
},
|
|
43
|
+
"qwen3.8-flash": {
|
|
44
|
+
contextWindow: 1_000_000,
|
|
45
|
+
note: "contextWindow 1,000,000: maintainer-confirmed against api.nan.builders (2026-09-05); both models.dev and https://nan.builders/docs/models still listed 262,144 ('262K token context, the model's native window') as of 2026-09-05 — re-verify against the gateway/docs when they update.",
|
|
46
|
+
},
|
|
47
|
+
};
|
|
@@ -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-05T11:12:14.773Z
|
|
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.
|
|
@@ -382,7 +382,7 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
|
|
|
382
382
|
"cacheRead": 0,
|
|
383
383
|
"cacheWrite": 0
|
|
384
384
|
},
|
|
385
|
-
"contextWindow":
|
|
385
|
+
"contextWindow": 1000000,
|
|
386
386
|
"maxTokens": 131072,
|
|
387
387
|
"compat": {
|
|
388
388
|
"supportsDeveloperRole": false,
|
|
@@ -391,7 +391,8 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
|
|
|
391
391
|
"maxTokensField": "max_tokens"
|
|
392
392
|
},
|
|
393
393
|
"notes": [
|
|
394
|
-
"compat matches the maintainer's working ~/.pi/agent/models.json LiteLLM config for api.nan.builders (2026-09-04): supportsDeveloperRole false, supportsReasoningEffort true, supportsUsageInStreaming true, maxTokensField max_tokens. NaN's docs example sets only supportsDeveloperRole: true and is not battle-tested."
|
|
394
|
+
"compat matches the maintainer's working ~/.pi/agent/models.json LiteLLM config for api.nan.builders (2026-09-04): supportsDeveloperRole false, supportsReasoningEffort true, supportsUsageInStreaming true, maxTokensField max_tokens. NaN's docs example sets only supportsDeveloperRole: true and is not battle-tested.",
|
|
395
|
+
"contextWindow 1,000,000: maintainer-confirmed against api.nan.builders (2026-09-05); both models.dev and https://nan.builders/docs/models still listed 262,144 ('262K token context, the model's native window') as of 2026-09-05 — re-verify against the gateway/docs when they update."
|
|
395
396
|
],
|
|
396
397
|
"extras": {
|
|
397
398
|
"id": "qwen3.8-flash",
|
|
@@ -430,11 +431,12 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
|
|
|
430
431
|
export const GENERATED_CATALOG_META = {
|
|
431
432
|
source: "https://models.dev/api.json",
|
|
432
433
|
modelsDevProvider: "nan",
|
|
433
|
-
fetchedAt: "2026-09-
|
|
434
|
+
fetchedAt: "2026-09-05T11:12:14.773Z",
|
|
434
435
|
modelCount: 7,
|
|
435
436
|
models: ["deepseek-v4-flash","gemma4","glm5.2","glm5.3-flash","mimo-v2.5","qwen3.6","qwen3.8-flash"],
|
|
436
437
|
notes: [
|
|
437
438
|
"compat matches the maintainer's working ~/.pi/agent/models.json LiteLLM config for api.nan.builders (2026-09-04): supportsDeveloperRole false, supportsReasoningEffort true, supportsUsageInStreaming true, maxTokensField max_tokens. NaN's docs example sets only supportsDeveloperRole: true and is not battle-tested.",
|
|
438
|
-
"input includes image: NaN serves the Vision-Exp variant (confirmed at https://nan.builders/docs/models, 'takes images as input'); models.dev provider nan lists text only."
|
|
439
|
+
"input includes image: NaN serves the Vision-Exp variant (confirmed at https://nan.builders/docs/models, 'takes images as input'); models.dev provider nan lists text only.",
|
|
440
|
+
"contextWindow 1,000,000: maintainer-confirmed against api.nan.builders (2026-09-05); both models.dev and https://nan.builders/docs/models still listed 262,144 ('262K token context, the model's native window') as of 2026-09-05 — re-verify against the gateway/docs when they update."
|
|
439
441
|
],
|
|
440
442
|
} as const;
|
package/src/index.ts
CHANGED
|
@@ -11,16 +11,22 @@
|
|
|
11
11
|
* without the native Provider overload, registration falls back to the
|
|
12
12
|
* legacy (name, config) form with the same baseline catalog.
|
|
13
13
|
*
|
|
14
|
+
* The entrypoint is async: pi awaits extension factories (0.83 and
|
|
15
|
+
* 0.84 alike), and the openai-completions streaming implementation is
|
|
16
|
+
* resolved dynamically — see provider-factory.ts for why.
|
|
17
|
+
*
|
|
14
18
|
* 2. MCP tools over pi's registerTool (pi intentionally has no MCP client):
|
|
15
19
|
* - `nan_web_search` via NaN's official remote MCP server
|
|
16
20
|
* (https://api.nan.builders/mcp) — on by default, NAN_MCP_TOOLS=0 to
|
|
17
21
|
* disable.
|
|
18
22
|
* - Media tools bridging the optional community `nan-mcp-server`
|
|
19
|
-
* (stdio, spawned per call) —
|
|
20
|
-
*
|
|
23
|
+
* (stdio, spawned per call) — on by default, NAN_MEDIA_MCP=0 to
|
|
24
|
+
* disable. The server spawns per tool call, so nothing runs unless
|
|
25
|
+
* audio/image/transcription is actually invoked.
|
|
21
26
|
*/
|
|
22
27
|
|
|
23
28
|
import type { ExtensionAPI, ProviderConfig } from "@earendil-works/pi-coding-agent";
|
|
29
|
+
import type { Provider } from "@earendil-works/pi-ai";
|
|
24
30
|
import { registerNanMcpCommand } from "./commands.ts";
|
|
25
31
|
import { baselineModels } from "./fetch-models.ts";
|
|
26
32
|
import { createNanWebSearchTool, webSearchBridgeEnabled, NAN_API_KEY_ENV } from "./mcp/nan-search.ts";
|
|
@@ -34,14 +40,18 @@ import { PROVIDERS } from "./providers.ts";
|
|
|
34
40
|
* env-var auth. The fallback loses stored-credential auth (env only) — a
|
|
35
41
|
* documented limitation of the legacy path, never a silent auth invention.
|
|
36
42
|
*/
|
|
37
|
-
function registerProviderCompat(
|
|
38
|
-
|
|
43
|
+
async function registerProviderCompat(
|
|
44
|
+
pi: ExtensionAPI,
|
|
45
|
+
config: OpenAICompatibleProviderConfig,
|
|
46
|
+
): Promise<void> {
|
|
47
|
+
let native: Provider<"openai-completions"> | undefined;
|
|
39
48
|
try {
|
|
49
|
+
native = await createNanCompatibleProvider(config);
|
|
40
50
|
pi.registerProvider(native);
|
|
41
51
|
return;
|
|
42
52
|
} catch (error) {
|
|
43
53
|
console.warn(
|
|
44
|
-
`[pi-nan-provider] native provider
|
|
54
|
+
`[pi-nan-provider] native provider path failed (${error instanceof Error ? error.message : String(error)}); ` +
|
|
45
55
|
"falling back to legacy config form (env-var auth only).",
|
|
46
56
|
);
|
|
47
57
|
}
|
|
@@ -98,9 +108,9 @@ function registerMcpToolsCompat(pi: ExtensionAPI): void {
|
|
|
98
108
|
}
|
|
99
109
|
}
|
|
100
110
|
|
|
101
|
-
export default function nanProviderExtension(pi: ExtensionAPI): void {
|
|
111
|
+
export default async function nanProviderExtension(pi: ExtensionAPI): Promise<void> {
|
|
102
112
|
for (const config of PROVIDERS) {
|
|
103
|
-
registerProviderCompat(pi, config);
|
|
113
|
+
await registerProviderCompat(pi, config);
|
|
104
114
|
}
|
|
105
115
|
registerMcpToolsCompat(pi);
|
|
106
116
|
}
|
package/src/mcp/nan-media.ts
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* exposes NaN's media tools: image generation/editing (flux-2-klein), TTS
|
|
5
5
|
* (kokoro), STT (whisper), plus voice listing.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* -
|
|
7
|
+
* Both MCP bridges are enabled by default and fully lazy:
|
|
8
|
+
* - ON by default. NAN_MEDIA_MCP=0 (or false/off) disables it for one
|
|
9
|
+
* session; `/nan-mcp disable nan-mcp-server` persists the toggle.
|
|
9
10
|
* - The server process is spawned per tool call and terminated right after —
|
|
10
11
|
* zero startup cost, nothing runs unless audio/image/transcription is
|
|
11
12
|
* actually invoked.
|
package/src/provider-factory.ts
CHANGED
|
@@ -6,18 +6,26 @@
|
|
|
6
6
|
* catalog data, never a parallel implementation.
|
|
7
7
|
*
|
|
8
8
|
* The resulting provider follows pi-ai's built-in provider shape (see
|
|
9
|
-
* `deepseekProvider()` in pi-ai): `createProvider` + `envApiKeyAuth` +
|
|
10
|
-
*
|
|
11
|
-
* /models listing with the generated fallback catalog.
|
|
9
|
+
* `deepseekProvider()` in pi-ai): `createProvider` + `envApiKeyAuth` + the
|
|
10
|
+
* openai-completions streaming API, with a `fetchModels` overlay that merges
|
|
11
|
+
* the live /models listing with the generated fallback catalog.
|
|
12
|
+
*
|
|
13
|
+
* pi-ai import rule (see test/extension-load.test.ts): extensions must
|
|
14
|
+
* statically import ONLY the bare `@earendil-works/pi-ai` root. pi's
|
|
15
|
+
* extension loader maps that specifier to the compat entrypoint on every
|
|
16
|
+
* supported runtime (bundled CLI, Node-mode aliases, compiled-binary
|
|
17
|
+
* virtualModules; pi 0.83 and 0.84 alike), and compat re-exports every lazy
|
|
18
|
+
* API factory. Subpath specifiers (`@earendil-works/pi-ai/api/...`) get the
|
|
19
|
+
* alias applied as a prefix and resolve to `<compat.js>/api/...`, which does
|
|
20
|
+
* not exist — the extension then fails to load entirely.
|
|
12
21
|
*/
|
|
13
22
|
|
|
14
|
-
import
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
23
|
+
import * as piAi from "@earendil-works/pi-ai";
|
|
24
|
+
import type {
|
|
25
|
+
Provider,
|
|
26
|
+
ProviderStreams,
|
|
27
|
+
RefreshModelsContext,
|
|
19
28
|
} from "@earendil-works/pi-ai";
|
|
20
|
-
import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
|
|
21
29
|
import {
|
|
22
30
|
baselineModels,
|
|
23
31
|
DEFAULT_MODELS_TIMEOUT_MS,
|
|
@@ -43,6 +51,35 @@ export interface NanCompatibleProviderOptions {
|
|
|
43
51
|
fetchImpl?: typeof fetch;
|
|
44
52
|
}
|
|
45
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Resolve the openai-completions streaming implementation at runtime.
|
|
56
|
+
*
|
|
57
|
+
* Under pi, the bare-root namespace is pi's compat entrypoint, which
|
|
58
|
+
* re-exports `openAICompletionsApi` on both pi-ai 0.83 and 0.84 — so the
|
|
59
|
+
* first branch always wins and no pi-ai subpath is ever resolved there.
|
|
60
|
+
* Outside pi (plain node/bun: tests and direct consumers) the real root
|
|
61
|
+
* does not export the lazy factory; the dynamic subpath import below uses
|
|
62
|
+
* the package's normal `./api/*` export. It is never reached under pi, so
|
|
63
|
+
* the alias-prefix pitfall cannot bite at runtime.
|
|
64
|
+
*/
|
|
65
|
+
type OpenAICompletionsApiFactory = () => ProviderStreams;
|
|
66
|
+
|
|
67
|
+
let cachedApiFactory: OpenAICompletionsApiFactory | undefined;
|
|
68
|
+
|
|
69
|
+
export async function resolveOpenAICompletionsApi(): Promise<OpenAICompletionsApiFactory> {
|
|
70
|
+
if (cachedApiFactory) return cachedApiFactory;
|
|
71
|
+
const fromRoot = (
|
|
72
|
+
piAi as unknown as Partial<Record<"openAICompletionsApi", OpenAICompletionsApiFactory>>
|
|
73
|
+
).openAICompletionsApi;
|
|
74
|
+
if (typeof fromRoot === "function") {
|
|
75
|
+
cachedApiFactory = fromRoot;
|
|
76
|
+
return cachedApiFactory;
|
|
77
|
+
}
|
|
78
|
+
cachedApiFactory = (await import("@earendil-works/pi-ai/api/openai-completions.lazy"))
|
|
79
|
+
.openAICompletionsApi;
|
|
80
|
+
return cachedApiFactory;
|
|
81
|
+
}
|
|
82
|
+
|
|
46
83
|
/**
|
|
47
84
|
* Build a complete pi-ai Provider for an OpenAI-compatible endpoint:
|
|
48
85
|
*
|
|
@@ -55,12 +92,14 @@ export interface NanCompatibleProviderOptions {
|
|
|
55
92
|
* - fetchModels: live `/models` IDs × generated capability data; falls back
|
|
56
93
|
* to the baseline when the endpoint is unreachable. pi's Models runtime
|
|
57
94
|
* drives refreshes (startup/periodic) and persists the overlay.
|
|
58
|
-
* - api:
|
|
95
|
+
* - api: the openai-completions streaming implementation (see
|
|
96
|
+
* `resolveOpenAICompletionsApi` for why this is resolved dynamically).
|
|
59
97
|
*/
|
|
60
|
-
export function createNanCompatibleProvider(
|
|
98
|
+
export async function createNanCompatibleProvider(
|
|
61
99
|
config: OpenAICompatibleProviderConfig,
|
|
62
100
|
options: NanCompatibleProviderOptions = {},
|
|
63
|
-
): Provider<"openai-completions"
|
|
101
|
+
): Promise<Provider<"openai-completions">> {
|
|
102
|
+
const apiFactory = await resolveOpenAICompletionsApi();
|
|
64
103
|
const source: CatalogSource = { providerId: config.id, baseUrl: config.baseUrl };
|
|
65
104
|
|
|
66
105
|
// Last successful live /models result, shared between fetchModels (writes)
|
|
@@ -70,11 +109,11 @@ export function createNanCompatibleProvider(
|
|
|
70
109
|
// `available` (e.g. premium-tier models you are not subscribed to).
|
|
71
110
|
let liveIds: Set<string> | undefined;
|
|
72
111
|
|
|
73
|
-
return createProvider({
|
|
112
|
+
return piAi.createProvider({
|
|
74
113
|
id: config.id,
|
|
75
114
|
name: config.name,
|
|
76
115
|
baseUrl: config.baseUrl,
|
|
77
|
-
auth: { apiKey: envApiKeyAuth(`${config.name} API key`, config.envVars) },
|
|
116
|
+
auth: { apiKey: piAi.envApiKeyAuth(`${config.name} API key`, config.envVars) },
|
|
78
117
|
models: baselineModels(source),
|
|
79
118
|
fetchModels: async (context: RefreshModelsContext) => {
|
|
80
119
|
const credential = context.credential;
|
|
@@ -91,6 +130,6 @@ export function createNanCompatibleProvider(
|
|
|
91
130
|
const current = liveIds;
|
|
92
131
|
return current ? models.filter((model) => current.has(model.id)) : models;
|
|
93
132
|
},
|
|
94
|
-
api:
|
|
133
|
+
api: apiFactory(),
|
|
95
134
|
});
|
|
96
135
|
}
|