@aliou/pi-neuralwatt 0.8.1 → 0.10.2

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,46 +78,11 @@ 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;
121
-
122
86
  let lastSseEmitAt = 0;
123
87
 
124
88
  const handleSseQuota = (line: string) => {
@@ -132,7 +96,10 @@ export default async function (pi: ExtensionAPI) {
132
96
  emitQuotas(quotas, "sse");
133
97
  };
134
98
 
135
- registerNeuralwattProvider(pi, handleSseQuota, hiddenModels);
99
+ registerNeuralwattProvider(pi, handleSseQuota);
100
+ let registeredProviderSettings = {
101
+ ...configLoader.getConfig().provider,
102
+ };
136
103
 
137
104
  const loadedFeatures = new Set<NeuralwattFeatureId>();
138
105
 
@@ -142,22 +109,17 @@ export default async function (pi: ExtensionAPI) {
142
109
  });
143
110
 
144
111
  pi.events.on(NEURALWATT_CONFIG_UPDATED_EVENT, () => {
145
- // Toggle may have enabled hidden models since startup. Seed from the disk
146
- // cache so previously discovered models are available immediately without
147
- // waiting for the next session_start revalidation.
112
+ const next = configLoader.getConfig().provider;
148
113
  if (
149
- configLoader.getConfig().provider.includeHiddenModels &&
150
- !hiddenModelsLoaded &&
151
- hiddenModels.length === 0
114
+ next.includeLegacyModelIds ===
115
+ registeredProviderSettings.includeLegacyModelIds &&
116
+ next.includeHiddenModels ===
117
+ registeredProviderSettings.includeHiddenModels
152
118
  ) {
153
- hiddenModels = loadCachedHiddenModels();
119
+ return;
154
120
  }
155
- registerNeuralwattProvider(pi, handleSseQuota, hiddenModels);
156
- });
157
-
158
- pi.on("session_shutdown", () => {
159
- hiddenModelsAbort?.abort();
160
- hiddenModelsAbort = undefined;
121
+ registeredProviderSettings = { ...next };
122
+ registerNeuralwattProvider(pi, handleSseQuota);
161
123
  });
162
124
 
163
125
  let lastHeaderEmitAt = 0;
@@ -179,6 +141,7 @@ export default async function (pi: ExtensionAPI) {
179
141
  // Used in message_end to rewrite the generic error text with
180
142
  // actionable details from Neuralwatt's response headers.
181
143
  let pendingRateLimitInfo: NeuralwattRateLimitInfo | undefined;
144
+ let currentModelRegistry: ModelRegistry | undefined;
182
145
 
183
146
  pi.on("message_end", (event, ctx) => {
184
147
  // Rewrite rate-limit errors with layer-specific details
@@ -246,11 +209,14 @@ export default async function (pi: ExtensionAPI) {
246
209
  emitQuotas(quotas, "header");
247
210
  });
248
211
 
249
- pi.events.on(NEURALWATT_QUOTAS_REQUEST_EVENT, async (data: unknown) => {
212
+ pi.events.on(NEURALWATT_QUOTAS_REQUEST_EVENT, async () => {
250
213
  if (quotaRequestInFlight) return;
251
214
  quotaRequestInFlight = true;
252
215
  try {
253
- const quotas = await fetchRequestedQuotas(data);
216
+ const apiKey = currentModelRegistry
217
+ ? await getNeuralwattApiKey(currentModelRegistry)
218
+ : undefined;
219
+ const quotas = await fetchRequestedQuotas(apiKey);
254
220
  if (quotas) emitQuotas(quotas, "api");
255
221
  } finally {
256
222
  quotaRequestInFlight = false;
@@ -263,6 +229,7 @@ export default async function (pi: ExtensionAPI) {
263
229
  });
264
230
 
265
231
  pi.on("session_start", async (_event, ctx) => {
232
+ currentModelRegistry = ctx.modelRegistry;
266
233
  pendingRateLimitInfo = undefined;
267
234
  const messages = [...new Set(configLoader.drainMessages())];
268
235
  if (messages.length > 0) {
@@ -273,32 +240,14 @@ export default async function (pi: ExtensionAPI) {
273
240
  pi.events.emit(NEURALWATT_EXTENSIONS_REQUEST_EVENT, undefined);
274
241
  emitConfigUpdated(pi);
275
242
 
276
- if (
277
- !hiddenModelsLoaded &&
278
- configLoader.getConfig().provider.includeHiddenModels
279
- ) {
280
- hiddenModelsLoaded = true;
281
- hiddenModelsAbort?.abort();
282
- hiddenModelsAbort = new AbortController();
283
- const fetched = await loadHiddenModels(
284
- ctx.modelRegistry.authStorage,
285
- hiddenModelsAbort.signal,
286
- );
287
- // Persist for the next startup so scoped models resolve without
288
- // warnings on Pi's subsequent launches. Always write the cache (even
289
- // when empty) and re-register, so graduated or removed hidden models
290
- // are purged from both the cache and the provider's model list.
291
- hiddenModels = fetched;
292
- await writeHiddenModelsCache(hiddenModels);
293
- if (!hiddenModelsAbort.signal.aborted) {
294
- registerNeuralwattProvider(pi, handleSseQuota, hiddenModels);
295
- }
296
- }
297
-
298
243
  if (ctx.model?.provider !== "neuralwatt") return;
299
- const apiKey = await getNeuralwattApiKey(ctx.modelRegistry.authStorage);
244
+ const apiKey = await getNeuralwattApiKey(ctx.modelRegistry);
300
245
  if (!apiKey) return;
301
246
  const quotaResult = await fetchQuotas(apiKey);
302
247
  if (quotaResult.success) emitQuotas(quotaResult.data.quotas, "api");
303
248
  });
249
+
250
+ pi.on("session_shutdown", () => {
251
+ currentModelRegistry = undefined;
252
+ });
304
253
  }
@@ -1,12 +1,13 @@
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
 
6
+ // Hidden aliases that work for authorized accounts but are omitted from the
7
+ // authenticated /v1/models response. Keep these gated by includeHiddenModels.
8
+ // Move an entry to public-models.ts once Neuralwatt advertises it publicly.
9
+ export const HIDDEN_NEURALWATT_MODELS: ProviderModelConfig[] = [];
10
+
10
11
  // Per-ID overrides for known hidden models. The authenticated /v1/models endpoint
11
12
  // exposes pricing and capabilities, but some Pi-specific behavior (thinking levels,
12
13
  // compat flags) has to be supplied by hand.
@@ -95,17 +96,17 @@ function applyHiddenOverride(
95
96
  *
96
97
  * Hidden models are any models returned by the API that are not already part of
97
98
  * the public hardcoded list. If the API key is missing or the request fails, an
98
- * empty array is returned silently.
99
+ * `undefined` distinguishes an unavailable/failed request from a successful
100
+ * empty hidden-model list, allowing refresh callers to preserve stale cache.
99
101
  */
100
102
  export async function loadHiddenModels(
101
- authStorage: AuthStorage,
103
+ apiKey: string,
102
104
  signal?: AbortSignal,
103
- ): Promise<ProviderModelConfig[]> {
104
- const apiKey = await getNeuralwattApiKey(authStorage);
105
- if (!apiKey) return [];
105
+ ): Promise<ProviderModelConfig[] | undefined> {
106
+ if (!apiKey) return undefined;
106
107
 
107
108
  const result = await fetchNeuralwattModels(apiKey, signal);
108
- if (!result.success) return [];
109
+ if (!result.success) return undefined;
109
110
 
110
111
  const publicIds = new Set(NEURALWATT_MODELS.map((model) => model.id));
111
112
 
@@ -2,14 +2,14 @@ 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
- export { loadHiddenModels } from "./hidden";
5
+ export { HIDDEN_NEURALWATT_MODELS, loadHiddenModels } from "./hidden";
7
6
  export {
8
7
  buildLegacyNeuralwattModels,
9
8
  LEGACY_MODEL_ALIAS_MAP,
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;
@@ -3,7 +3,29 @@ import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
3
3
  // Public models returned by https://api.neuralwatt.com/v1/models (unauthenticated view).
4
4
  // Pricing, capabilities, and limits are sourced from the API metadata fields.
5
5
  export const NEURALWATT_MODELS: ProviderModelConfig[] = [
6
+ // Gemma 4 31B - Google, served from NVIDIA's NVFP4 checkpoint
7
+ {
8
+ id: "gemma-4-31b",
9
+ name: "Gemma 4 31B",
10
+ reasoning: false,
11
+ input: ["text", "image"],
12
+ cost: {
13
+ input: 0.144,
14
+ output: 0.42,
15
+ cacheRead: 0.036,
16
+ cacheWrite: 0,
17
+ },
18
+ contextWindow: 262128,
19
+ maxTokens: 16384,
20
+ compat: {
21
+ supportsDeveloperRole: false,
22
+ maxTokensField: "max_tokens",
23
+ },
24
+ },
6
25
  // GLM-5.2 - ZhipuAI
26
+ // Native reasoning tiers: off (none), high, max. Exposes Pi's `max` level
27
+ // (introduced in Pi 0.80.6) for GLM's top tier; `xhigh` is an unsupported
28
+ // hole between `high` and `max`.
7
29
  {
8
30
  id: "glm-5.2",
9
31
  name: "GLM-5.2",
@@ -23,7 +45,8 @@ export const NEURALWATT_MODELS: ProviderModelConfig[] = [
23
45
  low: null,
24
46
  medium: null,
25
47
  high: "high",
26
- xhigh: "max",
48
+ xhigh: null,
49
+ max: "max",
27
50
  },
28
51
  compat: {
29
52
  supportsDeveloperRole: false,
@@ -70,7 +93,8 @@ export const NEURALWATT_MODELS: ProviderModelConfig[] = [
70
93
  low: null,
71
94
  medium: null,
72
95
  high: "high",
73
- xhigh: "max",
96
+ xhigh: null,
97
+ max: "max",
74
98
  },
75
99
  compat: {
76
100
  supportsDeveloperRole: false,
@@ -283,7 +307,8 @@ export const NEURALWATT_MODELS: ProviderModelConfig[] = [
283
307
  low: null,
284
308
  medium: null,
285
309
  high: "high",
286
- xhigh: "max",
310
+ xhigh: null,
311
+ max: "max",
287
312
  },
288
313
  compat: {
289
314
  supportsDeveloperRole: false,
@@ -366,7 +391,8 @@ export const NEURALWATT_MODELS: ProviderModelConfig[] = [
366
391
  low: null,
367
392
  medium: null,
368
393
  high: "high",
369
- xhigh: "max",
394
+ xhigh: null,
395
+ max: "max",
370
396
  },
371
397
  compat: {
372
398
  supportsDeveloperRole: false,
@@ -0,0 +1,149 @@
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 { HIDDEN_NEURALWATT_MODELS, 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 configuredHiddenModels(
71
+ discoveredModels: ProviderModelConfig[],
72
+ baselineModels: ProviderModelConfig[],
73
+ ): ProviderModelConfig[] {
74
+ const hardcodedIds = new Set(
75
+ HIDDEN_NEURALWATT_MODELS.map((model) => model.id),
76
+ );
77
+ return dedupeHiddenModels(
78
+ [
79
+ ...HIDDEN_NEURALWATT_MODELS,
80
+ ...discoveredModels.filter((model) => !hardcodedIds.has(model.id)),
81
+ ],
82
+ baselineModels,
83
+ );
84
+ }
85
+
86
+ function cachedHiddenModels(
87
+ stored: ModelsStoreEntry | undefined,
88
+ ): ProviderModelConfig[] {
89
+ if (!stored) return [];
90
+
91
+ const allStaticIds = new Set(configuredModels(true).map((model) => model.id));
92
+
93
+ return stored.models
94
+ .filter(
95
+ (model) => model.provider === PROVIDER_ID && !allStaticIds.has(model.id),
96
+ )
97
+ .map(toProviderModel);
98
+ }
99
+
100
+ async function persistModels(
101
+ context: RefreshModelsContext,
102
+ models: ProviderModelConfig[],
103
+ ): Promise<void> {
104
+ await context.store.write({
105
+ models: models.map(toStoredModel),
106
+ checkedAt: Date.now(),
107
+ });
108
+ }
109
+
110
+ /** Refresh the complete Neuralwatt catalog with Pi-managed persistence. */
111
+ export async function refreshNeuralwattModels(
112
+ context: RefreshModelsContext,
113
+ options: RefreshNeuralwattModelsOptions,
114
+ ): Promise<ProviderModelConfig[]> {
115
+ const baseline = configuredModels(options.includeLegacyModelIds);
116
+ const stored = await context.store.read();
117
+
118
+ if (!options.includeHiddenModels) {
119
+ await persistModels(context, baseline);
120
+ return baseline;
121
+ }
122
+
123
+ const cachedHidden = configuredHiddenModels(
124
+ cachedHiddenModels(stored),
125
+ baseline,
126
+ );
127
+ const cachedCatalog = [...baseline, ...cachedHidden];
128
+
129
+ if (!context.allowNetwork || context.signal?.aborted) {
130
+ return cachedCatalog;
131
+ }
132
+
133
+ const apiKey =
134
+ context.credential?.type === "api_key" ? context.credential.key : undefined;
135
+ if (!apiKey) return cachedCatalog;
136
+
137
+ const hidden = await (options.loadHidden ?? loadHiddenModels)(
138
+ apiKey,
139
+ context.signal,
140
+ );
141
+ if (context.signal?.aborted) return cachedCatalog;
142
+ if (!hidden) {
143
+ throw new Error("Neuralwatt model catalog refresh failed");
144
+ }
145
+
146
+ const catalog = [...baseline, ...configuredHiddenModels(hidden, baseline)];
147
+ await persistModels(context, catalog);
148
+ return catalog;
149
+ }
@@ -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;
@@ -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.1",
3
+ "version": "0.10.2",
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
- }