@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 +8 -11
- package/src/extensions/provider/index.ts +5 -35
- package/src/extensions/provider/models.ts +52 -32
- package/src/extensions/provider/models.test.ts +0 -347
- package/src/extensions/provider/provider-payload.ts +0 -12
- package/src/lib/fetch-models.ts +0 -187
- package/src/utils/is-offline.test.ts +0 -60
- package/src/utils/is-offline.ts +0 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aliou/pi-neuralwatt",
|
|
3
|
-
"version": "0.
|
|
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": "
|
|
39
|
-
"@earendil-works/pi-tui": "
|
|
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.
|
|
44
|
+
"@biomejs/biome": "^2.4.15",
|
|
45
45
|
"@changesets/cli": "^2.27.11",
|
|
46
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
47
|
-
"@earendil-works/pi-tui": "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
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
}
|
package/src/lib/fetch-models.ts
DELETED
|
@@ -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
|
-
});
|
package/src/utils/is-offline.ts
DELETED