@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 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 as the bare-root import: use the root
97
- export when present, else derive a FILE URL from
98
- `import.meta.resolve("@earendil-works/pi-ai")`
99
- (`api/openai-completions.lazy.js`, then `compat.js`). No bare pi-ai subpath
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.6.10",
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 { MANUAL_OVERRIDES } from "./manual-overrides.ts";
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
- 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.",
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-21T23:54:45.052Z
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-21T23:54:45.052Z",
386
- modelCount: 6,
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;
@@ -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. */
@@ -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
- * `import.meta.resolve("@earendil-works/pi-ai")` returns the *same instance*
22
- * the bare-root static import used in every environment measured (both the
23
- * host's 0.87 core on pi-web, and the extension-tree copy under plain
24
- * node/jiti). It is therefore the anchor: derive a **file URL** for the
25
- * sibling `api/openai-completions.lazy.js` (then `compat.js`) from that
26
- * root and dynamic-import the URL. A file URL bypasses package resolution
27
- * entirely, so the loaded module is guaranteed to be the host's instance.
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
- type ImportMetaWithResolve = ImportMeta & { resolve?: (specifier: string) => string };
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
- const defaultPiAiLoaderHost: PiAiLoaderHost = {
113
- namespace: piAi as unknown as ModuleNamespace,
114
- resolveSpecifier(specifier: string): string {
115
- const meta = import.meta as ImportMetaWithResolve;
116
- if (typeof meta.resolve === "function") {
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 resolved = meta.resolve(specifier);
119
- if (typeof resolved === "string" && resolved.length > 0) return resolved;
169
+ const anchored = resolve(specifier, options.anchorUrl);
170
+ if (typeof anchored === "string" && anchored.length > 0) return anchored;
120
171
  } catch {
121
- // Fall through to createRequire — same bare root, same instance.
172
+ // Runtimes without parent support fall through to the bare call.
122
173
  }
123
174
  }
124
- return createRequire(import.meta.url).resolve(specifier);
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 },