@gtrabanco/pi-nan-provider 0.5.1 → 0.6.1
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 +20 -5
- package/README.es.md +27 -9
- package/README.md +27 -8
- package/package.json +2 -1
- package/scripts/check-nan-mcp-server.ts +426 -0
- package/scripts/generate-models.ts +33 -2
- package/scripts/manual-overrides.ts +1 -5
- package/scripts/models.generated.ts +12 -66
- package/src/fetch-models.ts +45 -14
- package/src/mcp/nan-media.ts +21 -5
- package/src/mcp/stdio-client.ts +1 -1
package/AGENTS.md
CHANGED
|
@@ -89,9 +89,21 @@ Every PR that changes code MUST bump `package.json` version in the same PR; CI p
|
|
|
89
89
|
- Capability values that diverge from models.dev are recorded as build-time
|
|
90
90
|
`MANUAL_OVERRIDES` (mandatory provenance note) in `scripts/manual-overrides.ts`,
|
|
91
91
|
applied by `scripts/generate-models.ts` — never hand-edited into
|
|
92
|
-
`scripts/models.generated.ts` and never invented. e.g.
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
`scripts/models.generated.ts` and never invented. e.g. deepseek-v4-flash
|
|
93
|
+
image input (Vision-Exp variant; models.dev lists text only). Re-verify
|
|
94
|
+
overrides when the sources update: the qwen3.8-flash contextWindow 1,000,000
|
|
95
|
+
override (maintainer-confirmed 2026-09-05) was withdrawn 2026-09-07 — the
|
|
96
|
+
updated NaN docs still say 262K "the model's native window" and models.dev
|
|
97
|
+
agrees at 262,144.
|
|
98
|
+
- NaN's official chat model list (https://nan.builders/openapi.json `model`
|
|
99
|
+
param description + https://nan.builders/docs/models, checked 2026-09-07):
|
|
100
|
+
community `deepseek-v4-flash`, `mimo-v2.5`, `qwen3.8-flash`, `glm5.3-flash`,
|
|
101
|
+
`qwen3.6`, `gemma4` (all text+image vision) + premium-tier `glm5.3`
|
|
102
|
+
(~753B MoE, text-only input, 1M context, 400M tokens/rolling 4h window).
|
|
103
|
+
`glm5.2` was removed by the provider (2026-09-05); models.dev may still list
|
|
104
|
+
it — the generator excludes it via `PROVIDER_REMOVED_MODEL_IDS`. Non-chat
|
|
105
|
+
endpoints: qwen3-embedding, rerank, kokoro (TTS), whisper (STT),
|
|
106
|
+
flux-2-klein (images) — MCP-bridge territory, not chat catalog models.
|
|
95
107
|
- Relative imports inside this package use `.ts` extensions (pi's official
|
|
96
108
|
extension examples do the same; pi transpiles extension sources).
|
|
97
109
|
- pi intentionally has NO built-in MCP client (docs/usage.md). MCP integration
|
|
@@ -103,7 +115,10 @@ Every PR that changes code MUST bump `package.json` version in the same PR; CI p
|
|
|
103
115
|
"growing registry" — use tools/list to discover.
|
|
104
116
|
- Community `nan-mcp-server` (https://github.com/luciferfran/nan-mcp-server):
|
|
105
117
|
stdio MCP server, spawned per tool call (lazy), opt-in NAN_MEDIA_MCP=1,
|
|
106
|
-
version-pinned via NAN_MEDIA_MCP_VERSION (default 1.0.
|
|
118
|
+
version-pinned via NAN_MEDIA_MCP_VERSION (default 1.0.8) or a full command
|
|
107
119
|
override via NAN_MEDIA_MCP_COMMAND. Tools: generate_image, edit_image,
|
|
108
120
|
text_to_speech, list_voices, speech_to_text, embed, rerank, list_models
|
|
109
|
-
(we bridge the audio/image/transcription scope).
|
|
121
|
+
(we bridge the audio/image/transcription scope). An automated check
|
|
122
|
+
(scripts/check-nan-mcp-server.ts + .github/workflows/check-nan-mcp-server-update.yml)
|
|
123
|
+
compares the npm registry against the pin weekly and files a `dependencies` issue
|
|
124
|
+
with a breaking/safe verdict from the live server tool surface (unpkg).
|
package/README.es.md
CHANGED
|
@@ -98,14 +98,33 @@ Variables de entorno:
|
|
|
98
98
|
| Variable | Por defecto | Significado |
|
|
99
99
|
|---|---|---|
|
|
100
100
|
| `NAN_MEDIA_MCP` | — | Override por sesión del puente de media: cualquier valor explícito (incl. `0`) gana al toggle persistido de `/nan-mcp`; sin definir → persistido/por defecto |
|
|
101
|
-
| `NAN_MEDIA_MCP_VERSION` | `1.0.
|
|
102
|
-
| `NAN_MEDIA_MCP_COMMAND` | — | Comando personalizado completo, p. ej. `bunx nan-mcp-server@1.0.
|
|
101
|
+
| `NAN_MEDIA_MCP_VERSION` | `1.0.8` | Versión del servidor fijada para `npx -y nan-mcp-server@<v>` (recomendación de supply-chain del propio proyecto) |
|
|
102
|
+
| `NAN_MEDIA_MCP_COMMAND` | — | Comando personalizado completo, p. ej. `bunx nan-mcp-server@1.0.8` |
|
|
103
|
+
|
|
104
|
+
#### Detección automatizada de actualizaciones
|
|
105
|
+
|
|
106
|
+
Una nueva versión de `nan-mcp-server` no desviará silenciosamente el pin de esta conexión. El
|
|
107
|
+
planificador (`.github/workflows/check-nan-mcp-server-update.yml`) ejecuta `bun run scripts/check-nan-mcp-server.ts`
|
|
108
|
+
semanalmente y, cuando el registro npm muestra una versión más reciente, abre (o refresca) un único
|
|
109
|
+
issue etiquetado `dependencies` que describe si el bump es **breaking** o **seguro**, más la lista de
|
|
110
|
+
commits aguas arriba. Puedes ejecutarlo localmente en cualquier momento:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
bun run check-nan-mcp-server # informe legible
|
|
114
|
+
bun run check-nan-mcp-server --json # JSON para máquina
|
|
115
|
+
bun run check-nan-mcp-server --issue # crea/refresca el issue (requiere GITHUB_TOKEN)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
La decisión se toma a partir de la *superficie de tools* en vivo (vía unpkg), no de docs obsoletos: si
|
|
119
|
+
el último servidor sigue exponiendo todas las tools conectadas (`generate_image`, `edit_image`,
|
|
120
|
+
`text_to_speech`, `list_voices`, `speech_to_text`), el bump se reporta como **no rompente**; si elimina
|
|
121
|
+
o renombra alguna tool conectada, el issue se marca **breaking** para revisión manual antes de subir.
|
|
103
122
|
| `NAN_MEDIA_MCP_TIMEOUT_MS` | `120000` | Timeout por llamada; el proceso se mata al expirar |
|
|
104
123
|
| `NAN_MCP_TOOLS` | — | Override por sesión del puente oficial: `0`/`false`/`off` desactiva `nan_web_search`; sin definir → persistido/por defecto |
|
|
105
124
|
|
|
106
125
|
## Modelos
|
|
107
126
|
|
|
108
|
-
Catálogo base (de models.dev, proveedor `nan`, obtenido 2026-09-
|
|
127
|
+
Catálogo base (de models.dev, proveedor `nan`, obtenido 2026-09-07 y corregido contra [los docs de NaN](https://nan.builders/docs/models) y [openapi.json](https://nan.builders/openapi.json) — límites *servidos* por NaN, no máximos teóricos):
|
|
109
128
|
|
|
110
129
|
| Modelo | Contexto | Máx. salida | Entrada | Razonamiento |
|
|
111
130
|
|---|---|---|---|---|
|
|
@@ -113,19 +132,18 @@ Catálogo base (de models.dev, proveedor `nan`, obtenido 2026-09-04 — límites
|
|
|
113
132
|
| `gemma4` | 262,144 | 32,768 | texto, imagen | sí |
|
|
114
133
|
| `deepseek-v4-flash` | 1,000,000 | 384,000 | texto, imagen | sí |
|
|
115
134
|
| `mimo-v2.5` | 1,048,576 | 131,072 | texto, imagen | sí |
|
|
116
|
-
| `glm5.2` | 500,000 | 131,072 | texto | sí |
|
|
117
135
|
| `glm5.3-flash` | 1,000,000 | 131,072 | texto, imagen | sí |
|
|
118
|
-
| `qwen3.8-flash` |
|
|
136
|
+
| `qwen3.8-flash` | 262,144 | 131,072 | texto, imagen | sí |
|
|
119
137
|
|
|
120
138
|
Notas (grabadas por entrada en `scripts/models.generated.ts`):
|
|
121
139
|
|
|
122
|
-
-
|
|
123
|
-
|
|
124
|
-
- `
|
|
140
|
+
- `qwen3.8-flash` sirve 262K tokens, «la ventana nativa del modelo» ([docs de NaN](https://nan.builders/docs/models), 2026-09-07). Un override previo de 1M (confirmado por el mantenedor el 2026-09-05) se retiró cuando los docs actualizados siguieron diciendo 262K; models.dev coincide en 262,144. Este tipo de divergencias se registran como `MANUAL_OVERRIDES` en tiempo de build (con procedencia) en `scripts/manual-overrides.ts` — añade una ahí en vez de editar el fichero generado.
|
|
141
|
+
- `deepseek-v4-flash` incluye entrada de imagen porque NaN sirve la variante Vision-Exp ([docs de NaN](https://nan.builders/docs/models), confirmado por los content-parts de visión en [openapi.json](https://nan.builders/openapi.json)); models.dev la lista como solo texto.
|
|
142
|
+
- `glm5.2` fue eliminado por NaN (2026-09-05). models.dev aún lo listaba el 2026-09-07, así que el generador lo excluye vía `PROVIDER_REMOVED_MODEL_IDS` con la razón registrada — una regeneración no debe resucitar modelos retirados por el proveedor.
|
|
125
143
|
- `mimo-v2.5` es omnimodal (texto/imagen/audio) en NaN, pero el tipo de modelo de pi solo representa entrada texto/imagen, así que el audio se omite en `input`.
|
|
126
144
|
- NaN factura por cuota de membresía, que models.dev reporta como coste cero por token — el coste mostrado por pi será $0.
|
|
127
145
|
- Compat (`supportsDeveloperRole: false`, `supportsReasoningEffort: true`, `supportsUsageInStreaming: true`, `maxTokensField: "max_tokens"`) coincide con la config LiteLLM probada en batalla que este paquete reemplaza; el ejemplo de los docs de NaN (`supportsDeveloperRole: true`) no está probado.
|
|
128
|
-
- **Tier/cuota**: qué modelos puedes llamar lo decide tu membresía de NaN. Con clave, el fetch en vivo refleja exactamente eso (ver *Cómo funciona* — detección de tier). El
|
|
146
|
+
- **Tier/cuota**: qué modelos puedes llamar lo decide tu membresía de NaN. Con clave, el fetch en vivo refleja exactamente eso (ver *Cómo funciona* — detección de tier). El `glm5.3` de tier premium no está en el proveedor `nan` de models.dev y ninguna fuente documenta su límite de salida, así que no entra en el catálogo estático (marcado como no emitible en los metadatos); las claves premium lo reciben en vivo vía el refresh de `/models`, con límites conservadores (128K contexto / 4K salida). Solo está `glm5.3-flash` en el catálogo estático.
|
|
129
147
|
|
|
130
148
|
### Relación con `~/.pi/agent/models.json`
|
|
131
149
|
|
package/README.md
CHANGED
|
@@ -98,14 +98,33 @@ Environment variables:
|
|
|
98
98
|
| Variable | Default | Meaning |
|
|
99
99
|
|---|---|---|
|
|
100
100
|
| `NAN_MEDIA_MCP` | — | Per-session override for the media bridge: any explicit value (incl. `0`) beats the `/nan-mcp` persisted toggle; unset → persisted/default |
|
|
101
|
-
| `NAN_MEDIA_MCP_VERSION` | `1.0.
|
|
102
|
-
| `NAN_MEDIA_MCP_COMMAND` | — | Full custom command, e.g. `bunx nan-mcp-server@1.0.
|
|
101
|
+
| `NAN_MEDIA_MCP_VERSION` | `1.0.8` | Pinned server version for `npx -y nan-mcp-server@<v>` (upstream's own supply-chain recommendation) |
|
|
102
|
+
| `NAN_MEDIA_MCP_COMMAND` | — | Full custom command, e.g. `bunx nan-mcp-server@1.0.8` |
|
|
103
|
+
|
|
104
|
+
#### Automated update detection
|
|
105
|
+
|
|
106
|
+
A newer `nan-mcp-server` release won't silently drift this bridge's pin. The scheduler
|
|
107
|
+
(`.github/workflows/check-nan-mcp-server-update.yml`) runs `bun run scripts/check-nan-mcp-server.ts`
|
|
108
|
+
weekly and, when the npm registry shows a newer version, opens (or refreshes) one
|
|
109
|
+
`dependencies`-labelled issue describing whether the bump is **breaking** or **safe**, plus the
|
|
110
|
+
upstream commit list. Run it locally any time:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
bun run check-nan-mcp-server # human-readable report
|
|
114
|
+
bun run check-nan-mcp-server --json # machine-readable JSON
|
|
115
|
+
bun run check-nan-mcp-server --issue # create/refresh the issue (needs GITHUB_TOKEN)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The decision is made from the *live* server tool surface (via unpkg), not from stale docs: if the
|
|
119
|
+
latest server still exposes every bridged tool (`generate_image`, `edit_image`, `text_to_speech`,
|
|
120
|
+
`list_voices`, `speech_to_text`), the bump is reported as **non-breaking**; if it drops or renames a
|
|
121
|
+
bridged tool, the issue is flagged **breaking** for manual review before bumping.
|
|
103
122
|
| `NAN_MEDIA_MCP_TIMEOUT_MS` | `120000` | Per-call timeout; the process is killed after it |
|
|
104
123
|
| `NAN_MCP_TOOLS` | — | Per-session override for the official bridge: `0`/`false`/`off` disables `nan_web_search`; unset → persisted/default |
|
|
105
124
|
|
|
106
125
|
## Models
|
|
107
126
|
|
|
108
|
-
Baseline catalog (from models.dev, provider `nan`, fetched 2026-09-
|
|
127
|
+
Baseline catalog (from models.dev, provider `nan`, fetched 2026-09-07 and corrected against [NaN's docs](https://nan.builders/docs/models) and [openapi.json](https://nan.builders/openapi.json) — NaN's *served* limits, not raw model maxima):
|
|
109
128
|
|
|
110
129
|
| Model | Context | Max output | Input | Reasoning |
|
|
111
130
|
|---|---|---|---|---|
|
|
@@ -113,18 +132,18 @@ Baseline catalog (from models.dev, provider `nan`, fetched 2026-09-04 — NaN's
|
|
|
113
132
|
| `gemma4` | 262,144 | 32,768 | text, image | yes |
|
|
114
133
|
| `deepseek-v4-flash` | 1,000,000 | 384,000 | text, image | yes |
|
|
115
134
|
| `mimo-v2.5` | 1,048,576 | 131,072 | text, image | yes |
|
|
116
|
-
| `glm5.2` | 500,000 | 131,072 | text | yes |
|
|
117
135
|
| `glm5.3-flash` | 1,000,000 | 131,072 | text, image | yes |
|
|
118
|
-
| `qwen3.8-flash` |
|
|
136
|
+
| `qwen3.8-flash` | 262,144 | 131,072 | text, image | yes |
|
|
119
137
|
|
|
120
138
|
Notes (recorded per entry in `scripts/models.generated.ts`):
|
|
121
139
|
|
|
122
|
-
- `qwen3.8-flash`
|
|
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.
|
|
140
|
+
- `qwen3.8-flash` serves 262K tokens, "the model's native window" ([NaN docs](https://nan.builders/docs/models), 2026-09-07). An earlier 1M override (maintainer-confirmed 2026-09-05) was withdrawn once the updated docs still said 262K; models.dev agrees at 262,144. Divergences like this are recorded as build-time `MANUAL_OVERRIDES` (with provenance) in `scripts/manual-overrides.ts` — apply one instead of editing the generated file.
|
|
141
|
+
- `deepseek-v4-flash` includes image input because NaN serves the Vision-Exp variant ([NaN docs](https://nan.builders/docs/models), confirmed by the vision content-parts in [openapi.json](https://nan.builders/openapi.json)); models.dev lists text only.
|
|
142
|
+
- `glm5.2` was removed by NaN (2026-09-05). models.dev still listed it on 2026-09-07, so the generator excludes it via `PROVIDER_REMOVED_MODEL_IDS` with a recorded reason — a regeneration must not resurrect provider-removed models.
|
|
124
143
|
- `mimo-v2.5` is omnimodal (text/image/audio) on NaN, but pi's model type only represents text/image input, so audio is dropped from `input`.
|
|
125
144
|
- NaN bills via membership quota, which models.dev reports as zero per-token cost — pi's cost display will read $0.
|
|
126
145
|
- Compat (`supportsDeveloperRole: false`, `supportsReasoningEffort: true`, `supportsUsageInStreaming: true`, `maxTokensField: "max_tokens"`) matches the battle-tested LiteLLM config this package replaces; NaN's docs example (`supportsDeveloperRole: true`) is not battle-tested.
|
|
127
|
-
- **Tier/quota**: which models you can call is decided by your NaN membership. With a key, the live fetch reflects exactly that (see *How it works* — tier detection). The premium-tier
|
|
146
|
+
- **Tier/quota**: which models you can call is decided by your NaN membership. With a key, the live fetch reflects exactly that (see *How it works* — tier detection). The premium-tier `glm5.3` is absent from the models.dev `nan` provider and no source documents its max output tokens, so it is not in the static catalog (flagged as unemittable in the catalog metadata); premium keys still get it live via the `/models` refresh with conservative placeholder limits (128K context / 4K output). Only `glm5.3-flash` is in the static catalog.
|
|
128
147
|
|
|
129
148
|
### Relationship to `~/.pi/agent/models.json`
|
|
130
149
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gtrabanco/pi-nan-provider",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
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",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
],
|
|
36
36
|
"scripts": {
|
|
37
37
|
"generate-models": "bun run scripts/generate-models.ts",
|
|
38
|
+
"check-nan-mcp-server": "bun run scripts/check-nan-mcp-server.ts",
|
|
38
39
|
"prepublishOnly": "bun run generate-models && bun test && bun run typecheck",
|
|
39
40
|
"test": "bun test",
|
|
40
41
|
"typecheck": "bunx tsc --noEmit"
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Detect whether an updated `nan-mcp-server` is available and, if so, decide
|
|
4
|
+
* whether it is safe to bump the pinned version this package bridges.
|
|
5
|
+
*
|
|
6
|
+
* This is the automated companion to the manual "should we update?" question
|
|
7
|
+
* for the community stdio MCP server (`nan-mcp-server`, the media bridge). It
|
|
8
|
+
* exists because pi implements NO MCP client — we bridge the server's tools as
|
|
9
|
+
* native pi tools, and the bridge is pinned to a version via
|
|
10
|
+
* `DEFAULT_NAN_MEDIA_MCP_VERSION`. When the upstream server ships a new
|
|
11
|
+
* version we want a signal, not a silent drift.
|
|
12
|
+
*
|
|
13
|
+
* What it checks (all live, no human guesswork):
|
|
14
|
+
* 1. Latest published `nan-mcp-server` version on the npm registry.
|
|
15
|
+
* 2. Whether the latest version is newer than the pinned version.
|
|
16
|
+
* 3. The *tool surface* of the latest server vs the tools we bridge: if the
|
|
17
|
+
* latest server still exposes every bridged tool, the update is **safe**
|
|
18
|
+
* (non-breaking) even if the implementation changed. If it dropped or
|
|
19
|
+
* renamed a bridged tool, the bump is **breaking** and needs manual
|
|
20
|
+
* review before proceeding — an agent check cannot auto-bump that.
|
|
21
|
+
* 4. The upstream commit list between the pinned and latest versions, so a
|
|
22
|
+
* maintainer sees WHAT changed without re-deriving from stale docs.
|
|
23
|
+
*
|
|
24
|
+
* Output: a single structured `NanMcpServerCheckReport` (JSON). The script
|
|
25
|
+
* exits 0 when up-to-date or when the check cannot be completed (so a cron is
|
|
26
|
+
* quiet unless there is news), and it creates/updates a GitHub issue when an
|
|
27
|
+
* update is available and `GITHUB_TOKEN` is present.
|
|
28
|
+
*
|
|
29
|
+
* Usage:
|
|
30
|
+
* bun run scripts/check-nan-mcp-server.ts # report to stdout
|
|
31
|
+
* bun run scripts/check-nan-mcp-server.ts --json # machine-readable JSON
|
|
32
|
+
* bun run scripts/check-nan-mcp-server.ts --issue # create/update an issue (needs GITHUB_TOKEN)
|
|
33
|
+
*
|
|
34
|
+
* The pure decision logic (`parseVersion`, `compareVersions`, `extractServerTools`,
|
|
35
|
+
* `assessUpdate`, `buildReport`) is exported for unit tests so the whole verdict
|
|
36
|
+
* is verifiable without touching the network. The network fetchers accept an
|
|
37
|
+
* injected `fetch` so tests can stub the registry/unpkg/github sources.
|
|
38
|
+
*
|
|
39
|
+
* Exit codes: 0 always (a cron should be quiet unless a human acts). Use
|
|
40
|
+
* `--json` to consume the report and decide what to do with it.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import {
|
|
44
|
+
DEFAULT_NAN_MEDIA_MCP_VERSION,
|
|
45
|
+
NAN_MEDIA_MCP_SERVER_TOOLS,
|
|
46
|
+
} from "../src/mcp/nan-media.ts";
|
|
47
|
+
|
|
48
|
+
/** npm package name of the community stdio MCP server we bridge. */
|
|
49
|
+
export const NAN_MCP_SERVER_PACKAGE = "nan-mcp-server";
|
|
50
|
+
/** npm registry endpoint for the package's latest release. */
|
|
51
|
+
export const NAN_MCP_SERVER_REGISTRY_URL = "https://registry.npmjs.org/nan-mcp-server/latest";
|
|
52
|
+
/** unpkg CDN path to the published server source, used to extract the tool surface. */
|
|
53
|
+
export const NAN_MCP_SERVER_SOURCE_URL = (version: string) =>
|
|
54
|
+
`https://unpkg.com/nan-mcp-server@${version}/server.js`;
|
|
55
|
+
/** GitHub compare API for the upstream commit list between two tags. */
|
|
56
|
+
export const NAN_MCP_SERVER_COMPARE_URL = (from: string, to: string) =>
|
|
57
|
+
`https://api.github.com/repos/luciferfran/nan-mcp-server/compare/v${from.replace(/^v/, "")}...v${to.replace(/^v/, "")}`;
|
|
58
|
+
|
|
59
|
+
/** GitHub search-issues endpoint used to dedupe the update issue. */
|
|
60
|
+
const GITHUB_SEARCH_ISSUES_URL =
|
|
61
|
+
"https://api.github.com/search/issues?q=" +
|
|
62
|
+
"repo:gtrabanco/pi-nan-provider+is:issue+is:open+in:title" +
|
|
63
|
+
'+label:dependencies+type:issue+"nan-mcp-server update"';
|
|
64
|
+
|
|
65
|
+
/** Label to attach to the auto-created update issue (dedupe key + visibility). */
|
|
66
|
+
export const NAN_MCP_UPDATE_ISSUE_LABEL = "dependencies";
|
|
67
|
+
/** Title template; the version pair is appended so each update is its own issue. */
|
|
68
|
+
export const NAN_MCP_UPDATE_ISSUE_TITLE_PREFIX = "[auto] nan-mcp-server update available";
|
|
69
|
+
|
|
70
|
+
export interface NanMcpServerCheckReport {
|
|
71
|
+
/** npm package scanned. */
|
|
72
|
+
package: string;
|
|
73
|
+
/** Version pinned in this package's bridge (source of truth: nan-media.ts). */
|
|
74
|
+
pinnedVersion: string;
|
|
75
|
+
/** Latest version published on npm; null when the registry could not be read. */
|
|
76
|
+
latestVersion: string | null;
|
|
77
|
+
/** true when latestVersion is strictly newer than pinnedVersion. */
|
|
78
|
+
updateAvailable: boolean;
|
|
79
|
+
/** How the verdict was reached: registry in reach, no drift, or an error. */
|
|
80
|
+
status: "up-to-date" | "update-available" | "check-failed";
|
|
81
|
+
/** true when a bridged tool is missing from the latest server surface. */
|
|
82
|
+
breaking: boolean;
|
|
83
|
+
/** Tools added by the latest server that we do NOT bridge (informational). */
|
|
84
|
+
addedTools: string[];
|
|
85
|
+
/** Bridged tools dropped/renamed by the latest server (breaking → manual). */
|
|
86
|
+
removedTools: string[];
|
|
87
|
+
/** Upstream commit subjects between pinned and latest (when reachable). */
|
|
88
|
+
changelog: string[];
|
|
89
|
+
/** Human-readable verdict sentence. */
|
|
90
|
+
reason: string;
|
|
91
|
+
/** When the check could not reach a source (registry/unpkg/github). */
|
|
92
|
+
error?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Parse a semver-ish string (`1.0.8`, `v1.0.8`) into numeric major/minor/patch.
|
|
97
|
+
* Non-numeric release segments are clamped to 0 so `1.0.8` and `1.0.8-rc.1`
|
|
98
|
+
* compare sensibly; malformed input yields `[0, 0, 0]` (never throws).
|
|
99
|
+
*/
|
|
100
|
+
export function parseVersion(input: string): [number, number, number] {
|
|
101
|
+
const cleaned = String(input).trim().replace(/^v/, "").split(/[-+.]/);
|
|
102
|
+
const toNum = (segment: string | undefined) => {
|
|
103
|
+
const n = Number.parseInt(segment ?? "", 10);
|
|
104
|
+
return Number.isFinite(n) ? n : 0;
|
|
105
|
+
};
|
|
106
|
+
return [toNum(cleaned[0]), toNum(cleaned[1]), toNum(cleaned[2])];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Compare two version strings. Returns 1 when `a` is newer, -1 when older,
|
|
111
|
+
* 0 when equal. Comparable to a numeric semver compare for x.y.z inputs.
|
|
112
|
+
*/
|
|
113
|
+
export function compareVersions(a: string, b: string): number {
|
|
114
|
+
const [amaj, amin, apatch] = parseVersion(a);
|
|
115
|
+
const [bmaj, bmin, bpatch] = parseVersion(b);
|
|
116
|
+
if (amaj !== bmaj) return amaj > bmaj ? 1 : -1;
|
|
117
|
+
if (amin !== bmin) return amin > bmin ? 1 : -1;
|
|
118
|
+
if (apatch !== bpatch) return apatch > bpatch ? 1 : -1;
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Extract every `registerTool("NAME")` from a server.js source string. */
|
|
123
|
+
export function extractServerTools(source: string): string[] {
|
|
124
|
+
const names = new Set<string>();
|
|
125
|
+
const re = /registerTool\(\s*["']([^"']+)["']/g;
|
|
126
|
+
let match: RegExpExecArray | null;
|
|
127
|
+
while ((match = re.exec(source)) !== null) {
|
|
128
|
+
names.add(match[1]!);
|
|
129
|
+
}
|
|
130
|
+
return [...names].sort();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Compare the latest server tool surface against the bridged tools and decide
|
|
135
|
+
* whether the update is safe to bump. The only hard break is a bridged tool
|
|
136
|
+
* disappearing (dropped or renamed); anything else — new tools, bug fixes,
|
|
137
|
+
* schema tightening (e.g. `edit_image` now rejects >4 images) — is safe for
|
|
138
|
+
* our bridge, because we forward the params the schema already documents.
|
|
139
|
+
*/
|
|
140
|
+
export function assessUpdate(latestTools: string[]): {
|
|
141
|
+
breaking: boolean;
|
|
142
|
+
addedTools: string[];
|
|
143
|
+
removedTools: string[];
|
|
144
|
+
reason: string;
|
|
145
|
+
} {
|
|
146
|
+
const bridged: readonly string[] = [...NAN_MEDIA_MCP_SERVER_TOOLS];
|
|
147
|
+
const latest = new Set(latestTools.map((t) => t.trim()));
|
|
148
|
+
|
|
149
|
+
const removedTools = bridged.filter((tool) => !latest.has(tool));
|
|
150
|
+
const addedTools = latestTools.filter((tool) => !bridged.includes(tool));
|
|
151
|
+
|
|
152
|
+
const breaking = removedTools.length > 0;
|
|
153
|
+
|
|
154
|
+
let reason: string;
|
|
155
|
+
if (breaking) {
|
|
156
|
+
reason =
|
|
157
|
+
`BREAKING: the latest nan-mcp-server no longer exposes ${removedTools.join(", ")}. ` +
|
|
158
|
+
"Bumping the pin would break the bridged tool(s) — manual review required before deciding.";
|
|
159
|
+
} else if (addedTools.length > 0) {
|
|
160
|
+
reason = `Safe to bump (non-breaking): every bridged tool is still present. New upstream tool(s) ${addedTools.join(", ")} are NOT bridged by this package (optional future work).`;
|
|
161
|
+
} else {
|
|
162
|
+
reason = "Safe to bump (non-breaking): every bridged tool is still present, and no new upstream tools were added.";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { breaking, addedTools, removedTools, reason };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Build the full report from already-fetched data. Pure — no network. Missing
|
|
170
|
+
* or stale inputs degrade to `check-failed`, and an update is only reported
|
|
171
|
+
* when the latest version is strictly newer than the pinned one.
|
|
172
|
+
*/
|
|
173
|
+
export function buildReport(input: {
|
|
174
|
+
pinnedVersion: string;
|
|
175
|
+
latestVersion: string | null;
|
|
176
|
+
latestTools: string[];
|
|
177
|
+
changelog: string[];
|
|
178
|
+
error?: string;
|
|
179
|
+
}): NanMcpServerCheckReport {
|
|
180
|
+
const { pinnedVersion, latestVersion, latestTools, changelog, error } = input;
|
|
181
|
+
|
|
182
|
+
if (error) {
|
|
183
|
+
return {
|
|
184
|
+
package: NAN_MCP_SERVER_PACKAGE,
|
|
185
|
+
pinnedVersion,
|
|
186
|
+
latestVersion: null,
|
|
187
|
+
updateAvailable: false,
|
|
188
|
+
status: "check-failed",
|
|
189
|
+
breaking: false,
|
|
190
|
+
addedTools: [],
|
|
191
|
+
removedTools: [],
|
|
192
|
+
changelog: [],
|
|
193
|
+
reason: `Could not complete the check: ${error}`,
|
|
194
|
+
error,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (latestVersion === null) {
|
|
199
|
+
return {
|
|
200
|
+
package: NAN_MCP_SERVER_PACKAGE,
|
|
201
|
+
pinnedVersion,
|
|
202
|
+
latestVersion: null,
|
|
203
|
+
updateAvailable: false,
|
|
204
|
+
status: "check-failed",
|
|
205
|
+
breaking: false,
|
|
206
|
+
addedTools: [],
|
|
207
|
+
removedTools: [],
|
|
208
|
+
changelog: [],
|
|
209
|
+
reason: "Could not read the npm registry for the latest version.",
|
|
210
|
+
error: "registry-unreachable",
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const updateAvailable = compareVersions(latestVersion, pinnedVersion) > 0;
|
|
215
|
+
if (!updateAvailable) {
|
|
216
|
+
return {
|
|
217
|
+
package: NAN_MCP_SERVER_PACKAGE,
|
|
218
|
+
pinnedVersion,
|
|
219
|
+
latestVersion,
|
|
220
|
+
updateAvailable: false,
|
|
221
|
+
status: "up-to-date",
|
|
222
|
+
breaking: false,
|
|
223
|
+
addedTools: [],
|
|
224
|
+
removedTools: [],
|
|
225
|
+
changelog: [],
|
|
226
|
+
reason: `Up to date: pinned ${pinnedVersion} is not older than latest ${latestVersion}.`,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const verdict = assessUpdate(latestTools);
|
|
231
|
+
return {
|
|
232
|
+
package: NAN_MCP_SERVER_PACKAGE,
|
|
233
|
+
pinnedVersion,
|
|
234
|
+
latestVersion,
|
|
235
|
+
updateAvailable: true,
|
|
236
|
+
status: "update-available",
|
|
237
|
+
breaking: verdict.breaking,
|
|
238
|
+
addedTools: verdict.addedTools,
|
|
239
|
+
removedTools: verdict.removedTools,
|
|
240
|
+
changelog,
|
|
241
|
+
reason: verdict.reason,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** A minimal fetch-compatible signature so tests can stub it without undici types. */
|
|
246
|
+
export type FetchLike = (url: string, init?: { headers?: Record<string, string> }) => Promise<Response>;
|
|
247
|
+
|
|
248
|
+
async function readJson<T>(url: string, fetchImpl: FetchLike, headers?: Record<string, string>): Promise<T> {
|
|
249
|
+
const res = await fetchImpl(url, { headers });
|
|
250
|
+
if (!res.ok) {
|
|
251
|
+
throw new Error(`HTTP ${res.status} from ${url}`);
|
|
252
|
+
}
|
|
253
|
+
return (await res.json()) as T;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Fetch the latest published version of the package from the npm registry. */
|
|
257
|
+
export async function fetchLatestVersion(fetchImpl: FetchLike = fetch): Promise<string> {
|
|
258
|
+
const data = await readJson<{ version?: string; "dist-tags"?: { latest?: string } }>(
|
|
259
|
+
NAN_MCP_SERVER_REGISTRY_URL,
|
|
260
|
+
fetchImpl,
|
|
261
|
+
);
|
|
262
|
+
const version = data["dist-tags"]?.latest ?? data.version;
|
|
263
|
+
if (!version) throw new Error("npm registry response had no version");
|
|
264
|
+
return version;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Fetch the published server.js source and extract its tool surface. */
|
|
268
|
+
export async function fetchServerTools(
|
|
269
|
+
version: string,
|
|
270
|
+
fetchImpl: FetchLike = fetch,
|
|
271
|
+
): Promise<string[]> {
|
|
272
|
+
const res = await fetchImpl(NAN_MCP_SERVER_SOURCE_URL(version));
|
|
273
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} from ${NAN_MCP_SERVER_SOURCE_URL(version)}`);
|
|
274
|
+
return extractServerTools(await res.text());
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Fetch upstream commit subjects between two tags (best-effort; empty on failure). */
|
|
278
|
+
export async function fetchChangelog(
|
|
279
|
+
from: string,
|
|
280
|
+
to: string,
|
|
281
|
+
fetchImpl: FetchLike = fetch,
|
|
282
|
+
): Promise<string[]> {
|
|
283
|
+
try {
|
|
284
|
+
const data = await readJson<{ commits?: Array<{ commit?: { message?: string } }> }>(
|
|
285
|
+
NAN_MCP_SERVER_COMPARE_URL(from, to),
|
|
286
|
+
fetchImpl,
|
|
287
|
+
);
|
|
288
|
+
return (data.commits ?? [])
|
|
289
|
+
.map((c) => c.commit?.message?.split("\n")[0]?.trim() ?? "")
|
|
290
|
+
.filter(Boolean);
|
|
291
|
+
} catch {
|
|
292
|
+
return [];
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const ARGS = process.argv.slice(2);
|
|
297
|
+
/** Rendered issue body from a report — what a maintainer reads to decide. */
|
|
298
|
+
export function renderIssueBody(report: NanMcpServerCheckReport): string {
|
|
299
|
+
const lines: string[] = [];
|
|
300
|
+
lines.push(`**Package**: \`${report.package}\``);
|
|
301
|
+
lines.push(`**Pinned by this package**: \`${report.pinnedVersion}\``);
|
|
302
|
+
lines.push(`**Latest on npm**: \`${report.latestVersion ?? "unknown"}\``);
|
|
303
|
+
lines.push(`**Verdict**: ${report.breaking ? "⚠️ BREAKING — manual review required" : "✅ Safe to bump (non-breaking)"}`);
|
|
304
|
+
lines.push("");
|
|
305
|
+
lines.push(`> ${report.reason}`);
|
|
306
|
+
lines.push("");
|
|
307
|
+
if (report.changelog.length > 0) {
|
|
308
|
+
lines.push("### Upstream changes");
|
|
309
|
+
lines.push("", ...report.changelog.map((c) => `- ${c}`), "");
|
|
310
|
+
}
|
|
311
|
+
lines.push("### To bump");
|
|
312
|
+
lines.push(
|
|
313
|
+
`1. Edit \`DEFAULT_NAN_MEDIA_MCP_VERSION\` in \`src/mcp/nan-media.ts\` from \`${report.pinnedVersion}\` to \`${report.latestVersion}\`.`,
|
|
314
|
+
);
|
|
315
|
+
lines.push("2. Run \`bun test && bun run typecheck\`.");
|
|
316
|
+
lines.push(
|
|
317
|
+
report.breaking
|
|
318
|
+
? "3. Investigate the removed tool(s) — decide whether to bridge an alternative or keep the older pin."
|
|
319
|
+
: "3. Commit the bump and close this issue (it was auto-created).",
|
|
320
|
+
);
|
|
321
|
+
return lines.join("\n");
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** GitHub API root for this repository's issues. */
|
|
325
|
+
export const NAN_MCP_REPO_ISSUES_URL = "https://api.github.com/repos/gtrabanco/pi-nan-provider/issues";
|
|
326
|
+
|
|
327
|
+
async function sendIssueRequest<T>(
|
|
328
|
+
url: string,
|
|
329
|
+
method: string,
|
|
330
|
+
body: object | undefined,
|
|
331
|
+
token: string,
|
|
332
|
+
): Promise<T> {
|
|
333
|
+
const res = await fetch(url, {
|
|
334
|
+
method,
|
|
335
|
+
headers: {
|
|
336
|
+
Authorization: `Bearer ${token}`,
|
|
337
|
+
Accept: "application/vnd.github+json",
|
|
338
|
+
...(body ? { "Content-Type": "application/json" } : {}),
|
|
339
|
+
},
|
|
340
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
341
|
+
});
|
|
342
|
+
if (!res.ok) throw new Error(`GitHub API ${method} ${url} → HTTP ${res.status}`);
|
|
343
|
+
return (await res.json()) as T;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Create a pre-engineered update issue, or refresh an existing open one with
|
|
348
|
+
* the same title+label instead of stacking duplicates across cron runs.
|
|
349
|
+
* Returns the issue URL (web or API) so the caller can surface it.
|
|
350
|
+
*/
|
|
351
|
+
export async function createOrUpdateIssue(
|
|
352
|
+
report: NanMcpServerCheckReport,
|
|
353
|
+
token: string,
|
|
354
|
+
): Promise<string> {
|
|
355
|
+
const title = `${NAN_MCP_UPDATE_ISSUE_TITLE_PREFIX}: ${report.pinnedVersion} → ${report.latestVersion ?? "?"}`;
|
|
356
|
+
const body = renderIssueBody(report);
|
|
357
|
+
|
|
358
|
+
// Dedupe key: an OPEN issue whose title matches AND carries the label.
|
|
359
|
+
const searchUrl = `${GITHUB_SEARCH_ISSUES_URL}+${encodeURIComponent(`"${title}"`)}`;
|
|
360
|
+
const search = await sendIssueRequest<{ items?: Array<{ number: number; html_url: string }> }>(
|
|
361
|
+
searchUrl,
|
|
362
|
+
"GET",
|
|
363
|
+
undefined,
|
|
364
|
+
token,
|
|
365
|
+
);
|
|
366
|
+
const existing = search.items?.[0];
|
|
367
|
+
if (existing) {
|
|
368
|
+
const patchUrl = `${NAN_MCP_REPO_ISSUES_URL}/${existing.number}`;
|
|
369
|
+
await sendIssueRequest(patchUrl, "PATCH", { body }, token);
|
|
370
|
+
return existing.html_url;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const created = await sendIssueRequest<{ html_url: string }>(
|
|
374
|
+
NAN_MCP_REPO_ISSUES_URL,
|
|
375
|
+
"POST",
|
|
376
|
+
{ title, body, labels: [NAN_MCP_UPDATE_ISSUE_LABEL] },
|
|
377
|
+
token,
|
|
378
|
+
);
|
|
379
|
+
return created.html_url;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** Main entry point. */
|
|
383
|
+
async function main(): Promise<void> {
|
|
384
|
+
let report: NanMcpServerCheckReport;
|
|
385
|
+
try {
|
|
386
|
+
const pinnedVersion = DEFAULT_NAN_MEDIA_MCP_VERSION;
|
|
387
|
+
const latestVersion = await fetchLatestVersion();
|
|
388
|
+
let latestTools: string[] = [];
|
|
389
|
+
let changelog: string[] = [];
|
|
390
|
+
if (compareVersions(latestVersion, pinnedVersion) > 0) {
|
|
391
|
+
latestTools = await fetchServerTools(latestVersion).catch(() => []);
|
|
392
|
+
changelog = await fetchChangelog(pinnedVersion, latestVersion);
|
|
393
|
+
}
|
|
394
|
+
report = buildReport({ pinnedVersion, latestVersion, latestTools, changelog });
|
|
395
|
+
} catch (error) {
|
|
396
|
+
report = buildReport({
|
|
397
|
+
pinnedVersion: DEFAULT_NAN_MEDIA_MCP_VERSION,
|
|
398
|
+
latestVersion: null,
|
|
399
|
+
latestTools: [],
|
|
400
|
+
changelog: [],
|
|
401
|
+
error: error instanceof Error ? error.message : String(error),
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (ARGS.includes("--json")) {
|
|
406
|
+
console.log(JSON.stringify(report, null, 2));
|
|
407
|
+
} else {
|
|
408
|
+
console.log(`nan-mcp-server: pinned ${report.pinnedVersion}, latest ${report.latestVersion ?? "unknown"}`);
|
|
409
|
+
console.log(` status: ${report.status} breaking: ${report.breaking}`);
|
|
410
|
+
console.log(` ${report.reason}`);
|
|
411
|
+
if (report.changelog.length > 0) {
|
|
412
|
+
console.log(" upstream:");
|
|
413
|
+
for (const c of report.changelog) console.log(` - ${c}`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const token = process.env.GITHUB_TOKEN;
|
|
418
|
+
if (ARGS.includes("--issue") && report.status === "update-available" && token) {
|
|
419
|
+
const url = await createOrUpdateIssue(report, token);
|
|
420
|
+
console.log(` issue: ${url}`);
|
|
421
|
+
} else if (ARGS.includes("--issue") && report.status === "update-available" && !token) {
|
|
422
|
+
console.log(" --issue requested but GITHUB_TOKEN is not set; issue NOT created.");
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
await main();
|
|
@@ -37,9 +37,35 @@ const PI_SUPPORTED_INPUT = new Set(["text", "image"]);
|
|
|
37
37
|
* Manual corrections over models.dev live in scripts/manual-overrides.ts
|
|
38
38
|
* (shared with test/generated-catalog.test.ts, which pins that every
|
|
39
39
|
* override lands on the generated entry with its provenance note).
|
|
40
|
-
*
|
|
40
|
+
* Capability divergences from models.dev are recorded there, never here.
|
|
41
|
+
* MANUAL_NOTES adds provenance-only notes to otherwise-untouched entries.
|
|
41
42
|
*/
|
|
42
|
-
const MANUAL_NOTES: Record<string, string> = {
|
|
43
|
+
const MANUAL_NOTES: Record<string, string> = {
|
|
44
|
+
"qwen3.8-flash":
|
|
45
|
+
"contextWindow 262,144: the earlier 1,000,000 override (maintainer-confirmed 2026-09-05) was withdrawn 2026-09-07 — the updated https://nan.builders/docs/models still states '262K token context, the model's native window' and models.dev agrees at 262,144; NaN docs are treated as the most reliable source (maintainer instruction, 2026-09-07).",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Models the provider has removed but that models.dev may still list.
|
|
50
|
+
* Excluded at generation time with the recorded reason — a regeneration must
|
|
51
|
+
* never resurrect an entry the gateway no longer serves (this happened with
|
|
52
|
+
* glm5.2: hand-removed 2026-09-05, models.dev re-listed it by 2026-09-07).
|
|
53
|
+
*/
|
|
54
|
+
const PROVIDER_REMOVED_MODEL_IDS: Record<string, string> = {
|
|
55
|
+
"glm5.2":
|
|
56
|
+
"removed by NaN (2026-09-05); absent from the official chat model list in https://nan.builders/openapi.json and https://nan.builders/docs/models (checked 2026-09-07) while models.dev provider nan still listed it — excluded so regeneration does not resurrect it",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Models the provider documents and serves but that cannot be emitted yet:
|
|
61
|
+
* absent from models.dev, or missing a limit no source documents. Flagged in
|
|
62
|
+
* the catalog metadata instead of invented (no-fabrication rule); keys with
|
|
63
|
+
* access still receive them live via the /models refresh with conservative
|
|
64
|
+
* placeholder limits (UNKNOWN_MODEL_LIMITS).
|
|
65
|
+
*/
|
|
66
|
+
const KNOWN_UNEMITTABLE_MODEL_NOTES: readonly string[] = [
|
|
67
|
+
"glm5.3: served by NaN on the GLM 5.3 premium tier (https://nan.builders/docs/models + https://nan.builders/openapi.json, checked 2026-09-07) but absent from models.dev, and no source documents its max output tokens — no entry is generated (no-fabrication rule); premium keys still get it live via the /models refresh with conservative placeholder limits",
|
|
68
|
+
];
|
|
43
69
|
|
|
44
70
|
/**
|
|
45
71
|
* LiteLLM compat confirmed against the live api.nan.builders gateway by the
|
|
@@ -113,6 +139,10 @@ function normalizeInput(modalitiesInput: string[] | undefined, modelId: string):
|
|
|
113
139
|
}
|
|
114
140
|
|
|
115
141
|
function convertModel(modelId: string, m: ModelsDevModel): GeneratedModel | { skip: string } {
|
|
142
|
+
const removedReason = PROVIDER_REMOVED_MODEL_IDS[modelId];
|
|
143
|
+
if (removedReason) {
|
|
144
|
+
return { skip: `provider-removed: "${modelId}" excluded from the catalog (${removedReason})` };
|
|
145
|
+
}
|
|
116
146
|
const contextWindow = m.limit?.context;
|
|
117
147
|
const maxTokens = m.limit?.output;
|
|
118
148
|
if (typeof contextWindow !== "number" || contextWindow <= 0) {
|
|
@@ -195,6 +225,7 @@ async function main(): Promise<void> {
|
|
|
195
225
|
const fetchedAt = new Date().toISOString();
|
|
196
226
|
const allNotes = [
|
|
197
227
|
...skipped,
|
|
228
|
+
...KNOWN_UNEMITTABLE_MODEL_NOTES,
|
|
198
229
|
...new Set(entries.flatMap((entry) => entry.notes ?? [])),
|
|
199
230
|
];
|
|
200
231
|
|
|
@@ -38,10 +38,6 @@ export interface ManualModelOverride {
|
|
|
38
38
|
export const MANUAL_OVERRIDES: Record<string, ManualModelOverride> = {
|
|
39
39
|
"deepseek-v4-flash": {
|
|
40
40
|
input: ["text", "image"],
|
|
41
|
-
note: "input includes image: NaN serves the Vision-Exp variant (
|
|
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.",
|
|
41
|
+
note: "input includes image: NaN serves the Vision-Exp variant ('takes images as input', https://nan.builders/docs/models, checked 2026-09-07; the image_url content-parts in https://nan.builders/openapi.json list deepseek-v4-flash among the vision models); models.dev provider nan lists text only.",
|
|
46
42
|
},
|
|
47
43
|
};
|
|
@@ -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-08T22:49:12.806Z
|
|
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.
|
|
@@ -36,7 +36,7 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
|
|
|
36
36
|
},
|
|
37
37
|
"notes": [
|
|
38
38
|
"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.",
|
|
39
|
-
"input includes image: NaN serves the Vision-Exp variant (
|
|
39
|
+
"input includes image: NaN serves the Vision-Exp variant ('takes images as input', https://nan.builders/docs/models, checked 2026-09-07; the image_url content-parts in https://nan.builders/openapi.json list deepseek-v4-flash among the vision models); models.dev provider nan lists text only."
|
|
40
40
|
],
|
|
41
41
|
"extras": {
|
|
42
42
|
"id": "deepseek-v4-flash",
|
|
@@ -133,62 +133,6 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
|
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
},
|
|
136
|
-
{
|
|
137
|
-
"id": "glm5.2",
|
|
138
|
-
"name": "GLM-5.2",
|
|
139
|
-
"reasoning": true,
|
|
140
|
-
"input": [
|
|
141
|
-
"text"
|
|
142
|
-
],
|
|
143
|
-
"cost": {
|
|
144
|
-
"input": 0,
|
|
145
|
-
"output": 0,
|
|
146
|
-
"cacheRead": 0,
|
|
147
|
-
"cacheWrite": 0
|
|
148
|
-
},
|
|
149
|
-
"contextWindow": 500000,
|
|
150
|
-
"maxTokens": 131072,
|
|
151
|
-
"compat": {
|
|
152
|
-
"supportsDeveloperRole": false,
|
|
153
|
-
"supportsReasoningEffort": true,
|
|
154
|
-
"supportsUsageInStreaming": true,
|
|
155
|
-
"maxTokensField": "max_tokens"
|
|
156
|
-
},
|
|
157
|
-
"notes": [
|
|
158
|
-
"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."
|
|
159
|
-
],
|
|
160
|
-
"extras": {
|
|
161
|
-
"id": "glm5.2",
|
|
162
|
-
"name": "GLM-5.2",
|
|
163
|
-
"description": "Open flagship GLM for long-horizon coding agents, served at 500K context on the NaN premium tier",
|
|
164
|
-
"family": "glm",
|
|
165
|
-
"attachment": false,
|
|
166
|
-
"reasoning": true,
|
|
167
|
-
"reasoning_options": [],
|
|
168
|
-
"tool_call": true,
|
|
169
|
-
"structured_output": true,
|
|
170
|
-
"temperature": true,
|
|
171
|
-
"release_date": "2026-06-13",
|
|
172
|
-
"last_updated": "2026-06-13",
|
|
173
|
-
"modalities": {
|
|
174
|
-
"input": [
|
|
175
|
-
"text"
|
|
176
|
-
],
|
|
177
|
-
"output": [
|
|
178
|
-
"text"
|
|
179
|
-
]
|
|
180
|
-
},
|
|
181
|
-
"open_weights": true,
|
|
182
|
-
"limit": {
|
|
183
|
-
"context": 500000,
|
|
184
|
-
"output": 131072
|
|
185
|
-
},
|
|
186
|
-
"cost": {
|
|
187
|
-
"input": 0,
|
|
188
|
-
"output": 0
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
},
|
|
192
136
|
{
|
|
193
137
|
"id": "glm5.3-flash",
|
|
194
138
|
"name": "GLM-5.3-Flash",
|
|
@@ -236,7 +180,7 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
|
|
|
236
180
|
"text"
|
|
237
181
|
]
|
|
238
182
|
},
|
|
239
|
-
"open_weights":
|
|
183
|
+
"open_weights": true,
|
|
240
184
|
"limit": {
|
|
241
185
|
"context": 1000000,
|
|
242
186
|
"output": 131072
|
|
@@ -382,7 +326,7 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
|
|
|
382
326
|
"cacheRead": 0,
|
|
383
327
|
"cacheWrite": 0
|
|
384
328
|
},
|
|
385
|
-
"contextWindow":
|
|
329
|
+
"contextWindow": 262144,
|
|
386
330
|
"maxTokens": 131072,
|
|
387
331
|
"compat": {
|
|
388
332
|
"supportsDeveloperRole": false,
|
|
@@ -392,7 +336,7 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
|
|
|
392
336
|
},
|
|
393
337
|
"notes": [
|
|
394
338
|
"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
|
|
339
|
+
"contextWindow 262,144: the earlier 1,000,000 override (maintainer-confirmed 2026-09-05) was withdrawn 2026-09-07 — the updated https://nan.builders/docs/models still states '262K token context, the model's native window' and models.dev agrees at 262,144; NaN docs are treated as the most reliable source (maintainer instruction, 2026-09-07)."
|
|
396
340
|
],
|
|
397
341
|
"extras": {
|
|
398
342
|
"id": "qwen3.8-flash",
|
|
@@ -431,12 +375,14 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
|
|
|
431
375
|
export const GENERATED_CATALOG_META = {
|
|
432
376
|
source: "https://models.dev/api.json",
|
|
433
377
|
modelsDevProvider: "nan",
|
|
434
|
-
fetchedAt: "2026-09-
|
|
435
|
-
modelCount:
|
|
436
|
-
models: ["deepseek-v4-flash","gemma4","glm5.
|
|
378
|
+
fetchedAt: "2026-09-08T22:49:12.806Z",
|
|
379
|
+
modelCount: 6,
|
|
380
|
+
models: ["deepseek-v4-flash","gemma4","glm5.3-flash","mimo-v2.5","qwen3.6","qwen3.8-flash"],
|
|
437
381
|
notes: [
|
|
382
|
+
"provider-removed: \"glm5.2\" excluded from the catalog (removed by NaN (2026-09-05); absent from the official chat model list in https://nan.builders/openapi.json and https://nan.builders/docs/models (checked 2026-09-07) while models.dev provider nan still listed it — excluded so regeneration does not resurrect it)",
|
|
383
|
+
"glm5.3: served by NaN on the GLM 5.3 premium tier (https://nan.builders/docs/models + https://nan.builders/openapi.json, checked 2026-09-07) but absent from models.dev, and no source documents its max output tokens — no entry is generated (no-fabrication rule); premium keys still get it live via the /models refresh with conservative placeholder limits",
|
|
438
384
|
"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.",
|
|
439
|
-
"input includes image: NaN serves the Vision-Exp variant (
|
|
440
|
-
"contextWindow 1,000,000
|
|
385
|
+
"input includes image: NaN serves the Vision-Exp variant ('takes images as input', https://nan.builders/docs/models, checked 2026-09-07; the image_url content-parts in https://nan.builders/openapi.json list deepseek-v4-flash among the vision models); models.dev provider nan lists text only.",
|
|
386
|
+
"contextWindow 262,144: the earlier 1,000,000 override (maintainer-confirmed 2026-09-05) was withdrawn 2026-09-07 — the updated https://nan.builders/docs/models still states '262K token context, the model's native window' and models.dev agrees at 262,144; NaN docs are treated as the most reliable source (maintainer instruction, 2026-09-07)."
|
|
441
387
|
],
|
|
442
388
|
} as const;
|
package/src/fetch-models.ts
CHANGED
|
@@ -12,12 +12,16 @@
|
|
|
12
12
|
* only model `id`s — no capability fields. It is used solely to confirm
|
|
13
13
|
* which model IDs are currently live.
|
|
14
14
|
*
|
|
15
|
-
* Merge: live IDs × generated capability data
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
15
|
+
* Merge: live IDs × generated capability data — an allowlist, not an open
|
|
16
|
+
* ingest. Only allowlisted IDs surface: a live ID with generated data keeps
|
|
17
|
+
* its generated capabilities, and an allowlisted uncatalogued ID (premium
|
|
18
|
+
* live-only) gets conservative placeholder limits (the same defaults used in
|
|
19
|
+
* custom-provider.md's dynamic-discovery example) with no reasoning support -
|
|
20
|
+
* capabilities stay "unknown", nothing is fabricated. Every other live ID is
|
|
21
|
+
* dropped so undocumented models (and any model NaN starts serving later)
|
|
22
|
+
* and non-chat endpoints never surface. On fetch failure, timeout, or an
|
|
23
|
+
* unusable response, callers fall back to the generated catalog so startup
|
|
24
|
+
* is never blocked.
|
|
21
25
|
*/
|
|
22
26
|
|
|
23
27
|
import type { Model, OpenAICompletionsCompat } from "@earendil-works/pi-ai";
|
|
@@ -60,6 +64,28 @@ export const NAN_COMPAT_API = "openai-completions" as const;
|
|
|
60
64
|
*/
|
|
61
65
|
export const UNKNOWN_MODEL_LIMITS = { contextWindow: 128_000, maxTokens: 4_096 } as const;
|
|
62
66
|
|
|
67
|
+
/**
|
|
68
|
+
* The allowlist of model ids this package may surface. Everything a live
|
|
69
|
+
* /models response returns that is NOT here is dropped — so models that are
|
|
70
|
+
* undocumented (and might become available later, e.g. one NaN starts
|
|
71
|
+
* serving after a release) can never leak through the package.
|
|
72
|
+
*
|
|
73
|
+
* Every current chat model is listed: the six models.dev-documented entries
|
|
74
|
+
* plus the premium live-only glm5.3 (which NaN serves via /models but that
|
|
75
|
+
* models.dev omits — see the "unemittable" note in scripts/generate-models.ts).
|
|
76
|
+
* Non-chat endpoints (flux-2-klein, kokoro, whisper, qwen3-embedding, rerank)
|
|
77
|
+
* are deliberately excluded: they are MCP-bridge territory, not chat models.
|
|
78
|
+
*/
|
|
79
|
+
const ALLOWED_MODEL_IDS = [
|
|
80
|
+
"deepseek-v4-flash",
|
|
81
|
+
"gemma4",
|
|
82
|
+
"glm5.3-flash",
|
|
83
|
+
"glm5.3",
|
|
84
|
+
"mimo-v2.5",
|
|
85
|
+
"qwen3.6",
|
|
86
|
+
"qwen3.8-flash",
|
|
87
|
+
] as const;
|
|
88
|
+
|
|
63
89
|
/** Timeout for the live /models fetch; matches the pi-synthetic-provider precedent (~3s). */
|
|
64
90
|
export const DEFAULT_MODELS_TIMEOUT_MS = 3_000;
|
|
65
91
|
|
|
@@ -87,9 +113,10 @@ export function toModel(entry: GeneratedModelEntry, source: CatalogSource): Mode
|
|
|
87
113
|
};
|
|
88
114
|
}
|
|
89
115
|
|
|
90
|
-
/** The generated fallback catalog as pi-ai Models for the given provider. */
|
|
116
|
+
/** The generated fallback catalog as pi-ai Models for the given provider (allowlisted entries only). */
|
|
91
117
|
export function baselineModels(source: CatalogSource): Model<"openai-completions">[] {
|
|
92
|
-
|
|
118
|
+
const allowed = new Set<string>(ALLOWED_MODEL_IDS);
|
|
119
|
+
return NAN_GENERATED_MODELS.filter((entry) => allowed.has(entry.id)).map((entry) => toModel(entry, source));
|
|
93
120
|
}
|
|
94
121
|
|
|
95
122
|
export interface LiveModelListOptions {
|
|
@@ -142,15 +169,17 @@ export interface MergedCatalog {
|
|
|
142
169
|
models: Model<"openai-completions">[];
|
|
143
170
|
/** Live IDs resolved against generated capability data. */
|
|
144
171
|
matched: string[];
|
|
145
|
-
/**
|
|
172
|
+
/** Allowlisted live IDs kept with unknown capabilities (conservative limits). */
|
|
146
173
|
unknown: string[];
|
|
147
174
|
}
|
|
148
175
|
|
|
149
176
|
/**
|
|
150
|
-
* Merge live model IDs with the generated capability catalog
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
177
|
+
* Merge live model IDs with the generated capability catalog, gated by the
|
|
178
|
+
* allowlist. Only allowlisted IDs surface: live IDs with generated data keep
|
|
179
|
+
* their generated capabilities; allowlisted uncatalogued IDs (glm5.3) get
|
|
180
|
+
* conservative placeholder limits, `reasoning: false`, and zero cost
|
|
181
|
+
* (documented defaults, not invented capabilities). Every non-allowlisted
|
|
182
|
+
* live ID is dropped.
|
|
154
183
|
*/
|
|
155
184
|
export function mergeLiveWithGenerated(
|
|
156
185
|
liveIds: readonly string[],
|
|
@@ -158,11 +187,13 @@ export function mergeLiveWithGenerated(
|
|
|
158
187
|
generated: readonly GeneratedModelEntry[] = NAN_GENERATED_MODELS,
|
|
159
188
|
): MergedCatalog {
|
|
160
189
|
const byId = new Map(generated.map((entry) => [entry.id, entry]));
|
|
190
|
+
const allowed = new Set<string>(ALLOWED_MODEL_IDS);
|
|
161
191
|
const models: Model<"openai-completions">[] = [];
|
|
162
192
|
const matched: string[] = [];
|
|
163
193
|
const unknown: string[] = [];
|
|
164
194
|
|
|
165
195
|
for (const id of liveIds) {
|
|
196
|
+
if (!allowed.has(id)) continue;
|
|
166
197
|
const entry = byId.get(id);
|
|
167
198
|
if (entry) {
|
|
168
199
|
models.push(toModel(entry, source));
|
|
@@ -205,7 +236,7 @@ export interface ResolvedCatalog {
|
|
|
205
236
|
* when the live fetch failed and the generated fallback was used.
|
|
206
237
|
*/
|
|
207
238
|
liveIds: Set<string> | undefined;
|
|
208
|
-
/**
|
|
239
|
+
/** Allowlisted live IDs kept with unknown capabilities (present only when liveIds is set). */
|
|
209
240
|
unknownIds: string[];
|
|
210
241
|
}
|
|
211
242
|
|
package/src/mcp/nan-media.ts
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* other env vars inherit from your environment (generated files land in
|
|
15
15
|
* ~/nan-mcp-output/ by default).
|
|
16
16
|
* - Version pinning follows the upstream server's own supply-chain guidance:
|
|
17
|
-
* NAN_MEDIA_MCP_VERSION (default "1.0.
|
|
18
|
-
* NAN_MEDIA_MCP_COMMAND (space-separated, e.g. "bunx nan-mcp-server@1.0.
|
|
17
|
+
* NAN_MEDIA_MCP_VERSION (default "1.0.8"), or pass a custom command with
|
|
18
|
+
* NAN_MEDIA_MCP_COMMAND (space-separated, e.g. "bunx nan-mcp-server@1.0.8").
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { Type, type TSchema } from "@earendil-works/pi-ai";
|
|
@@ -28,7 +28,21 @@ export const NAN_MEDIA_MCP_ENV = "NAN_MEDIA_MCP";
|
|
|
28
28
|
export const NAN_MEDIA_MCP_VERSION_ENV = "NAN_MEDIA_MCP_VERSION";
|
|
29
29
|
export const NAN_MEDIA_MCP_COMMAND_ENV = "NAN_MEDIA_MCP_COMMAND";
|
|
30
30
|
export const NAN_MEDIA_MCP_TIMEOUT_ENV = "NAN_MEDIA_MCP_TIMEOUT_MS";
|
|
31
|
-
export const DEFAULT_NAN_MEDIA_MCP_VERSION = "1.0.
|
|
31
|
+
export const DEFAULT_NAN_MEDIA_MCP_VERSION = "1.0.8";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The MCP tool names on the stdio nan-mcp-server that this package bridges as
|
|
35
|
+
* pi tools. Kept as the single source of truth for the check script
|
|
36
|
+
* (scripts/check-nan-mcp-server.ts) and to type each spec's `mcpTool` so the
|
|
37
|
+
* bridge cannot drift from the set it claims to expose.
|
|
38
|
+
*/
|
|
39
|
+
export const NAN_MEDIA_MCP_SERVER_TOOLS = [
|
|
40
|
+
"generate_image",
|
|
41
|
+
"edit_image",
|
|
42
|
+
"text_to_speech",
|
|
43
|
+
"list_voices",
|
|
44
|
+
"speech_to_text",
|
|
45
|
+
] as const;
|
|
32
46
|
export const DEFAULT_MEDIA_MCP_TIMEOUT_MS = 120_000;
|
|
33
47
|
|
|
34
48
|
/** Media tools bridged from the stdio MCP server (audio/image/transcription scope). */
|
|
@@ -84,7 +98,7 @@ function mediaMcpTimeoutMs(): number {
|
|
|
84
98
|
|
|
85
99
|
interface MediaToolSpec<TParams extends TSchema = TSchema> {
|
|
86
100
|
name: (typeof NAN_MEDIA_TOOLS)[number];
|
|
87
|
-
mcpTool:
|
|
101
|
+
mcpTool: (typeof NAN_MEDIA_MCP_SERVER_TOOLS)[number];
|
|
88
102
|
label: string;
|
|
89
103
|
description: string;
|
|
90
104
|
promptSnippet: string;
|
|
@@ -129,7 +143,7 @@ function defineMediaTool<TParams extends TSchema>(spec: MediaToolSpec<TParams>):
|
|
|
129
143
|
/**
|
|
130
144
|
* Build the media tool set. Registered only when NAN_MEDIA_MCP=1 and the
|
|
131
145
|
* runtime supports registerTool; execution spawns the MCP server per call.
|
|
132
|
-
* Schemas mirror nan-mcp-server's zod input schemas (v1.0.
|
|
146
|
+
* Schemas mirror nan-mcp-server's zod input schemas (v1.0.8).
|
|
133
147
|
*/
|
|
134
148
|
export function createNanMediaTools(): ToolDefinition[] {
|
|
135
149
|
return [
|
|
@@ -160,6 +174,8 @@ export function createNanMediaTools(): ToolDefinition[] {
|
|
|
160
174
|
prompt: Type.String({ description: "Description of the edit or transformation to apply" }),
|
|
161
175
|
images: Type.Array(Type.String(), {
|
|
162
176
|
description: "Absolute paths to reference image files (PNG, JPEG, WebP; up to 4, each < 25MB)",
|
|
177
|
+
minItems: 1,
|
|
178
|
+
maxItems: 4,
|
|
163
179
|
}),
|
|
164
180
|
size: sizeProperty(),
|
|
165
181
|
n: Type.Optional(Type.Integer({ description: "Number of images to generate (1-4). Default 1", minimum: 1, maximum: 4 })),
|
package/src/mcp/stdio-client.ts
CHANGED
|
@@ -18,7 +18,7 @@ const PROTOCOL_VERSION = "2024-11-05";
|
|
|
18
18
|
const CLIENT_INFO = { name: "pi-nan-provider", version: "0.2.0" };
|
|
19
19
|
|
|
20
20
|
export interface StdioMcpCallOptions {
|
|
21
|
-
/** Command to spawn, e.g. ["npx", "-y", "nan-mcp-server@1.0.
|
|
21
|
+
/** Command to spawn, e.g. ["npx", "-y", "nan-mcp-server@1.0.8"]. */
|
|
22
22
|
command: readonly string[];
|
|
23
23
|
/** Extra environment for the child (merged over process.env). */
|
|
24
24
|
env?: Record<string, string | undefined>;
|