@aliou/pi-neuralwatt 0.12.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,7 +18,8 @@ import type { NeuralwattQuotas } from "../../src/types/quota-api";
18
18
  import { getNeuralwattApiKey } from "../_shared/auth";
19
19
  import { registerNeuralwattSettings } from "./commands/settings";
20
20
  import { normalizeNeuralwattContextOverflowError } from "./context-overflow";
21
- import { getNeuralwattModels, refreshNeuralwattModels } from "./models";
21
+ import { getNeuralwattModels } from "./models";
22
+ import { createNeuralwattProvider } from "./provider";
22
23
  import { buildQuotasFromHeaders, fetchRequestedQuotas } from "./quota-store";
23
24
  import {
24
25
  type NeuralwattRateLimitInfo,
@@ -42,24 +43,24 @@ function registerNeuralwattProvider(
42
43
  ): void {
43
44
  const { provider: providerConfig } = configLoader.getConfig();
44
45
 
45
- const models = getNeuralwattModels({
46
+ const staticModels = getNeuralwattModels({
46
47
  includeLegacyModelIds: providerConfig.includeLegacyModelIds,
47
48
  includeAliasedModelIds: providerConfig.includeAliasedModelIds,
48
49
  });
49
50
 
50
- const config: Parameters<ExtensionAPI["registerProvider"]>[1] = {
51
- name: "Neuralwatt",
52
- baseUrl: "https://api.neuralwatt.com/v1",
53
- apiKey: "$NEURALWATT_API_KEY",
54
- api: "openai-completions",
55
- authHeader: true,
56
- headers: {
57
- Referer: "https://pi.dev",
58
- "X-Title": "npm:@aliou/pi-neuralwatt",
59
- },
60
- models,
61
- refreshModels: (context) =>
62
- refreshNeuralwattModels(context, {
51
+ const apiProvider = getApiProvider("openai-completions");
52
+ const baseStreamSimple = apiProvider?.streamSimple;
53
+ const streamSimple = baseStreamSimple
54
+ ? (wrapNeuralwattStreamSimple(
55
+ baseStreamSimple as never,
56
+ onSseQuota,
57
+ ) as never)
58
+ : undefined;
59
+
60
+ pi.registerProvider(
61
+ createNeuralwattProvider(
62
+ staticModels,
63
+ () => ({
63
64
  includeLegacyModelIds:
64
65
  configLoader.getConfig().provider.includeLegacyModelIds,
65
66
  includeAliasedModelIds:
@@ -67,18 +68,9 @@ function registerNeuralwattProvider(
67
68
  includeEarlyAccessModels:
68
69
  configLoader.getConfig().provider.includeEarlyAccessModels,
69
70
  }),
70
- };
71
-
72
- const provider = getApiProvider("openai-completions");
73
- const baseStreamSimple = provider?.streamSimple;
74
- if (baseStreamSimple) {
75
- config.streamSimple = wrapNeuralwattStreamSimple(
76
- baseStreamSimple as never,
77
- onSseQuota,
78
- ) as never;
79
- }
80
-
81
- pi.registerProvider("neuralwatt", config);
71
+ streamSimple,
72
+ ),
73
+ );
82
74
  }
83
75
 
84
76
  export default async function (pi: ExtensionAPI) {
@@ -1,4 +1,8 @@
1
1
  import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
2
+ import type {
3
+ NeuralwattApiModelReasoning,
4
+ NeuralwattReasoningEffort,
5
+ } from "../../../src/types/models-api";
2
6
 
3
7
  export type ThinkingLevelMap = NonNullable<
4
8
  ProviderModelConfig["thinkingLevelMap"]
@@ -26,8 +30,11 @@ export interface NeuralwattCost {
26
30
  export interface NeuralwattModelFamily {
27
31
  cost: NeuralwattCost;
28
32
  vision: boolean;
29
- /** Thinking levels used by reasoning variants of this family. */
30
- thinkingLevelMap?: ThinkingLevelMap;
33
+ /**
34
+ * Reasoning contract snapshot from `/v1/models` for reasoning variants.
35
+ * `buildThinkingLevelMap` turns it into the Pi thinking level map.
36
+ */
37
+ reasoningMetadata?: NeuralwattReasoningMapSource;
31
38
  }
32
39
 
33
40
  export interface NeuralwattVariantSpec {
@@ -48,7 +55,54 @@ export interface NeuralwattVariantSpec {
48
55
  */
49
56
  costMultiplier?: number;
50
57
  vision?: boolean;
51
- thinkingLevelMap?: ThinkingLevelMap;
58
+ /** Override the family reasoning contract for this variant. */
59
+ reasoningMetadata?: NeuralwattReasoningMapSource;
60
+ }
61
+
62
+ /**
63
+ * Subset of the API reasoning block needed to build the Pi thinking level map.
64
+ * Kept narrow so public snapshots stay small and offline-friendly.
65
+ */
66
+ export type NeuralwattReasoningMapSource = Pick<
67
+ NeuralwattApiModelReasoning,
68
+ "supported_efforts" | "mandatory"
69
+ >;
70
+
71
+ /**
72
+ * Build the Pi thinking level map from the Neuralwatt reasoning contract.
73
+ *
74
+ * Pure identity mapping: a Pi level is enabled iff it appears in
75
+ * `supported_efforts` (mapped to its own name), `null` otherwise. `off` maps to
76
+ * `"none"` when the model permits disabling reasoning (`!mandatory` and
77
+ * `"none"` is supported).
78
+ *
79
+ * When the reasoning block is missing (e.g. Kimi K2.7 Code, whose API metadata
80
+ * exposes none), falls back to a conservative `high`-only map with `off: null`,
81
+ * matching the upstream binary thinking toggle.
82
+ *
83
+ * `default_effort` and `effort_aliases` are deliberately ignored: Pi has no
84
+ * default-reasoning field, and we expose native supported efforts rather than
85
+ * aliasing unsupported ones.
86
+ */
87
+ export function buildThinkingLevelMap(
88
+ reasoning: NeuralwattReasoningMapSource | undefined,
89
+ ): ThinkingLevelMap {
90
+ // Conservative fallback for models whose API metadata has no reasoning
91
+ // block. Expose one known-good level and forbid disabling reasoning.
92
+ const supported = new Set<NeuralwattReasoningEffort>(
93
+ reasoning?.supported_efforts ?? ["high"],
94
+ );
95
+ const mandatory = reasoning?.mandatory ?? true;
96
+
97
+ return {
98
+ off: !mandatory && supported.has("none") ? "none" : null,
99
+ minimal: supported.has("minimal") ? "minimal" : null,
100
+ low: supported.has("low") ? "low" : null,
101
+ medium: supported.has("medium") ? "medium" : null,
102
+ high: supported.has("high") ? "high" : null,
103
+ xhigh: supported.has("xhigh") ? "xhigh" : null,
104
+ max: supported.has("max") ? "max" : null,
105
+ };
52
106
  }
53
107
 
54
108
  /**
@@ -97,15 +151,14 @@ export function buildNeuralwattModel(
97
151
  };
98
152
 
99
153
  if (variant.reasoning) {
100
- const thinkingLevelMap =
101
- variant.thinkingLevelMap ?? family.thinkingLevelMap;
102
- if (!thinkingLevelMap) {
103
- throw new Error(
104
- `Missing thinkingLevelMap for reasoning model ${variant.id}`,
105
- );
106
- }
107
- // Clone so variants never share a family map instance.
108
- model.thinkingLevelMap = { ...thinkingLevelMap };
154
+ // Clone so variants never share a family map instance. The map is derived
155
+ // from the API reasoning contract; missing metadata falls back to a
156
+ // high-only map rather than throwing.
157
+ model.thinkingLevelMap = {
158
+ ...buildThinkingLevelMap(
159
+ variant.reasoningMetadata ?? family.reasoningMetadata,
160
+ ),
161
+ };
109
162
  }
110
163
 
111
164
  return model;
@@ -1,7 +1,7 @@
1
1
  import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
2
2
  import { fetchNeuralwattModels } from "../../../src/lib/neuralwatt-api";
3
3
  import type { NeuralwattApiModel } from "../../../src/types/models-api";
4
- import { resolveMaxTokens } from "./build";
4
+ import { buildThinkingLevelMap, resolveMaxTokens } from "./build";
5
5
  import { NEURALWATT_MODELS } from "./public-models";
6
6
 
7
7
  // Pre-release models. Neuralwatt ships these to authorized accounts before they
@@ -13,10 +13,16 @@ export const EARLY_ACCESS_NEURALWATT_MODELS: ProviderModelConfig[] = [];
13
13
 
14
14
  // Per-ID overrides for known early-access models. The authenticated /v1/models
15
15
  // endpoint exposes pricing and capabilities, but some Pi-specific behavior
16
- // (thinking levels, compat flags) has to be supplied by hand.
16
+ // (compat flags, context window, max tokens) has to be supplied by hand.
17
+ // Reasoning config is always derived from the endpoint's `reasoning` block via
18
+ // `buildThinkingLevelMap`, so `reasoning` and `thinkingLevelMap` are not part
19
+ // of the override shape.
17
20
  // Models that have since gone public now live in public-models.ts.
18
21
  const EARLY_ACCESS_MODEL_OVERRIDES: Partial<
19
- Record<string, Partial<ProviderModelConfig>>
22
+ Record<
23
+ string,
24
+ Omit<Partial<ProviderModelConfig>, "reasoning" | "thinkingLevelMap">
25
+ >
20
26
  > = {};
21
27
 
22
28
  function buildEarlyAccessModel(
@@ -57,13 +63,10 @@ function buildEarlyAccessModel(
57
63
  };
58
64
 
59
65
  if (reasoning) {
60
- model.thinkingLevelMap = override?.thinkingLevelMap ?? {
61
- minimal: null,
62
- low: null,
63
- medium: "medium",
64
- high: null,
65
- xhigh: null,
66
- };
66
+ // Reasoning levels come straight from the endpoint's `reasoning` block:
67
+ // `supported_efforts` (identity) and `mandatory` (gates `off`). A missing
68
+ // block falls back to a high-only map inside `buildThinkingLevelMap`.
69
+ model.thinkingLevelMap = buildThinkingLevelMap(meta?.reasoning);
67
70
  }
68
71
 
69
72
  if (override) {
@@ -80,11 +83,7 @@ function applyEarlyAccessOverride(
80
83
  const result: ProviderModelConfig = { ...model };
81
84
 
82
85
  if (override.name !== undefined) result.name = override.name;
83
- if (override.reasoning !== undefined) result.reasoning = override.reasoning;
84
86
  if (override.input !== undefined) result.input = override.input;
85
- if (override.thinkingLevelMap !== undefined) {
86
- result.thinkingLevelMap = override.thinkingLevelMap;
87
- }
88
87
  if (override.contextWindow !== undefined) {
89
88
  result.contextWindow = override.contextWindow;
90
89
  }
@@ -96,6 +95,9 @@ function applyEarlyAccessOverride(
96
95
  result.compat = { ...model.compat, ...override.compat };
97
96
  }
98
97
 
98
+ // `reasoning` and `thinkingLevelMap` are intentionally not overridable:
99
+ // reasoning config is derived from the endpoint's `reasoning` block.
100
+
99
101
  return result;
100
102
  }
101
103
 
@@ -4,119 +4,83 @@ import {
4
4
  FLEX_COST_MULTIPLIER,
5
5
  type NeuralwattModelFamily,
6
6
  type NeuralwattVariantSpec,
7
- type ThinkingLevelMap,
8
7
  } from "./build";
9
8
 
10
9
  // Public models returned by https://api.neuralwatt.com/v1/models.
11
10
  // Pricing, capabilities, and limits are sourced from the API metadata fields;
12
11
  // `maxTokens` is `metadata.limits.max_output_tokens ?? max_model_len`.
13
12
  //
14
- // Models are declared per family so every variant (`-fast`, `-flex`, `-short`)
15
- // inherits the family's pricing, modalities, and thinking levels. See
16
- // `models.test.ts` for the drift check against the live catalog.
17
-
18
- // GLM natively supports `high` and `max` reasoning efforts. `xhigh` is an
19
- // unsupported hole between them. Pi added the `max` level in 0.80.6.
20
- const GLM_THINKING: ThinkingLevelMap = {
21
- off: "none",
22
- minimal: null,
23
- low: null,
24
- medium: null,
25
- high: "high",
26
- xhigh: null,
27
- max: "max",
28
- };
29
-
30
- // Binary thinking control (Kimi K2.x, Qwen3.x): no graded `reasoning_effort`
31
- // upstream, only a thinking on/off toggle. Expose a single known-good Pi
32
- // level; "high" stands in for standard full thinking.
33
- const BINARY_THINKING: ThinkingLevelMap = {
34
- minimal: null,
35
- low: null,
36
- medium: null,
37
- high: "high",
38
- xhigh: null,
39
- };
13
+ // Each reasoning family snapshots its `reasoning.supported_efforts` +
14
+ // `reasoning.mandatory` from the API; `buildThinkingLevelMap` turns that into
15
+ // the Pi thinking level map by identity (no aliasing). See `models.test.ts`
16
+ // for the drift check against the live catalog.
40
17
 
18
+ // DeepSeek V4 Flash: efforts max/high/none, not mandatory.
19
+ // https://api-docs.deepseek.com/guides/thinking_mode/
41
20
  const DEEPSEEK_V4_FLASH: NeuralwattModelFamily = {
42
21
  cost: { input: 0.14, output: 0.28, cacheRead: 0.028 },
43
22
  vision: false,
44
- // DeepSeek V4 Flash accepts reasoning_effort low/high/max (default high);
45
- // there is no "medium" tier, so Pi's low/high/max map directly and
46
- // minimal/medium/xhigh are unsupported holes.
47
- // https://api-docs.deepseek.com/guides/thinking_mode/
48
- thinkingLevelMap: {
49
- off: "none",
50
- minimal: null,
51
- low: "low",
52
- medium: null,
53
- high: "high",
54
- xhigh: null,
55
- max: "max",
23
+ reasoningMetadata: {
24
+ supported_efforts: ["max", "high", "none"],
25
+ mandatory: false,
56
26
  },
57
27
  };
58
28
 
59
29
  // Google, served from NVIDIA's NVFP4 checkpoint. Gemma 4's chat template
60
- // takes a boolean rather than an effort level, so it has a single reasoning
61
- // depth (`max`) plus thinking-off; every non-`none` value resolves to `max`.
30
+ // takes a boolean rather than an effort level, so the API only advertises
31
+ // `max` and `none`; every non-`none` request resolves to `max` upstream.
62
32
  // It does not reason by default (`default_enabled: false`), but the model
63
33
  // can produce reasoning traces when asked. See
64
34
  // https://portal.neuralwatt.com/docs/api/chat-completions#reasoning-effort
65
- const GEMMA_4_THINKING: ThinkingLevelMap = {
66
- off: "none",
67
- minimal: null,
68
- low: null,
69
- medium: null,
70
- high: null,
71
- xhigh: null,
72
- max: "max",
73
- };
74
-
75
35
  const GEMMA_4: NeuralwattModelFamily = {
76
36
  cost: { input: 0.144, output: 0.42, cacheRead: 0.0144 },
77
37
  vision: true,
78
- thinkingLevelMap: GEMMA_4_THINKING,
38
+ reasoningMetadata: {
39
+ supported_efforts: ["max", "none"],
40
+ mandatory: false,
41
+ },
79
42
  };
80
43
 
81
- // ZhipuAI.
44
+ // ZhipuAI. GLM-5.2 natively supports `high` and `max` reasoning efforts;
45
+ // `xhigh` is an unsupported hole between them. Pi's `max` level (0.80.6) maps
46
+ // to GLM's top tier.
82
47
  const GLM_5_2: NeuralwattModelFamily = {
83
48
  cost: { input: 1.45, output: 4.5, cacheRead: 0.145 },
84
49
  vision: false,
85
- thinkingLevelMap: GLM_THINKING,
50
+ reasoningMetadata: {
51
+ supported_efforts: ["max", "high", "none"],
52
+ mandatory: false,
53
+ },
86
54
  };
87
55
 
88
- // MoonshotAI. K3 is the largest open-weight model ever released, served in
89
- // preview with limited concurrency. K3 always reasons (thinking cannot be
90
- // disabled) and supports `reasoning_effort` values "low", "high", and "max"
91
- // (default "max"). There is no "medium" tier upstream, so Pi's low/high/max
92
- // map directly and `off`, `minimal`, `medium`, and `xhigh` are unsupported
93
- // holes. The `-fast` endpoint is a shorthand to set thinking to off.
56
+ // MoonshotAI. K3 supports reasoning efforts low/high/max (default max) and
57
+ // can be turned off (`mandatory: false`). The `-fast` endpoint is a shorthand
58
+ // to set thinking to off.
94
59
  const KIMI_K3: NeuralwattModelFamily = {
95
60
  cost: { input: 3, output: 15, cacheRead: 0.3 },
96
61
  vision: true,
97
- thinkingLevelMap: {
98
- off: null,
99
- minimal: null,
100
- low: "low",
101
- medium: null,
102
- high: "high",
103
- xhigh: null,
104
- max: "max",
62
+ reasoningMetadata: {
63
+ supported_efforts: ["max", "high", "low", "none"],
64
+ mandatory: false,
105
65
  },
106
66
  };
107
67
 
108
- // MoonshotAI.
68
+ // MoonshotAI. The K2.7 Code API exposes no `reasoning` block, so this family
69
+ // omits `reasoningMetadata`; `buildThinkingLevelMap` falls back to a high-only
70
+ // map with `off: null`, matching the upstream binary thinking toggle.
109
71
  const KIMI_K2_7_CODE: NeuralwattModelFamily = {
110
72
  cost: { input: 0.95, output: 4.0, cacheRead: 0.095 },
111
73
  vision: true,
112
- thinkingLevelMap: { off: null, ...BINARY_THINKING },
113
74
  };
114
75
 
115
- // Qwen.
76
+ // Qwen. Qwen3.6 35B only advertises `high` and `none`.
116
77
  const QWEN_3_6_35B: NeuralwattModelFamily = {
117
78
  cost: { input: 0.29, output: 1.15, cacheRead: 0.029 },
118
79
  vision: true,
119
- thinkingLevelMap: BINARY_THINKING,
80
+ reasoningMetadata: {
81
+ supported_efforts: ["high", "none"],
82
+ mandatory: false,
83
+ },
120
84
  };
121
85
 
122
86
  const FAMILIES: [NeuralwattModelFamily, NeuralwattVariantSpec[]][] = [
@@ -216,28 +180,37 @@ const FAMILIES: [NeuralwattModelFamily, NeuralwattVariantSpec[]][] = [
216
180
  },
217
181
  ],
218
182
  ],
183
+ // The kimi-k3 endpoint rejects anything above 327,680 total tokens with
184
+ // `400: max_completion_tokens is too large … supports at most 327680
185
+ // completion tokens` (verified at runtime), even though the API advertises
186
+ // `max_model_len: 1048560` with a null output cap for the whole family.
187
+ // The -fast/-flex endpoints don't enforce any cap server-side yet (they
188
+ // accept max_completion_tokens beyond the advertised window), but they are
189
+ // the same K3 deployment and are expected to share the 327,680 limit, so
190
+ // all three variants are pinned to it. The drift check in models.test.ts
191
+ // whitelists this divergence via CONTEXT_WINDOW_OVERRIDES.
219
192
  [
220
193
  KIMI_K3,
221
194
  [
222
195
  {
223
196
  id: "kimi-k3",
224
197
  name: "Kimi K3",
225
- contextWindow: 1048560,
226
- maxOutputTokens: null,
198
+ contextWindow: 327680,
199
+ maxOutputTokens: 327680,
227
200
  reasoning: true,
228
201
  },
229
202
  {
230
203
  id: "kimi-k3-fast",
231
204
  name: "Kimi K3 Fast",
232
- contextWindow: 1048560,
233
- maxOutputTokens: null,
205
+ contextWindow: 327680,
206
+ maxOutputTokens: 327680,
234
207
  reasoning: false,
235
208
  },
236
209
  {
237
210
  id: "kimi-k3-flex",
238
211
  name: "Kimi K3 (flex)",
239
- contextWindow: 1048560,
240
- maxOutputTokens: null,
212
+ contextWindow: 327680,
213
+ maxOutputTokens: 327680,
241
214
  reasoning: true,
242
215
  costMultiplier: FLEX_COST_MULTIPLIER,
243
216
  },
@@ -5,10 +5,6 @@ import type {
5
5
  RefreshModelsContext,
6
6
  } from "@earendil-works/pi-ai";
7
7
  import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
8
- import {
9
- persistModels,
10
- readStoredModels,
11
- } from "../../../src/refresh-store-compat";
12
8
  import {
13
9
  ALIAS_NEURALWATT_MODEL_IDS,
14
10
  buildAliasNeuralwattModels,
@@ -24,6 +20,8 @@ const PROVIDER_ID = "neuralwatt";
24
20
  const BASE_URL = "https://api.neuralwatt.com/v1";
25
21
  const API = "openai-completions" as const;
26
22
 
23
+ export type StoredProviderModels = readonly Model<Api>[];
24
+
27
25
  export interface RefreshNeuralwattModelsOptions {
28
26
  includeLegacyModelIds: boolean;
29
27
  includeAliasedModelIds: boolean;
@@ -129,25 +127,27 @@ function persistCatalog(
129
127
  context: RefreshModelsContext,
130
128
  models: ProviderModelConfig[],
131
129
  ): Promise<boolean> {
132
- return persistModels(context, {
133
- models: models.map(toStoredModel),
134
- checkedAt: Date.now(),
130
+ return context.publish({
131
+ persist: {
132
+ models: models.map(toStoredModel),
133
+ checkedAt: Date.now(),
134
+ },
135
135
  });
136
136
  }
137
137
 
138
- /** Refresh the complete Neuralwatt catalog with Pi-managed persistence. */
138
+ /** Refresh the complete Neuralwatt catalog; undefined = failed (stale store kept). */
139
139
  export async function refreshNeuralwattModels(
140
140
  context: RefreshModelsContext,
141
141
  options: RefreshNeuralwattModelsOptions,
142
- ): Promise<ProviderModelConfig[]> {
142
+ ): Promise<ProviderModelConfig[] | undefined> {
143
143
  const baseline = configuredModels(
144
144
  options.includeLegacyModelIds,
145
145
  options.includeAliasedModelIds,
146
146
  );
147
- const stored = await readStoredModels(context);
147
+ const stored = context.stored;
148
148
 
149
149
  if (!options.includeEarlyAccessModels) {
150
- await persistCatalog(context, baseline);
150
+ await persistCatalog(context, baseline).catch(() => false);
151
151
  return baseline;
152
152
  }
153
153
 
@@ -161,29 +161,31 @@ export async function refreshNeuralwattModels(
161
161
  cachedEarlyAccess,
162
162
  );
163
163
 
164
- if (!context.allowNetwork || context.signal?.aborted) {
164
+ if (!context.allowNetwork || context.signal.aborted) {
165
165
  return cachedCatalog;
166
166
  }
167
167
 
168
+ // Anonymous credential (empty or missing key): keep the public catalog and
169
+ // skip discovery, which requires a real key.
168
170
  const apiKey =
169
- context.credential?.type === "api_key" ? context.credential.key : undefined;
171
+ context.credential?.type === "api_key" && context.credential.key
172
+ ? context.credential.key
173
+ : undefined;
170
174
  if (!apiKey) return cachedCatalog;
171
175
 
172
176
  const earlyAccess = await (options.loadEarlyAccess ?? loadEarlyAccessModels)(
173
177
  apiKey,
174
178
  context.signal,
175
179
  );
176
- if (context.signal?.aborted) return cachedCatalog;
177
- if (!earlyAccess) {
178
- throw new Error("Neuralwatt model catalog refresh failed");
179
- }
180
+ if (context.signal.aborted) return cachedCatalog;
181
+ if (!earlyAccess) return undefined;
180
182
 
181
183
  const catalog = configuredModels(
182
184
  options.includeLegacyModelIds,
183
185
  options.includeAliasedModelIds,
184
186
  configuredEarlyAccessModels(earlyAccess, baseline),
185
187
  );
186
- context.signal?.throwIfAborted();
187
- await persistCatalog(context, catalog);
188
+ context.signal.throwIfAborted();
189
+ await persistCatalog(context, catalog).catch(() => false);
188
190
  return catalog;
189
191
  }
@@ -0,0 +1,113 @@
1
+ import type {
2
+ Api,
3
+ Model,
4
+ Provider,
5
+ ProviderStreamOptions,
6
+ } from "@earendil-works/pi-ai";
7
+ import { stream, streamSimple } from "@earendil-works/pi-ai/compat";
8
+ import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
9
+ import type {
10
+ RefreshNeuralwattModelsOptions,
11
+ StoredProviderModels,
12
+ } from "./models/refresh";
13
+ import { refreshNeuralwattModels } from "./models/refresh";
14
+ import type { AnyStreamSimple } from "./stream-simple";
15
+
16
+ export const NEURALWATT_PROVIDER_ID = "neuralwatt";
17
+ export const NEURALWATT_BASE_URL = "https://api.neuralwatt.com/v1";
18
+ export const NEURALWATT_API_KEY_ENV = "NEURALWATT_API_KEY";
19
+
20
+ const NEURALWATT_REQUEST_HEADERS = {
21
+ Referer: "https://pi.dev",
22
+ "X-Title": "npm:@aliou/pi-neuralwatt",
23
+ };
24
+
25
+ const API = "openai-completions" as const;
26
+
27
+ function toProviderModels(
28
+ models: readonly ProviderModelConfig[],
29
+ ): Model<Api>[] {
30
+ return models.map((model) => ({
31
+ ...model,
32
+ api: model.api ?? API,
33
+ provider: NEURALWATT_PROVIDER_ID,
34
+ baseUrl: model.baseUrl ?? NEURALWATT_BASE_URL,
35
+ headers: NEURALWATT_REQUEST_HEADERS,
36
+ }));
37
+ }
38
+
39
+ export function createNeuralwattProvider(
40
+ staticModels: ProviderModelConfig[],
41
+ refreshOptions: () => RefreshNeuralwattModelsOptions,
42
+ streamSimpleOverride?: AnyStreamSimple,
43
+ ): Provider {
44
+ let liveModels = toProviderModels(staticModels);
45
+
46
+ return {
47
+ id: NEURALWATT_PROVIDER_ID,
48
+ name: "Neuralwatt",
49
+ baseUrl: NEURALWATT_BASE_URL,
50
+ headers: NEURALWATT_REQUEST_HEADERS,
51
+ auth: {
52
+ apiKey: {
53
+ name: "Neuralwatt API key",
54
+ login: async (interaction) => ({
55
+ type: "api_key",
56
+ key: await interaction.prompt({
57
+ type: "secret",
58
+ message: "Enter Neuralwatt API key",
59
+ }),
60
+ }),
61
+ check: async ({ ctx, credential }) => {
62
+ if (credential?.type === "api_key" && credential.key) {
63
+ return { type: "api_key", source: "stored credential" };
64
+ }
65
+ if (await ctx.env(NEURALWATT_API_KEY_ENV)) {
66
+ return { type: "api_key", source: NEURALWATT_API_KEY_ENV };
67
+ }
68
+ return undefined;
69
+ },
70
+ resolve: async ({ ctx, credential, signal }) => {
71
+ signal.throwIfAborted();
72
+ if (credential?.type === "api_key" && credential.key) {
73
+ return {
74
+ auth: { apiKey: credential.key },
75
+ env: credential.env,
76
+ source: "stored credential",
77
+ };
78
+ }
79
+ const envKey = await ctx.env(NEURALWATT_API_KEY_ENV);
80
+ signal.throwIfAborted();
81
+ if (envKey) {
82
+ return { auth: { apiKey: envKey }, source: NEURALWATT_API_KEY_ENV };
83
+ }
84
+ // Resolve never fails: without a key the catalog is the hardcoded
85
+ // one and early-access discovery is skipped (anonymous playground
86
+ // traffic authenticates at stream time).
87
+ return { auth: { apiKey: "" }, source: "anonymous" };
88
+ },
89
+ },
90
+ },
91
+ getModels: () => liveModels,
92
+ refreshModels: async (context) => {
93
+ const refreshed = await refreshNeuralwattModels(
94
+ context,
95
+ refreshOptions(),
96
+ );
97
+ // Fresh or offline store: the refresh intentionally skipped the
98
+ // network; adopt the persisted catalog anyway so getModels reflects it
99
+ // (statics otherwise).
100
+ const next = refreshed ?? context.stored?.models;
101
+ if (!next || next.length === 0) return;
102
+ const adopted = toProviderModels(next as StoredProviderModels);
103
+ await context.publish({
104
+ update: () => {
105
+ liveModels = adopted;
106
+ },
107
+ });
108
+ },
109
+ stream: (model, context, options) =>
110
+ stream(model, context, options as ProviderStreamOptions | undefined),
111
+ streamSimple: streamSimpleOverride ?? streamSimple,
112
+ };
113
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aliou/pi-neuralwatt",
3
- "version": "0.12.1",
3
+ "version": "0.14.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "private": false,
@@ -38,8 +38,8 @@
38
38
  "@aliou/pi-utils-ui": "^0.5.0"
39
39
  },
40
40
  "peerDependencies": {
41
- "@earendil-works/pi-ai": ">=0.80.8",
42
- "@earendil-works/pi-coding-agent": ">=0.80.8",
41
+ "@earendil-works/pi-ai": ">=0.84.0",
42
+ "@earendil-works/pi-coding-agent": ">=0.84.0",
43
43
  "@earendil-works/pi-tui": "*"
44
44
  },
45
45
  "devDependencies": {
@@ -18,6 +18,46 @@ export interface NeuralwattApiModelCapabilities {
18
18
  developer_role: boolean;
19
19
  }
20
20
 
21
+ /**
22
+ * Reasoning effort values Neuralwatt accepts on the wire. Mirrors Pi's
23
+ * `ModelThinkingLevel` (minus `off`, which the API spells `"none"`).
24
+ */
25
+ export type NeuralwattReasoningEffort =
26
+ | "none"
27
+ | "minimal"
28
+ | "low"
29
+ | "medium"
30
+ | "high"
31
+ | "xhigh"
32
+ | "max";
33
+
34
+ /**
35
+ * Per-model reasoning contract from `/v1/models`.
36
+ *
37
+ * `supported_efforts` is authoritative for which Pi thinking levels to expose:
38
+ * the Pi map is built by identity (a level is enabled iff it appears here),
39
+ * see `buildThinkingLevelMap` in `extensions/provider/models/build.ts`.
40
+ * `default_effort` and `effort_aliases` are typed for fidelity but are not
41
+ * consumed — Pi has no default-reasoning field and we expose native efforts
42
+ * rather than aliasing unsupported ones.
43
+ */
44
+ export interface NeuralwattApiModelReasoning {
45
+ /** Whether the model reasons by default. */
46
+ default_enabled: boolean;
47
+ /** Whether reasoning cannot be turned off. Forces `off: null` in the map. */
48
+ mandatory: boolean;
49
+ /** Efforts the model truly supports; drives the Pi thinking level map. */
50
+ supported_efforts: NeuralwattReasoningEffort[];
51
+ /** Efforts the API accepts but aliases onto a supported one. Not consumed. */
52
+ accepted_efforts?: NeuralwattReasoningEffort[];
53
+ /** Server-side default. Not consumed; Pi has no default-reasoning field. */
54
+ default_effort: NeuralwattReasoningEffort;
55
+ /** Wire-level aliases from accepted to supported efforts. Not consumed. */
56
+ effort_aliases?: Partial<
57
+ Record<NeuralwattReasoningEffort, NeuralwattReasoningEffort>
58
+ >;
59
+ }
60
+
21
61
  export interface NeuralwattApiModelLimits {
22
62
  max_context_length: number;
23
63
  max_output_tokens: number | null;
@@ -31,6 +71,7 @@ export interface NeuralwattApiModelMetadata {
31
71
  huggingface_id: string | null;
32
72
  pricing: NeuralwattApiModelPricing;
33
73
  capabilities: NeuralwattApiModelCapabilities;
74
+ reasoning?: NeuralwattApiModelReasoning;
34
75
  limits: NeuralwattApiModelLimits;
35
76
  deprecated: boolean;
36
77
  deprecated_message: string | null;
@@ -1,69 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // Backward-compat shim for the Pi coding-agent provider refresh context.
3
- //
4
- // Pi 0.84 replaced dynamic `context.store` read/write access with the
5
- // read-only `context.stored` snapshot and the generation-checked
6
- // `context.publish({ persist })` transaction. This module detects the
7
- // available API shape at runtime so the extension works on both <0.84 (store)
8
- // and >=0.84 (publish) hosts.
9
- //
10
- // Once the minimum supported @earendil-works/pi-coding-agent version is
11
- // >=0.84, delete this file and:
12
- // - replace `readStoredModels(context)` with `context.stored`
13
- // - replace `persistModels(context, entry)` with
14
- // `await context.publish({ persist: entry })` (skip when aborted)
15
- // ---------------------------------------------------------------------------
16
-
17
- import type {
18
- ModelsStoreEntry,
19
- RefreshModelsContext,
20
- } from "@earendil-works/pi-ai";
21
-
22
- type LegacyRefreshModelsContext = RefreshModelsContext & {
23
- store?: {
24
- read(): Promise<ModelsStoreEntry | undefined>;
25
- write(entry: ModelsStoreEntry): Promise<unknown>;
26
- };
27
- };
28
-
29
- /**
30
- * Returns the persisted catalog entry for the current provider, reading from
31
- * the 0.84+ `context.stored` snapshot when available and falling back to the
32
- * legacy `context.store.read()` on older hosts.
33
- */
34
- export async function readStoredModels(
35
- context: RefreshModelsContext,
36
- ): Promise<ModelsStoreEntry | undefined> {
37
- if (context.stored !== undefined) return context.stored;
38
- return readLegacyStore(context);
39
- }
40
-
41
- function readLegacyStore(
42
- context: RefreshModelsContext,
43
- ): Promise<ModelsStoreEntry | undefined> {
44
- const legacy = context as LegacyRefreshModelsContext;
45
- return legacy.store ? legacy.store.read() : Promise.resolve(undefined);
46
- }
47
-
48
- /**
49
- * Persists the catalog entry, publishing through
50
- * `context.publish({ persist: entry })` on 0.84+ hosts and writing through
51
- * the legacy `context.store.write(entry)` on older hosts.
52
- *
53
- * Returns true when the entry was persisted. On 0.84+ hosts a return value of
54
- * false means a newer refresh superseded this publication (generation check).
55
- */
56
- export async function persistModels(
57
- context: RefreshModelsContext,
58
- entry: ModelsStoreEntry,
59
- ): Promise<boolean> {
60
- if (typeof context.publish === "function") {
61
- return context.publish({ persist: entry });
62
- }
63
- const legacy = context as LegacyRefreshModelsContext;
64
- if (legacy.store) {
65
- await legacy.store.write(entry);
66
- return true;
67
- }
68
- return false;
69
- }