@aliou/pi-neuralwatt 0.7.1 → 0.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aliou/pi-neuralwatt",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "private": false,
@@ -21,7 +21,12 @@ import {
21
21
  type NeuralwattQuotasUpdatedPayload,
22
22
  } from "../../types/quota-events";
23
23
  import { normalizeNeuralwattContextOverflowError } from "./context-overflow";
24
- import { getNeuralwattModels, loadHiddenModels } from "./models";
24
+ import {
25
+ getNeuralwattModels,
26
+ loadCachedHiddenModels,
27
+ loadHiddenModels,
28
+ writeHiddenModelsCache,
29
+ } from "./models";
25
30
  import { buildQuotasFromHeaders, fetchRequestedQuotas } from "./quota-store";
26
31
  import {
27
32
  type NeuralwattRateLimitInfo,
@@ -72,8 +77,24 @@ export default async function (pi: ExtensionAPI) {
72
77
  await configLoader.load();
73
78
 
74
79
  let latestQuotas: NeuralwattQuotas | undefined;
80
+
81
+ // Stale-while-revalidate seed for hidden models.
82
+ //
83
+ // Hidden models are only discoverable by hitting the authenticated
84
+ // `/v1/models` endpoint, which we can do inside `session_start` (Pi does not
85
+ // expose `authStorage` to extension factories). However, Pi validates scoped
86
+ // models (e.g. `neuralwatt/glm-5.2-short`) during startup, *before*
87
+ // `session_start` fires. To avoid "No models match pattern" warnings on saved
88
+ // scoped models, we synchronously restore the previous session's fetch from
89
+ // the on-disk cache so the provider is registered with hidden models at
90
+ // load time. `session_start` then revalidates from the live API and writes
91
+ // the cache back. First run with no cache still warns once.
75
92
  let hiddenModels: ProviderModelConfig[] = [];
93
+ if (configLoader.getConfig().includeHiddenModels) {
94
+ hiddenModels = loadCachedHiddenModels();
95
+ }
76
96
  let hiddenModelsLoaded = false;
97
+ let hiddenModelsAbort: AbortController | undefined;
77
98
 
78
99
  const handleSseQuota = (line: string) => {
79
100
  const quotas = updateQuotasFromSseComment(latestQuotas, line);
@@ -85,7 +106,7 @@ export default async function (pi: ExtensionAPI) {
85
106
  });
86
107
  };
87
108
 
88
- registerNeuralwattProvider(pi, handleSseQuota);
109
+ registerNeuralwattProvider(pi, handleSseQuota, hiddenModels);
89
110
 
90
111
  const loadedFeatures = new Set<NeuralwattFeatureId>();
91
112
 
@@ -95,9 +116,24 @@ export default async function (pi: ExtensionAPI) {
95
116
  });
96
117
 
97
118
  pi.events.on(NEURALWATT_CONFIG_UPDATED_EVENT, () => {
119
+ // Toggle may have enabled hidden models since startup. Seed from the disk
120
+ // cache so previously discovered models are available immediately without
121
+ // waiting for the next session_start revalidation.
122
+ if (
123
+ configLoader.getConfig().includeHiddenModels &&
124
+ !hiddenModelsLoaded &&
125
+ hiddenModels.length === 0
126
+ ) {
127
+ hiddenModels = loadCachedHiddenModels();
128
+ }
98
129
  registerNeuralwattProvider(pi, handleSseQuota, hiddenModels);
99
130
  });
100
131
 
132
+ pi.on("session_shutdown", () => {
133
+ hiddenModelsAbort?.abort();
134
+ hiddenModelsAbort = undefined;
135
+ });
136
+
101
137
  let lastHeaderEmitAt = 0;
102
138
  let quotaRequestInFlight = false;
103
139
 
@@ -203,10 +239,20 @@ export default async function (pi: ExtensionAPI) {
203
239
 
204
240
  if (!hiddenModelsLoaded && configLoader.getConfig().includeHiddenModels) {
205
241
  hiddenModelsLoaded = true;
206
- const fetched = await loadHiddenModels(ctx.modelRegistry.authStorage);
242
+ hiddenModelsAbort?.abort();
243
+ hiddenModelsAbort = new AbortController();
244
+ const fetched = await loadHiddenModels(
245
+ ctx.modelRegistry.authStorage,
246
+ hiddenModelsAbort.signal,
247
+ );
248
+ // Persist for the next startup so scoped models resolve without warnings
249
+ // on Pi's subsequent launches.
207
250
  if (fetched.length > 0) {
208
251
  hiddenModels = fetched;
209
- registerNeuralwattProvider(pi, handleSseQuota, hiddenModels);
252
+ await writeHiddenModelsCache(hiddenModels);
253
+ if (!hiddenModelsAbort.signal.aborted) {
254
+ registerNeuralwattProvider(pi, handleSseQuota, hiddenModels);
255
+ }
210
256
  }
211
257
  }
212
258
 
@@ -0,0 +1,77 @@
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
+ }
@@ -108,11 +108,12 @@ function applyHiddenOverride(
108
108
  */
109
109
  export async function loadHiddenModels(
110
110
  authStorage: AuthStorage,
111
+ signal?: AbortSignal,
111
112
  ): Promise<ProviderModelConfig[]> {
112
113
  const apiKey = await getNeuralwattApiKey(authStorage);
113
114
  if (!apiKey) return [];
114
115
 
115
- const result = await fetchNeuralwattModels(apiKey);
116
+ const result = await fetchNeuralwattModels(apiKey, signal);
116
117
  if (!result.success) return [];
117
118
 
118
119
  const publicIds = new Set(NEURALWATT_MODELS.map((model) => model.id));
@@ -2,6 +2,7 @@ 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";
5
6
  export { loadHiddenModels } from "./hidden";
6
7
  export {
7
8
  buildLegacyNeuralwattModels,