@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 +10 -2
- package/extensions/_shared/auth.ts +3 -4
- package/extensions/command-quotas/command.ts +1 -1
- package/extensions/provider/index.ts +43 -91
- package/extensions/provider/models/hidden.ts +7 -11
- package/extensions/provider/models/index.ts +1 -1
- package/extensions/provider/models/refresh.ts +130 -0
- package/extensions/provider/quota-store.ts +1 -7
- package/extensions/quota-warnings/index.ts +5 -13
- package/extensions/quota-warnings/notifier.ts +7 -6
- package/extensions/sub-bar-integration/index.ts +2 -16
- package/package.json +6 -6
- package/extensions/provider/models/cache.ts +0 -77
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
|
-
|
|
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.
|
|
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 {
|
|
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
|
-
|
|
14
|
+
modelRegistry: ModelRegistry,
|
|
15
15
|
): Promise<string | undefined> {
|
|
16
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
source: "sse",
|
|
129
|
-
});
|
|
94
|
+
|
|
95
|
+
lastSseEmitAt = now;
|
|
96
|
+
emitQuotas(quotas, "sse");
|
|
130
97
|
};
|
|
131
98
|
|
|
132
|
-
registerNeuralwattProvider(pi, handleSseQuota
|
|
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
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
114
|
+
next.includeLegacyModelIds ===
|
|
115
|
+
registeredProviderSettings.includeLegacyModelIds &&
|
|
116
|
+
next.includeHiddenModels ===
|
|
117
|
+
registeredProviderSettings.includeHiddenModels
|
|
149
118
|
) {
|
|
150
|
-
|
|
119
|
+
return;
|
|
151
120
|
}
|
|
152
|
-
|
|
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 (
|
|
212
|
+
pi.events.on(NEURALWATT_QUOTAS_REQUEST_EVENT, async () => {
|
|
247
213
|
if (quotaRequestInFlight) return;
|
|
248
214
|
quotaRequestInFlight = true;
|
|
249
215
|
try {
|
|
250
|
-
const
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
98
|
+
apiKey: string,
|
|
102
99
|
signal?: AbortSignal,
|
|
103
|
-
): Promise<ProviderModelConfig[]> {
|
|
104
|
-
|
|
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
|
-
|
|
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 (
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
if (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
50
|
-
"@earendil-works/pi-coding-agent": "0.80.
|
|
51
|
-
"@earendil-works/pi-tui": "0.80.
|
|
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
|
-
}
|