@gtrabanco/pi-nan-provider 0.6.10 → 0.7.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 +7 -4
- package/README.md +16 -0
- package/package.json +3 -2
- package/scripts/generate-models.ts +78 -1
- package/scripts/manual-overrides.ts +57 -1
- package/scripts/models.generated.ts +61 -6
- package/src/fetch-models.ts +7 -0
- package/src/pi-ai-loader.ts +81 -17
- package/src/usage.ts +1 -0
package/AGENTS.md
CHANGED
|
@@ -93,10 +93,13 @@ Every PR that changes code MUST bump `package.json` version in the same PR; CI p
|
|
|
93
93
|
`~/.pi/agent/npm/node_modules`; its `estimateMessageTokens` lacks the `system`
|
|
94
94
|
branch and crashes pi 0.87's string-content `system` transcript with
|
|
95
95
|
`block.name.length`. `src/pi-ai-loader.ts` therefore binds the streaming
|
|
96
|
-
factory to the same package instance
|
|
97
|
-
|
|
98
|
-
`
|
|
99
|
-
(
|
|
96
|
+
factory to the same package instance the host loaded: use the root export
|
|
97
|
+
when present, else resolve the bare root **from the host process entrypoint**
|
|
98
|
+
(`process.argv[1]`) via
|
|
99
|
+
`import.meta.resolve("@earendil-works/pi-ai", hostAnchor)` — an
|
|
100
|
+
extension-relative resolve returns the extension tree's stale copy (the
|
|
101
|
+
v0.6.10 regression that left #8 open) — then derive a FILE URL for
|
|
102
|
+
`api/openai-completions.lazy.js` (then `compat.js`). No bare pi-ai subpath
|
|
100
103
|
specifier is imported anywhere in `src/` (static or dynamic); failure is loud
|
|
101
104
|
(`PiAiStreamingApiResolutionError`). Guarded by `test/extension-load.test.ts`
|
|
102
105
|
and `test/issue-8-pi-ai-instance.test.ts`.
|
package/README.md
CHANGED
|
@@ -236,9 +236,25 @@ Baseline catalog (verified against [NaN docs](https://nan.builders/docs/models)
|
|
|
236
236
|
| `gemma4` | 262,144 | 32,768 | text, image | ✅ |
|
|
237
237
|
| `deepseek-v4-flash` | 1,000,000 | 384,000 | text, image | ✅ |
|
|
238
238
|
| `mimo-v2.5` | 1,048,576 | 131,072 | text, image | ✅ |
|
|
239
|
+
| `mimo-v2.6-flash` | 1,048,576 | 131,072 | text, image | ✅ |
|
|
239
240
|
| `glm5.3-flash` | 1,000,000 | 131,072 | text, image | ✅ |
|
|
240
241
|
| `qwen3.8-flash` | 262,144 | 131,072 | text, image | ✅ |
|
|
241
242
|
|
|
243
|
+
> [!NOTE]
|
|
244
|
+
> `mimo-v2.6-flash` is served by NaN but not yet listed on models.dev provider `nan`; it enters the catalog through a manual-only entry with the same limits as `mimo-v2.5`. The model will be auto-detected from models.dev once added there.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## 🧠 Reasoning controls
|
|
249
|
+
|
|
250
|
+
NaN's `reasoning_effort` parameter controls how much the model thinks before answering — but the degree of control varies by model:
|
|
251
|
+
|
|
252
|
+
| Model | Reasoning effort | How it works |
|
|
253
|
+
| :--- | :--- | :--- |
|
|
254
|
+
| `glm5.3`, `glm5.3-flash` | `low` · `medium` · `high` · `max` | Fully controllable — higher values let the model reason longer |
|
|
255
|
+
| `qwen3.6`, `gemma4` | `none` · `minimal` · `low` · `medium` · `high` · `max` | `none`/`minimal` skip reasoning entirely; others cap at 2K / 8K / 16K / 32K tokens |
|
|
256
|
+
| `deepseek-v4-flash`, `qwen3.8-flash`, `mimo-v2.5`, `mimo-v2.6-flash` | *(accepted but not adjustable)* | The parameter is accepted and never rejected, but the model manages its own reasoning depth — it is never an error to send a value these models don't adjust |
|
|
257
|
+
|
|
242
258
|
---
|
|
243
259
|
|
|
244
260
|
## 🚀 Development
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gtrabanco/pi-nan-provider",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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",
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
"check-nan-mcp-server": "bun run scripts/check-nan-mcp-server.ts",
|
|
39
39
|
"prepublishOnly": "bun run generate-models && bun test && bun run typecheck",
|
|
40
40
|
"test": "bun test",
|
|
41
|
-
"typecheck": "bunx tsc --noEmit"
|
|
41
|
+
"typecheck": "bunx tsc --noEmit",
|
|
42
|
+
"check-pi-sdk-versions": "node scripts/check-pi-sdk-versions.mjs --report"
|
|
42
43
|
},
|
|
43
44
|
"peerDependencies": {
|
|
44
45
|
"@earendil-works/pi-ai": ">=0.83.0 <1",
|
|
@@ -21,7 +21,28 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import type { GeneratedModelEntry } from "../src/fetch-models.ts";
|
|
24
|
-
import {
|
|
24
|
+
import type { ManualModelOverride } from "./manual-overrides.ts";
|
|
25
|
+
import {
|
|
26
|
+
MANUAL_OVERRIDES,
|
|
27
|
+
MANUAL_ONLY_MODEL_IDS,
|
|
28
|
+
REASONING_EFFORT_VALUES,
|
|
29
|
+
} from "./manual-overrides.ts";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Reasoning effort values sourced from the NaN docs (https://nan.builders/docs/models
|
|
33
|
+
* #controlling-reasoning, checked 2026-09-25). models.dev has no reasoning_effort_values
|
|
34
|
+
* field — only reasoning_options which is [{type:"toggle"}] or [] — so the actual
|
|
35
|
+
* effort levels must be hand-maintained from the docs. An empty array means the
|
|
36
|
+
* parameter is accepted but depth is not adjustable by the user.
|
|
37
|
+
*/
|
|
38
|
+
const REASONING_EFFORT_VALUES_FROM_DOCS: Record<string, string[]> = {
|
|
39
|
+
...REASONING_EFFORT_VALUES,
|
|
40
|
+
// deepseek-v4-flash: any value (no effect) — model decides per request
|
|
41
|
+
"deepseek-v4-flash": [],
|
|
42
|
+
// qwen3.8-flash, mimo-v2.5: accepted, depth not adjustable
|
|
43
|
+
"qwen3.8-flash": [],
|
|
44
|
+
"mimo-v2.5": [],
|
|
45
|
+
};
|
|
25
46
|
|
|
26
47
|
const MODELS_DEV_API_URL = "https://models.dev/api.json";
|
|
27
48
|
const SOURCE_PROVIDER_ID = "nan";
|
|
@@ -158,6 +179,43 @@ function normalizeInput(modalitiesInput: string[] | undefined, modelId: string):
|
|
|
158
179
|
return input;
|
|
159
180
|
}
|
|
160
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Build a GeneratedModelEntry for a model that is not yet on models.dev
|
|
184
|
+
* (MANUAL_ONLY_MODEL_IDS). Uses conservative but accurate defaults from the
|
|
185
|
+
* NaN docs so that the live /models refresh does not hand them
|
|
186
|
+
* UNKNOWN_MODEL_LIMITS.
|
|
187
|
+
*/
|
|
188
|
+
function buildManualOnlyModelEntry(
|
|
189
|
+
modelId: string,
|
|
190
|
+
detail: string,
|
|
191
|
+
override: ManualModelOverride | undefined,
|
|
192
|
+
): GeneratedModel {
|
|
193
|
+
const reasoningEffortValues =
|
|
194
|
+
override?.reasoningEffortValues ??
|
|
195
|
+
REASONING_EFFORT_VALUES_FROM_DOCS[modelId] ??
|
|
196
|
+
undefined;
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
entry: {
|
|
200
|
+
id: modelId,
|
|
201
|
+
name: override?.name ?? modelId,
|
|
202
|
+
reasoning: override?.reasoning ?? true,
|
|
203
|
+
input: override?.input ?? ["text", "image"],
|
|
204
|
+
cost: override?.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
205
|
+
contextWindow: override?.contextWindow ?? 1_048_576,
|
|
206
|
+
maxTokens: override?.maxTokens ?? 131_072,
|
|
207
|
+
reasoningEffortValues,
|
|
208
|
+
compat: { ...NAN_COMPAT },
|
|
209
|
+
notes: [
|
|
210
|
+
NAN_COMPAT_NOTE,
|
|
211
|
+
`manual-only: "${modelId}" not yet on models.dev provider nan (${detail}).`,
|
|
212
|
+
...(override ? [override.note] : []),
|
|
213
|
+
],
|
|
214
|
+
extras: {},
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
161
219
|
function convertModel(modelId: string, m: ModelsDevModel): GeneratedModel | { skip: string } {
|
|
162
220
|
const removedReason = PROVIDER_REMOVED_MODEL_IDS[modelId];
|
|
163
221
|
if (removedReason) {
|
|
@@ -184,6 +242,13 @@ function convertModel(modelId: string, m: ModelsDevModel): GeneratedModel | { sk
|
|
|
184
242
|
console.log(`generate-models: manual override for "${modelId}" (${overridden.join(", ")})`);
|
|
185
243
|
}
|
|
186
244
|
|
|
245
|
+
// Reasoning effort values sourced from the NaN docs (models.dev does not
|
|
246
|
+
// expose them — only reasoning_options: [{type:"toggle"}] or []).
|
|
247
|
+
// An empty array means the parameter is accepted but depth is not
|
|
248
|
+
// adjustable by the user; the model manages its own reasoning depth.
|
|
249
|
+
const reasoningEffortValues =
|
|
250
|
+
override?.reasoningEffortValues ?? REASONING_EFFORT_VALUES_FROM_DOCS[modelId] ?? undefined;
|
|
251
|
+
|
|
187
252
|
return {
|
|
188
253
|
entry: {
|
|
189
254
|
id: modelId,
|
|
@@ -198,6 +263,7 @@ function convertModel(modelId: string, m: ModelsDevModel): GeneratedModel | { sk
|
|
|
198
263
|
},
|
|
199
264
|
contextWindow: override?.contextWindow ?? contextWindow,
|
|
200
265
|
maxTokens: override?.maxTokens ?? maxTokens,
|
|
266
|
+
reasoningEffortValues,
|
|
201
267
|
compat: { ...NAN_COMPAT },
|
|
202
268
|
notes: [
|
|
203
269
|
NAN_COMPAT_NOTE,
|
|
@@ -235,6 +301,17 @@ async function main(): Promise<void> {
|
|
|
235
301
|
}
|
|
236
302
|
}
|
|
237
303
|
|
|
304
|
+
// Add manual-only models (not yet on models.dev).
|
|
305
|
+
for (const [modelId, detail] of Object.entries(MANUAL_ONLY_MODEL_IDS)) {
|
|
306
|
+
const override = MANUAL_OVERRIDES[modelId];
|
|
307
|
+
if (override) {
|
|
308
|
+
console.log(`generate-models: manual override for "${modelId}" (${Object.keys(override).filter((k) => k !== "note").join(", ")})`);
|
|
309
|
+
}
|
|
310
|
+
const result = buildManualOnlyModelEntry(modelId, detail, MANUAL_OVERRIDES[modelId]);
|
|
311
|
+
entries.push(result.entry);
|
|
312
|
+
console.log(`generate-models: added manual-only model "${modelId}"`);
|
|
313
|
+
}
|
|
314
|
+
|
|
238
315
|
entries.sort((a, b) => a.id.localeCompare(b.id));
|
|
239
316
|
|
|
240
317
|
const entryIds = new Set(entries.map((entry) => entry.id));
|
|
@@ -35,13 +35,69 @@ export interface ManualModelOverride {
|
|
|
35
35
|
contextWindow?: number;
|
|
36
36
|
/** Max output tokens override. */
|
|
37
37
|
maxTokens?: number;
|
|
38
|
+
/** Reasoning effort values as declared by NaN docs. An empty array means the parameter is accepted but depth is model-managed (not adjustable by the user). */
|
|
39
|
+
reasoningEffortValues?: string[];
|
|
38
40
|
/** Required provenance note; emitted verbatim onto the generated entry. */
|
|
39
41
|
note: string;
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Reasoning effort values sourced from the NaN docs (https://nan.builders/docs/models
|
|
46
|
+
* #controlling-reasoning, checked 2026-09-25).
|
|
47
|
+
*
|
|
48
|
+
* models.dev has NO `reasoning_effort_values` field — only `reasoning_options`
|
|
49
|
+
* (which is [{type:"toggle"}] or []), so this mapping must be hand-maintained
|
|
50
|
+
* from the docs. The values flow through MANUAL_OVERRIDES.reasoningEffortValues
|
|
51
|
+
* and appear in GeneratedModelEntry.reasoningEffortValues, where the pi
|
|
52
|
+
* model-selector can read them to show the correct granularity.
|
|
53
|
+
*
|
|
54
|
+
* Per-model contract from the docs:
|
|
55
|
+
* - glm5.3, glm5.3-flash: fully controllable (low/medium/high/max)
|
|
56
|
+
* - qwen3.6, gemma4: none/minimal skip reasoning, others cap depth
|
|
57
|
+
* - deepseek-v4-flash: any value accepted but model decides per-request
|
|
58
|
+
* - qwen3.8-flash, mimo-v2.5, mimo-v2.6-flash: accepted, depth not adjustable
|
|
59
|
+
*
|
|
60
|
+
* A model with reasoning_effort_values=[] means the parameter is accepted but
|
|
61
|
+
* the model manages its own reasoning depth — it is never an error.
|
|
62
|
+
*/
|
|
63
|
+
export const REASONING_EFFORT_VALUES: Record<string, string[]> = {
|
|
64
|
+
"glm5.3": ["low", "medium", "high", "max"],
|
|
65
|
+
"glm5.3-flash": ["low", "medium", "high", "max"],
|
|
66
|
+
"qwen3.6": ["none", "minimal", "low", "medium", "high", "max"],
|
|
67
|
+
"gemma4": ["none", "minimal", "low", "medium", "high", "max"],
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Models that NaN serves but models.dev provider nan hasn't listed yet.
|
|
72
|
+
* Mirrors the generator's MANUAL_ONLY_MODEL_IDS — kept in sync so the
|
|
73
|
+
* generator and the tests share the same source.
|
|
74
|
+
*/
|
|
75
|
+
export const MANUAL_ONLY_MODEL_IDS: Record<string, string> = {
|
|
76
|
+
"mimo-v2.6-flash":
|
|
77
|
+
"omnimodal model (text, image, audio input) served by NaN, not yet listed on models.dev provider nan (checked 2026-09-25); included so the live /models refresh does not hand it UNKNOWN_MODEL_LIMITS. Same limits as mimo-v2.5: 1,048,576 / 131,072 / 1.0B monthly quota per member. Reasoning: accepted, depth not adjustable. Tool calling: yes. Streaming: yes. Input modalities (pi-representable): text, image (audio not representable in pi's Model type).",
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Manual-only model entries with the full override shape for test assertions.
|
|
82
|
+
* Mirrors MANUAL_ONLY_MODEL_IDS so the generator and the tests share the
|
|
83
|
+
* same set of models.
|
|
84
|
+
*/
|
|
85
|
+
export const MANUAL_ONLY_MODELS: Record<string, ManualModelOverride> = {
|
|
86
|
+
"mimo-v2.6-flash": {
|
|
87
|
+
input: ["text", "image"],
|
|
88
|
+
reasoning: true,
|
|
89
|
+
reasoningEffortValues: [],
|
|
90
|
+
contextWindow: 1_048_576,
|
|
91
|
+
maxTokens: 131_072,
|
|
92
|
+
note: "omnimodal model (text, image, audio input) served by NaN, not yet listed on models.dev provider nan (checked 2026-09-25); included so the live /models refresh does not hand it UNKNOWN_MODEL_LIMITS. Same limits as mimo-v2.5: 1,048,576 / 131,072 / 1.0B monthly quota per member. Reasoning: accepted, depth not adjustable. Tool calling: yes. Streaming: yes. Input modalities (pi-representable): text, image (audio not representable in pi's Model type).",
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
42
96
|
export const MANUAL_OVERRIDES: Record<string, ManualModelOverride> = {
|
|
43
97
|
"deepseek-v4-flash": {
|
|
44
98
|
input: ["text", "image"],
|
|
45
|
-
|
|
99
|
+
// Any value accepted but model decides per-request (no effect).
|
|
100
|
+
reasoningEffortValues: [],
|
|
101
|
+
note: "input includes image: NaN serves the Vision-Exp variant ('takes images as input', https://nan.builders/docs/models; the image_url content-parts in https://nan.builders/openapi.json list deepseek-v4-flash among the vision models). models.dev provider nan also lists text+image now (DeepSeek V4.1 Flash entry, checked 2026-09-13; its 2026-09-07 snapshot listed text only), so this override is kept as a pin for the vision capability rather than as a divergence. reasoning_effort_values=[]: the model decides per-request how much to reason (https://nan.builders/docs/models, checked 2026-09-25).",
|
|
46
102
|
},
|
|
47
103
|
};
|
|
@@ -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-25T22:43:00.930Z
|
|
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.
|
|
@@ -28,6 +28,7 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
|
|
|
28
28
|
},
|
|
29
29
|
"contextWindow": 1000000,
|
|
30
30
|
"maxTokens": 384000,
|
|
31
|
+
"reasoningEffortValues": [],
|
|
31
32
|
"compat": {
|
|
32
33
|
"supportsDeveloperRole": false,
|
|
33
34
|
"supportsReasoningEffort": true,
|
|
@@ -37,7 +38,7 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
|
|
|
37
38
|
},
|
|
38
39
|
"notes": [
|
|
39
40
|
"compat matches the maintainer's working ~/.pi/agent/models.json LiteLLM config for api.nan.builders (2026-09-04): supportsDeveloperRole false, supportsReasoningEffort true, maxTokensField max_tokens. NaN's docs example sets only supportsDeveloperRole: true and is not battle-tested. supportsFinishReason true (2026-09-13, issue #2): the LiteLLM gateway intermittently closes SSE streams before emitting finish_reason; with true pi-ai raises 'Stream ended without finish_reason', which matches pi-ai's retryable-provider pattern ('ended without') and is retried automatically, whereas false silently synthesized stop/toolUse and stalled the turn mid-answer. supportsUsageInStreaming true (2026-09-16, issue #7): NaN's published schema is silent about stream_options, but the live gateway honors it — two identical streaming calls per model, differing only in stream_options: { include_usage: true }, returned 0 usage chunks without it and exactly 1 with it (prompt/completion/reasoning/cached token counts) on deepseek-v4-flash, glm5.3-flash, qwen3.6, mimo-v2.5 and gemma4, and a real pi session then recorded token counts where it recorded zeros. pi-ai only sends stream_options when this is not false, and the sanitizer forwards it when the model declares true, so chat models opt in by default and usage is reported (issue #4). A model that does not report streaming usage can still opt out per model with a models.json compat override (supportsUsageInStreaming: false); the sanitizer then strips stream_options and the payload stays strict.",
|
|
40
|
-
"input includes image: NaN serves the Vision-Exp variant ('takes images as input', https://nan.builders/docs/models; the image_url content-parts in https://nan.builders/openapi.json list deepseek-v4-flash among the vision models). models.dev provider nan also lists text+image now (DeepSeek V4.1 Flash entry, checked 2026-09-13; its 2026-09-07 snapshot listed text only), so this override is kept as a pin for the vision capability rather than as a divergence."
|
|
41
|
+
"input includes image: NaN serves the Vision-Exp variant ('takes images as input', https://nan.builders/docs/models; the image_url content-parts in https://nan.builders/openapi.json list deepseek-v4-flash among the vision models). models.dev provider nan also lists text+image now (DeepSeek V4.1 Flash entry, checked 2026-09-13; its 2026-09-07 snapshot listed text only), so this override is kept as a pin for the vision capability rather than as a divergence. reasoning_effort_values=[]: the model decides per-request how much to reason (https://nan.builders/docs/models, checked 2026-09-25)."
|
|
41
42
|
],
|
|
42
43
|
"extras": {
|
|
43
44
|
"id": "deepseek-v4-flash",
|
|
@@ -89,6 +90,14 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
|
|
|
89
90
|
},
|
|
90
91
|
"contextWindow": 262144,
|
|
91
92
|
"maxTokens": 32768,
|
|
93
|
+
"reasoningEffortValues": [
|
|
94
|
+
"none",
|
|
95
|
+
"minimal",
|
|
96
|
+
"low",
|
|
97
|
+
"medium",
|
|
98
|
+
"high",
|
|
99
|
+
"max"
|
|
100
|
+
],
|
|
92
101
|
"compat": {
|
|
93
102
|
"supportsDeveloperRole": false,
|
|
94
103
|
"supportsReasoningEffort": true,
|
|
@@ -152,6 +161,12 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
|
|
|
152
161
|
},
|
|
153
162
|
"contextWindow": 1000000,
|
|
154
163
|
"maxTokens": 131072,
|
|
164
|
+
"reasoningEffortValues": [
|
|
165
|
+
"low",
|
|
166
|
+
"medium",
|
|
167
|
+
"high",
|
|
168
|
+
"max"
|
|
169
|
+
],
|
|
155
170
|
"compat": {
|
|
156
171
|
"supportsDeveloperRole": false,
|
|
157
172
|
"supportsReasoningEffort": true,
|
|
@@ -211,6 +226,7 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
|
|
|
211
226
|
},
|
|
212
227
|
"contextWindow": 1048576,
|
|
213
228
|
"maxTokens": 131072,
|
|
229
|
+
"reasoningEffortValues": [],
|
|
214
230
|
"compat": {
|
|
215
231
|
"supportsDeveloperRole": false,
|
|
216
232
|
"supportsReasoningEffort": true,
|
|
@@ -255,6 +271,35 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
|
|
|
255
271
|
}
|
|
256
272
|
}
|
|
257
273
|
},
|
|
274
|
+
{
|
|
275
|
+
"id": "mimo-v2.6-flash",
|
|
276
|
+
"name": "mimo-v2.6-flash",
|
|
277
|
+
"reasoning": true,
|
|
278
|
+
"input": [
|
|
279
|
+
"text",
|
|
280
|
+
"image"
|
|
281
|
+
],
|
|
282
|
+
"cost": {
|
|
283
|
+
"input": 0,
|
|
284
|
+
"output": 0,
|
|
285
|
+
"cacheRead": 0,
|
|
286
|
+
"cacheWrite": 0
|
|
287
|
+
},
|
|
288
|
+
"contextWindow": 1048576,
|
|
289
|
+
"maxTokens": 131072,
|
|
290
|
+
"compat": {
|
|
291
|
+
"supportsDeveloperRole": false,
|
|
292
|
+
"supportsReasoningEffort": true,
|
|
293
|
+
"supportsUsageInStreaming": true,
|
|
294
|
+
"supportsFinishReason": true,
|
|
295
|
+
"maxTokensField": "max_tokens"
|
|
296
|
+
},
|
|
297
|
+
"notes": [
|
|
298
|
+
"compat matches the maintainer's working ~/.pi/agent/models.json LiteLLM config for api.nan.builders (2026-09-04): supportsDeveloperRole false, supportsReasoningEffort true, maxTokensField max_tokens. NaN's docs example sets only supportsDeveloperRole: true and is not battle-tested. supportsFinishReason true (2026-09-13, issue #2): the LiteLLM gateway intermittently closes SSE streams before emitting finish_reason; with true pi-ai raises 'Stream ended without finish_reason', which matches pi-ai's retryable-provider pattern ('ended without') and is retried automatically, whereas false silently synthesized stop/toolUse and stalled the turn mid-answer. supportsUsageInStreaming true (2026-09-16, issue #7): NaN's published schema is silent about stream_options, but the live gateway honors it — two identical streaming calls per model, differing only in stream_options: { include_usage: true }, returned 0 usage chunks without it and exactly 1 with it (prompt/completion/reasoning/cached token counts) on deepseek-v4-flash, glm5.3-flash, qwen3.6, mimo-v2.5 and gemma4, and a real pi session then recorded token counts where it recorded zeros. pi-ai only sends stream_options when this is not false, and the sanitizer forwards it when the model declares true, so chat models opt in by default and usage is reported (issue #4). A model that does not report streaming usage can still opt out per model with a models.json compat override (supportsUsageInStreaming: false); the sanitizer then strips stream_options and the payload stays strict.",
|
|
299
|
+
"manual-only: \"mimo-v2.6-flash\" not yet on models.dev provider nan (omnimodal model (text, image, audio input) served by NaN, not yet listed on models.dev provider nan (checked 2026-09-25); included so the live /models refresh does not hand it UNKNOWN_MODEL_LIMITS. Same limits as mimo-v2.5: 1,048,576 / 131,072 / 1.0B monthly quota per member. Reasoning: accepted, depth not adjustable. Tool calling: yes. Streaming: yes. Input modalities (pi-representable): text, image (audio not representable in pi's Model type).)."
|
|
300
|
+
],
|
|
301
|
+
"extras": {}
|
|
302
|
+
},
|
|
258
303
|
{
|
|
259
304
|
"id": "qwen3.6",
|
|
260
305
|
"name": "Qwen3.6 35B-A3B",
|
|
@@ -271,6 +316,14 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
|
|
|
271
316
|
},
|
|
272
317
|
"contextWindow": 262144,
|
|
273
318
|
"maxTokens": 65536,
|
|
319
|
+
"reasoningEffortValues": [
|
|
320
|
+
"none",
|
|
321
|
+
"minimal",
|
|
322
|
+
"low",
|
|
323
|
+
"medium",
|
|
324
|
+
"high",
|
|
325
|
+
"max"
|
|
326
|
+
],
|
|
274
327
|
"compat": {
|
|
275
328
|
"supportsDeveloperRole": false,
|
|
276
329
|
"supportsReasoningEffort": true,
|
|
@@ -334,6 +387,7 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
|
|
|
334
387
|
},
|
|
335
388
|
"contextWindow": 262144,
|
|
336
389
|
"maxTokens": 131072,
|
|
390
|
+
"reasoningEffortValues": [],
|
|
337
391
|
"compat": {
|
|
338
392
|
"supportsDeveloperRole": false,
|
|
339
393
|
"supportsReasoningEffort": true,
|
|
@@ -382,14 +436,15 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
|
|
|
382
436
|
export const GENERATED_CATALOG_META = {
|
|
383
437
|
source: "https://models.dev/api.json",
|
|
384
438
|
modelsDevProvider: "nan",
|
|
385
|
-
fetchedAt: "2026-09-
|
|
386
|
-
modelCount:
|
|
387
|
-
models: ["deepseek-v4-flash","gemma4","glm5.3-flash","mimo-v2.5","qwen3.6","qwen3.8-flash"],
|
|
439
|
+
fetchedAt: "2026-09-25T22:43:00.930Z",
|
|
440
|
+
modelCount: 7,
|
|
441
|
+
models: ["deepseek-v4-flash","gemma4","glm5.3-flash","mimo-v2.5","mimo-v2.6-flash","qwen3.6","qwen3.8-flash"],
|
|
388
442
|
notes: [
|
|
389
443
|
"live-only: \"glm5.3\" kept out of the static catalog (premium-tier model (models.dev now documents it with 1M context / 131,072 max output; NaN docs https://nan.builders/docs/models + https://nan.builders/openapi.json, checked 2026-09-13) kept live-only so a non-premium key never sees a model it cannot call when the live /models fetch is unavailable; premium keys still get it via the /models refresh with conservative placeholder limits)",
|
|
390
444
|
"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)",
|
|
391
445
|
"compat matches the maintainer's working ~/.pi/agent/models.json LiteLLM config for api.nan.builders (2026-09-04): supportsDeveloperRole false, supportsReasoningEffort true, maxTokensField max_tokens. NaN's docs example sets only supportsDeveloperRole: true and is not battle-tested. supportsFinishReason true (2026-09-13, issue #2): the LiteLLM gateway intermittently closes SSE streams before emitting finish_reason; with true pi-ai raises 'Stream ended without finish_reason', which matches pi-ai's retryable-provider pattern ('ended without') and is retried automatically, whereas false silently synthesized stop/toolUse and stalled the turn mid-answer. supportsUsageInStreaming true (2026-09-16, issue #7): NaN's published schema is silent about stream_options, but the live gateway honors it — two identical streaming calls per model, differing only in stream_options: { include_usage: true }, returned 0 usage chunks without it and exactly 1 with it (prompt/completion/reasoning/cached token counts) on deepseek-v4-flash, glm5.3-flash, qwen3.6, mimo-v2.5 and gemma4, and a real pi session then recorded token counts where it recorded zeros. pi-ai only sends stream_options when this is not false, and the sanitizer forwards it when the model declares true, so chat models opt in by default and usage is reported (issue #4). A model that does not report streaming usage can still opt out per model with a models.json compat override (supportsUsageInStreaming: false); the sanitizer then strips stream_options and the payload stays strict.",
|
|
392
|
-
"input includes image: NaN serves the Vision-Exp variant ('takes images as input', https://nan.builders/docs/models; the image_url content-parts in https://nan.builders/openapi.json list deepseek-v4-flash among the vision models). models.dev provider nan also lists text+image now (DeepSeek V4.1 Flash entry, checked 2026-09-13; its 2026-09-07 snapshot listed text only), so this override is kept as a pin for the vision capability rather than as a divergence.",
|
|
446
|
+
"input includes image: NaN serves the Vision-Exp variant ('takes images as input', https://nan.builders/docs/models; the image_url content-parts in https://nan.builders/openapi.json list deepseek-v4-flash among the vision models). models.dev provider nan also lists text+image now (DeepSeek V4.1 Flash entry, checked 2026-09-13; its 2026-09-07 snapshot listed text only), so this override is kept as a pin for the vision capability rather than as a divergence. reasoning_effort_values=[]: the model decides per-request how much to reason (https://nan.builders/docs/models, checked 2026-09-25).",
|
|
447
|
+
"manual-only: \"mimo-v2.6-flash\" not yet on models.dev provider nan (omnimodal model (text, image, audio input) served by NaN, not yet listed on models.dev provider nan (checked 2026-09-25); included so the live /models refresh does not hand it UNKNOWN_MODEL_LIMITS. Same limits as mimo-v2.5: 1,048,576 / 131,072 / 1.0B monthly quota per member. Reasoning: accepted, depth not adjustable. Tool calling: yes. Streaming: yes. Input modalities (pi-representable): text, image (audio not representable in pi's Model type).).",
|
|
393
448
|
"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)."
|
|
394
449
|
],
|
|
395
450
|
} as const;
|
package/src/fetch-models.ts
CHANGED
|
@@ -39,6 +39,13 @@ export interface GeneratedModelEntry {
|
|
|
39
39
|
cost: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
40
40
|
contextWindow: number;
|
|
41
41
|
maxTokens: number;
|
|
42
|
+
/**
|
|
43
|
+
* Reasoning effort values as declared by NaN docs.
|
|
44
|
+
* An empty array means the parameter is accepted but depth is not
|
|
45
|
+
* adjustable by the user — the model manages its own reasoning depth.
|
|
46
|
+
* An undefined array means the field was not set (legacy / uncatalogued).
|
|
47
|
+
*/
|
|
48
|
+
reasoningEffortValues?: string[];
|
|
42
49
|
/** Compat applied to every NaN-compatible model (LiteLLM-confirmed, see scripts/generate-models.ts). */
|
|
43
50
|
compat?: OpenAICompletionsCompat;
|
|
44
51
|
/** Provenance notes for values overriding models.dev or needing manual confirmation. */
|
package/src/pi-ai-loader.ts
CHANGED
|
@@ -18,13 +18,15 @@
|
|
|
18
18
|
* `undefined is not an object (evaluating 'block.name.length')` — before the
|
|
19
19
|
* request is ever sent, so it reads like a NaN/gateway failure.
|
|
20
20
|
*
|
|
21
|
-
* `
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
21
|
+
* The anchor is the **host process entrypoint** (`process.argv[1]`), passed to
|
|
22
|
+
* `import.meta.resolve(specifier, parent)`. An extension-relative resolve is
|
|
23
|
+
* NOT enough: it returns whatever the extension's own tree holds, which under
|
|
24
|
+
* pi-web is exactly the stale 0.85.1 copy (that was the v0.6.10 regression:
|
|
25
|
+
* the "host-resolved root" was still the extension's copy). From the
|
|
26
|
+
* host-resolved root derive a **file URL** for the sibling
|
|
27
|
+
* `api/openai-completions.lazy.js` (then `compat.js`) and dynamic-import the
|
|
28
|
+
* URL. A file URL bypasses package resolution entirely, so the loaded module is
|
|
29
|
+
* guaranteed to be the host's instance.
|
|
28
30
|
*
|
|
29
31
|
* Under the bundled CLI / Node-mode aliases / compiled binary, the bare root
|
|
30
32
|
* is the compat entrypoint and already exposes the factory, so the first
|
|
@@ -42,6 +44,7 @@
|
|
|
42
44
|
import * as piAi from "@earendil-works/pi-ai";
|
|
43
45
|
import type { ProviderStreams } from "@earendil-works/pi-ai";
|
|
44
46
|
import { createRequire } from "node:module";
|
|
47
|
+
import { pathToFileURL } from "node:url";
|
|
45
48
|
|
|
46
49
|
/** The only pi-ai specifier this package may import. */
|
|
47
50
|
export const PI_AI_PACKAGE_SPECIFIER = "@earendil-works/pi-ai";
|
|
@@ -106,22 +109,83 @@ export function openAICompletionsApiFrom(namespace: unknown): OpenAICompletionsA
|
|
|
106
109
|
* `import.meta.resolve` is absent from bun-types' `ImportMeta`, so read it
|
|
107
110
|
* through an explicit shape. Bun/Node expose it at runtime; when it is missing
|
|
108
111
|
* or throws, `createRequire` resolves the same bare root from this module.
|
|
112
|
+
*
|
|
113
|
+
* Referenced directly (not via a variable or type cast) so pi's jiti loader
|
|
114
|
+
* can rewrite it. PR #11 (jiti compatibility) and PR #12 (host-anchored
|
|
115
|
+
* resolution) merge: the direct reference fixes jiti, the host anchor fixes
|
|
116
|
+
* pi-web.
|
|
109
117
|
*/
|
|
110
|
-
|
|
118
|
+
function readImportMetaResolve(): ((specifier: string, parent?: string) => string) | undefined {
|
|
119
|
+
if (typeof import.meta.resolve !== "function") return undefined;
|
|
120
|
+
return (specifier, parent) =>
|
|
121
|
+
parent === undefined ? import.meta.resolve(specifier) : import.meta.resolve(specifier, parent);
|
|
122
|
+
}
|
|
111
123
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
124
|
+
/**
|
|
125
|
+
* File URL of the host process entrypoint (pi-web's `sessiond.js`, the pi CLI,
|
|
126
|
+
* a test runner). Resolving FROM it pins the result to the host's module graph
|
|
127
|
+
* instead of the extension's own tree.
|
|
128
|
+
*/
|
|
129
|
+
export function hostAnchorUrl(entry: string | undefined): string | undefined {
|
|
130
|
+
if (typeof entry !== "string" || entry.length === 0) return undefined;
|
|
131
|
+
try {
|
|
132
|
+
return pathToFileURL(entry).href;
|
|
133
|
+
} catch {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Injection seam for {@link resolvePiAiSpecifier}. */
|
|
139
|
+
export interface PiAiSpecifierResolution {
|
|
140
|
+
/** Anchor URL to resolve FROM; omitted means extension-relative resolution. */
|
|
141
|
+
anchorUrl?: string;
|
|
142
|
+
/** `import.meta.resolve`, when the runtime exposes it. */
|
|
143
|
+
resolve?: (specifier: string, parent?: string) => string;
|
|
144
|
+
/** Last-resort CJS resolver; only used when neither `resolve` call succeeds. */
|
|
145
|
+
fallback?: (specifier: string) => string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Resolve a pi-ai specifier the way the host runtime does.
|
|
150
|
+
*
|
|
151
|
+
* The `anchorUrl` is the whole point. Under pi-web the extension's own tree
|
|
152
|
+
* holds a stale hoisted `@earendil-works/pi-ai` (0.85.1), so an
|
|
153
|
+
* extension-relative resolve returns a package the host never loaded, and the
|
|
154
|
+
* derived "host-resolved root" is still that stale copy. Passing the host
|
|
155
|
+
* entrypoint as the resolver's parent returns the instance the host itself
|
|
156
|
+
* uses (the v0.6.10 fix anchored on `import.meta.resolve` alone and therefore
|
|
157
|
+
* still selected the stale copy).
|
|
158
|
+
*/
|
|
159
|
+
export function resolvePiAiSpecifier(
|
|
160
|
+
specifier: string,
|
|
161
|
+
options: PiAiSpecifierResolution = {},
|
|
162
|
+
): string {
|
|
163
|
+
const resolve = options.resolve ?? readImportMetaResolve();
|
|
164
|
+
const fallback =
|
|
165
|
+
options.fallback ?? ((value: string) => createRequire(import.meta.url).resolve(value));
|
|
166
|
+
if (resolve !== undefined) {
|
|
167
|
+
if (options.anchorUrl !== undefined) {
|
|
117
168
|
try {
|
|
118
|
-
const
|
|
119
|
-
if (typeof
|
|
169
|
+
const anchored = resolve(specifier, options.anchorUrl);
|
|
170
|
+
if (typeof anchored === "string" && anchored.length > 0) return anchored;
|
|
120
171
|
} catch {
|
|
121
|
-
//
|
|
172
|
+
// Runtimes without parent support fall through to the bare call.
|
|
122
173
|
}
|
|
123
174
|
}
|
|
124
|
-
|
|
175
|
+
try {
|
|
176
|
+
const resolved = resolve(specifier);
|
|
177
|
+
if (typeof resolved === "string" && resolved.length > 0) return resolved;
|
|
178
|
+
} catch {
|
|
179
|
+
// Fall through to createRequire — same bare root, same instance.
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return fallback(specifier);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const defaultPiAiLoaderHost: PiAiLoaderHost = {
|
|
186
|
+
namespace: piAi as unknown as ModuleNamespace,
|
|
187
|
+
resolveSpecifier(specifier: string): string {
|
|
188
|
+
return resolvePiAiSpecifier(specifier, { anchorUrl: hostAnchorUrl(process.argv[1]) });
|
|
125
189
|
},
|
|
126
190
|
importModule: (url: string) => import(url) as Promise<ModuleNamespace>,
|
|
127
191
|
};
|
package/src/usage.ts
CHANGED
|
@@ -37,6 +37,7 @@ export interface ModelQuota {
|
|
|
37
37
|
export const MODEL_QUOTAS: readonly ModelQuota[] = [
|
|
38
38
|
{ model: "deepseek-v4-flash", label: "DeepSeek V4 Flash", monthlyCap: 3_000_000_000, rollingWindowCap: 0, rollingWindowHours: 0, premium: false },
|
|
39
39
|
{ model: "mimo-v2.5", label: "MiMo V2.5", monthlyCap: 1_000_000_000, rollingWindowCap: 0, rollingWindowHours: 0, premium: false },
|
|
40
|
+
{ model: "mimo-v2.6-flash", label: "MiMo V2.6 Flash", monthlyCap: 1_000_000_000, rollingWindowCap: 0, rollingWindowHours: 0, premium: false },
|
|
40
41
|
{ model: "qwen3.6", label: "Qwen 3.6", monthlyCap: 0, rollingWindowCap: 0, rollingWindowHours: 0, premium: false },
|
|
41
42
|
{ model: "gemma4", label: "Gemma 4", monthlyCap: 0, rollingWindowCap: 0, rollingWindowHours: 0, premium: false },
|
|
42
43
|
{ model: "qwen3.8-flash", label: "Qwen 3.8 Flash", monthlyCap: 500_000_000, rollingWindowCap: 0, rollingWindowHours: 0, premium: false },
|