@aliou/pi-neuralwatt 0.2.0 → 0.4.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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- ![banner](https://assets.aliou.me/pi-extensions/banners/pi-neuralwatt.png)
1
+ ![banner](https://assets.aliou.me/github/aliou/pi-neuralwatt/banner.png)
2
2
 
3
3
  # Pi Neuralwatt Extension
4
4
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aliou/pi-neuralwatt",
3
- "version": "0.2.0",
3
+ "version": "0.4.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "private": false,
@@ -20,7 +20,7 @@
20
20
  "./src/extensions/quota-warnings/index.ts",
21
21
  "./src/extensions/sub-bar-integration/index.ts"
22
22
  ],
23
- "video": "https://assets.aliou.me/pi-extensions/demos/pi-neuralwatt.mp4"
23
+ "video": "https://assets.aliou.me/github/aliou/pi-neuralwatt/demo.mp4"
24
24
  },
25
25
  "publishConfig": {
26
26
  "access": "public"
@@ -31,20 +31,20 @@
31
31
  "README.md"
32
32
  ],
33
33
  "dependencies": {
34
- "@aliou/pi-utils-settings": "^0.9.0",
35
- "@aliou/pi-utils-ui": "^0.1.0"
34
+ "@aliou/pi-utils-settings": "^0.15.0",
35
+ "@aliou/pi-utils-ui": "^0.4.0"
36
36
  },
37
37
  "peerDependencies": {
38
- "@mariozechner/pi-coding-agent": ">=0.67.68",
39
- "@mariozechner/pi-tui": ">=0.67.68",
38
+ "@earendil-works/pi-coding-agent": "0.74.0",
39
+ "@earendil-works/pi-tui": "0.74.0",
40
40
  "@sinclair/typebox": ">=0.34.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@aliou/biome-plugins": "^0.8.1",
44
44
  "@biomejs/biome": "^2.4.12",
45
45
  "@changesets/cli": "^2.27.11",
46
- "@mariozechner/pi-coding-agent": "0.67.68",
47
- "@mariozechner/pi-tui": "0.67.68",
46
+ "@earendil-works/pi-coding-agent": "0.74.0",
47
+ "@earendil-works/pi-tui": "0.74.0",
48
48
  "@types/node": "^25.0.10",
49
49
  "husky": "^9.1.7",
50
50
  "ts-json-schema-generator": "^2.4.0",
@@ -52,10 +52,10 @@
52
52
  "vitest": "^4.0.18"
53
53
  },
54
54
  "peerDependenciesMeta": {
55
- "@mariozechner/pi-coding-agent": {
55
+ "@earendil-works/pi-coding-agent": {
56
56
  "optional": true
57
57
  },
58
- "@mariozechner/pi-tui": {
58
+ "@earendil-works/pi-tui": {
59
59
  "optional": true
60
60
  },
61
61
  "@sinclair/typebox": {
package/src/config.ts CHANGED
@@ -3,8 +3,8 @@ import {
3
3
  registerSettingsCommand,
4
4
  type SettingsSection,
5
5
  } from "@aliou/pi-utils-settings";
6
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
7
- import type { SettingItem } from "@mariozechner/pi-tui";
6
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
7
+ import type { SettingItem } from "@earendil-works/pi-tui";
8
8
 
9
9
  export type NeuralwattFeatureId =
10
10
  | "quotaCommand"
@@ -1,5 +1,8 @@
1
1
  import { join } from "node:path";
2
- import { type ExtensionAPI, getAgentDir } from "@mariozechner/pi-coding-agent";
2
+ import {
3
+ type ExtensionAPI,
4
+ getAgentDir,
5
+ } from "@earendil-works/pi-coding-agent";
3
6
  import { getNeuralwattApiKey } from "../../lib/env";
4
7
  import { fetchQuotas } from "../../utils/quotas";
5
8
  import { QuotasComponent } from "./components/quotas-display";
@@ -1,5 +1,5 @@
1
- import type { Theme } from "@mariozechner/pi-coding-agent";
2
- import { truncateToWidth } from "@mariozechner/pi-tui";
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import { truncateToWidth } from "@earendil-works/pi-tui";
3
3
  import type { NeuralwattQuotas } from "../../../types/quota-api";
4
4
  import {
5
5
  percentCreditsRemaining,
@@ -1,7 +1,7 @@
1
- import type { Theme } from "@mariozechner/pi-coding-agent";
2
- import { DynamicBorder } from "@mariozechner/pi-coding-agent";
3
- import type { Component, TUI } from "@mariozechner/pi-tui";
4
- import { Loader, matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import { DynamicBorder } from "@earendil-works/pi-coding-agent";
3
+ import type { Component, TUI } from "@earendil-works/pi-tui";
4
+ import { Loader, matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
5
5
  import type { NeuralwattQuotas } from "../../../types/quota-api";
6
6
  import {
7
7
  renderCreditsTab,
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import {
3
3
  configLoader,
4
4
  NEURALWATT_EXTENSIONS_REGISTER_EVENT,
@@ -0,0 +1,31 @@
1
+ interface AssistantErrorLike {
2
+ role: string;
3
+ stopReason?: string;
4
+ provider?: string;
5
+ errorMessage?: string;
6
+ }
7
+
8
+ const NEURALWATT_CONTEXT_OVERFLOW_PATTERN =
9
+ /request exceeds model'?s maximum context length/i;
10
+
11
+ /**
12
+ * Normalize Neuralwatt context overflow errors so Pi's native overflow
13
+ * compaction path can detect them and perform compact-and-retry.
14
+ */
15
+ export function normalizeNeuralwattContextOverflowError<
16
+ TMessage extends AssistantErrorLike,
17
+ >(message: TMessage, currentProvider?: string): TMessage | undefined {
18
+ if (message.role !== "assistant") return;
19
+ if (message.stopReason !== "error") return;
20
+ if (message.provider !== "neuralwatt" && currentProvider !== "neuralwatt")
21
+ return;
22
+
23
+ const errorMessage = message.errorMessage ?? "";
24
+ if (errorMessage.includes("context_length_exceeded")) return;
25
+ if (!NEURALWATT_CONTEXT_OVERFLOW_PATTERN.test(errorMessage)) return;
26
+
27
+ return {
28
+ ...message,
29
+ errorMessage: `context_length_exceeded: ${errorMessage}`,
30
+ };
31
+ }
@@ -1,4 +1,4 @@
1
- import type { AuthStorage, ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import {
3
3
  configLoader,
4
4
  emitConfigUpdated,
@@ -14,22 +14,16 @@ import {
14
14
  NEURALWATT_QUOTAS_REQUEST_EVENT,
15
15
  NEURALWATT_QUOTAS_UPDATED_EVENT,
16
16
  type NeuralwattQuotasUpdatedPayload,
17
- parseQuotaHeaders,
18
17
  } from "../../types/quota-events";
18
+ import { isOffline } from "../../utils/is-offline";
19
19
  import { fetchQuotas } from "../../utils/quotas";
20
+ import { normalizeNeuralwattContextOverflowError } from "./context-overflow";
20
21
  import type { NeuralwattModelConfig } from "./models";
21
22
  import { NEURALWATT_MODELS_CACHE } from "./models";
23
+ import { buildModelsPayload } from "./provider-payload";
24
+ import { buildQuotasFromHeaders, fetchRequestedQuotas } from "./quota-store";
22
25
 
23
- function buildModelsPayload(models: NeuralwattModelConfig[]) {
24
- return models.map(({ fast: _fast, ...model }) => ({
25
- ...model,
26
- compat: {
27
- supportsDeveloperRole: false,
28
- maxTokensField: "max_tokens",
29
- ...model.compat,
30
- },
31
- }));
32
- }
26
+ const HEADER_EMIT_THROTTLE_MS = 5_000;
33
27
 
34
28
  function registerNeuralwattProvider(
35
29
  pi: ExtensionAPI,
@@ -54,17 +48,15 @@ export default async function (pi: ExtensionAPI) {
54
48
  // Register with hardcoded cache immediately so models are available on startup
55
49
  registerNeuralwattProvider(pi, NEURALWATT_MODELS_CACHE);
56
50
 
57
- // Track which feature extensions loaded
58
51
  const loadedFeatures = new Set<NeuralwattFeatureId>();
59
52
 
60
- // Register settings (in the provider, so it's always available)
53
+ // Register settings in the provider so it is always available.
61
54
  registerNeuralwattSettings(pi, {
62
55
  getLoadedFeatures: () => loadedFeatures,
63
56
  });
64
57
 
65
- // --- Quota store (event-based) ---
66
58
  let lastHeaderEmitAt = 0;
67
- const HEADER_EMIT_THROTTLE_MS = 5_000;
59
+ let quotaRequestInFlight = false;
68
60
 
69
61
  function emitQuotas(
70
62
  quotas: NeuralwattQuotas,
@@ -77,94 +69,67 @@ export default async function (pi: ExtensionAPI) {
77
69
  pi.events.emit(NEURALWATT_QUOTAS_UPDATED_EVENT, { quotas, source });
78
70
  }
79
71
 
80
- // Ingest quotas from response headers
72
+ pi.on("message_end", (event, ctx) => {
73
+ const message = normalizeNeuralwattContextOverflowError(
74
+ event.message,
75
+ ctx.model?.provider,
76
+ );
77
+ if (!message) return;
78
+ return { message };
79
+ });
80
+
81
81
  pi.on("after_provider_response", (event, ctx) => {
82
82
  if (ctx.model?.provider !== "neuralwatt") return;
83
- const headerQuotas = parseQuotaHeaders(event.headers);
84
- if (!headerQuotas) return;
85
-
86
- const quotas: NeuralwattQuotas = {
87
- snapshot_at: new Date().toISOString(),
88
- balance: {
89
- credits_remaining_usd: headerQuotas.allowanceRemainingUsd,
90
- total_credits_usd: 0,
91
- credits_used_usd: 0,
92
- accounting_method: "token",
93
- },
94
- usage: {
95
- lifetime: { cost_usd: 0, requests: 0, tokens: 0, energy_kwh: 0 },
96
- current_month: { cost_usd: 0, requests: 0, tokens: 0, energy_kwh: 0 },
97
- },
98
- limits: { overage_limit_usd: null, rate_limit_tier: "standard" },
99
- subscription:
100
- headerQuotas.subscriptionPlan !== "none" &&
101
- headerQuotas.energyRemaining !== undefined
102
- ? {
103
- plan: headerQuotas.subscriptionPlan,
104
- status: "active",
105
- billing_interval: "month",
106
- current_period_start: "",
107
- current_period_end: "",
108
- auto_renew: false,
109
- kwh_included: headerQuotas.energyIncluded ?? 0,
110
- kwh_used: headerQuotas.energyUsed ?? 0,
111
- kwh_remaining: headerQuotas.energyRemaining,
112
- in_overage: false,
113
- }
114
- : null,
115
- key: { name: "", allowance: null },
116
- };
117
-
83
+ const quotas = buildQuotasFromHeaders(event.headers);
84
+ if (!quotas) return;
118
85
  emitQuotas(quotas, "header");
119
86
  });
120
87
 
121
- // Respond to quota requests from other extensions
122
- let quotaRequestInFlight = false;
123
88
  pi.events.on(NEURALWATT_QUOTAS_REQUEST_EVENT, async (data: unknown) => {
124
89
  if (quotaRequestInFlight) return;
125
90
  quotaRequestInFlight = true;
126
91
  try {
127
- if (!data || typeof data !== "object") return;
128
- const { authStorage } = data as { authStorage?: AuthStorage };
129
- if (!authStorage) return;
130
- const apiKey = await getNeuralwattApiKey(authStorage);
131
- if (!apiKey) return;
132
- const result = await fetchQuotas(apiKey);
133
- if (result.success) emitQuotas(result.data.quotas, "api");
92
+ const quotas = await fetchRequestedQuotas(data);
93
+ if (quotas)
94
+ pi.events.emit(NEURALWATT_QUOTAS_UPDATED_EVENT, {
95
+ quotas,
96
+ source: "api",
97
+ });
134
98
  } finally {
135
99
  quotaRequestInFlight = false;
136
100
  }
137
101
  });
138
102
 
139
- // Collect which feature extensions are loaded
140
103
  pi.events.on(NEURALWATT_EXTENSIONS_REGISTER_EVENT, (data: unknown) => {
141
104
  const { feature } = data as { feature: NeuralwattFeatureId };
142
105
  loadedFeatures.add(feature);
143
106
  });
144
107
 
145
- // On session start: fetch live models, request extensions, emit config, fetch quotas
146
108
  pi.on("session_start", async (_event, ctx) => {
147
109
  loadedFeatures.clear();
148
110
  pi.events.emit(NEURALWATT_EXTENSIONS_REQUEST_EVENT, undefined);
149
111
  emitConfigUpdated(pi);
150
112
 
151
- // Fetch live models from the API and re-register if successful
152
- const result = await fetchModels();
153
- if (result.success) {
154
- const cacheIds = new Set(NEURALWATT_MODELS_CACHE.map((m) => m.id));
155
- const liveIds = new Set(result.models.map((m) => m.id));
156
- const added = result.models.filter((m) => !cacheIds.has(m.id));
157
- const removed = NEURALWATT_MODELS_CACHE.filter((m) => !liveIds.has(m.id));
158
- if (added.length > 0 || removed.length > 0) {
159
- const parts: string[] = [];
160
- if (added.length > 0) parts.push(`${added.length} new`);
161
- if (removed.length > 0) parts.push(`${removed.length} removed`);
162
- ctx.ui.notify(
163
- `Neuralwatt models updated (${parts.join(", ")})`,
164
- "info",
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),
165
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);
166
132
  }
167
- registerNeuralwattProvider(pi, result.models);
168
133
  }
169
134
 
170
135
  if (ctx.model?.provider !== "neuralwatt") return;
@@ -207,9 +207,13 @@ describe("Neuralwatt models", () => {
207
207
  expect(result.input).toEqual(["text"]);
208
208
  expect(result.cost.input).toBe(0.03);
209
209
  expect(result.cost.output).toBe(0.16);
210
- expect(
211
- (result.compat as Record<string, unknown>)?.supportsReasoningEffort,
212
- ).toBe(true);
210
+ expect(result.thinkingLevelMap).toEqual({
211
+ minimal: "low",
212
+ low: "low",
213
+ medium: "medium",
214
+ high: "high",
215
+ xhigh: null,
216
+ });
213
217
  expect(result.fast).toBeUndefined();
214
218
  });
215
219
 
@@ -308,9 +312,13 @@ describe("Neuralwatt models", () => {
308
312
  const result = mapApiModel(visionModel);
309
313
  expect(result.input).toEqual(["text", "image"]);
310
314
  expect(result.reasoning).toBe(true);
311
- expect(
312
- (result.compat as Record<string, unknown>)?.supportsReasoningEffort,
313
- ).toBeUndefined();
315
+ expect(result.thinkingLevelMap).toEqual({
316
+ minimal: null,
317
+ low: null,
318
+ medium: "medium",
319
+ high: null,
320
+ xhigh: null,
321
+ });
314
322
  });
315
323
 
316
324
  it("should use defaults when metadata is missing", () => {
@@ -3,19 +3,19 @@
3
3
  // Pricing: https://portal.neuralwatt.com/pricing
4
4
  // max_model_len from /v1/models, pricing from /pricing page
5
5
 
6
- import type { ProviderModelConfig } from "@mariozechner/pi-coding-agent";
6
+ import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
7
7
 
8
8
  export interface NeuralwattModelConfig extends ProviderModelConfig {
9
9
  /** Fast variant of a parent model (e.g. "glm-5-fast" is the fast variant of "zai-org/GLM-5.1-FP8"). */
10
10
  fast?: boolean;
11
11
  }
12
12
 
13
- const NEURALWATT_REASONING_EFFORT_MAP = {
14
- minimal: "low",
15
- low: "low",
13
+ const NEURALWATT_BINARY_THINKING_LEVEL_MAP = {
14
+ minimal: null,
15
+ low: null,
16
16
  medium: "medium",
17
- high: "high",
18
- xhigh: "high",
17
+ high: null,
18
+ xhigh: null,
19
19
  } as const;
20
20
 
21
21
  /** Hardcoded model cache. Used as a fallback on startup before live models are fetched.
@@ -34,7 +34,7 @@ export const NEURALWATT_MODELS_CACHE: NeuralwattModelConfig[] = [
34
34
  cacheRead: 0,
35
35
  cacheWrite: 0,
36
36
  },
37
- contextWindow: 262144,
37
+ contextWindow: 262128,
38
38
  maxTokens: 32768,
39
39
  compat: {
40
40
  supportsDeveloperRole: false,
@@ -54,7 +54,7 @@ export const NEURALWATT_MODELS_CACHE: NeuralwattModelConfig[] = [
54
54
  cacheRead: 0,
55
55
  cacheWrite: 0,
56
56
  },
57
- contextWindow: 202752,
57
+ contextWindow: 202736,
58
58
  maxTokens: 32768,
59
59
  compat: {
60
60
  supportsDeveloperRole: false,
@@ -73,8 +73,9 @@ export const NEURALWATT_MODELS_CACHE: NeuralwattModelConfig[] = [
73
73
  cacheRead: 0,
74
74
  cacheWrite: 0,
75
75
  },
76
- contextWindow: 202752,
76
+ contextWindow: 202736,
77
77
  maxTokens: 32768,
78
+ thinkingLevelMap: NEURALWATT_BINARY_THINKING_LEVEL_MAP,
78
79
  compat: {
79
80
  supportsDeveloperRole: false,
80
81
  maxTokensField: "max_tokens",
@@ -93,7 +94,7 @@ export const NEURALWATT_MODELS_CACHE: NeuralwattModelConfig[] = [
93
94
  cacheRead: 0,
94
95
  cacheWrite: 0,
95
96
  },
96
- contextWindow: 202752,
97
+ contextWindow: 202736,
97
98
  maxTokens: 32768,
98
99
  compat: {
99
100
  supportsDeveloperRole: false,
@@ -112,12 +113,17 @@ export const NEURALWATT_MODELS_CACHE: NeuralwattModelConfig[] = [
112
113
  cacheRead: 0,
113
114
  cacheWrite: 0,
114
115
  },
115
- contextWindow: 16384,
116
+ contextWindow: 16368,
116
117
  maxTokens: 4096,
118
+ thinkingLevelMap: {
119
+ minimal: "low",
120
+ low: "low",
121
+ medium: "medium",
122
+ high: "high",
123
+ xhigh: null,
124
+ },
117
125
  compat: {
118
126
  supportsDeveloperRole: false,
119
- supportsReasoningEffort: true,
120
- reasoningEffortMap: NEURALWATT_REASONING_EFFORT_MAP,
121
127
  maxTokensField: "max_tokens",
122
128
  },
123
129
  },
@@ -133,8 +139,9 @@ export const NEURALWATT_MODELS_CACHE: NeuralwattModelConfig[] = [
133
139
  cacheRead: 0,
134
140
  cacheWrite: 0,
135
141
  },
136
- contextWindow: 262144,
142
+ contextWindow: 262128,
137
143
  maxTokens: 65536,
144
+ thinkingLevelMap: NEURALWATT_BINARY_THINKING_LEVEL_MAP,
138
145
  compat: {
139
146
  supportsDeveloperRole: false,
140
147
  maxTokensField: "max_tokens",
@@ -153,7 +160,7 @@ export const NEURALWATT_MODELS_CACHE: NeuralwattModelConfig[] = [
153
160
  cacheRead: 0,
154
161
  cacheWrite: 0,
155
162
  },
156
- contextWindow: 262144,
163
+ contextWindow: 262128,
157
164
  maxTokens: 65536,
158
165
  compat: {
159
166
  supportsDeveloperRole: false,
@@ -172,8 +179,9 @@ export const NEURALWATT_MODELS_CACHE: NeuralwattModelConfig[] = [
172
179
  cacheRead: 0,
173
180
  cacheWrite: 0,
174
181
  },
175
- contextWindow: 262144,
182
+ contextWindow: 262128,
176
183
  maxTokens: 65536,
184
+ thinkingLevelMap: NEURALWATT_BINARY_THINKING_LEVEL_MAP,
177
185
  compat: {
178
186
  supportsDeveloperRole: false,
179
187
  maxTokensField: "max_tokens",
@@ -192,7 +200,7 @@ export const NEURALWATT_MODELS_CACHE: NeuralwattModelConfig[] = [
192
200
  cacheRead: 0,
193
201
  cacheWrite: 0,
194
202
  },
195
- contextWindow: 262144,
203
+ contextWindow: 262128,
196
204
  maxTokens: 65536,
197
205
  compat: {
198
206
  supportsDeveloperRole: false,
@@ -211,8 +219,9 @@ export const NEURALWATT_MODELS_CACHE: NeuralwattModelConfig[] = [
211
219
  cacheRead: 0,
212
220
  cacheWrite: 0,
213
221
  },
214
- contextWindow: 196608,
222
+ contextWindow: 196592,
215
223
  maxTokens: 65536,
224
+ thinkingLevelMap: NEURALWATT_BINARY_THINKING_LEVEL_MAP,
216
225
  compat: {
217
226
  supportsDeveloperRole: false,
218
227
  maxTokensField: "max_tokens",
@@ -230,8 +239,9 @@ export const NEURALWATT_MODELS_CACHE: NeuralwattModelConfig[] = [
230
239
  cacheRead: 0,
231
240
  cacheWrite: 0,
232
241
  },
233
- contextWindow: 262144,
242
+ contextWindow: 262128,
234
243
  maxTokens: 65536,
244
+ thinkingLevelMap: NEURALWATT_BINARY_THINKING_LEVEL_MAP,
235
245
  compat: {
236
246
  supportsDeveloperRole: false,
237
247
  maxTokensField: "max_tokens",
@@ -250,7 +260,7 @@ export const NEURALWATT_MODELS_CACHE: NeuralwattModelConfig[] = [
250
260
  cacheRead: 0,
251
261
  cacheWrite: 0,
252
262
  },
253
- contextWindow: 262144,
263
+ contextWindow: 262128,
254
264
  maxTokens: 65536,
255
265
  compat: {
256
266
  supportsDeveloperRole: false,
@@ -262,15 +272,16 @@ export const NEURALWATT_MODELS_CACHE: NeuralwattModelConfig[] = [
262
272
  id: "Qwen/Qwen3.6-35B-A3B",
263
273
  name: "Qwen3.6 35B",
264
274
  reasoning: true,
265
- input: ["text"],
275
+ input: ["text", "image"],
266
276
  cost: {
267
277
  input: 0.05,
268
278
  output: 0.1,
269
279
  cacheRead: 0,
270
280
  cacheWrite: 0,
271
281
  },
272
- contextWindow: 131072,
282
+ contextWindow: 131056,
273
283
  maxTokens: 32768,
284
+ thinkingLevelMap: NEURALWATT_BINARY_THINKING_LEVEL_MAP,
274
285
  compat: {
275
286
  supportsDeveloperRole: false,
276
287
  maxTokensField: "max_tokens",
@@ -282,14 +293,14 @@ export const NEURALWATT_MODELS_CACHE: NeuralwattModelConfig[] = [
282
293
  name: "Qwen3.6 35B Fast",
283
294
  reasoning: false,
284
295
  fast: true,
285
- input: ["text"],
296
+ input: ["text", "image"],
286
297
  cost: {
287
298
  input: 0.05,
288
299
  output: 0.1,
289
300
  cacheRead: 0,
290
301
  cacheWrite: 0,
291
302
  },
292
- contextWindow: 131072,
303
+ contextWindow: 131056,
293
304
  maxTokens: 32768,
294
305
  compat: {
295
306
  supportsDeveloperRole: false,
@@ -0,0 +1,12 @@
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
+ }
@@ -0,0 +1,57 @@
1
+ import type { AuthStorage } from "@earendil-works/pi-coding-agent";
2
+ import { getNeuralwattApiKey } from "../../lib/env";
3
+ import type { NeuralwattQuotas } from "../../types/quota-api";
4
+ import { parseQuotaHeaders } from "../../types/quota-events";
5
+ import { fetchQuotas } from "../../utils/quotas";
6
+
7
+ export function buildQuotasFromHeaders(
8
+ headers: Record<string, string>,
9
+ ): NeuralwattQuotas | undefined {
10
+ const headerQuotas = parseQuotaHeaders(headers);
11
+ if (!headerQuotas) return;
12
+
13
+ return {
14
+ snapshot_at: new Date().toISOString(),
15
+ balance: {
16
+ credits_remaining_usd: headerQuotas.allowanceRemainingUsd,
17
+ total_credits_usd: 0,
18
+ credits_used_usd: 0,
19
+ accounting_method: "token",
20
+ },
21
+ usage: {
22
+ lifetime: { cost_usd: 0, requests: 0, tokens: 0, energy_kwh: 0 },
23
+ current_month: { cost_usd: 0, requests: 0, tokens: 0, energy_kwh: 0 },
24
+ },
25
+ limits: { overage_limit_usd: null, rate_limit_tier: "standard" },
26
+ subscription:
27
+ headerQuotas.subscriptionPlan !== "none" &&
28
+ headerQuotas.energyRemaining !== undefined
29
+ ? {
30
+ plan: headerQuotas.subscriptionPlan,
31
+ status: "active",
32
+ billing_interval: "month",
33
+ current_period_start: "",
34
+ current_period_end: "",
35
+ auto_renew: false,
36
+ kwh_included: headerQuotas.energyIncluded ?? 0,
37
+ kwh_used: headerQuotas.energyUsed ?? 0,
38
+ kwh_remaining: headerQuotas.energyRemaining,
39
+ in_overage: false,
40
+ }
41
+ : null,
42
+ key: { name: "", allowance: null },
43
+ };
44
+ }
45
+
46
+ export async function fetchRequestedQuotas(
47
+ data: unknown,
48
+ ): Promise<NeuralwattQuotas | undefined> {
49
+ if (!data || typeof data !== "object") return;
50
+ const { authStorage } = data as { authStorage?: AuthStorage };
51
+ if (!authStorage) return;
52
+ const apiKey = await getNeuralwattApiKey(authStorage);
53
+ if (!apiKey) return;
54
+ const result = await fetchQuotas(apiKey);
55
+ if (!result.success) return;
56
+ return result.data.quotas;
57
+ }
@@ -1,7 +1,7 @@
1
1
  import type {
2
2
  ExtensionAPI,
3
3
  ExtensionContext,
4
- } from "@mariozechner/pi-coding-agent";
4
+ } from "@earendil-works/pi-coding-agent";
5
5
  import {
6
6
  configLoader,
7
7
  NEURALWATT_CONFIG_UPDATED_EVENT,
@@ -1,4 +1,4 @@
1
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import type { NeuralwattQuotas } from "../../types/quota-api";
3
3
  import { formatKwh, formatUsd } from "../../utils/quota-format";
4
4
 
@@ -3,7 +3,7 @@ import type {
3
3
  ExtensionAPI,
4
4
  ExtensionContext,
5
5
  Theme,
6
- } from "@mariozechner/pi-coding-agent";
6
+ } from "@earendil-works/pi-coding-agent";
7
7
  import {
8
8
  configLoader,
9
9
  NEURALWATT_CONFIG_UPDATED_EVENT,
package/src/lib/env.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AuthStorage } from "@mariozechner/pi-coding-agent";
1
+ import type { AuthStorage } from "@earendil-works/pi-coding-agent";
2
2
 
3
3
  const PROVIDER_ID = "neuralwatt";
4
4
 
@@ -2,6 +2,22 @@ import type { NeuralwattModelConfig } from "../extensions/provider/models";
2
2
 
3
3
  const FETCH_TIMEOUT_MS = 15_000;
4
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
+
5
21
  export interface ApiModelMetadata {
6
22
  display_name: string;
7
23
  description: string | null;
@@ -50,14 +66,6 @@ export interface ApiResponse {
50
66
  data: ApiModel[];
51
67
  }
52
68
 
53
- const NEURALWATT_REASONING_EFFORT_MAP = {
54
- minimal: "low",
55
- low: "low",
56
- medium: "medium",
57
- high: "high",
58
- xhigh: "high",
59
- } as const;
60
-
61
69
  /** Identify fast variants by their owned_by field or naming convention. */
62
70
  function isFastModel(model: ApiModel): boolean {
63
71
  if (model.owned_by === "neuralwatt") return true;
@@ -98,13 +106,11 @@ export function mapApiModel(model: ApiModel): NeuralwattModelConfig {
98
106
  result.maxTokens = meta.limits.max_output_tokens;
99
107
  }
100
108
 
101
- // Reasoning effort support
102
- if (meta?.capabilities.reasoning_effort) {
103
- result.compat = {
104
- ...result.compat,
105
- supportsReasoningEffort: true,
106
- reasoningEffortMap: NEURALWATT_REASONING_EFFORT_MAP,
107
- };
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;
108
114
  }
109
115
 
110
116
  return result;
@@ -0,0 +1,60 @@
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
+ });
@@ -0,0 +1,4 @@
1
+ export function isOffline(): boolean {
2
+ const value = process.env.PI_OFFLINE;
3
+ return value === "1" || value === "true" || value === "yes";
4
+ }
@@ -1,4 +1,4 @@
1
- import type { Theme } from "@mariozechner/pi-coding-agent";
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
2
 
3
3
  export type Severity = "success" | "warning" | "error";
4
4