@aliou/pi-neuralwatt 0.8.0 → 0.9.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.
package/README.md CHANGED
@@ -77,14 +77,22 @@ Configure features with `/neuralwatt:settings`:
77
77
  - **Quota command** — Show/hide `/neuralwatt:quota`
78
78
  - **Quota warnings** — Enable/disable low quota notifications
79
79
  - **Sub-bar integration** — Show/hide usage in status bar
80
+ - **Legacy model IDs** — Include deprecated model aliases
81
+ - **Hidden models** — Include models available only to the configured API key
80
82
 
81
83
  The provider itself cannot be disabled — it is always loaded.
82
84
 
83
85
  Configuration uses nested per-feature sections. Existing flat config files are migrated automatically, with a backup written next to the migrated config.
84
86
 
87
+ ### Model Refresh
88
+
89
+ Neuralwatt registers its public models without network access. When hidden models are enabled, opening `/model` refreshes the authenticated catalog in the background. `pi update --models` forces an immediate refresh.
90
+
91
+ Pi stores the complete effective Neuralwatt catalog in `~/.pi/agent/models-store.json` for offline startup. Current hardcoded public and legacy definitions remain authoritative when cached models are restored.
92
+
85
93
  ## Adding or Updating Models
86
94
 
87
- Models are hardcoded in `extensions/provider/models/public-models.ts` and validated against the live API. To update:
95
+ Public models are hardcoded in `extensions/provider/models/public-models.ts` and validated against the live API. To update:
88
96
 
89
97
  1. Run `pnpm test` — it fetches `/v1/models` and compares against hardcoded definitions
90
98
  2. Fix any discrepancies (missing models, changed context windows)
@@ -129,7 +137,7 @@ This repository uses [Changesets](https://github.com/changesets/changesets) for
129
137
 
130
138
  ## Requirements
131
139
 
132
- - Pi coding agent v0.67.68+
140
+ - Pi coding agent v0.80.8+
133
141
  - Neuralwatt API key (configured in `~/.pi/agent/auth.json` or via `NEURALWATT_API_KEY`)
134
142
 
135
143
  ## Links
@@ -1,4 +1,4 @@
1
- import type { AuthStorage } from "@earendil-works/pi-coding-agent";
1
+ import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
2
2
 
3
3
  const PROVIDER_ID = "neuralwatt";
4
4
 
@@ -11,8 +11,7 @@ const PROVIDER_ID = "neuralwatt";
11
11
  * 3. Environment variable NEURALWATT_API_KEY
12
12
  */
13
13
  export async function getNeuralwattApiKey(
14
- authStorage: AuthStorage,
14
+ modelRegistry: ModelRegistry,
15
15
  ): Promise<string | undefined> {
16
- const key = await authStorage.getApiKey(PROVIDER_ID);
17
- return key ?? process.env.NEURALWATT_API_KEY;
16
+ return modelRegistry.getApiKeyForProvider(PROVIDER_ID);
18
17
  }
@@ -16,7 +16,7 @@ export function registerQuotasCommand(pi: ExtensionAPI): void {
16
16
  pi.registerCommand("neuralwatt:quota", {
17
17
  description: "Display Neuralwatt API usage and quota",
18
18
  handler: async (_args, ctx) => {
19
- const apiKey = await getNeuralwattApiKey(ctx.modelRegistry.authStorage);
19
+ const apiKey = await getNeuralwattApiKey(ctx.modelRegistry);
20
20
  if (!apiKey) {
21
21
  ctx.ui.notify(missingAuthMessage(), "warning");
22
22
  return;
@@ -1,7 +1,7 @@
1
1
  import { getApiProvider } from "@earendil-works/pi-ai/compat";
2
2
  import type {
3
3
  ExtensionAPI,
4
- ProviderModelConfig,
4
+ ModelRegistry,
5
5
  } from "@earendil-works/pi-coding-agent";
6
6
  import { configLoader } from "../../src/config";
7
7
  import {
@@ -18,12 +18,7 @@ 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 {
22
- getNeuralwattModels,
23
- loadCachedHiddenModels,
24
- loadHiddenModels,
25
- writeHiddenModelsCache,
26
- } from "./models";
21
+ import { getNeuralwattModels, refreshNeuralwattModels } from "./models";
27
22
  import { buildQuotasFromHeaders, fetchRequestedQuotas } from "./quota-store";
28
23
  import {
29
24
  type NeuralwattRateLimitInfo,
@@ -44,18 +39,15 @@ function emitConfigUpdated(pi: ExtensionAPI): void {
44
39
  function registerNeuralwattProvider(
45
40
  pi: ExtensionAPI,
46
41
  onSseQuota: (line: string) => void,
47
- hiddenModels: ProviderModelConfig[] = [],
48
42
  ): void {
49
43
  const { provider: providerConfig } = configLoader.getConfig();
50
44
 
51
- const publicModels = getNeuralwattModels({
45
+ const models = getNeuralwattModels({
52
46
  includeLegacyModelIds: providerConfig.includeLegacyModelIds,
53
47
  });
54
- const resolvedHiddenModels = providerConfig.includeHiddenModels
55
- ? dedupeHiddenModels(hiddenModels, publicModels)
56
- : [];
57
48
 
58
49
  const config: Parameters<ExtensionAPI["registerProvider"]>[1] = {
50
+ name: "Neuralwatt",
59
51
  baseUrl: "https://api.neuralwatt.com/v1",
60
52
  apiKey: "$NEURALWATT_API_KEY",
61
53
  api: "openai-completions",
@@ -64,7 +56,14 @@ function registerNeuralwattProvider(
64
56
  Referer: "https://pi.dev",
65
57
  "X-Title": "npm:@aliou/pi-neuralwatt",
66
58
  },
67
- models: [...publicModels, ...resolvedHiddenModels],
59
+ models,
60
+ refreshModels: (context) =>
61
+ refreshNeuralwattModels(context, {
62
+ includeLegacyModelIds:
63
+ configLoader.getConfig().provider.includeLegacyModelIds,
64
+ includeHiddenModels:
65
+ configLoader.getConfig().provider.includeHiddenModels,
66
+ }),
68
67
  };
69
68
 
70
69
  const provider = getApiProvider("openai-completions");
@@ -79,57 +78,28 @@ function registerNeuralwattProvider(
79
78
  pi.registerProvider("neuralwatt", config);
80
79
  }
81
80
 
82
- /**
83
- * Drop any hidden model whose ID collides with a public or legacy model.
84
- *
85
- * Models can graduate from hidden (authenticated /v1/models only) to public
86
- * (unauthenticated list). When that happens, a stale on-disk cache may still
87
- * list the now-public ID, which would register it twice and make Pi treat the
88
- * scoped model as ambiguous ("No models match pattern"). Dedupe against the
89
- * public list so a stale cache can never shadow a public model.
90
- */
91
- function dedupeHiddenModels(
92
- hiddenModels: ProviderModelConfig[],
93
- publicModels: ProviderModelConfig[],
94
- ): ProviderModelConfig[] {
95
- const publicIds = new Set(publicModels.map((m) => m.id));
96
- return hiddenModels.filter((m) => !publicIds.has(m.id));
97
- }
98
-
99
81
  export default async function (pi: ExtensionAPI) {
100
82
  await configLoader.load();
101
83
 
102
84
  let latestQuotas: NeuralwattQuotas | undefined;
103
85
 
104
- // Stale-while-revalidate seed for hidden models.
105
- //
106
- // Hidden models are only discoverable by hitting the authenticated
107
- // `/v1/models` endpoint, which we can do inside `session_start` (Pi does not
108
- // expose `authStorage` to extension factories). However, Pi validates scoped
109
- // models (e.g. `neuralwatt/glm-5.2-short`) during startup, *before*
110
- // `session_start` fires. To avoid "No models match pattern" warnings on saved
111
- // scoped models, we synchronously restore the previous session's fetch from
112
- // the on-disk cache so the provider is registered with hidden models at
113
- // load time. `session_start` then revalidates from the live API and writes
114
- // the cache back. First run with no cache still warns once.
115
- let hiddenModels: ProviderModelConfig[] = [];
116
- if (configLoader.getConfig().provider.includeHiddenModels) {
117
- hiddenModels = loadCachedHiddenModels();
118
- }
119
- let hiddenModelsLoaded = false;
120
- let hiddenModelsAbort: AbortController | undefined;
86
+ let lastSseEmitAt = 0;
121
87
 
122
88
  const handleSseQuota = (line: string) => {
89
+ const now = Date.now();
90
+ if (now - lastSseEmitAt < HEADER_EMIT_THROTTLE_MS) return;
91
+
123
92
  const quotas = updateQuotasFromSseComment(latestQuotas, line);
124
93
  if (!quotas || quotas === latestQuotas) return;
125
- latestQuotas = quotas;
126
- pi.events.emit(NEURALWATT_QUOTAS_UPDATED_EVENT, {
127
- quotas,
128
- source: "sse",
129
- });
94
+
95
+ lastSseEmitAt = now;
96
+ emitQuotas(quotas, "sse");
130
97
  };
131
98
 
132
- registerNeuralwattProvider(pi, handleSseQuota, hiddenModels);
99
+ registerNeuralwattProvider(pi, handleSseQuota);
100
+ let registeredProviderSettings = {
101
+ ...configLoader.getConfig().provider,
102
+ };
133
103
 
134
104
  const loadedFeatures = new Set<NeuralwattFeatureId>();
135
105
 
@@ -139,22 +109,17 @@ export default async function (pi: ExtensionAPI) {
139
109
  });
140
110
 
141
111
  pi.events.on(NEURALWATT_CONFIG_UPDATED_EVENT, () => {
142
- // Toggle may have enabled hidden models since startup. Seed from the disk
143
- // cache so previously discovered models are available immediately without
144
- // waiting for the next session_start revalidation.
112
+ const next = configLoader.getConfig().provider;
145
113
  if (
146
- configLoader.getConfig().provider.includeHiddenModels &&
147
- !hiddenModelsLoaded &&
148
- hiddenModels.length === 0
114
+ next.includeLegacyModelIds ===
115
+ registeredProviderSettings.includeLegacyModelIds &&
116
+ next.includeHiddenModels ===
117
+ registeredProviderSettings.includeHiddenModels
149
118
  ) {
150
- hiddenModels = loadCachedHiddenModels();
119
+ return;
151
120
  }
152
- registerNeuralwattProvider(pi, handleSseQuota, hiddenModels);
153
- });
154
-
155
- pi.on("session_shutdown", () => {
156
- hiddenModelsAbort?.abort();
157
- hiddenModelsAbort = undefined;
121
+ registeredProviderSettings = { ...next };
122
+ registerNeuralwattProvider(pi, handleSseQuota);
158
123
  });
159
124
 
160
125
  let lastHeaderEmitAt = 0;
@@ -176,6 +141,7 @@ export default async function (pi: ExtensionAPI) {
176
141
  // Used in message_end to rewrite the generic error text with
177
142
  // actionable details from Neuralwatt's response headers.
178
143
  let pendingRateLimitInfo: NeuralwattRateLimitInfo | undefined;
144
+ let currentModelRegistry: ModelRegistry | undefined;
179
145
 
180
146
  pi.on("message_end", (event, ctx) => {
181
147
  // Rewrite rate-limit errors with layer-specific details
@@ -243,11 +209,14 @@ export default async function (pi: ExtensionAPI) {
243
209
  emitQuotas(quotas, "header");
244
210
  });
245
211
 
246
- pi.events.on(NEURALWATT_QUOTAS_REQUEST_EVENT, async (data: unknown) => {
212
+ pi.events.on(NEURALWATT_QUOTAS_REQUEST_EVENT, async () => {
247
213
  if (quotaRequestInFlight) return;
248
214
  quotaRequestInFlight = true;
249
215
  try {
250
- const quotas = await fetchRequestedQuotas(data);
216
+ const apiKey = currentModelRegistry
217
+ ? await getNeuralwattApiKey(currentModelRegistry)
218
+ : undefined;
219
+ const quotas = await fetchRequestedQuotas(apiKey);
251
220
  if (quotas) emitQuotas(quotas, "api");
252
221
  } finally {
253
222
  quotaRequestInFlight = false;
@@ -260,6 +229,7 @@ export default async function (pi: ExtensionAPI) {
260
229
  });
261
230
 
262
231
  pi.on("session_start", async (_event, ctx) => {
232
+ currentModelRegistry = ctx.modelRegistry;
263
233
  pendingRateLimitInfo = undefined;
264
234
  const messages = [...new Set(configLoader.drainMessages())];
265
235
  if (messages.length > 0) {
@@ -270,32 +240,14 @@ export default async function (pi: ExtensionAPI) {
270
240
  pi.events.emit(NEURALWATT_EXTENSIONS_REQUEST_EVENT, undefined);
271
241
  emitConfigUpdated(pi);
272
242
 
273
- if (
274
- !hiddenModelsLoaded &&
275
- configLoader.getConfig().provider.includeHiddenModels
276
- ) {
277
- hiddenModelsLoaded = true;
278
- hiddenModelsAbort?.abort();
279
- hiddenModelsAbort = new AbortController();
280
- const fetched = await loadHiddenModels(
281
- ctx.modelRegistry.authStorage,
282
- hiddenModelsAbort.signal,
283
- );
284
- // Persist for the next startup so scoped models resolve without
285
- // warnings on Pi's subsequent launches. Always write the cache (even
286
- // when empty) and re-register, so graduated or removed hidden models
287
- // are purged from both the cache and the provider's model list.
288
- hiddenModels = fetched;
289
- await writeHiddenModelsCache(hiddenModels);
290
- if (!hiddenModelsAbort.signal.aborted) {
291
- registerNeuralwattProvider(pi, handleSseQuota, hiddenModels);
292
- }
293
- }
294
-
295
243
  if (ctx.model?.provider !== "neuralwatt") return;
296
- const apiKey = await getNeuralwattApiKey(ctx.modelRegistry.authStorage);
244
+ const apiKey = await getNeuralwattApiKey(ctx.modelRegistry);
297
245
  if (!apiKey) return;
298
246
  const quotaResult = await fetchQuotas(apiKey);
299
247
  if (quotaResult.success) emitQuotas(quotaResult.data.quotas, "api");
300
248
  });
249
+
250
+ pi.on("session_shutdown", () => {
251
+ currentModelRegistry = undefined;
252
+ });
301
253
  }
@@ -1,10 +1,6 @@
1
- import type {
2
- AuthStorage,
3
- ProviderModelConfig,
4
- } from "@earendil-works/pi-coding-agent";
1
+ import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
5
2
  import { fetchNeuralwattModels } from "../../../src/lib/neuralwatt-api";
6
3
  import type { NeuralwattApiModel } from "../../../src/types/models-api";
7
- import { getNeuralwattApiKey } from "../../_shared/auth";
8
4
  import { NEURALWATT_MODELS } from "./public-models";
9
5
 
10
6
  // Per-ID overrides for known hidden models. The authenticated /v1/models endpoint
@@ -95,17 +91,17 @@ function applyHiddenOverride(
95
91
  *
96
92
  * Hidden models are any models returned by the API that are not already part of
97
93
  * the public hardcoded list. If the API key is missing or the request fails, an
98
- * empty array is returned silently.
94
+ * `undefined` distinguishes an unavailable/failed request from a successful
95
+ * empty hidden-model list, allowing refresh callers to preserve stale cache.
99
96
  */
100
97
  export async function loadHiddenModels(
101
- authStorage: AuthStorage,
98
+ apiKey: string,
102
99
  signal?: AbortSignal,
103
- ): Promise<ProviderModelConfig[]> {
104
- const apiKey = await getNeuralwattApiKey(authStorage);
105
- if (!apiKey) return [];
100
+ ): Promise<ProviderModelConfig[] | undefined> {
101
+ if (!apiKey) return undefined;
106
102
 
107
103
  const result = await fetchNeuralwattModels(apiKey, signal);
108
- if (!result.success) return [];
104
+ if (!result.success) return undefined;
109
105
 
110
106
  const publicIds = new Set(NEURALWATT_MODELS.map((model) => model.id));
111
107
 
@@ -2,7 +2,6 @@ import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
2
2
  import { buildLegacyNeuralwattModels } from "./legacy";
3
3
  import { NEURALWATT_MODELS } from "./public-models";
4
4
 
5
- export { loadCachedHiddenModels, writeHiddenModelsCache } from "./cache";
6
5
  export { loadHiddenModels } from "./hidden";
7
6
  export {
8
7
  buildLegacyNeuralwattModels,
@@ -10,6 +9,7 @@ export {
10
9
  LEGACY_NEURALWATT_MODEL_IDS,
11
10
  } from "./legacy";
12
11
  export { NEURALWATT_MODELS } from "./public-models";
12
+ export { refreshNeuralwattModels } from "./refresh";
13
13
 
14
14
  export function getNeuralwattModels(options?: {
15
15
  includeLegacyModelIds?: boolean;
@@ -0,0 +1,130 @@
1
+ import type {
2
+ Api,
3
+ Model,
4
+ ModelsStoreEntry,
5
+ RefreshModelsContext,
6
+ } from "@earendil-works/pi-ai";
7
+ import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
8
+ import { loadHiddenModels } from "./hidden";
9
+ import { buildLegacyNeuralwattModels } from "./legacy";
10
+ import { NEURALWATT_MODELS } from "./public-models";
11
+
12
+ const PROVIDER_ID = "neuralwatt";
13
+ const BASE_URL = "https://api.neuralwatt.com/v1";
14
+ const API = "openai-completions" as const;
15
+
16
+ export interface RefreshNeuralwattModelsOptions {
17
+ includeLegacyModelIds: boolean;
18
+ includeHiddenModels: boolean;
19
+ loadHidden?: typeof loadHiddenModels;
20
+ }
21
+
22
+ function configuredModels(
23
+ includeLegacyModelIds: boolean,
24
+ ): ProviderModelConfig[] {
25
+ return includeLegacyModelIds
26
+ ? [...NEURALWATT_MODELS, ...buildLegacyNeuralwattModels()]
27
+ : [...NEURALWATT_MODELS];
28
+ }
29
+
30
+ function toStoredModel(model: ProviderModelConfig): Model<Api> {
31
+ return {
32
+ ...model,
33
+ provider: PROVIDER_ID,
34
+ api: model.api ?? API,
35
+ baseUrl: model.baseUrl ?? BASE_URL,
36
+ };
37
+ }
38
+
39
+ function toProviderModel(model: Model<Api>): ProviderModelConfig {
40
+ return {
41
+ id: model.id,
42
+ name: model.name,
43
+ api: model.api,
44
+ baseUrl: model.baseUrl,
45
+ reasoning: model.reasoning,
46
+ thinkingLevelMap: model.thinkingLevelMap,
47
+ input: [...model.input],
48
+ cost: structuredClone(model.cost),
49
+ contextWindow: model.contextWindow,
50
+ maxTokens: model.maxTokens,
51
+ headers: model.headers
52
+ ? Object.fromEntries(
53
+ Object.entries(model.headers).filter(
54
+ (entry): entry is [string, string] => entry[1] !== null,
55
+ ),
56
+ )
57
+ : undefined,
58
+ compat: model.compat ? structuredClone(model.compat) : undefined,
59
+ };
60
+ }
61
+
62
+ function dedupeHiddenModels(
63
+ hiddenModels: ProviderModelConfig[],
64
+ baselineModels: ProviderModelConfig[],
65
+ ): ProviderModelConfig[] {
66
+ const baselineIds = new Set(baselineModels.map((model) => model.id));
67
+ return hiddenModels.filter((model) => !baselineIds.has(model.id));
68
+ }
69
+
70
+ function cachedHiddenModels(
71
+ stored: ModelsStoreEntry | undefined,
72
+ ): ProviderModelConfig[] {
73
+ if (!stored) return [];
74
+
75
+ const allStaticIds = new Set(configuredModels(true).map((model) => model.id));
76
+
77
+ return stored.models
78
+ .filter(
79
+ (model) => model.provider === PROVIDER_ID && !allStaticIds.has(model.id),
80
+ )
81
+ .map(toProviderModel);
82
+ }
83
+
84
+ async function persistModels(
85
+ context: RefreshModelsContext,
86
+ models: ProviderModelConfig[],
87
+ ): Promise<void> {
88
+ await context.store.write({
89
+ models: models.map(toStoredModel),
90
+ checkedAt: Date.now(),
91
+ });
92
+ }
93
+
94
+ /** Refresh the complete Neuralwatt catalog with Pi-managed persistence. */
95
+ export async function refreshNeuralwattModels(
96
+ context: RefreshModelsContext,
97
+ options: RefreshNeuralwattModelsOptions,
98
+ ): Promise<ProviderModelConfig[]> {
99
+ const baseline = configuredModels(options.includeLegacyModelIds);
100
+ const stored = await context.store.read();
101
+
102
+ if (!options.includeHiddenModels) {
103
+ await persistModels(context, baseline);
104
+ return baseline;
105
+ }
106
+
107
+ const cachedHidden = dedupeHiddenModels(cachedHiddenModels(stored), baseline);
108
+ const cachedCatalog = [...baseline, ...cachedHidden];
109
+
110
+ if (!context.allowNetwork || context.signal?.aborted) {
111
+ return cachedCatalog;
112
+ }
113
+
114
+ const apiKey =
115
+ context.credential?.type === "api_key" ? context.credential.key : undefined;
116
+ if (!apiKey) return cachedCatalog;
117
+
118
+ const hidden = await (options.loadHidden ?? loadHiddenModels)(
119
+ apiKey,
120
+ context.signal,
121
+ );
122
+ if (context.signal?.aborted) return cachedCatalog;
123
+ if (!hidden) {
124
+ throw new Error("Neuralwatt model catalog refresh failed");
125
+ }
126
+
127
+ const catalog = [...baseline, ...dedupeHiddenModels(hidden, baseline)];
128
+ await persistModels(context, catalog);
129
+ return catalog;
130
+ }
@@ -1,8 +1,6 @@
1
- import type { AuthStorage } from "@earendil-works/pi-coding-agent";
2
1
  import { parseQuotaHeaders } from "../../src/events";
3
2
  import { fetchQuotas } from "../../src/lib/neuralwatt-api";
4
3
  import type { NeuralwattQuotas } from "../../src/types/quota-api";
5
- import { getNeuralwattApiKey } from "../_shared/auth";
6
4
 
7
5
  export function buildQuotasFromHeaders(
8
6
  headers: Record<string, string>,
@@ -44,12 +42,8 @@ export function buildQuotasFromHeaders(
44
42
  }
45
43
 
46
44
  export async function fetchRequestedQuotas(
47
- data: unknown,
45
+ apiKey: string | undefined,
48
46
  ): 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
47
  if (!apiKey) return;
54
48
  const result = await fetchQuotas(apiKey);
55
49
  if (!result.success) return;
@@ -17,7 +17,6 @@ export default async function (pi: ExtensionAPI) {
17
17
  await configLoader.load();
18
18
 
19
19
  let enabled = configLoader.getConfig().quotaWarnings.enabled;
20
- let currentProvider: string | undefined;
21
20
  let currentContext: ExtensionContext | undefined;
22
21
 
23
22
  // Listen for config changes at runtime
@@ -33,36 +32,29 @@ export default async function (pi: ExtensionAPI) {
33
32
  pi.events.on(NEURALWATT_QUOTAS_UPDATED_EVENT, (data: unknown) => {
34
33
  if (!enabled) return;
35
34
  if (!data || typeof data !== "object") return;
36
- if (currentProvider !== "neuralwatt" || !currentContext) return;
37
- const { quotas, source } = data as NeuralwattQuotasUpdatedPayload;
38
- checkQuotas(currentContext, quotas, source === "header");
35
+ if (!currentContext) return;
36
+ if (currentContext.model?.provider !== "neuralwatt") return;
37
+
38
+ const { quotas } = data as NeuralwattQuotasUpdatedPayload;
39
+ checkQuotas(currentContext, quotas);
39
40
  });
40
41
 
41
42
  pi.on("session_start", async (_event, ctx) => {
42
43
  currentContext = ctx;
43
- currentProvider = ctx.model?.provider;
44
44
  if (ctx.model?.provider !== "neuralwatt") return;
45
45
  clearAlertState();
46
46
  });
47
47
 
48
48
  pi.on("model_select", (_event, ctx) => {
49
49
  currentContext = ctx;
50
- currentProvider = ctx.model?.provider;
51
- if (ctx.model?.provider !== "neuralwatt") {
52
- clearAlertState();
53
- return;
54
- }
55
- clearAlertState();
56
50
  });
57
51
 
58
52
  pi.on("session_before_switch", (_event, ctx) => {
59
53
  currentContext = ctx;
60
- currentProvider = ctx.model?.provider;
61
54
  });
62
55
 
63
56
  pi.on("session_shutdown", () => {
64
57
  currentContext = undefined;
65
- currentProvider = undefined;
66
58
  clearAlertState();
67
59
  });
68
60
 
@@ -11,6 +11,8 @@ interface AlertState {
11
11
  lastNotifiedAt: number;
12
12
  }
13
13
 
14
+ // Module-level state so cooldowns survive across invocations of checkQuotas()
15
+ // within the same Pi runtime.
14
16
  const alerts = new Map<string, AlertState>();
15
17
 
16
18
  export function clearAlertState(): void {
@@ -22,9 +24,9 @@ function shouldNotify(key: string, severity: WarningSeverity): boolean {
22
24
  if (!state) return true;
23
25
 
24
26
  const order: WarningSeverity[] = ["warning", "critical"];
25
- if (order.indexOf(severity) > order.indexOf(state.lastSeverity)) return true;
26
-
27
- if (severity === "critical") return true;
27
+ const currentIndex = order.indexOf(severity);
28
+ const lastIndex = order.indexOf(state.lastSeverity);
29
+ if (currentIndex > lastIndex) return true;
28
30
 
29
31
  return Date.now() - state.lastNotifiedAt >= COOLDOWN_MS;
30
32
  }
@@ -40,7 +42,6 @@ function markNotified(key: string, severity: WarningSeverity): void {
40
42
  export function checkQuotas(
41
43
  ctx: ExtensionContext,
42
44
  quotas: NeuralwattQuotas,
43
- skipAlreadyWarned: boolean,
44
45
  ): void {
45
46
  if (!ctx.hasUI) return;
46
47
 
@@ -55,7 +56,7 @@ export function checkQuotas(
55
56
  if (pct <= 25) {
56
57
  const severity: WarningSeverity = pct <= 10 ? "critical" : "warning";
57
58
  const key = "credits";
58
- if (!skipAlreadyWarned || shouldNotify(key, severity)) {
59
+ if (shouldNotify(key, severity)) {
59
60
  markNotified(key, severity);
60
61
  warnings.push(
61
62
  `Credits: ${pct.toFixed(0)}% remaining (${formatUsd(credits_remaining_usd)} of ${formatUsd(total_credits_usd)})`,
@@ -77,7 +78,7 @@ export function checkQuotas(
77
78
  ? "critical"
78
79
  : "warning";
79
80
  const key = "energy";
80
- if (!skipAlreadyWarned || shouldNotify(key, severity)) {
81
+ if (shouldNotify(key, severity)) {
81
82
  markNotified(key, severity);
82
83
  const tag = in_overage ? " [OVERAGE]" : "";
83
84
  warnings.push(
@@ -1,5 +1,4 @@
1
1
  import type {
2
- AuthStorage,
3
2
  ExtensionAPI,
4
3
  ExtensionContext,
5
4
  Theme,
@@ -58,7 +57,6 @@ export default async function (pi: ExtensionAPI) {
58
57
  let enabled = configLoader.getConfig().subBarIntegration.enabled;
59
58
  let subCoreReady = false;
60
59
  let currentProvider: string | undefined;
61
- let currentAuthStorage: AuthStorage | undefined;
62
60
  let currentContext: ExtensionContext | undefined;
63
61
 
64
62
  // Listen for config changes at runtime
@@ -81,10 +79,7 @@ export default async function (pi: ExtensionAPI) {
81
79
  }
82
80
 
83
81
  function requestQuotas(): void {
84
- if (!currentAuthStorage) return;
85
- pi.events.emit(NEURALWATT_QUOTAS_REQUEST_EVENT, {
86
- authStorage: currentAuthStorage,
87
- });
82
+ pi.events.emit(NEURALWATT_QUOTAS_REQUEST_EVENT, undefined);
88
83
  }
89
84
 
90
85
  pi.events.on(NEURALWATT_QUOTAS_UPDATED_EVENT, (data: unknown) => {
@@ -93,7 +88,7 @@ export default async function (pi: ExtensionAPI) {
93
88
  const { quotas } = data as NeuralwattQuotasUpdatedPayload;
94
89
  emitUsage(quotas);
95
90
 
96
- if (currentContext?.hasUI) {
91
+ if (currentContext) {
97
92
  currentContext.ui.setStatus(
98
93
  "neuralwatt-usage",
99
94
  formatStatus(quotas, currentContext.ui.theme),
@@ -107,13 +102,11 @@ export default async function (pi: ExtensionAPI) {
107
102
 
108
103
  pi.on("session_start", async (_event, ctx) => {
109
104
  currentProvider = ctx.model?.provider;
110
- currentAuthStorage = ctx.modelRegistry.authStorage;
111
105
  currentContext = ctx;
112
106
  });
113
107
 
114
108
  pi.on("model_select", async (_event, ctx) => {
115
109
  currentProvider = ctx.model?.provider;
116
- currentAuthStorage = ctx.modelRegistry.authStorage;
117
110
  currentContext = ctx;
118
111
 
119
112
  if (subCoreReady && isActive() && enabled) {
@@ -121,15 +114,8 @@ export default async function (pi: ExtensionAPI) {
121
114
  }
122
115
  });
123
116
 
124
- pi.on("session_before_switch", (_event, ctx) => {
125
- currentProvider = ctx.model?.provider;
126
- currentAuthStorage = ctx.modelRegistry.authStorage;
127
- currentContext = ctx;
128
- });
129
-
130
117
  pi.on("session_shutdown", () => {
131
118
  currentProvider = undefined;
132
- currentAuthStorage = undefined;
133
119
  currentContext = undefined;
134
120
  });
135
121
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aliou/pi-neuralwatt",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "private": false,
@@ -38,17 +38,17 @@
38
38
  "@aliou/pi-utils-ui": "^0.5.0"
39
39
  },
40
40
  "peerDependencies": {
41
- "@earendil-works/pi-ai": "*",
42
- "@earendil-works/pi-coding-agent": "*",
41
+ "@earendil-works/pi-ai": ">=0.80.8",
42
+ "@earendil-works/pi-coding-agent": ">=0.80.8",
43
43
  "@earendil-works/pi-tui": "*"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@aliou/biome-plugins": "^0.11.0",
47
47
  "@biomejs/biome": "^2.4.15",
48
48
  "@changesets/cli": "^2.27.11",
49
- "@earendil-works/pi-ai": "0.80.3",
50
- "@earendil-works/pi-coding-agent": "0.80.3",
51
- "@earendil-works/pi-tui": "0.80.3",
49
+ "@earendil-works/pi-ai": "0.80.8",
50
+ "@earendil-works/pi-coding-agent": "0.80.8",
51
+ "@earendil-works/pi-tui": "0.80.8",
52
52
  "@types/node": "^25.0.10",
53
53
  "husky": "^9.1.7",
54
54
  "ts-json-schema-generator": "^2.4.0",
@@ -1,77 +0,0 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { mkdir, writeFile } from "node:fs/promises";
3
- import { dirname, join } from "node:path";
4
- import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
5
- import { getAgentDir } from "@earendil-works/pi-coding-agent";
6
-
7
- /**
8
- * Stale-while-revalidate disk cache for hidden Neuralwatt models.
9
- *
10
- * Hidden models are discovered by hitting the authenticated `/v1/models`
11
- * endpoint, which only happens inside `session_start` (Pi does not expose
12
- * `authStorage` to the extension factory). Because Pi validates scoped models
13
- * during startup — before `session_start` fires — we persist the last fetch to
14
- * disk so the provider can be registered with cached models instantly on the
15
- * next launch. The first run with no cache still warns once; subsequent runs
16
- * resolve cleanly.
17
- *
18
- * File shape: `{ version: 1, models: ProviderModelConfig[] }`.
19
- */
20
-
21
- const CACHE_VERSION = 1;
22
- const CACHE_FILENAME = "neuralwatt-hidden-models.json";
23
-
24
- function cachePath(): string {
25
- return join(getAgentDir(), "cache", CACHE_FILENAME);
26
- }
27
-
28
- interface HiddenModelsCacheFile {
29
- version?: unknown;
30
- models?: unknown;
31
- }
32
-
33
- /**
34
- * Read cached hidden models synchronously.
35
- *
36
- * Designed to be called from the provider extension factory body, where Pi
37
- * has not entered the event loop yet. Returns an empty array if the cache is
38
- * missing, unreadable, or malformed.
39
- */
40
- export function loadCachedHiddenModels(): ProviderModelConfig[] {
41
- try {
42
- const path = cachePath();
43
- if (!existsSync(path)) return [];
44
-
45
- const parsed: HiddenModelsCacheFile = JSON.parse(
46
- readFileSync(path, "utf8"),
47
- );
48
- if (!Array.isArray(parsed?.models)) return [];
49
-
50
- return parsed.models as ProviderModelConfig[];
51
- } catch {
52
- return [];
53
- }
54
- }
55
-
56
- /**
57
- * Persist hidden models to disk for the next startup.
58
- *
59
- * Called after a successful `/v1/models` fetch in `session_start`. Failures are
60
- * swallowed since a missing cache only degrades to first-run behavior.
61
- */
62
- export async function writeHiddenModelsCache(
63
- models: ProviderModelConfig[],
64
- ): Promise<void> {
65
- try {
66
- const path = cachePath();
67
- await mkdir(dirname(path), { recursive: true });
68
- await writeFile(
69
- path,
70
- `${JSON.stringify({ version: CACHE_VERSION, models }, null, 2)}\n`,
71
- "utf8",
72
- );
73
- } catch {
74
- // Cache writes are best-effort. A missing cache only falls back to the
75
- // first-run path (next session revalidates and writes again).
76
- }
77
- }