@gtrabanco/pi-nan-provider 1.0.1 → 1.2.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.
@@ -58,19 +58,29 @@ export interface ManualModelOverride {
58
58
  * and appear in GeneratedModelEntry.reasoningEffortValues, where the pi
59
59
  * model-selector can read them to show the correct granularity.
60
60
  *
61
- * Per-model contract from the docs:
61
+ * Per-model contract from the docs, corrected by live measurement where the
62
+ * two disagree (maintainer instruction: the measurement wins over the docs):
62
63
  * - glm5.3, glm5.3-flash: fully controllable (low/medium/high/max)
63
64
  * + minimal also accepted (issue #16, measured 2026-09-27): minimal
64
65
  * yields 38 reasoning tokens vs 2,288 for high — the docs simply
65
66
  * omitted it because it was not originally advertised, but it IS
66
67
  * accepted and effective by the gateway.
67
68
  * - qwen3.6, gemma4: none/minimal skip reasoning, others cap depth
68
- * - deepseek-v4-flash: any value accepted but only "none" deterministically
69
- * disables reasoning (issue #16, measured 2026-09-27); every other value
70
- * (minimal/low/medium/high/omitted) leaves 13-14K reasoning tokens with
71
- * runaway reasoning and finish_reason:"length" with ZERO answer. Since only
72
- * "none" has a measurable effect, the catalog declares ["none"].
73
- * - qwen3.8-flash, mimo-v2.6-flash: accepted, depth not adjustable
69
+ * (2,048 / 8,192 / 16,384 / 32,768).
70
+ * - deepseek-v4-flash: `none` reproducibly disables reasoning (0 reasoning
71
+ * tokens, full answer); every other value — `minimal`, `low`, `high` and
72
+ * omitting the parameter — leaves it ON (issue #16, 2026-09-27,
73
+ * re-confirmed live 2026-10-14: none 0 / low 57 / omitted 63 reasoning
74
+ * tokens). The docs say "any value (no effect)"; the measurement wins, so
75
+ * the catalog declares only ["none"].
76
+ * - mimo-v2.6-flash: the docs list it as "accepted, depth not adjustable",
77
+ * but the live re-probe (2026-10-14) shows `none` → 0 reasoning tokens
78
+ * while `low` → 22 and omission → 17. Adjustable at the off end, so it
79
+ * carries `thinkingLevelMap { off: "none" }`.
80
+ * - qwen3.8-flash: the docs say "accepted, depth not adjustable"; the live
81
+ * probe measured 0 reasoning tokens for `none` (2026-10-14) but no other
82
+ * value was sampled, so no `thinkingLevelMap` is declared — the docs state
83
+ * its default is reasoning ON, so `off` sends nothing and the model decides.
74
84
  *
75
85
  * (mimo-v2.5, which used to share that last row, was removed by NaN — absent
76
86
  * from the docs and from openapi.json, checked 2026-09-29.)
@@ -91,7 +101,7 @@ export const REASONING_EFFORT_VALUES: Record<string, string[]> = {
91
101
  * sides assert the exact same string.
92
102
  */
93
103
  const MANUAL_ONLY_NOTE =
94
- "omnimodal model (text, image, audio input) served by NaN; limits and modalities from https://nan.builders/docs/models#mimo-v2-6-flash (checked 2026-09-25, quotas re-checked 2026-09-29): 1,048,576 context / 131,072 max output / 1.0B monthly quota per member. Reasoning: accepted, depth not adjustable (docs table, checked 2026-09-29). Tool calling: yes. Streaming: yes. Input modalities (pi-representable): text, image (audio not representable in pi's Model type). Fallback entry: emitted only while models.dev provider nan does not list the model — models.dev started listing it on 2026-09-29, so the generator then keeps the models.dev values and attaches this note to that entry instead of emitting a second one.";
104
+ "omnimodal model (text, image, audio input) served by NaN; limits and modalities from https://nan.builders/docs/models#mimo-v2-6-flash (checked 2026-09-25, quotas re-checked 2026-09-29): 1,048,576 context / 131,072 max output / 1.0B monthly quota per member. Tool calling: yes. Streaming: yes. Input modalities (pi-representable): text, image (audio not representable in pi's Model type). Fallback entry: emitted only while models.dev provider nan does not list the model — models.dev started listing it on 2026-09-29, so the generator then keeps the models.dev values and attaches this note to that entry instead of emitting a second one.";
95
105
 
96
106
  /**
97
107
  * Models NaN serves that models.dev provider nan may not list. Mirrors the
@@ -139,26 +149,40 @@ export const MANUAL_ONLY_MODELS: Record<string, ManualModelOverride> = {
139
149
  * docs are explicit about both the default and the `none` behavior.
140
150
  */
141
151
  export const MANUAL_OVERRIDES: Record<string, ManualModelOverride> = {
152
+ "qwen3.8-flash": {
153
+ contextWindow: 1_048_576,
154
+ note:
155
+ "contextWindow 1,048,576: NaN docs state '1M token context (1,048,576 tokens)' and 'Max answer 131K tokens' (https://nan.builders/docs/models, checked 2026-10-14) — models.dev still lists 262,144. The earlier override was withdrawn 2026-09-07 when the docs read '262K token context, the model's native window'; the docs now state 1M and they are the source of truth (maintainer instruction: docs win over models.dev, which a user updates later). maxTokens stays 131,072, which models.dev already matches and the docs confirm as the 131K max answer.",
156
+ },
142
157
  "deepseek-v4-flash": {
143
158
  input: ["text", "image"],
159
+ contextWindow: 1_048_576,
144
160
  maxTokens: 32_768,
145
161
  reasoningEffortValues: ["none"],
146
162
  thinkingLevelMap: { off: "none" },
147
163
  note:
148
- "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. " +
164
+ "contextWindow 1,048,576: NaN docs state '1M token context' for this model (https://nan.builders/docs/models, checked 2026-10-14). models.dev lists 1,000,000, a rounded form of the same window; the docs value is authoritative (maintainer instruction: docs win over models.dev, which a user updates later). " +
165
+ "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), so this stays a pin for the vision capability rather than a divergence. " +
149
166
  "maxTokens 32768 per the official NaN pi docs (https://nan.builders/docs/pi, 'the same set the NaN CLI writes', checked 2026-10-02) closing issue #20 — the gateway now rejects max_tokens above ~256k for this model with 403 permission_error (measured 2026-10-02: 256,000 → 200; 262,143 → 403), and the previous models.dev-derived 384000 made every pi request fail since pi-ai sends max_tokens = model.maxTokens when no cap is set. models.dev still lists 384000, so this override pins the documented value. " +
150
167
  "reasoning_effort_values=[\"none\"] (issue #16, measured 2026-09-27 against live gateway, repro included in the issue): only \"none\" deterministically disables reasoning (0 reasoning tokens, answer produced). Every other value leaves 13-14K reasoning tokens and causes runaway reasoning (finish_reason:\"length\" with ZERO answer text). The catalog declares only [\"none\"] because the remaining values are useless noise. " +
168
+ "docs vs measurement (re-probed live 2026-10-14): https://nan.builders/docs/models#controlling-reasoning documents the parameter as 'any value (no effect) — the model decides per request', but the live gateway contradicts that: `none` gave 0 reasoning tokens / 0 reasoning chars / a 134-char answer, while `low` gave 57 reasoning tokens and omitting the parameter gave 63 (`minimal` gave 109). The docs are stale here; the measurement wins (maintainer instruction), so the measured behavior is kept. " +
151
169
  "thinkingLevelMap { off: \"none\" } (issue #16, measured 2026-09-27): when pi maps reasoning:\"off\" to reasoningEffort:undefined, the thinkingLevelMap.off=\"none\" supplies the effort so the gateway receives reasoning_effort:\"none\" and does NOT enter runaway reasoning mode.",
152
170
  },
153
171
  "glm5.3-flash": {
172
+ contextWindow: 1_048_576,
154
173
  thinkingLevelMap: { off: "minimal" },
155
174
  note:
175
+ "contextWindow 1,048,576: NaN docs state '1M token context' for this model (https://nan.builders/docs/models, checked 2026-10-14). models.dev lists 1,000,000, a rounded form of the same window; the docs value is authoritative (maintainer instruction: docs win over models.dev, which a user updates later). " +
156
176
  "thinkingLevelMap { off: \"minimal\" } (issue #16, measured 2026-09-27 against live gateway, repro included in the issue): when pi maps reasoning:\"off\" to reasoningEffort:undefined, the thinkingLevelMap.off=\"minimal\" supplies the effort so the gateway receives reasoning_effort:\"minimal\" (38 reasoning tokens + answer). Note: \"none\" does NOT suppress reasoning on glm5.3-flash (13,382 tokens) — only \"minimal\" was measured to be effective. " +
157
- "reasoning_effort_values includes \"minimal\" (not documented by NaN docs but accepted and effective by the gateway, issue #16).",
177
+ "reasoning_effort_values includes \"minimal\" (the docs' glm row lists low/medium/high/max, but the 2026-09-27 measurement shows minimal is accepted and effective, issue #16).",
158
178
  },
159
- "qwen3.6": {
179
+ "mimo-v2.6-flash": {
160
180
  thinkingLevelMap: { off: "none" },
161
181
  note:
182
+ "thinkingLevelMap { off: \"none\" } (live re-probe 2026-10-14): the docs list this model as 'accepted, depth not adjustable', but the gateway makes the parameter effective at the off end — `none` gave 0 reasoning tokens / a full answer, while `low` gave 22 reasoning tokens and omitting the parameter gave 17. Measured behavior wins over the docs, so `thinking: off` now sends reasoning_effort:\"none\" and actually disables reasoning. No other level was sampled, so only `off` is mapped.",
183
+ },
184
+ "qwen3.6": {
185
+ thinkingLevelMap: { off: "none" }, note:
162
186
  "thinkingLevelMap { off: \"none\" } (issue #16): NaN docs state that with no reasoning_effort parameter every model uses its own default and that default is reasoning ON for qwen3.6 (16,384-token budget), while `none` and `minimal` skip the reasoning phase entirely (https://nan.builders/docs/models#controlling-reasoning, checked 2026-10-01). pi maps reasoning:\"off\" to reasoningEffort:undefined, so without this map `thinking: off` sent no parameter and the model reasoned anyway; the map sends reasoning_effort:\"none\" and actually disables reasoning.",
163
187
  },
164
188
  "gemma4": {
@@ -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-10-02T22:53:40.291Z
4
+ // Source: https://models.dev/api.json (provider "nan"), fetched 2026-10-16T17:43:45.590Z
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.
@@ -26,7 +26,7 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
26
26
  "cacheRead": 0,
27
27
  "cacheWrite": 0
28
28
  },
29
- "contextWindow": 1000000,
29
+ "contextWindow": 1048576,
30
30
  "maxTokens": 32768,
31
31
  "reasoningEffortValues": [
32
32
  "none"
@@ -43,7 +43,7 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
43
43
  },
44
44
  "notes": [
45
45
  "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.",
46
- "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. maxTokens 32768 per the official NaN pi docs (https://nan.builders/docs/pi, 'the same set the NaN CLI writes', checked 2026-10-02) closing issue #20 — the gateway now rejects max_tokens above ~256k for this model with 403 permission_error (measured 2026-10-02: 256,000 → 200; 262,143 → 403), and the previous models.dev-derived 384000 made every pi request fail since pi-ai sends max_tokens = model.maxTokens when no cap is set. models.dev still lists 384000, so this override pins the documented value. reasoning_effort_values=[\"none\"] (issue #16, measured 2026-09-27 against live gateway, repro included in the issue): only \"none\" deterministically disables reasoning (0 reasoning tokens, answer produced). Every other value leaves 13-14K reasoning tokens and causes runaway reasoning (finish_reason:\"length\" with ZERO answer text). The catalog declares only [\"none\"] because the remaining values are useless noise. thinkingLevelMap { off: \"none\" } (issue #16, measured 2026-09-27): when pi maps reasoning:\"off\" to reasoningEffort:undefined, the thinkingLevelMap.off=\"none\" supplies the effort so the gateway receives reasoning_effort:\"none\" and does NOT enter runaway reasoning mode."
46
+ "contextWindow 1,048,576: NaN docs state '1M token context' for this model (https://nan.builders/docs/models, checked 2026-10-14). models.dev lists 1,000,000, a rounded form of the same window; the docs value is authoritative (maintainer instruction: docs win over models.dev, which a user updates later). 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), so this stays a pin for the vision capability rather than a divergence. maxTokens 32768 per the official NaN pi docs (https://nan.builders/docs/pi, 'the same set the NaN CLI writes', checked 2026-10-02) closing issue #20 — the gateway now rejects max_tokens above ~256k for this model with 403 permission_error (measured 2026-10-02: 256,000 → 200; 262,143 → 403), and the previous models.dev-derived 384000 made every pi request fail since pi-ai sends max_tokens = model.maxTokens when no cap is set. models.dev still lists 384000, so this override pins the documented value. reasoning_effort_values=[\"none\"] (issue #16, measured 2026-09-27 against live gateway, repro included in the issue): only \"none\" deterministically disables reasoning (0 reasoning tokens, answer produced). Every other value leaves 13-14K reasoning tokens and causes runaway reasoning (finish_reason:\"length\" with ZERO answer text). The catalog declares only [\"none\"] because the remaining values are useless noise. docs vs measurement (re-probed live 2026-10-14): https://nan.builders/docs/models#controlling-reasoning documents the parameter as 'any value (no effect) — the model decides per request', but the live gateway contradicts that: `none` gave 0 reasoning tokens / 0 reasoning chars / a 134-char answer, while `low` gave 57 reasoning tokens and omitting the parameter gave 63 (`minimal` gave 109). The docs are stale here; the measurement wins (maintainer instruction), so the measured behavior is kept. thinkingLevelMap { off: \"none\" } (issue #16, measured 2026-09-27): when pi maps reasoning:\"off\" to reasoningEffort:undefined, the thinkingLevelMap.off=\"none\" supplies the effort so the gateway receives reasoning_effort:\"none\" and does NOT enter runaway reasoning mode."
47
47
  ],
48
48
  "extras": {
49
49
  "id": "deepseek-v4-flash",
@@ -177,7 +177,7 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
177
177
  "cacheRead": 0,
178
178
  "cacheWrite": 0
179
179
  },
180
- "contextWindow": 1000000,
180
+ "contextWindow": 1048576,
181
181
  "maxTokens": 131072,
182
182
  "reasoningEffortValues": [
183
183
  "minimal",
@@ -198,7 +198,7 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
198
198
  },
199
199
  "notes": [
200
200
  "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.",
201
- "thinkingLevelMap { off: \"minimal\" } (issue #16, measured 2026-09-27 against live gateway, repro included in the issue): when pi maps reasoning:\"off\" to reasoningEffort:undefined, the thinkingLevelMap.off=\"minimal\" supplies the effort so the gateway receives reasoning_effort:\"minimal\" (38 reasoning tokens + answer). Note: \"none\" does NOT suppress reasoning on glm5.3-flash (13,382 tokens) — only \"minimal\" was measured to be effective. reasoning_effort_values includes \"minimal\" (not documented by NaN docs but accepted and effective by the gateway, issue #16)."
201
+ "contextWindow 1,048,576: NaN docs state '1M token context' for this model (https://nan.builders/docs/models, checked 2026-10-14). models.dev lists 1,000,000, a rounded form of the same window; the docs value is authoritative (maintainer instruction: docs win over models.dev, which a user updates later). thinkingLevelMap { off: \"minimal\" } (issue #16, measured 2026-09-27 against live gateway, repro included in the issue): when pi maps reasoning:\"off\" to reasoningEffort:undefined, the thinkingLevelMap.off=\"minimal\" supplies the effort so the gateway receives reasoning_effort:\"minimal\" (38 reasoning tokens + answer). Note: \"none\" does NOT suppress reasoning on glm5.3-flash (13,382 tokens) — only \"minimal\" was measured to be effective. reasoning_effort_values includes \"minimal\" (the docs' glm row lists low/medium/high/max, but the 2026-09-27 measurement shows minimal is accepted and effective, issue #16)."
202
202
  ],
203
203
  "extras": {
204
204
  "id": "glm5.3-flash",
@@ -261,6 +261,9 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
261
261
  "contextWindow": 1048576,
262
262
  "maxTokens": 131072,
263
263
  "reasoningEffortValues": [],
264
+ "thinkingLevelMap": {
265
+ "off": "none"
266
+ },
264
267
  "compat": {
265
268
  "supportsDeveloperRole": false,
266
269
  "supportsReasoningEffort": true,
@@ -270,7 +273,8 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
270
273
  },
271
274
  "notes": [
272
275
  "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.",
273
- "manual-only: \"mimo-v2.6-flash\" skipped: models.dev provider \"nan\" now lists it, so the manual-only entry is not emitted and the models.dev values are used (omnimodal model (text, image, audio input) served by NaN; limits and modalities from https://nan.builders/docs/models#mimo-v2-6-flash (checked 2026-09-25, quotas re-checked 2026-09-29): 1,048,576 context / 131,072 max output / 1.0B monthly quota per member. Reasoning: accepted, depth not adjustable (docs table, checked 2026-09-29). Tool calling: yes. Streaming: yes. Input modalities (pi-representable): text, image (audio not representable in pi's Model type). Fallback entry: emitted only while models.dev provider nan does not list the model — models.dev started listing it on 2026-09-29, so the generator then keeps the models.dev values and attaches this note to that entry instead of emitting a second one.)"
276
+ "thinkingLevelMap { off: \"none\" } (live re-probe 2026-10-14): the docs list this model as 'accepted, depth not adjustable', but the gateway makes the parameter effective at the off end — `none` gave 0 reasoning tokens / a full answer, while `low` gave 22 reasoning tokens and omitting the parameter gave 17. Measured behavior wins over the docs, so `thinking: off` now sends reasoning_effort:\"none\" and actually disables reasoning. No other level was sampled, so only `off` is mapped.",
277
+ "manual-only: \"mimo-v2.6-flash\" skipped: models.dev provider \"nan\" now lists it, so the manual-only entry is not emitted and the models.dev values are used (omnimodal model (text, image, audio input) served by NaN; limits and modalities from https://nan.builders/docs/models#mimo-v2-6-flash (checked 2026-09-25, quotas re-checked 2026-09-29): 1,048,576 context / 131,072 max output / 1.0B monthly quota per member. Tool calling: yes. Streaming: yes. Input modalities (pi-representable): text, image (audio not representable in pi's Model type). Fallback entry: emitted only while models.dev provider nan does not list the model — models.dev started listing it on 2026-09-29, so the generator then keeps the models.dev values and attaches this note to that entry instead of emitting a second one.)"
274
278
  ],
275
279
  "extras": {
276
280
  "id": "mimo-v2.6-flash",
@@ -403,7 +407,7 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
403
407
  "cacheRead": 0,
404
408
  "cacheWrite": 0
405
409
  },
406
- "contextWindow": 262144,
410
+ "contextWindow": 1048576,
407
411
  "maxTokens": 131072,
408
412
  "reasoningEffortValues": [],
409
413
  "compat": {
@@ -415,7 +419,7 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
415
419
  },
416
420
  "notes": [
417
421
  "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.",
418
- "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)."
422
+ "contextWindow 1,048,576: NaN docs state '1M token context (1,048,576 tokens)' and 'Max answer 131K tokens' (https://nan.builders/docs/models, checked 2026-10-14) — models.dev still lists 262,144. The earlier override was withdrawn 2026-09-07 when the docs read '262K token context, the model's native window'; the docs now state 1M and they are the source of truth (maintainer instruction: docs win over models.dev, which a user updates later). maxTokens stays 131,072, which models.dev already matches and the docs confirm as the 131K max answer."
419
423
  ],
420
424
  "extras": {
421
425
  "id": "qwen3.8-flash",
@@ -455,7 +459,7 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
455
459
  export const GENERATED_CATALOG_META = {
456
460
  source: "https://models.dev/api.json",
457
461
  modelsDevProvider: "nan",
458
- fetchedAt: "2026-10-02T22:53:40.291Z",
462
+ fetchedAt: "2026-10-16T17:43:45.590Z",
459
463
  modelCount: 6,
460
464
  models: ["deepseek-v4-flash","gemma4","glm5.3-flash","mimo-v2.6-flash","qwen3.6","qwen3.8-flash"],
461
465
  notes: [
@@ -469,11 +473,12 @@ export const GENERATED_CATALOG_META = {
469
473
  "non-chat: \"whisper\" excluded from the chat catalog (speech-to-text (STT) endpoint (https://nan.builders/docs/models, checked 2026-10-01) — not a chat model)",
470
474
  "non-chat: \"flux-2-klein\" excluded from the chat catalog (image generation endpoint (https://nan.builders/docs/models, checked 2026-10-01) — not a chat model)",
471
475
  "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.",
472
- "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. maxTokens 32768 per the official NaN pi docs (https://nan.builders/docs/pi, 'the same set the NaN CLI writes', checked 2026-10-02) closing issue #20 — the gateway now rejects max_tokens above ~256k for this model with 403 permission_error (measured 2026-10-02: 256,000 → 200; 262,143 → 403), and the previous models.dev-derived 384000 made every pi request fail since pi-ai sends max_tokens = model.maxTokens when no cap is set. models.dev still lists 384000, so this override pins the documented value. reasoning_effort_values=[\"none\"] (issue #16, measured 2026-09-27 against live gateway, repro included in the issue): only \"none\" deterministically disables reasoning (0 reasoning tokens, answer produced). Every other value leaves 13-14K reasoning tokens and causes runaway reasoning (finish_reason:\"length\" with ZERO answer text). The catalog declares only [\"none\"] because the remaining values are useless noise. thinkingLevelMap { off: \"none\" } (issue #16, measured 2026-09-27): when pi maps reasoning:\"off\" to reasoningEffort:undefined, the thinkingLevelMap.off=\"none\" supplies the effort so the gateway receives reasoning_effort:\"none\" and does NOT enter runaway reasoning mode.",
476
+ "contextWindow 1,048,576: NaN docs state '1M token context' for this model (https://nan.builders/docs/models, checked 2026-10-14). models.dev lists 1,000,000, a rounded form of the same window; the docs value is authoritative (maintainer instruction: docs win over models.dev, which a user updates later). 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), so this stays a pin for the vision capability rather than a divergence. maxTokens 32768 per the official NaN pi docs (https://nan.builders/docs/pi, 'the same set the NaN CLI writes', checked 2026-10-02) closing issue #20 — the gateway now rejects max_tokens above ~256k for this model with 403 permission_error (measured 2026-10-02: 256,000 → 200; 262,143 → 403), and the previous models.dev-derived 384000 made every pi request fail since pi-ai sends max_tokens = model.maxTokens when no cap is set. models.dev still lists 384000, so this override pins the documented value. reasoning_effort_values=[\"none\"] (issue #16, measured 2026-09-27 against live gateway, repro included in the issue): only \"none\" deterministically disables reasoning (0 reasoning tokens, answer produced). Every other value leaves 13-14K reasoning tokens and causes runaway reasoning (finish_reason:\"length\" with ZERO answer text). The catalog declares only [\"none\"] because the remaining values are useless noise. docs vs measurement (re-probed live 2026-10-14): https://nan.builders/docs/models#controlling-reasoning documents the parameter as 'any value (no effect) — the model decides per request', but the live gateway contradicts that: `none` gave 0 reasoning tokens / 0 reasoning chars / a 134-char answer, while `low` gave 57 reasoning tokens and omitting the parameter gave 63 (`minimal` gave 109). The docs are stale here; the measurement wins (maintainer instruction), so the measured behavior is kept. thinkingLevelMap { off: \"none\" } (issue #16, measured 2026-09-27): when pi maps reasoning:\"off\" to reasoningEffort:undefined, the thinkingLevelMap.off=\"none\" supplies the effort so the gateway receives reasoning_effort:\"none\" and does NOT enter runaway reasoning mode.",
473
477
  "thinkingLevelMap { off: \"none\" } (issue #16): NaN docs state that with no reasoning_effort parameter gemma4 defaults to reasoning ON (16,384-token budget), while `none`/`minimal` skip reasoning entirely (https://nan.builders/docs/models#controlling-reasoning, checked 2026-10-01). pi maps reasoning:\"off\" to reasoningEffort:undefined, so without this map `thinking: off` sent no parameter and the model reasoned anyway; the map sends reasoning_effort:\"none\" and actually disables reasoning.",
474
- "thinkingLevelMap { off: \"minimal\" } (issue #16, measured 2026-09-27 against live gateway, repro included in the issue): when pi maps reasoning:\"off\" to reasoningEffort:undefined, the thinkingLevelMap.off=\"minimal\" supplies the effort so the gateway receives reasoning_effort:\"minimal\" (38 reasoning tokens + answer). Note: \"none\" does NOT suppress reasoning on glm5.3-flash (13,382 tokens) — only \"minimal\" was measured to be effective. reasoning_effort_values includes \"minimal\" (not documented by NaN docs but accepted and effective by the gateway, issue #16).",
475
- "manual-only: \"mimo-v2.6-flash\" skipped: models.dev provider \"nan\" now lists it, so the manual-only entry is not emitted and the models.dev values are used (omnimodal model (text, image, audio input) served by NaN; limits and modalities from https://nan.builders/docs/models#mimo-v2-6-flash (checked 2026-09-25, quotas re-checked 2026-09-29): 1,048,576 context / 131,072 max output / 1.0B monthly quota per member. Reasoning: accepted, depth not adjustable (docs table, checked 2026-09-29). Tool calling: yes. Streaming: yes. Input modalities (pi-representable): text, image (audio not representable in pi's Model type). Fallback entry: emitted only while models.dev provider nan does not list the model — models.dev started listing it on 2026-09-29, so the generator then keeps the models.dev values and attaches this note to that entry instead of emitting a second one.)",
478
+ "contextWindow 1,048,576: NaN docs state '1M token context' for this model (https://nan.builders/docs/models, checked 2026-10-14). models.dev lists 1,000,000, a rounded form of the same window; the docs value is authoritative (maintainer instruction: docs win over models.dev, which a user updates later). thinkingLevelMap { off: \"minimal\" } (issue #16, measured 2026-09-27 against live gateway, repro included in the issue): when pi maps reasoning:\"off\" to reasoningEffort:undefined, the thinkingLevelMap.off=\"minimal\" supplies the effort so the gateway receives reasoning_effort:\"minimal\" (38 reasoning tokens + answer). Note: \"none\" does NOT suppress reasoning on glm5.3-flash (13,382 tokens) — only \"minimal\" was measured to be effective. reasoning_effort_values includes \"minimal\" (the docs' glm row lists low/medium/high/max, but the 2026-09-27 measurement shows minimal is accepted and effective, issue #16).",
479
+ "thinkingLevelMap { off: \"none\" } (live re-probe 2026-10-14): the docs list this model as 'accepted, depth not adjustable', but the gateway makes the parameter effective at the off end — `none` gave 0 reasoning tokens / a full answer, while `low` gave 22 reasoning tokens and omitting the parameter gave 17. Measured behavior wins over the docs, so `thinking: off` now sends reasoning_effort:\"none\" and actually disables reasoning. No other level was sampled, so only `off` is mapped.",
480
+ "manual-only: \"mimo-v2.6-flash\" skipped: models.dev provider \"nan\" now lists it, so the manual-only entry is not emitted and the models.dev values are used (omnimodal model (text, image, audio input) served by NaN; limits and modalities from https://nan.builders/docs/models#mimo-v2-6-flash (checked 2026-09-25, quotas re-checked 2026-09-29): 1,048,576 context / 131,072 max output / 1.0B monthly quota per member. Tool calling: yes. Streaming: yes. Input modalities (pi-representable): text, image (audio not representable in pi's Model type). Fallback entry: emitted only while models.dev provider nan does not list the model — models.dev started listing it on 2026-09-29, so the generator then keeps the models.dev values and attaches this note to that entry instead of emitting a second one.)",
476
481
  "thinkingLevelMap { off: \"none\" } (issue #16): NaN docs state that with no reasoning_effort parameter every model uses its own default and that default is reasoning ON for qwen3.6 (16,384-token budget), while `none` and `minimal` skip the reasoning phase entirely (https://nan.builders/docs/models#controlling-reasoning, checked 2026-10-01). pi maps reasoning:\"off\" to reasoningEffort:undefined, so without this map `thinking: off` sent no parameter and the model reasoned anyway; the map sends reasoning_effort:\"none\" and actually disables reasoning.",
477
- "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)."
482
+ "contextWindow 1,048,576: NaN docs state '1M token context (1,048,576 tokens)' and 'Max answer 131K tokens' (https://nan.builders/docs/models, checked 2026-10-14) — models.dev still lists 262,144. The earlier override was withdrawn 2026-09-07 when the docs read '262K token context, the model's native window'; the docs now state 1M and they are the source of truth (maintainer instruction: docs win over models.dev, which a user updates later). maxTokens stays 131,072, which models.dev already matches and the docs confirm as the 131K max answer."
478
483
  ],
479
484
  } as const;
package/src/commands.ts CHANGED
@@ -1,24 +1,19 @@
1
1
  /**
2
- * `/nan-mcp` — slash command configuring both native MCP servers this
2
+ * `/nan-mcp` — slash command configuring the native MCP server this
3
3
  * package registers:
4
4
  *
5
- * - `nan-search` — the official NaN remote MCP server (api.nan.builders/mcp),
6
- * exposing `mcp__nan_search__web_search` (pi >=0.99.2). Default: enabled.
7
- * (pi 0.99.0–0.99.1 used the hyphenated form `mcp__nan-search__web_search`.)
8
5
  * - `nan-media` — the community stdio media server (flux-2-klein / kokoro /
9
6
  * whisper), exposing `mcp__nan_media__generate_image` etc. Default: enabled.
10
7
  * (pi 0.99.0–0.99.1 used the hyphenated form `mcp__nan-media__*`.)
11
8
  *
12
- * Both are session-scoped native servers (visible in /mcp, source "extension").
13
- * /nan-mcp enables/disables them via pi.registerMcpServer / pi.unregisterMcpServer,
9
+ * The server is session-scoped (visible in /mcp, source "extension").
10
+ * /nan-mcp enables/disables it via pi.registerMcpServer / pi.unregisterMcpServer,
14
11
  * persisting the toggle in nan-provider.json.
15
12
  *
16
- * Targets accept aliases: `web-search` (`search`, `web_search`, `official`,
17
- * `nan-web-search`) and `nan-mcp-server` (`media`, `media-mcp`, `nan-media`).
18
- * Explicit env vars (NAN_MCP_TOOLS, NAN_MEDIA_MCP) override the persisted
19
- * toggles for the session.
13
+ * Target aliases: `nan-mcp-server` (`media`, `media-mcp`, `nan-media`).
14
+ * Explicit env var (NAN_MEDIA_MCP) overrides the persisted toggle for the session.
20
15
  *
21
- * Native behavior changes from the old bridge:
16
+ * Native behavior:
22
17
  * - `disable` IMMEDIATELY hides tools (unregisterMcpServer disconnects the
23
18
  * server; the MCP extension hides tools when disconnected).
24
19
  * - `enable` IMMEDIATELY registers the server for the current session.
@@ -29,55 +24,31 @@
29
24
 
30
25
  import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
31
26
  import { mediaMcpCommand, mediaMcpEnabled, mediaMcpSource, mediaMcpEnvExplicit, mediaMcpEnvTruthy, mediaMcpTimeoutSec } from "./mcp/media-server.ts";
32
- import { webSearchBridgeEnabled, webSearchBridgeSource, mcpToolsEnvExplicit, mcpToolsDisabled } from "./mcp/api-key.ts";
33
27
  import { NAN_API_KEY_ENV, tryResolveNanApiKeyViaRegistry, resolveStoredNanApiKey } from "./mcp/api-key.ts";
34
28
  import { NAN_STATE_FILE, MCP_SERVER_NAMES, readBridgeState, writeBridgeState, type BridgeKey, readState, writeState } from "./mcp/state.ts";
35
29
  import { connectorMissingMessage, hasMcpConnector } from "./mcp/host-support.ts";
36
30
 
37
31
  const USAGE =
38
- "Usage: /nan-mcp [status] · /nan-mcp enable [web-search|nan-mcp-server] · /nan-mcp disable [web-search|nan-mcp-server]";
32
+ "Usage: /nan-mcp [status] · /nan-mcp enable [nan-mcp-server] · /nan-mcp disable [nan-mcp-server]";
39
33
 
40
34
  /** Canonical bridge targets with aliases. */
41
35
  const TARGETS: Record<string, BridgeKey> = {
42
- "web-search": "webSearch",
43
- search: "webSearch",
44
- web_search: "webSearch",
45
- official: "webSearch",
46
- "nan-web-search": "webSearch",
47
36
  "nan-mcp-server": "mediaMcp",
48
37
  media: "mediaMcp",
49
38
  "media-mcp": "mediaMcp",
50
39
  "nan-media": "mediaMcp",
51
40
  };
52
41
 
53
- function targetName(bridge: BridgeKey): string {
54
- return bridge === "webSearch" ? "web-search" : "nan-mcp-server";
42
+ function envOverrideLabel(): string {
43
+ return `env NAN_MEDIA_MCP=${process.env.NAN_MEDIA_MCP ?? ""} (overrides persisted)`;
55
44
  }
56
45
 
57
- function envOverrideLabel(bridge: BridgeKey): string {
58
- return bridge === "webSearch"
59
- ? `env NAN_MCP_TOOLS=${process.env.NAN_MCP_TOOLS ?? ""} (overrides persisted)`
60
- : `env NAN_MEDIA_MCP=${process.env.NAN_MEDIA_MCP ?? ""} (overrides persisted)`;
61
- }
62
-
63
- function sourceLabel(bridge: BridgeKey, source: "env" | "persisted" | "default"): string {
64
- if (source === "env") return envOverrideLabel(bridge);
46
+ function sourceLabel(source: "env" | "persisted" | "default"): string {
47
+ if (source === "env") return envOverrideLabel();
65
48
  if (source === "persisted") {
66
- return `persisted in <agentDir>/${NAN_STATE_FILE} (${targetName(bridge)}: ${readBridgeState(bridge)})`;
49
+ return `persisted in <agentDir>/${NAN_STATE_FILE} (nan-mcp-server: ${readBridgeState("mediaMcp")})`;
67
50
  }
68
- return "default (both bridges are enabled by default)";
69
- }
70
-
71
- /** Resolve a bridge key to its native server name. */
72
- function serverName(bridge: BridgeKey): string {
73
- return MCP_SERVER_NAMES[bridge];
74
- }
75
-
76
- /** Describe the bridge in the status message. */
77
- function bridgeDescription(bridge: BridgeKey): string {
78
- return bridge === "webSearch"
79
- ? "web-search bridge (official NaN MCP → mcp__nan_search__web_search)"
80
- : "nan-mcp-server bridge (community media MCP → mcp__nan_media__generate_image/edit_image/text_to_speech/list_voices/speech_to_text)";
51
+ return "default (bridge is enabled by default)";
81
52
  }
82
53
 
83
54
  /** Check if a native server of this name is currently registered. */
@@ -89,39 +60,27 @@ function nativeServerRegistered(pi: ExtensionAPI, name: string): boolean {
89
60
  }
90
61
  }
91
62
 
92
- /** Status message listing both bridges' gate state and native registration.
63
+ /** Status message listing the media bridge gate state and native registration.
93
64
  * `ctx` is the command context (provides modelRegistry for key lookup).
94
65
  */
95
66
  async function statusMessage(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promise<string> {
96
67
  const lines: string[] = [];
97
68
 
98
69
  if (!hasMcpConnector(pi)) {
99
- lines.push(connectorMissingMessage([MCP_SERVER_NAMES.webSearch, MCP_SERVER_NAMES.mediaMcp]));
70
+ lines.push(connectorMissingMessage([MCP_SERVER_NAMES.mediaMcp]));
100
71
  }
101
72
 
102
- for (const bridge of ["webSearch" as BridgeKey, "mediaMcp" as BridgeKey]) {
103
- const enabled = bridge === "webSearch" ? webSearchBridgeEnabled() : mediaMcpEnabled();
104
- const source = bridge === "webSearch" ? webSearchBridgeSource() : mediaMcpSource();
105
- const regStatus = nativeServerRegistered(pi, serverName(bridge)) ? "registered (native)" : "not registered";
106
- lines.push(`${bridgeDescription(bridge)}: ${enabled ? "enabled" : "disabled"} — ${sourceLabel(bridge, source)}, ${regStatus}.`);
107
-
108
- if (bridge === "webSearch") {
109
- // Key status for web-search
110
- const keyStatus = await resolveKeyStatus(ctx);
111
- lines.push(` API key: ${keyStatus}.`);
112
- }
113
- if (bridge === "mediaMcp") {
114
- lines.push(` Media spawn command: ${mediaMcpCommand().join(" ")}.`);
115
- }
116
- }
117
-
118
- lines.push("Configure with /nan-mcp enable|disable [web-search|nan-mcp-server]; no target = both.");
73
+ const enabled = mediaMcpEnabled();
74
+ const source = mediaMcpSource();
75
+ const regStatus = nativeServerRegistered(pi, MCP_SERVER_NAMES.mediaMcp) ? "registered (native)" : "not registered";
76
+ lines.push(`nan-mcp-server bridge: ${enabled ? "enabled" : "disabled"} — ${sourceLabel(source)}, ${regStatus}.`);
77
+ lines.push(` Media spawn command: ${mediaMcpCommand().join(" ")}.`);
78
+ lines.push(` API key: ${await resolveKeyStatus(ctx)}.`);
79
+ lines.push("Configure with /nan-mcp enable|disable [nan-mcp-server]; no target for the media server.");
119
80
  return lines.join("\n");
120
81
  }
121
82
 
122
- /** Check API key resolution status for web-search.
123
- * `ctx` is the command context (provides modelRegistry).
124
- */
83
+ /** Check API key resolution status. */
125
84
  async function resolveKeyStatus(ctx: ExtensionCommandContext): Promise<string> {
126
85
  const registryKey = await tryResolveNanApiKeyViaRegistry(ctx.modelRegistry);
127
86
  const storedKey = resolveStoredNanApiKey();
@@ -130,37 +89,22 @@ async function resolveKeyStatus(ctx: ExtensionCommandContext): Promise<string> {
130
89
  return "NOT SET — run /login nan or export NAN_API_KEY";
131
90
  }
132
91
 
133
- /** Parse an optional target argument; no target = both bridges. */
134
- function parseTarget(token: string | undefined): { bridges: BridgeKey[]; target?: string } | undefined {
135
- if (token === undefined) return { bridges: ["webSearch", "mediaMcp"] };
136
- const key = TARGETS[token.toLowerCase()];
137
- if (!key) return undefined;
138
- return { bridges: [key], target: token.toLowerCase() };
139
- }
140
-
141
- function describeBridges(bridges: BridgeKey[]): string {
142
- const names = bridges.map((bridge) => targetName(bridge));
143
- return names.length === 2 ? "both bridges" : names.join(" and ");
144
- }
145
-
146
- /**
147
- * Check if a bridge is effectively enabled (gate resolved).
148
- */
149
- function bridgeEffectivelyEnabled(bridge: BridgeKey): boolean {
150
- return bridge === "webSearch" ? webSearchBridgeEnabled() : mediaMcpEnabled();
92
+ /** Parse an optional target argument; no target = media server. */
93
+ function parseTarget(token: string | undefined): string | undefined {
94
+ if (token === undefined) return "mediaMcp";
95
+ return TARGETS[token.toLowerCase()];
151
96
  }
152
97
 
153
98
  export function registerNanMcpCommand(pi: ExtensionAPI): void {
154
99
  pi.registerCommand("nan-mcp", {
155
100
  description:
156
- "NaN MCP configuration: enable/disable/status for both native servers (official web-search + community media)",
101
+ "NaN MCP configuration: enable/disable/status for the community media server",
157
102
  getArgumentCompletions: (argumentPrefix: string) => {
158
103
  const prefix = argumentPrefix.trim().toLowerCase();
159
104
  const items = [
160
- { value: "enable", label: "enable", description: "Enable a native server (or both) and persist it" },
161
- { value: "disable", label: "disable", description: "Disable a native server (or both) persistently" },
105
+ { value: "enable", label: "enable", description: "Enable the media server and persist it" },
106
+ { value: "disable", label: "disable", description: "Disable the media server persistently" },
162
107
  { value: "status", label: "status", description: "Show current server status and registration" },
163
- { value: "enable web-search", label: "enable web-search", description: "Enable the official NaN web-search server" },
164
108
  { value: "enable nan-mcp-server", label: "enable nan-mcp-server", description: "Enable the community media server" },
165
109
  ];
166
110
  const filtered = items.filter((item) => item.value.startsWith(prefix));
@@ -181,20 +125,17 @@ export function registerNanMcpCommand(pi: ExtensionAPI): void {
181
125
  if (parsed === undefined || rest.length > 0) {
182
126
  ctx.ui.notify(
183
127
  parsed === undefined
184
- ? `Unknown target "${[rawTarget, ...rest].filter(Boolean).join(" ")}". Targets: web-search, nan-mcp-server (or omit for both).`
128
+ ? `Unknown target "${[rawTarget, ...rest].filter(Boolean).join(" ")}". Target: nan-mcp-server (or omit).`
185
129
  : USAGE,
186
130
  "warning",
187
131
  );
188
132
  return;
189
133
  }
190
- const { bridges, target } = parsed;
191
134
  const enabled = subcommand === "enable";
135
+ const where = rawTarget ? `for "${rawTarget.toLowerCase()}"` : "for the media server";
192
136
 
193
- for (const bridge of bridges) {
194
- writeBridgeState(bridge, enabled);
195
- }
137
+ writeBridgeState("mediaMcp", enabled);
196
138
 
197
- const where = target ? `for "${target}"` : "for both bridges";
198
139
  const persistence = `persisted in <agentDir>/${NAN_STATE_FILE}`;
199
140
 
200
141
  // Without an extension that handles `mcp_servers_change` pi cannot connect the
@@ -203,84 +144,56 @@ export function registerNanMcpCommand(pi: ExtensionAPI): void {
203
144
  // so it applies as soon as a connector (built-in `mcp`) is available.
204
145
  if (enabled && !hasMcpConnector(pi)) {
205
146
  ctx.ui.notify(
206
- `Enabled ${describeBridges(bridges)} ${where}, ${persistence} — NOT registered. ` +
207
- connectorMissingMessage(bridges.map(serverName)),
147
+ `Enabled ${where}, ${persistence} — NOT registered. ` +
148
+ connectorMissingMessage([MCP_SERVER_NAMES.mediaMcp]),
208
149
  "warning",
209
150
  );
210
151
  return;
211
152
  }
212
153
 
213
154
  // Immediate native registration/unregistration.
214
- for (const bridge of bridges) {
215
- const name = serverName(bridge);
216
- if (enabled) {
217
- // Only register if gate allows it.
218
- if (bridgeEffectivelyEnabled(bridge)) {
219
- if (bridge === "webSearch") {
220
- // Resolve key: stored credential → registry → env.
221
- const registryKey = await tryResolveNanApiKeyViaRegistry(ctx.modelRegistry);
222
- const storedKey = resolveStoredNanApiKey();
223
- const apiKey = storedKey ?? registryKey ?? process.env[NAN_API_KEY_ENV];
224
- if (apiKey) {
225
- pi.registerMcpServer(name, {
226
- type: "http",
227
- url: "https://api.nan.builders/mcp",
228
- headers: { Authorization: `Bearer ${apiKey}` },
229
- exposure: "direct",
230
- });
231
- } else {
232
- ctx.ui.notify(
233
- `${NAN_API_KEY_ENV} is not set. Export it or run /login nan.`,
234
- "warning",
235
- );
236
- }
237
- } else {
238
- // Resolve key for media: stored credential → registry → env.
239
- const registryKey = await tryResolveNanApiKeyViaRegistry(ctx.modelRegistry);
240
- const storedKey = resolveStoredNanApiKey();
241
- const apiKey = storedKey ?? registryKey ?? process.env[NAN_API_KEY_ENV];
242
- if (apiKey) {
243
- const mediaArgs = mediaMcpCommand();
244
- pi.registerMcpServer(name, {
245
- type: "stdio",
246
- command: mediaArgs[0]!,
247
- args: mediaArgs.slice(1),
248
- env: { [NAN_API_KEY_ENV]: apiKey },
249
- exposure: "direct",
250
- timeout: mediaMcpTimeoutSec(),
251
- });
252
- } else {
253
- ctx.ui.notify(
254
- `${NAN_API_KEY_ENV} is not set. Export it or run /login nan.`,
255
- "warning",
256
- );
257
- }
258
- }
155
+ const name = MCP_SERVER_NAMES.mediaMcp;
156
+ if (enabled) {
157
+ if (mediaMcpEnabled()) {
158
+ const registryKey = await tryResolveNanApiKeyViaRegistry(ctx.modelRegistry);
159
+ const storedKey = resolveStoredNanApiKey();
160
+ const apiKey = storedKey ?? registryKey ?? process.env[NAN_API_KEY_ENV];
161
+ if (apiKey) {
162
+ const mediaArgs = mediaMcpCommand();
163
+ pi.registerMcpServer(name, {
164
+ type: "stdio",
165
+ command: mediaArgs[0]!,
166
+ args: mediaArgs.slice(1),
167
+ env: { [NAN_API_KEY_ENV]: apiKey },
168
+ exposure: "direct",
169
+ timeout: mediaMcpTimeoutSec(),
170
+ });
171
+ } else {
172
+ ctx.ui.notify(
173
+ `${NAN_API_KEY_ENV} is not set. Export it or run /login nan.`,
174
+ "warning",
175
+ );
259
176
  }
260
- } else {
261
- pi.unregisterMcpServer(name);
262
177
  }
178
+ } else {
179
+ pi.unregisterMcpServer(name);
263
180
  }
264
181
 
265
182
  if (enabled) {
266
- // Only claim success if servers were actually registered.
267
- const allRegistered = bridges.every((b: BridgeKey) =>
268
- nativeServerRegistered(pi, serverName(b)),
269
- );
270
- if (allRegistered) {
183
+ if (nativeServerRegistered(pi, name)) {
271
184
  ctx.ui.notify(
272
- `Enabled ${describeBridges(bridges)} ${where}, ${persistence}. Native servers registered for this session.`,
185
+ `Enabled ${where}, ${persistence}. Native server registered for this session.`,
273
186
  "info",
274
187
  );
275
188
  } else {
276
189
  ctx.ui.notify(
277
- `Enabled ${describeBridges(bridges)} ${where}, ${persistence} — API key missing; servers not registered.`,
190
+ `Enabled ${where}, ${persistence} — API key missing; server not registered.`,
278
191
  "warning",
279
192
  );
280
193
  }
281
194
  } else {
282
195
  ctx.ui.notify(
283
- `Disabled ${describeBridges(bridges)} ${where}, ${persistence}. Native servers unregistered — tools hidden immediately.`,
196
+ `Disabled ${where}, ${persistence}. Native server unregistered — tools hidden immediately.`,
284
197
  "warning",
285
198
  );
286
199
  }