@aliou/pi-neuralwatt 0.7.1 → 0.7.3

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.3",
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
 
@@ -135,12 +171,21 @@ export default async function (pi: ExtensionAPI) {
135
171
  return { message };
136
172
  }
137
173
 
174
+ // Fallback for 429s where no layer-specific headers were captured. The
175
+ // streamSimple wrap (wrapNeuralwattStreamSimple) already formats a
176
+ // detailed message via formatRateLimitError when it captures headers;
177
+ // detect that case by the `"429 rate limit:"` prefix it emits and leave
178
+ // it untouched. This branch only fires for genuinely headerless 429s
179
+ // (e.g. anonymous playground limits, or a 429 from infra in front of
180
+ // Neuralwatt), since after_provider_response cannot observe 429s — the
181
+ // OpenAI SDK throws before Pi's onResponse hook runs.
138
182
  if (
139
183
  event.message.role === "assistant" &&
140
184
  event.message.stopReason === "error" &&
141
185
  (event.message.provider === "neuralwatt" ||
142
186
  ctx.model?.provider === "neuralwatt") &&
143
- event.message.errorMessage?.includes("429")
187
+ event.message.errorMessage?.includes("429") &&
188
+ !event.message.errorMessage.startsWith("429 rate limit:")
144
189
  ) {
145
190
  return {
146
191
  message: normalizeNeuralwattRateLimitError(event.message, {
@@ -203,10 +248,20 @@ export default async function (pi: ExtensionAPI) {
203
248
 
204
249
  if (!hiddenModelsLoaded && configLoader.getConfig().includeHiddenModels) {
205
250
  hiddenModelsLoaded = true;
206
- const fetched = await loadHiddenModels(ctx.modelRegistry.authStorage);
251
+ hiddenModelsAbort?.abort();
252
+ hiddenModelsAbort = new AbortController();
253
+ const fetched = await loadHiddenModels(
254
+ ctx.modelRegistry.authStorage,
255
+ hiddenModelsAbort.signal,
256
+ );
257
+ // Persist for the next startup so scoped models resolve without warnings
258
+ // on Pi's subsequent launches.
207
259
  if (fetched.length > 0) {
208
260
  hiddenModels = fetched;
209
- registerNeuralwattProvider(pi, handleSseQuota, hiddenModels);
261
+ await writeHiddenModelsCache(hiddenModels);
262
+ if (!hiddenModelsAbort.signal.aborted) {
263
+ registerNeuralwattProvider(pi, handleSseQuota, hiddenModels);
264
+ }
210
265
  }
211
266
  }
212
267
 
@@ -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
+ }
@@ -10,19 +10,10 @@ import { NEURALWATT_MODELS } from "./public-models";
10
10
  // Per-ID overrides for known hidden models. The authenticated /v1/models endpoint
11
11
  // exposes pricing and capabilities, but some Pi-specific behavior (thinking levels,
12
12
  // compat flags) has to be supplied by hand.
13
+ // Previously hidden models that have since gone public now live in public-models.ts.
13
14
  const HIDDEN_MODEL_OVERRIDES: Partial<
14
15
  Record<string, Partial<ProviderModelConfig>>
15
- > = {
16
- "glm-5.2-short": {
17
- thinkingLevelMap: {
18
- minimal: null,
19
- low: null,
20
- medium: null,
21
- high: "high",
22
- xhigh: "max",
23
- },
24
- },
25
- };
16
+ > = {};
26
17
 
27
18
  function buildHiddenModel(apiModel: NeuralwattApiModel): ProviderModelConfig {
28
19
  const meta = apiModel.metadata;
@@ -108,11 +99,12 @@ function applyHiddenOverride(
108
99
  */
109
100
  export async function loadHiddenModels(
110
101
  authStorage: AuthStorage,
102
+ signal?: AbortSignal,
111
103
  ): Promise<ProviderModelConfig[]> {
112
104
  const apiKey = await getNeuralwattApiKey(authStorage);
113
105
  if (!apiKey) return [];
114
106
 
115
- const result = await fetchNeuralwattModels(apiKey);
107
+ const result = await fetchNeuralwattModels(apiKey, signal);
116
108
  if (!result.success) return [];
117
109
 
118
110
  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,
@@ -49,6 +49,52 @@ export const NEURALWATT_MODELS: ProviderModelConfig[] = [
49
49
  maxTokensField: "max_tokens",
50
50
  },
51
51
  },
52
+ // GLM-5.2 Short - ZhipuAI (200K context, bounded reasoning budget)
53
+ {
54
+ id: "glm-5.2-short",
55
+ name: "GLM-5.2 Short",
56
+ reasoning: true,
57
+ input: ["text"],
58
+ cost: {
59
+ input: 1.45,
60
+ output: 4.5,
61
+ cacheRead: 0.3625,
62
+ cacheWrite: 0,
63
+ },
64
+ contextWindow: 199984,
65
+ maxTokens: 65536,
66
+ thinkingLevelMap: {
67
+ minimal: null,
68
+ low: null,
69
+ medium: null,
70
+ high: "high",
71
+ xhigh: "max",
72
+ },
73
+ compat: {
74
+ supportsDeveloperRole: false,
75
+ maxTokensField: "max_tokens",
76
+ requiresReasoningContentOnAssistantMessages: true,
77
+ },
78
+ },
79
+ // GLM-5.2 Short Fast - ZhipuAI (200K context, reasoning disabled)
80
+ {
81
+ id: "glm-5.2-short-fast",
82
+ name: "GLM-5.2 Short Fast",
83
+ reasoning: false,
84
+ input: ["text"],
85
+ cost: {
86
+ input: 1.45,
87
+ output: 4.5,
88
+ cacheRead: 0.3625,
89
+ cacheWrite: 0,
90
+ },
91
+ contextWindow: 199984,
92
+ maxTokens: 65536,
93
+ compat: {
94
+ supportsDeveloperRole: false,
95
+ maxTokensField: "max_tokens",
96
+ },
97
+ },
52
98
  // Kimi K2.5 - MoonshotAI
53
99
  {
54
100
  id: "moonshotai/Kimi-K2.5",