@aliou/pi-neuralwatt 0.4.2 → 0.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aliou/pi-neuralwatt",
3
- "version": "0.4.2",
3
+ "version": "0.5.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "private": false,
@@ -28,23 +28,23 @@
28
28
  "files": [
29
29
  "src",
30
30
  "schema.json",
31
- "README.md"
31
+ "README.md",
32
+ "!src/**/*.test.ts"
32
33
  ],
33
34
  "dependencies": {
34
35
  "@aliou/pi-utils-settings": "^0.15.0",
35
36
  "@aliou/pi-utils-ui": "^0.4.0"
36
37
  },
37
38
  "peerDependencies": {
38
- "@earendil-works/pi-coding-agent": "0.74.0",
39
- "@earendil-works/pi-tui": "0.74.0",
40
- "@sinclair/typebox": ">=0.34.0"
39
+ "@earendil-works/pi-coding-agent": "*",
40
+ "@earendil-works/pi-tui": "*"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@aliou/biome-plugins": "^0.8.1",
44
- "@biomejs/biome": "^2.4.12",
44
+ "@biomejs/biome": "^2.4.15",
45
45
  "@changesets/cli": "^2.27.11",
46
- "@earendil-works/pi-coding-agent": "0.74.0",
47
- "@earendil-works/pi-tui": "0.74.0",
46
+ "@earendil-works/pi-coding-agent": "0.77.0",
47
+ "@earendil-works/pi-tui": "0.77.0",
48
48
  "@types/node": "^25.0.10",
49
49
  "husky": "^9.1.7",
50
50
  "ts-json-schema-generator": "^2.4.0",
@@ -57,9 +57,6 @@
57
57
  },
58
58
  "@earendil-works/pi-tui": {
59
59
  "optional": true
60
- },
61
- "@sinclair/typebox": {
62
- "optional": true
63
60
  }
64
61
  },
65
62
  "scripts": {
@@ -8,45 +8,37 @@ import {
8
8
  registerNeuralwattSettings,
9
9
  } from "../../config";
10
10
  import { getNeuralwattApiKey } from "../../lib/env";
11
- import { fetchModels } from "../../lib/fetch-models";
12
11
  import type { NeuralwattQuotas } from "../../types/quota-api";
13
12
  import {
14
13
  NEURALWATT_QUOTAS_REQUEST_EVENT,
15
14
  NEURALWATT_QUOTAS_UPDATED_EVENT,
16
15
  type NeuralwattQuotasUpdatedPayload,
17
16
  } from "../../types/quota-events";
18
- import { isOffline } from "../../utils/is-offline";
19
17
  import { fetchQuotas } from "../../utils/quotas";
20
18
  import { normalizeNeuralwattContextOverflowError } from "./context-overflow";
21
- import type { NeuralwattModelConfig } from "./models";
22
- import { NEURALWATT_MODELS_CACHE } from "./models";
23
- import { buildModelsPayload } from "./provider-payload";
19
+ import { NEURALWATT_MODELS } from "./models";
24
20
  import { buildQuotasFromHeaders, fetchRequestedQuotas } from "./quota-store";
25
21
 
26
22
  const HEADER_EMIT_THROTTLE_MS = 5_000;
27
23
 
28
- function registerNeuralwattProvider(
29
- pi: ExtensionAPI,
30
- models: NeuralwattModelConfig[],
31
- ): void {
24
+ function registerNeuralwattProvider(pi: ExtensionAPI): void {
32
25
  pi.registerProvider("neuralwatt", {
33
26
  baseUrl: "https://api.neuralwatt.com/v1",
34
- apiKey: "NEURALWATT_API_KEY",
27
+ apiKey: "$NEURALWATT_API_KEY",
35
28
  api: "openai-completions",
36
29
  authHeader: true,
37
30
  headers: {
38
31
  Referer: "https://pi.dev",
39
32
  "X-Title": "npm:@aliou/pi-neuralwatt",
40
33
  },
41
- models: buildModelsPayload(models),
34
+ models: NEURALWATT_MODELS,
42
35
  });
43
36
  }
44
37
 
45
38
  export default async function (pi: ExtensionAPI) {
46
39
  await configLoader.load();
47
40
 
48
- // Register with hardcoded cache immediately so models are available on startup
49
- registerNeuralwattProvider(pi, NEURALWATT_MODELS_CACHE);
41
+ registerNeuralwattProvider(pi);
50
42
 
51
43
  const loadedFeatures = new Set<NeuralwattFeatureId>();
52
44
 
@@ -110,28 +102,6 @@ export default async function (pi: ExtensionAPI) {
110
102
  pi.events.emit(NEURALWATT_EXTENSIONS_REQUEST_EVENT, undefined);
111
103
  emitConfigUpdated(pi);
112
104
 
113
- if (!isOffline()) {
114
- const result = await fetchModels();
115
- if (result.success) {
116
- const cacheIds = new Set(NEURALWATT_MODELS_CACHE.map((m) => m.id));
117
- const liveIds = new Set(result.models.map((m) => m.id));
118
- const added = result.models.filter((m) => !cacheIds.has(m.id));
119
- const removed = NEURALWATT_MODELS_CACHE.filter(
120
- (m) => !liveIds.has(m.id),
121
- );
122
- if (added.length > 0 || removed.length > 0) {
123
- const parts: string[] = [];
124
- if (added.length > 0) parts.push(`${added.length} new`);
125
- if (removed.length > 0) parts.push(`${removed.length} removed`);
126
- ctx.ui.notify(
127
- `Neuralwatt models updated (${parts.join(", ")})`,
128
- "info",
129
- );
130
- }
131
- registerNeuralwattProvider(pi, result.models);
132
- }
133
- }
134
-
135
105
  if (ctx.model?.provider !== "neuralwatt") return;
136
106
  const apiKey = await getNeuralwattApiKey(ctx.modelRegistry.authStorage);
137
107
  if (!apiKey) return;
@@ -1,27 +1,10 @@
1
1
  // Hardcoded models from Neuralwatt API
2
2
  // Source: https://api.neuralwatt.com/v1/models
3
- // Pricing: https://portal.neuralwatt.com/pricing
4
- // max_model_len from /v1/models, pricing from /pricing page
3
+ // Pricing, capabilities, and limits from metadata fields in /v1/models
5
4
 
6
5
  import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
7
6
 
8
- export interface NeuralwattModelConfig extends ProviderModelConfig {
9
- /** Fast variant of a parent model (e.g. "glm-5-fast" is the fast variant of "zai-org/GLM-5.1-FP8"). */
10
- fast?: boolean;
11
- }
12
-
13
- const NEURALWATT_BINARY_THINKING_LEVEL_MAP = {
14
- minimal: null,
15
- low: null,
16
- medium: "medium",
17
- high: null,
18
- xhigh: null,
19
- } as const;
20
-
21
- /** Hardcoded model cache. Used as a fallback on startup before live models are fetched.
22
- * Updated from https://api.neuralwatt.com/v1/models and https://portal.neuralwatt.com/pricing
23
- */
24
- export const NEURALWATT_MODELS_CACHE: NeuralwattModelConfig[] = [
7
+ export const NEURALWATT_MODELS: ProviderModelConfig[] = [
25
8
  // Devstral Small 2 - Mistral
26
9
  {
27
10
  id: "mistralai/Devstral-Small-2-24B-Instruct-2512",
@@ -46,7 +29,6 @@ export const NEURALWATT_MODELS_CACHE: NeuralwattModelConfig[] = [
46
29
  id: "glm-5-fast",
47
30
  name: "GLM-5 Fast",
48
31
  reasoning: false,
49
- fast: true,
50
32
  input: ["text"],
51
33
  cost: {
52
34
  input: 1.1,
@@ -75,10 +57,17 @@ export const NEURALWATT_MODELS_CACHE: NeuralwattModelConfig[] = [
75
57
  },
76
58
  contextWindow: 202736,
77
59
  maxTokens: 32768,
78
- thinkingLevelMap: NEURALWATT_BINARY_THINKING_LEVEL_MAP,
60
+ thinkingLevelMap: {
61
+ minimal: null,
62
+ low: null,
63
+ medium: "medium",
64
+ high: null,
65
+ xhigh: null,
66
+ },
79
67
  compat: {
80
68
  supportsDeveloperRole: false,
81
69
  maxTokensField: "max_tokens",
70
+ requiresReasoningContentOnAssistantMessages: true,
82
71
  },
83
72
  },
84
73
  // GLM-5.1 Fast - ZhipuAI
@@ -86,7 +75,6 @@ export const NEURALWATT_MODELS_CACHE: NeuralwattModelConfig[] = [
86
75
  id: "glm-5.1-fast",
87
76
  name: "GLM-5.1 Fast",
88
77
  reasoning: false,
89
- fast: true,
90
78
  input: ["text"],
91
79
  cost: {
92
80
  input: 1.1,
@@ -125,6 +113,7 @@ export const NEURALWATT_MODELS_CACHE: NeuralwattModelConfig[] = [
125
113
  compat: {
126
114
  supportsDeveloperRole: false,
127
115
  maxTokensField: "max_tokens",
116
+ requiresReasoningContentOnAssistantMessages: true,
128
117
  },
129
118
  },
130
119
  // Kimi K2.5 - MoonshotAI
@@ -141,10 +130,17 @@ export const NEURALWATT_MODELS_CACHE: NeuralwattModelConfig[] = [
141
130
  },
142
131
  contextWindow: 262128,
143
132
  maxTokens: 65536,
144
- thinkingLevelMap: NEURALWATT_BINARY_THINKING_LEVEL_MAP,
133
+ thinkingLevelMap: {
134
+ minimal: null,
135
+ low: null,
136
+ medium: "medium",
137
+ high: null,
138
+ xhigh: null,
139
+ },
145
140
  compat: {
146
141
  supportsDeveloperRole: false,
147
142
  maxTokensField: "max_tokens",
143
+ requiresReasoningContentOnAssistantMessages: true,
148
144
  },
149
145
  },
150
146
  // Kimi K2.5 Fast - MoonshotAI
@@ -152,7 +148,6 @@ export const NEURALWATT_MODELS_CACHE: NeuralwattModelConfig[] = [
152
148
  id: "kimi-k2.5-fast",
153
149
  name: "Kimi K2.5 Fast",
154
150
  reasoning: false,
155
- fast: true,
156
151
  input: ["text", "image"],
157
152
  cost: {
158
153
  input: 0.52,
@@ -181,10 +176,17 @@ export const NEURALWATT_MODELS_CACHE: NeuralwattModelConfig[] = [
181
176
  },
182
177
  contextWindow: 262128,
183
178
  maxTokens: 65536,
184
- thinkingLevelMap: NEURALWATT_BINARY_THINKING_LEVEL_MAP,
179
+ thinkingLevelMap: {
180
+ minimal: null,
181
+ low: null,
182
+ medium: "medium",
183
+ high: null,
184
+ xhigh: null,
185
+ },
185
186
  compat: {
186
187
  supportsDeveloperRole: false,
187
188
  maxTokensField: "max_tokens",
189
+ requiresReasoningContentOnAssistantMessages: true,
188
190
  },
189
191
  },
190
192
  // Kimi K2.6 Fast - MoonshotAI
@@ -192,7 +194,6 @@ export const NEURALWATT_MODELS_CACHE: NeuralwattModelConfig[] = [
192
194
  id: "kimi-k2.6-fast",
193
195
  name: "Kimi K2.6 Fast",
194
196
  reasoning: false,
195
- fast: true,
196
197
  input: ["text", "image"],
197
198
  cost: {
198
199
  input: 0.69,
@@ -221,10 +222,17 @@ export const NEURALWATT_MODELS_CACHE: NeuralwattModelConfig[] = [
221
222
  },
222
223
  contextWindow: 196592,
223
224
  maxTokens: 65536,
224
- thinkingLevelMap: NEURALWATT_BINARY_THINKING_LEVEL_MAP,
225
+ thinkingLevelMap: {
226
+ minimal: null,
227
+ low: null,
228
+ medium: "medium",
229
+ high: null,
230
+ xhigh: null,
231
+ },
225
232
  compat: {
226
233
  supportsDeveloperRole: false,
227
234
  maxTokensField: "max_tokens",
235
+ requiresReasoningContentOnAssistantMessages: true,
228
236
  },
229
237
  },
230
238
  // Qwen3.5 397B - Qwen
@@ -241,10 +249,17 @@ export const NEURALWATT_MODELS_CACHE: NeuralwattModelConfig[] = [
241
249
  },
242
250
  contextWindow: 262128,
243
251
  maxTokens: 65536,
244
- thinkingLevelMap: NEURALWATT_BINARY_THINKING_LEVEL_MAP,
252
+ thinkingLevelMap: {
253
+ minimal: null,
254
+ low: null,
255
+ medium: "medium",
256
+ high: null,
257
+ xhigh: null,
258
+ },
245
259
  compat: {
246
260
  supportsDeveloperRole: false,
247
261
  maxTokensField: "max_tokens",
262
+ requiresReasoningContentOnAssistantMessages: true,
248
263
  },
249
264
  },
250
265
  // Qwen3.5 397B Fast - Qwen
@@ -252,7 +267,6 @@ export const NEURALWATT_MODELS_CACHE: NeuralwattModelConfig[] = [
252
267
  id: "qwen3.5-397b-fast",
253
268
  name: "Qwen3.5 397B Fast",
254
269
  reasoning: false,
255
- fast: true,
256
270
  input: ["text"],
257
271
  cost: {
258
272
  input: 0.69,
@@ -281,18 +295,24 @@ export const NEURALWATT_MODELS_CACHE: NeuralwattModelConfig[] = [
281
295
  },
282
296
  contextWindow: 131056,
283
297
  maxTokens: 32768,
284
- thinkingLevelMap: NEURALWATT_BINARY_THINKING_LEVEL_MAP,
298
+ thinkingLevelMap: {
299
+ minimal: null,
300
+ low: null,
301
+ medium: "medium",
302
+ high: null,
303
+ xhigh: null,
304
+ },
285
305
  compat: {
286
306
  supportsDeveloperRole: false,
287
307
  maxTokensField: "max_tokens",
308
+ requiresReasoningContentOnAssistantMessages: true,
288
309
  },
289
310
  },
290
- // Qwen3.6 35B Fast (qwen3.6-35b-fast) - Qwen
311
+ // Qwen3.6 35B Fast - Qwen
291
312
  {
292
313
  id: "qwen3.6-35b-fast",
293
314
  name: "Qwen3.6 35B Fast",
294
315
  reasoning: false,
295
- fast: true,
296
316
  input: ["text", "image"],
297
317
  cost: {
298
318
  input: 0.29,
@@ -1,347 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import type {
3
- ApiModel as FullApiModel,
4
- ApiResponse as FullApiResponse,
5
- } from "../../lib/fetch-models";
6
- import { mapApiModel } from "../../lib/fetch-models";
7
- import { NEURALWATT_MODELS_CACHE } from "./models";
8
-
9
- interface Discrepancy {
10
- model: string;
11
- field: string;
12
- hardcoded: unknown;
13
- api: unknown;
14
- }
15
-
16
- async function fetchApiModels(): Promise<FullApiModel[]> {
17
- const apiKey = process.env.NEURALWATT_API_KEY;
18
- const headers: Record<string, string> = {
19
- "Content-Type": "application/json",
20
- Referer: "https://github.com/aliou/pi-neuralwatt",
21
- };
22
- if (apiKey) {
23
- headers.Authorization = `Bearer ${apiKey}`;
24
- }
25
-
26
- const response = await fetch("https://api.neuralwatt.com/v1/models", {
27
- headers,
28
- });
29
-
30
- if (!response.ok) {
31
- throw new Error(
32
- `API request failed: ${response.status} ${response.statusText}`,
33
- );
34
- }
35
-
36
- const data: FullApiResponse = await response.json();
37
- return data.data;
38
- }
39
-
40
- function compareModels(
41
- apiModels: FullApiModel[],
42
- hardcodedModels: typeof NEURALWATT_MODELS_CACHE,
43
- ): Discrepancy[] {
44
- const discrepancies: Discrepancy[] = [];
45
-
46
- for (const hardcoded of hardcodedModels) {
47
- const apiModel = apiModels.find((m) => m.id === hardcoded.id);
48
-
49
- if (!apiModel) {
50
- discrepancies.push({
51
- model: hardcoded.id,
52
- field: "exists",
53
- hardcoded: true,
54
- api: false,
55
- });
56
- continue;
57
- }
58
-
59
- // Check context window
60
- if (apiModel.max_model_len !== hardcoded.contextWindow) {
61
- discrepancies.push({
62
- model: hardcoded.id,
63
- field: "contextWindow",
64
- hardcoded: hardcoded.contextWindow,
65
- api: apiModel.max_model_len,
66
- });
67
- }
68
-
69
- // Check metadata-driven fields if available
70
- const meta = apiModel.metadata;
71
- if (meta) {
72
- // Check reasoning
73
- if (meta.capabilities.reasoning !== hardcoded.reasoning) {
74
- discrepancies.push({
75
- model: hardcoded.id,
76
- field: "reasoning",
77
- hardcoded: hardcoded.reasoning,
78
- api: meta.capabilities.reasoning,
79
- });
80
- }
81
-
82
- // Check pricing
83
- if (meta.pricing.input_per_million !== hardcoded.cost.input) {
84
- discrepancies.push({
85
- model: hardcoded.id,
86
- field: "cost.input",
87
- hardcoded: hardcoded.cost.input,
88
- api: meta.pricing.input_per_million,
89
- });
90
- }
91
- if (meta.pricing.output_per_million !== hardcoded.cost.output) {
92
- discrepancies.push({
93
- model: hardcoded.id,
94
- field: "cost.output",
95
- hardcoded: hardcoded.cost.output,
96
- api: meta.pricing.output_per_million,
97
- });
98
- }
99
-
100
- // Check vision
101
- const hasVision = hardcoded.input.includes("image");
102
- if (meta.capabilities.vision !== hasVision) {
103
- discrepancies.push({
104
- model: hardcoded.id,
105
- field: "input (vision)",
106
- hardcoded: hasVision,
107
- api: meta.capabilities.vision,
108
- });
109
- }
110
- }
111
- }
112
-
113
- // Check for API models not in hardcoded list
114
- for (const apiModel of apiModels) {
115
- if (apiModel.metadata?.deprecated || apiModel.metadata?.pricing.pricing_tbd)
116
- continue;
117
- const hardcoded = hardcodedModels.find((m) => m.id === apiModel.id);
118
- if (!hardcoded) {
119
- discrepancies.push({
120
- model: apiModel.id,
121
- field: "exists",
122
- hardcoded: false,
123
- api: true,
124
- });
125
- }
126
- }
127
-
128
- return discrepancies;
129
- }
130
-
131
- describe("Neuralwatt models", () => {
132
- it("should match API model definitions", { timeout: 30000 }, async () => {
133
- const apiModels = await fetchApiModels();
134
- const discrepancies = compareModels(apiModels, NEURALWATT_MODELS_CACHE);
135
-
136
- if (discrepancies.length > 0) {
137
- console.error("\nModel discrepancies found:");
138
- console.error("==========================");
139
- for (const d of discrepancies) {
140
- if (d.field === "exists") {
141
- if (d.hardcoded) {
142
- console.error(` ${d.model}: Missing from API`);
143
- } else {
144
- console.error(` ${d.model}: Missing from hardcoded models (NEW)`);
145
- }
146
- } else {
147
- console.error(` ${d.model}.${d.field}:`);
148
- console.error(` hardcoded: ${JSON.stringify(d.hardcoded)}`);
149
- console.error(` api: ${JSON.stringify(d.api)}`);
150
- }
151
- }
152
- console.error("==========================\n");
153
- }
154
-
155
- expect(discrepancies).toHaveLength(0);
156
- });
157
-
158
- it("should map API models with metadata correctly", () => {
159
- // Simulate a reasoning model with reasoning_effort support (like gpt-oss-20b)
160
- const apiModelWithEffort: FullApiModel = {
161
- id: "openai/gpt-oss-20b",
162
- object: "model",
163
- created: 1777467968,
164
- owned_by: "vllm",
165
- root: "openai/gpt-oss-20b",
166
- parent: null,
167
- max_model_len: 16384,
168
- metadata: {
169
- display_name: "GPT-OSS 20B",
170
- description: "OpenAI GPT-OSS 20B",
171
- provider: "OpenAI",
172
- huggingface_id: null,
173
- pricing: {
174
- input_per_million: 0.03,
175
- output_per_million: 0.16,
176
- cached_input_per_million: null,
177
- cached_output_per_million: null,
178
- currency: "USD",
179
- pricing_tbd: false,
180
- },
181
- capabilities: {
182
- tools: true,
183
- json_mode: true,
184
- vision: false,
185
- reasoning: true,
186
- reasoning_effort: true,
187
- streaming: true,
188
- system_role: true,
189
- developer_role: false,
190
- },
191
- limits: {
192
- max_context_length: 16384,
193
- max_output_tokens: 4096,
194
- max_images: null,
195
- },
196
- deprecated: false,
197
- deprecated_message: null,
198
- },
199
- };
200
-
201
- const result = mapApiModel(apiModelWithEffort);
202
- expect(result.id).toBe("openai/gpt-oss-20b");
203
- expect(result.name).toBe("GPT-OSS 20B");
204
- expect(result.reasoning).toBe(true);
205
- expect(result.contextWindow).toBe(16384);
206
- expect(result.maxTokens).toBe(4096);
207
- expect(result.input).toEqual(["text"]);
208
- expect(result.cost.input).toBe(0.03);
209
- expect(result.cost.output).toBe(0.16);
210
- expect(result.thinkingLevelMap).toEqual({
211
- minimal: "low",
212
- low: "low",
213
- medium: "medium",
214
- high: "high",
215
- xhigh: null,
216
- });
217
- expect(result.fast).toBeUndefined();
218
- });
219
-
220
- it("should map fast variants correctly", () => {
221
- // Simulate a fast variant (owned by "neuralwatt")
222
- const fastModel: FullApiModel = {
223
- id: "qwen3.6-35b-fast",
224
- object: "model",
225
- created: 0,
226
- owned_by: "neuralwatt",
227
- max_model_len: 131072,
228
- metadata: {
229
- display_name: "Qwen3.6 35B Fast",
230
- description: "Fast variant",
231
- provider: "Qwen",
232
- huggingface_id: null,
233
- pricing: {
234
- input_per_million: 0.05,
235
- output_per_million: 0.1,
236
- cached_input_per_million: null,
237
- cached_output_per_million: null,
238
- currency: "USD",
239
- pricing_tbd: false,
240
- },
241
- capabilities: {
242
- tools: true,
243
- json_mode: true,
244
- vision: false,
245
- reasoning: false,
246
- reasoning_effort: false,
247
- streaming: true,
248
- system_role: true,
249
- developer_role: false,
250
- },
251
- limits: {
252
- max_context_length: 131072,
253
- max_output_tokens: null,
254
- max_images: null,
255
- },
256
- deprecated: false,
257
- deprecated_message: null,
258
- },
259
- };
260
-
261
- const result = mapApiModel(fastModel);
262
- expect(result.id).toBe("qwen3.6-35b-fast");
263
- expect(result.fast).toBe(true);
264
- expect(result.reasoning).toBe(false);
265
- expect(
266
- (result.compat as Record<string, unknown>)?.supportsReasoningEffort,
267
- ).toBeUndefined();
268
- });
269
-
270
- it("should map vision models correctly", () => {
271
- const visionModel: FullApiModel = {
272
- id: "moonshotai/Kimi-K2.6",
273
- object: "model",
274
- created: 1777467965,
275
- owned_by: "vllm",
276
- root: "moonshotai/Kimi-K2.6",
277
- parent: null,
278
- max_model_len: 262144,
279
- metadata: {
280
- display_name: "Kimi K2.6",
281
- description: "Moonshot Kimi K2.6",
282
- provider: "MoonshotAI",
283
- huggingface_id: null,
284
- pricing: {
285
- input_per_million: 0.69,
286
- output_per_million: 3.22,
287
- cached_input_per_million: null,
288
- cached_output_per_million: null,
289
- currency: "USD",
290
- pricing_tbd: false,
291
- },
292
- capabilities: {
293
- tools: true,
294
- json_mode: true,
295
- vision: true,
296
- reasoning: true,
297
- reasoning_effort: false,
298
- streaming: true,
299
- system_role: true,
300
- developer_role: false,
301
- },
302
- limits: {
303
- max_context_length: 262144,
304
- max_output_tokens: null,
305
- max_images: 20,
306
- },
307
- deprecated: false,
308
- deprecated_message: null,
309
- },
310
- };
311
-
312
- const result = mapApiModel(visionModel);
313
- expect(result.input).toEqual(["text", "image"]);
314
- expect(result.reasoning).toBe(true);
315
- expect(result.thinkingLevelMap).toEqual({
316
- minimal: null,
317
- low: null,
318
- medium: "medium",
319
- high: null,
320
- xhigh: null,
321
- });
322
- });
323
-
324
- it("should use defaults when metadata is missing", () => {
325
- const bareModel: FullApiModel = {
326
- id: "test/model",
327
- object: "model",
328
- created: 0,
329
- owned_by: "vllm",
330
- max_model_len: 8192,
331
- };
332
-
333
- const result = mapApiModel(bareModel);
334
- expect(result.id).toBe("test/model");
335
- expect(result.name).toBe("test/model");
336
- expect(result.reasoning).toBe(false);
337
- expect(result.contextWindow).toBe(8192);
338
- expect(result.maxTokens).toBe(65536);
339
- expect(result.input).toEqual(["text"]);
340
- expect(result.cost.input).toBe(0);
341
- expect(result.cost.output).toBe(0);
342
- expect(result.fast).toBeUndefined();
343
- expect(
344
- (result.compat as Record<string, unknown>)?.supportsReasoningEffort,
345
- ).toBeUndefined();
346
- });
347
- });
@@ -1,12 +0,0 @@
1
- import type { NeuralwattModelConfig } from "./models";
2
-
3
- export function buildModelsPayload(models: NeuralwattModelConfig[]) {
4
- return models.map(({ fast: _fast, ...model }) => ({
5
- ...model,
6
- compat: {
7
- supportsDeveloperRole: false,
8
- maxTokensField: "max_tokens" as const,
9
- ...model.compat,
10
- },
11
- }));
12
- }
@@ -1,187 +0,0 @@
1
- import type { NeuralwattModelConfig } from "../extensions/provider/models";
2
-
3
- const FETCH_TIMEOUT_MS = 15_000;
4
-
5
- const NEURALWATT_BINARY_THINKING_LEVEL_MAP = {
6
- minimal: null,
7
- low: null,
8
- medium: "medium",
9
- high: null,
10
- xhigh: null,
11
- } as const;
12
-
13
- const GPT_OSS_THINKING_LEVEL_MAP = {
14
- minimal: "low",
15
- low: "low",
16
- medium: "medium",
17
- high: "high",
18
- xhigh: null,
19
- } as const;
20
-
21
- export interface ApiModelMetadata {
22
- display_name: string;
23
- description: string | null;
24
- provider: string;
25
- huggingface_id: string | null;
26
- pricing: {
27
- input_per_million: number;
28
- output_per_million: number;
29
- cached_input_per_million: number | null;
30
- cached_output_per_million: number | null;
31
- currency: string;
32
- pricing_tbd: boolean;
33
- };
34
- capabilities: {
35
- tools: boolean;
36
- json_mode: boolean;
37
- vision: boolean;
38
- reasoning: boolean;
39
- reasoning_effort: boolean;
40
- streaming: boolean;
41
- system_role: boolean;
42
- developer_role: boolean;
43
- };
44
- limits: {
45
- max_context_length: number;
46
- max_output_tokens: number | null;
47
- max_images: number | null;
48
- };
49
- deprecated: boolean;
50
- deprecated_message: string | null;
51
- }
52
-
53
- export interface ApiModel {
54
- id: string;
55
- object: string;
56
- created: number;
57
- owned_by: string;
58
- root?: string;
59
- parent?: string | null;
60
- max_model_len: number;
61
- metadata?: ApiModelMetadata;
62
- }
63
-
64
- export interface ApiResponse {
65
- object: "list";
66
- data: ApiModel[];
67
- }
68
-
69
- /** Identify fast variants by their owned_by field or naming convention. */
70
- function isFastModel(model: ApiModel): boolean {
71
- if (model.owned_by === "neuralwatt") return true;
72
- return model.id.endsWith("-fast");
73
- }
74
-
75
- /** Map API model data to NeuralwattModelConfig. */
76
- export function mapApiModel(model: ApiModel): NeuralwattModelConfig {
77
- const meta = model.metadata;
78
- const fast = isFastModel(model);
79
-
80
- // Base fields from top-level API data
81
- const result: NeuralwattModelConfig = {
82
- id: model.id,
83
- name: meta?.display_name ?? model.id,
84
- reasoning: meta?.capabilities.reasoning ?? false,
85
- contextWindow: model.max_model_len,
86
- maxTokens: 65536, // sensible default
87
- cost: {
88
- input: meta?.pricing.input_per_million ?? 0,
89
- output: meta?.pricing.output_per_million ?? 0,
90
- cacheRead: meta?.pricing.cached_input_per_million ?? 0,
91
- cacheWrite: meta?.pricing.cached_output_per_million ?? 0,
92
- },
93
- input: meta?.capabilities.vision ? ["text", "image"] : ["text"],
94
- compat: {
95
- supportsDeveloperRole: false,
96
- maxTokensField: "max_tokens",
97
- },
98
- };
99
-
100
- if (fast) {
101
- result.fast = true;
102
- }
103
-
104
- // Override maxTokens from limits if available
105
- if (meta?.limits.max_output_tokens) {
106
- result.maxTokens = meta.limits.max_output_tokens;
107
- }
108
-
109
- if (result.reasoning) {
110
- result.thinkingLevelMap =
111
- model.id === "openai/gpt-oss-20b"
112
- ? GPT_OSS_THINKING_LEVEL_MAP
113
- : NEURALWATT_BINARY_THINKING_LEVEL_MAP;
114
- }
115
-
116
- return result;
117
- }
118
-
119
- export type FetchModelsResult =
120
- | { success: true; models: NeuralwattModelConfig[] }
121
- | {
122
- success: false;
123
- error: { message: string; kind: "timeout" | "network" | "cancelled" };
124
- };
125
-
126
- /**
127
- * Fetch live model definitions from the Neuralwatt /v1/models endpoint.
128
- *
129
- * When the API returns metadata (pricing, capabilities, limits), those values
130
- * are used directly. Fields not exposed by the API fall back to sensible
131
- * defaults.
132
- */
133
- export async function fetchModels(
134
- signal?: AbortSignal,
135
- ): Promise<FetchModelsResult> {
136
- const signals: AbortSignal[] = [AbortSignal.timeout(FETCH_TIMEOUT_MS)];
137
- if (signal) signals.push(signal);
138
- const combined = AbortSignal.any(signals);
139
-
140
- try {
141
- const response = await fetch("https://api.neuralwatt.com/v1/models", {
142
- headers: {
143
- Referer: "https://pi.dev",
144
- "X-Title": "npm:@aliou/pi-neuralwatt",
145
- },
146
- signal: combined,
147
- });
148
-
149
- if (!response.ok) {
150
- return {
151
- success: false,
152
- error: {
153
- message: `Failed to fetch models: ${response.status} ${response.statusText}`,
154
- kind: "network",
155
- },
156
- };
157
- }
158
-
159
- const data: ApiResponse = await response.json();
160
-
161
- // Filter out deprecated models
162
- const active = data.data.filter(
163
- (m) => !m.metadata?.deprecated && !m.metadata?.pricing.pricing_tbd,
164
- );
165
-
166
- const models = active.map(mapApiModel);
167
- return { success: true, models };
168
- } catch (err: unknown) {
169
- if (err instanceof DOMException && err.name === "AbortError") {
170
- if (
171
- combined.reason instanceof DOMException &&
172
- combined.reason.name === "TimeoutError"
173
- ) {
174
- return {
175
- success: false,
176
- error: { message: "Fetch models timed out", kind: "timeout" },
177
- };
178
- }
179
- return {
180
- success: false,
181
- error: { message: "Fetch models cancelled", kind: "cancelled" },
182
- };
183
- }
184
- const message = err instanceof Error ? err.message : "Unknown error";
185
- return { success: false, error: { message, kind: "network" } };
186
- }
187
- }
@@ -1,60 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { isOffline } from "./is-offline";
3
-
4
- describe("isOffline", () => {
5
- it("returns true when PI_OFFLINE is 1", () => {
6
- const original = process.env.PI_OFFLINE;
7
- process.env.PI_OFFLINE = "1";
8
- expect(isOffline()).toBe(true);
9
- process.env.PI_OFFLINE = original;
10
- });
11
-
12
- it("returns true when PI_OFFLINE is true", () => {
13
- const original = process.env.PI_OFFLINE;
14
- process.env.PI_OFFLINE = "true";
15
- expect(isOffline()).toBe(true);
16
- process.env.PI_OFFLINE = original;
17
- });
18
-
19
- it("returns true when PI_OFFLINE is yes", () => {
20
- const original = process.env.PI_OFFLINE;
21
- process.env.PI_OFFLINE = "yes";
22
- expect(isOffline()).toBe(true);
23
- process.env.PI_OFFLINE = original;
24
- });
25
-
26
- it("returns false when PI_OFFLINE is unset", () => {
27
- const original = process.env.PI_OFFLINE;
28
- delete process.env.PI_OFFLINE;
29
- expect(isOffline()).toBe(false);
30
- process.env.PI_OFFLINE = original;
31
- });
32
-
33
- it("returns false when PI_OFFLINE is 0", () => {
34
- const original = process.env.PI_OFFLINE;
35
- process.env.PI_OFFLINE = "0";
36
- expect(isOffline()).toBe(false);
37
- process.env.PI_OFFLINE = original;
38
- });
39
-
40
- it("returns false when PI_OFFLINE is false", () => {
41
- const original = process.env.PI_OFFLINE;
42
- process.env.PI_OFFLINE = "false";
43
- expect(isOffline()).toBe(false);
44
- process.env.PI_OFFLINE = original;
45
- });
46
-
47
- it("returns false when PI_OFFLINE is no", () => {
48
- const original = process.env.PI_OFFLINE;
49
- process.env.PI_OFFLINE = "no";
50
- expect(isOffline()).toBe(false);
51
- process.env.PI_OFFLINE = original;
52
- });
53
-
54
- it("returns false for other values", () => {
55
- const original = process.env.PI_OFFLINE;
56
- process.env.PI_OFFLINE = "maybe";
57
- expect(isOffline()).toBe(false);
58
- process.env.PI_OFFLINE = original;
59
- });
60
- });
@@ -1,4 +0,0 @@
1
- export function isOffline(): boolean {
2
- const value = process.env.PI_OFFLINE;
3
- return value === "1" || value === "true" || value === "yes";
4
- }