@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 +10 -2
- package/extensions/_shared/auth.ts +3 -4
- package/extensions/command-quotas/command.ts +1 -1
- package/extensions/provider/index.ts +36 -87
- package/extensions/provider/models/hidden.ts +12 -11
- package/extensions/provider/models/index.ts +2 -2
- package/extensions/provider/models/public-models.ts +30 -4
- package/extensions/provider/models/refresh.ts +149 -0
- package/extensions/provider/quota-store.ts +1 -7
- 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,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
|
|
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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
114
|
+
next.includeLegacyModelIds ===
|
|
115
|
+
registeredProviderSettings.includeLegacyModelIds &&
|
|
116
|
+
next.includeHiddenModels ===
|
|
117
|
+
registeredProviderSettings.includeHiddenModels
|
|
152
118
|
) {
|
|
153
|
-
|
|
119
|
+
return;
|
|
154
120
|
}
|
|
155
|
-
|
|
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 (
|
|
212
|
+
pi.events.on(NEURALWATT_QUOTAS_REQUEST_EVENT, async () => {
|
|
250
213
|
if (quotaRequestInFlight) return;
|
|
251
214
|
quotaRequestInFlight = true;
|
|
252
215
|
try {
|
|
253
|
-
const
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
103
|
+
apiKey: string,
|
|
102
104
|
signal?: AbortSignal,
|
|
103
|
-
): Promise<ProviderModelConfig[]> {
|
|
104
|
-
|
|
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 {
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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.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.
|
|
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
|
-
}
|