@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 +1 -1
- package/src/extensions/provider/index.ts +60 -5
- package/src/extensions/provider/models/cache.ts +77 -0
- package/src/extensions/provider/models/hidden.ts +4 -12
- package/src/extensions/provider/models/index.ts +1 -0
- package/src/extensions/provider/models/public-models.ts +46 -0
package/package.json
CHANGED
|
@@ -21,7 +21,12 @@ import {
|
|
|
21
21
|
type NeuralwattQuotasUpdatedPayload,
|
|
22
22
|
} from "../../types/quota-events";
|
|
23
23
|
import { normalizeNeuralwattContextOverflowError } from "./context-overflow";
|
|
24
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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",
|