@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.
- package/AGENTS.md +94 -30
- package/README.es.md +56 -33
- package/README.md +55 -34
- package/package.json +4 -4
- package/scripts/check-nan-mcp-server.ts +1 -1
- package/scripts/generate-models.ts +3 -5
- package/scripts/manual-overrides.ts +35 -11
- package/scripts/models.generated.ts +18 -13
- package/src/commands.ts +62 -149
- package/src/fetch-models.ts +58 -0
- package/src/index.ts +7 -32
- package/src/mcp/api-key.ts +1 -26
- package/src/mcp/media-server.ts +1 -1
- package/src/mcp/state.ts +0 -3
- package/src/reasoning-only-stream-guard.ts +8 -3
|
@@ -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
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
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.
|
|
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
|
-
"
|
|
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\" (
|
|
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
|
-
"
|
|
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-
|
|
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":
|
|
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
|
|
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":
|
|
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\" (
|
|
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
|
-
"
|
|
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":
|
|
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
|
|
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-
|
|
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
|
|
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\" (
|
|
475
|
-
"
|
|
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
|
|
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
|
|
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
|
-
*
|
|
13
|
-
* /nan-mcp enables/disables
|
|
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
|
-
*
|
|
17
|
-
*
|
|
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
|
|
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 [
|
|
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
|
|
54
|
-
return
|
|
42
|
+
function envOverrideLabel(): string {
|
|
43
|
+
return `env NAN_MEDIA_MCP=${process.env.NAN_MEDIA_MCP ?? ""} (overrides persisted)`;
|
|
55
44
|
}
|
|
56
45
|
|
|
57
|
-
function
|
|
58
|
-
|
|
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} (
|
|
49
|
+
return `persisted in <agentDir>/${NAN_STATE_FILE} (nan-mcp-server: ${readBridgeState("mediaMcp")})`;
|
|
67
50
|
}
|
|
68
|
-
return "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
|
|
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.
|
|
70
|
+
lines.push(connectorMissingMessage([MCP_SERVER_NAMES.mediaMcp]));
|
|
100
71
|
}
|
|
101
72
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
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 =
|
|
134
|
-
function parseTarget(token: string | undefined):
|
|
135
|
-
if (token === undefined) return
|
|
136
|
-
|
|
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
|
|
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
|
|
161
|
-
{ value: "disable", label: "disable", description: "Disable
|
|
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(" ")}".
|
|
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
|
-
|
|
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 ${
|
|
207
|
-
connectorMissingMessage(
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
if (
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
196
|
+
`Disabled ${where}, ${persistence}. Native server unregistered — tools hidden immediately.`,
|
|
284
197
|
"warning",
|
|
285
198
|
);
|
|
286
199
|
}
|