@2tle/pi-provider-manager 0.1.0 → 0.1.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/2tle-pi-provider-manager-0.1.2.tgz +0 -0
- package/README.md +77 -2
- package/index.ts +194 -494
- package/package.json +1 -1
- package/src/config.ts +117 -0
- package/src/models.ts +81 -0
- package/src/provider.ts +51 -0
- package/src/types.ts +78 -0
- package/src/ui.ts +63 -0
package/index.ts
CHANGED
|
@@ -1,344 +1,47 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { keyHint } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { loadSecrets, loadState, saveSecrets, saveState, validateBaseUrl, validateProviderId } from "./src/config.js";
|
|
4
|
+
import { createManagedProvider } from "./src/provider.js";
|
|
5
|
+
import { SecretInputDialog } from "./src/ui.js";
|
|
6
|
+
import { REFRESH_TIMEOUT_MS, type StoredProvider, type StoredSecrets, type StoredState } from "./src/types.js";
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const API = "openai-completions" as const;
|
|
11
|
-
const DEFAULT_CONTEXT_WINDOW = 128_000;
|
|
12
|
-
const DEFAULT_MAX_TOKENS = 16_384;
|
|
13
|
-
const REFRESH_TIMEOUT_MS = 30_000;
|
|
8
|
+
// Preserve the helper's existing public export for consumers and tests.
|
|
9
|
+
export { maskInputLine } from "./src/ui.js";
|
|
14
10
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
name: string;
|
|
18
|
-
baseUrl: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
interface StoredState {
|
|
22
|
-
providers: StoredProvider[];
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
interface StoredSecrets {
|
|
26
|
-
apiKeys: Record<string, string>;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
interface ProviderModelConfig {
|
|
30
|
-
id: string;
|
|
31
|
-
name: string;
|
|
32
|
-
api: typeof API;
|
|
33
|
-
reasoning: boolean;
|
|
34
|
-
input: ("text" | "image")[];
|
|
35
|
-
cost: {
|
|
36
|
-
input: number;
|
|
37
|
-
output: number;
|
|
38
|
-
cacheRead: number;
|
|
39
|
-
cacheWrite: number;
|
|
40
|
-
};
|
|
41
|
-
contextWindow: number;
|
|
42
|
-
maxTokens: number;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
interface ManagedProviderConfig {
|
|
46
|
-
name: string;
|
|
47
|
-
baseUrl: string;
|
|
48
|
-
api: typeof API;
|
|
49
|
-
apiKey: string;
|
|
50
|
-
models: ProviderModelConfig[];
|
|
51
|
-
refreshModels(context: RefreshModelsContext): Promise<ProviderModelConfig[]>;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
interface OpenAIModelPayload {
|
|
55
|
-
id?: unknown;
|
|
56
|
-
name?: unknown;
|
|
57
|
-
context_window?: unknown;
|
|
58
|
-
contextWindow?: unknown;
|
|
59
|
-
max_tokens?: unknown;
|
|
60
|
-
maxTokens?: unknown;
|
|
61
|
-
reasoning?: unknown;
|
|
62
|
-
supports_reasoning?: unknown;
|
|
63
|
-
input?: unknown;
|
|
64
|
-
cost?: unknown;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
interface OpenAIModelsPayload {
|
|
68
|
-
data?: unknown;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
72
|
-
return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : undefined;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function asNonEmptyString(value: unknown): string | undefined {
|
|
76
|
-
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function asPositiveNumber(value: unknown, fallback: number): number {
|
|
80
|
-
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function normalizeBaseUrl(value: string): string {
|
|
84
|
-
return value.trim().replace(/\/+$/, "");
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function validateProviderId(id: string): void {
|
|
88
|
-
if (!/^[a-z0-9][a-z0-9._-]*$/.test(id)) {
|
|
89
|
-
throw new Error("Provider ID may contain only lowercase letters, numbers, '.', '_' or '-'.");
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function validateBaseUrl(value: string): string {
|
|
94
|
-
const baseUrl = normalizeBaseUrl(value);
|
|
95
|
-
let parsed: URL;
|
|
96
|
-
try {
|
|
97
|
-
parsed = new URL(baseUrl);
|
|
98
|
-
} catch {
|
|
99
|
-
throw new Error("Base URL is not a valid URL.");
|
|
100
|
-
}
|
|
101
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
102
|
-
throw new Error("Base URL must use the http or https protocol.");
|
|
103
|
-
}
|
|
104
|
-
return baseUrl;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function modelsUrl(baseUrl: string): string {
|
|
108
|
-
return new URL("models", `${normalizeBaseUrl(baseUrl)}/`).toString();
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async function loadState(): Promise<StoredState> {
|
|
112
|
-
try {
|
|
113
|
-
const raw = await readFile(CONFIG_PATH, "utf8");
|
|
114
|
-
const parsed = asRecord(JSON.parse(raw));
|
|
115
|
-
const providers = Array.isArray(parsed?.providers) ? parsed.providers : [];
|
|
116
|
-
const normalized: StoredProvider[] = [];
|
|
117
|
-
for (const item of providers) {
|
|
118
|
-
const record = asRecord(item);
|
|
119
|
-
const id = asNonEmptyString(record?.id);
|
|
120
|
-
const name = asNonEmptyString(record?.name);
|
|
121
|
-
const baseUrl = asNonEmptyString(record?.baseUrl);
|
|
122
|
-
if (!id || !name || !baseUrl) continue;
|
|
123
|
-
try {
|
|
124
|
-
validateProviderId(id);
|
|
125
|
-
normalized.push({ id, name, baseUrl: validateBaseUrl(baseUrl) });
|
|
126
|
-
} catch {
|
|
127
|
-
// Ignore malformed entries so one broken provider does not prevent startup.
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return { providers: normalized };
|
|
131
|
-
} catch (error) {
|
|
132
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
133
|
-
return { providers: [] };
|
|
134
|
-
}
|
|
135
|
-
throw new Error(`Unable to read provider configuration: ${error instanceof Error ? error.message : String(error)}`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
async function loadSecrets(): Promise<StoredSecrets> {
|
|
140
|
-
try {
|
|
141
|
-
const raw = await readFile(SECRETS_PATH, "utf8");
|
|
142
|
-
const parsed = asRecord(JSON.parse(raw));
|
|
143
|
-
const apiKeys: Record<string, string> = {};
|
|
144
|
-
const rawApiKeys = asRecord(parsed?.apiKeys);
|
|
145
|
-
for (const [id, value] of Object.entries(rawApiKeys ?? {})) {
|
|
146
|
-
if (typeof value === "string" && value.trim()) apiKeys[id] = value;
|
|
147
|
-
}
|
|
148
|
-
return { apiKeys };
|
|
149
|
-
} catch (error) {
|
|
150
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
151
|
-
return { apiKeys: {} };
|
|
152
|
-
}
|
|
153
|
-
throw new Error(`Unable to read provider API keys: ${error instanceof Error ? error.message : String(error)}`);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
async function saveJsonFile(path: string, value: object): Promise<void> {
|
|
158
|
-
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
159
|
-
const temporaryPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
160
|
-
const content = `${JSON.stringify(value, null, 2)}\n`;
|
|
161
|
-
try {
|
|
162
|
-
await writeFile(temporaryPath, content, { encoding: "utf8", mode: 0o600 });
|
|
163
|
-
await chmod(temporaryPath, 0o600);
|
|
164
|
-
await rename(temporaryPath, path);
|
|
165
|
-
} finally {
|
|
166
|
-
await unlink(temporaryPath).catch(() => undefined);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
async function saveState(state: StoredState): Promise<void> {
|
|
171
|
-
await saveJsonFile(CONFIG_PATH, state);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
async function saveSecrets(secrets: StoredSecrets): Promise<void> {
|
|
175
|
-
await saveJsonFile(SECRETS_PATH, secrets);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function modelFromPayload(
|
|
179
|
-
provider: StoredProvider,
|
|
180
|
-
payload: OpenAIModelPayload,
|
|
181
|
-
): ProviderModelConfig | undefined {
|
|
182
|
-
const id = asNonEmptyString(payload.id);
|
|
183
|
-
if (!id) return undefined;
|
|
184
|
-
|
|
185
|
-
const cost = asRecord(payload.cost);
|
|
186
|
-
const input: ("text" | "image")[] =
|
|
187
|
-
Array.isArray(payload.input) && payload.input.includes("image") ? ["text", "image"] : ["text"];
|
|
188
|
-
|
|
189
|
-
return {
|
|
190
|
-
id,
|
|
191
|
-
name: asNonEmptyString(payload.name) ?? id,
|
|
192
|
-
api: API,
|
|
193
|
-
reasoning: payload.reasoning === true || payload.supports_reasoning === true,
|
|
194
|
-
input,
|
|
195
|
-
cost: {
|
|
196
|
-
input: asPositiveNumber(cost?.input, 0),
|
|
197
|
-
output: asPositiveNumber(cost?.output, 0),
|
|
198
|
-
cacheRead: asPositiveNumber(cost?.cacheRead, 0),
|
|
199
|
-
cacheWrite: asPositiveNumber(cost?.cacheWrite, 0),
|
|
200
|
-
},
|
|
201
|
-
contextWindow: asPositiveNumber(payload.context_window ?? payload.contextWindow, DEFAULT_CONTEXT_WINDOW),
|
|
202
|
-
maxTokens: asPositiveNumber(payload.max_tokens ?? payload.maxTokens, DEFAULT_MAX_TOKENS),
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
async function fetchProviderModels(
|
|
207
|
-
provider: StoredProvider,
|
|
208
|
-
context: RefreshModelsContext,
|
|
209
|
-
getApiKey: () => string | undefined,
|
|
210
|
-
): Promise<ProviderModelConfig[]> {
|
|
211
|
-
const apiKey = getApiKey();
|
|
212
|
-
if (!apiKey) {
|
|
213
|
-
throw new Error(`API key for provider '${provider.id}' is not configured.`);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const response = await fetch(modelsUrl(provider.baseUrl), {
|
|
217
|
-
signal: context.signal,
|
|
218
|
-
headers: {
|
|
219
|
-
Accept: "application/json",
|
|
220
|
-
Authorization: `Bearer ${apiKey}`,
|
|
221
|
-
},
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
if (!response.ok) {
|
|
225
|
-
throw new Error(`Failed to fetch model list (${response.status} ${response.statusText})`);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const payload = (await response.json()) as OpenAIModelsPayload | unknown[];
|
|
229
|
-
const data = Array.isArray(payload) ? payload : asRecord(payload)?.data;
|
|
230
|
-
if (!Array.isArray(data)) {
|
|
231
|
-
throw new Error("Model list response does not contain a data array.");
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return data
|
|
235
|
-
.map((item) => {
|
|
236
|
-
const record = asRecord(item);
|
|
237
|
-
return record ? modelFromPayload(provider, record as OpenAIModelPayload) : undefined;
|
|
238
|
-
})
|
|
239
|
-
.filter((model): model is ProviderModelConfig => model !== undefined);
|
|
11
|
+
function parseArgs(args: string): string[] {
|
|
12
|
+
return args.trim().split(/\s+/).filter(Boolean);
|
|
240
13
|
}
|
|
241
14
|
|
|
242
|
-
function
|
|
243
|
-
|
|
244
|
-
getApiKey: () => string | undefined,
|
|
245
|
-
): ManagedProviderConfig {
|
|
246
|
-
return {
|
|
247
|
-
name: config.name,
|
|
248
|
-
baseUrl: config.baseUrl,
|
|
249
|
-
api: API,
|
|
250
|
-
// The API key is held outside the regular provider config and is only
|
|
251
|
-
// read when the provider is registered or refreshed.
|
|
252
|
-
apiKey: getApiKey() ?? "local",
|
|
253
|
-
models: [],
|
|
254
|
-
refreshModels: (context) => fetchProviderModels(config, context, getApiKey),
|
|
255
|
-
};
|
|
15
|
+
function formatTokenCount(value: number | undefined): string {
|
|
16
|
+
return value === undefined ? "API/default" : value.toLocaleString();
|
|
256
17
|
}
|
|
257
18
|
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
if (line.startsWith(CURSOR_MARKER, index)) {
|
|
265
|
-
result += CURSOR_MARKER;
|
|
266
|
-
index += CURSOR_MARKER.length;
|
|
267
|
-
continue;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
if (line[index] === "\x1b") {
|
|
271
|
-
const ansi = line.slice(index).match(/^\x1b\[[0-9;?]*[ -/]*[@-~]/)?.[0];
|
|
272
|
-
if (ansi) {
|
|
273
|
-
result += ansi;
|
|
274
|
-
index += ansi.length;
|
|
275
|
-
continue;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const character = line[index++];
|
|
280
|
-
result += /\s/u.test(character) ? character : "*";
|
|
281
|
-
}
|
|
282
|
-
return prompt + result;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
class MaskedInput extends Input {
|
|
286
|
-
override render(width: number): string[] {
|
|
287
|
-
const terminalWidth = process.stdout.columns;
|
|
288
|
-
const safeWidth = Math.max(1, Number.isFinite(terminalWidth) ? Math.min(width, terminalWidth) : width);
|
|
289
|
-
return super.render(safeWidth).map((line) => truncateToWidth(maskInputLine(line), safeWidth, "", false));
|
|
290
|
-
}
|
|
19
|
+
function parseTokenCount(value: string): number | undefined {
|
|
20
|
+
const match = value.trim().toLowerCase().match(/^(\d+(?:\.\d+)?)\s*(k|m|b)?$/);
|
|
21
|
+
if (!match) return undefined;
|
|
22
|
+
const multiplier = match[2] === "k" ? 1_000 : match[2] === "m" ? 1_000_000 : match[2] === "b" ? 1_000_000_000 : 1;
|
|
23
|
+
const parsed = Number(match[1]) * multiplier;
|
|
24
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
291
25
|
}
|
|
292
26
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
constructor(
|
|
298
|
-
done: (value: string | undefined) => void,
|
|
299
|
-
title: string,
|
|
300
|
-
helpText: string,
|
|
301
|
-
border: (text: string) => string,
|
|
302
|
-
) {
|
|
303
|
-
super();
|
|
304
|
-
// Match Pi's built-in ctx.ui.input() layout while keeping the secret masked.
|
|
305
|
-
this.addChild(new DynamicBorder(border));
|
|
306
|
-
this.addChild(new Spacer(1));
|
|
307
|
-
this.addChild(new Text(title, 1, 0));
|
|
308
|
-
this.addChild(new Spacer(1));
|
|
309
|
-
this.addChild(this.input);
|
|
310
|
-
this.addChild(new Spacer(1));
|
|
311
|
-
this.addChild(new Text(helpText, 1, 0));
|
|
312
|
-
this.addChild(new Spacer(1));
|
|
313
|
-
this.addChild(new DynamicBorder(border));
|
|
314
|
-
this.done = done;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
private readonly done: (value: string | undefined) => void;
|
|
318
|
-
|
|
319
|
-
get focused(): boolean {
|
|
320
|
-
return this._focused;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
set focused(value: boolean) {
|
|
324
|
-
this._focused = value;
|
|
325
|
-
this.input.focused = value;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
handleInput(data: string): void {
|
|
329
|
-
const keybindings = getKeybindings();
|
|
330
|
-
if (keybindings.matches(data, "tui.select.confirm") || data === "\n") {
|
|
331
|
-
this.done(this.input.getValue());
|
|
332
|
-
} else if (keybindings.matches(data, "tui.select.cancel")) {
|
|
333
|
-
this.done(undefined);
|
|
334
|
-
} else {
|
|
335
|
-
this.input.handleInput(data);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
27
|
+
function providerChoiceLabel(provider: StoredProvider, ctx: ExtensionCommandContext): string {
|
|
28
|
+
const modelCount = ctx.modelRegistry.getProvider(provider.id)?.getModels().length ?? 0;
|
|
29
|
+
const overrideCount = Object.keys(provider.modelOverrides ?? {}).length;
|
|
30
|
+
return `${provider.id} — ${provider.name} · ${modelCount} models · ${overrideCount} overrides`;
|
|
338
31
|
}
|
|
339
32
|
|
|
340
|
-
function
|
|
341
|
-
|
|
33
|
+
async function chooseManagedProvider(
|
|
34
|
+
ctx: ExtensionCommandContext,
|
|
35
|
+
managed: ReadonlyMap<string, StoredProvider>,
|
|
36
|
+
action: string,
|
|
37
|
+
): Promise<StoredProvider | undefined> {
|
|
38
|
+
if (!ctx.hasUI) throw new Error(`${action} requires an interactive UI.`);
|
|
39
|
+
const providers = [...managed.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
40
|
+
if (providers.length === 0) throw new Error("No managed providers.");
|
|
41
|
+
const labels = providers.map((provider) => providerChoiceLabel(provider, ctx));
|
|
42
|
+
const selected = await ctx.ui.select(action, labels);
|
|
43
|
+
if (selected === undefined) return undefined;
|
|
44
|
+
return providers[labels.indexOf(selected)];
|
|
342
45
|
}
|
|
343
46
|
|
|
344
47
|
function usage(): string {
|
|
@@ -346,7 +49,8 @@ function usage(): string {
|
|
|
346
49
|
"Usage:",
|
|
347
50
|
" /provider add",
|
|
348
51
|
" /provider list",
|
|
349
|
-
" /provider edit
|
|
52
|
+
" /provider edit [provider_name]",
|
|
53
|
+
" /provider model [provider_name] [model_id]",
|
|
350
54
|
" /provider reload --all",
|
|
351
55
|
" /provider reload <provider_name>",
|
|
352
56
|
" /provider delete <provider_name>",
|
|
@@ -357,145 +61,172 @@ function commandNotify(ctx: ExtensionCommandContext, message: string, type: "inf
|
|
|
357
61
|
if (ctx.hasUI) ctx.ui.notify(message, type);
|
|
358
62
|
}
|
|
359
63
|
|
|
360
|
-
async function refreshProviders(
|
|
361
|
-
ctx: ExtensionCommandContext,
|
|
362
|
-
providerIds: readonly string[],
|
|
363
|
-
): Promise<void> {
|
|
64
|
+
async function refreshProviders(ctx: ExtensionCommandContext, providerIds: readonly string[]): Promise<void> {
|
|
364
65
|
if (providerIds.length === 0) {
|
|
365
66
|
commandNotify(ctx, "No providers to refresh.", "warning");
|
|
366
67
|
return;
|
|
367
68
|
}
|
|
368
|
-
|
|
369
|
-
const signal = AbortSignal.timeout(REFRESH_TIMEOUT_MS);
|
|
370
69
|
const result = await ctx.modelRegistry.refresh({
|
|
371
70
|
providers: providerIds,
|
|
372
71
|
allowNetwork: true,
|
|
373
72
|
force: true,
|
|
374
|
-
signal,
|
|
73
|
+
signal: AbortSignal.timeout(REFRESH_TIMEOUT_MS),
|
|
375
74
|
});
|
|
75
|
+
if (result.aborted) throw new Error("Model list refresh was cancelled or timed out.");
|
|
376
76
|
|
|
377
|
-
|
|
378
|
-
throw new Error("Model list refresh was cancelled or timed out.");
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
const messages: string[] = [];
|
|
382
|
-
for (const providerId of providerIds) {
|
|
77
|
+
const messages = providerIds.map((providerId) => {
|
|
383
78
|
const error = result.errors.get(providerId);
|
|
384
|
-
if (error) {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const count = provider?.getModels().length ?? 0;
|
|
390
|
-
messages.push(`${providerId}: ${count} model${count === 1 ? "" : "s"}`);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
const failed = result.errors.size > 0;
|
|
394
|
-
commandNotify(ctx, `Model list refresh complete\n${messages.join("\n")}`, failed ? "warning" : "info");
|
|
79
|
+
if (error) return `${providerId}: failed — ${error.message}`;
|
|
80
|
+
const count = ctx.modelRegistry.getProvider(providerId)?.getModels().length ?? 0;
|
|
81
|
+
return `${providerId}: ${count} model${count === 1 ? "" : "s"}`;
|
|
82
|
+
});
|
|
83
|
+
commandNotify(ctx, `Model list refresh complete\n${messages.join("\n")}`, result.errors.size > 0 ? "warning" : "info");
|
|
395
84
|
}
|
|
396
85
|
|
|
397
86
|
async function promptForProvider(ctx: ExtensionCommandContext): Promise<StoredProvider | undefined> {
|
|
398
87
|
if (!ctx.hasUI) throw new Error("/provider add requires an interactive UI.");
|
|
399
|
-
|
|
400
88
|
const id = (await ctx.ui.input("Provider ID", "e.g. lmstudio"))?.trim();
|
|
401
89
|
if (!id) return undefined;
|
|
402
90
|
validateProviderId(id);
|
|
403
|
-
|
|
404
91
|
const name = (await ctx.ui.input("Provider display name", id))?.trim() || id;
|
|
405
|
-
const
|
|
406
|
-
if (!
|
|
407
|
-
|
|
408
|
-
return { id, name, baseUrl: validateBaseUrl(baseUrlInput) };
|
|
92
|
+
const baseUrl = await ctx.ui.input("OpenAI-compatible Base URL", "e.g. http://localhost:1234/v1");
|
|
93
|
+
if (!baseUrl?.trim()) return undefined;
|
|
94
|
+
return { id, name, baseUrl: validateBaseUrl(baseUrl) };
|
|
409
95
|
}
|
|
410
96
|
|
|
411
97
|
async function promptForApiKey(ctx: ExtensionCommandContext): Promise<string | undefined> {
|
|
412
98
|
if (!ctx.hasUI) throw new Error("API key entry requires an interactive UI.");
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
(text: string) => theme.fg("accent", text),
|
|
422
|
-
),
|
|
423
|
-
)
|
|
424
|
-
: await ctx.ui.input("API key", "sk-... or local");
|
|
425
|
-
|
|
99
|
+
const apiKey = ctx.mode === "tui"
|
|
100
|
+
? await ctx.ui.custom<string | undefined>((_tui, theme, _keybindings, done) => new SecretInputDialog(
|
|
101
|
+
done,
|
|
102
|
+
theme.fg("accent", "API key"),
|
|
103
|
+
`${keyHint("tui.select.confirm", "submit")} ${keyHint("tui.select.cancel", "cancel")}`,
|
|
104
|
+
(text: string) => theme.fg("accent", text),
|
|
105
|
+
))
|
|
106
|
+
: await ctx.ui.input("API key", "sk-... or local");
|
|
426
107
|
if (apiKey === undefined) return undefined;
|
|
427
108
|
if (!apiKey.trim()) throw new Error("API key cannot be empty.");
|
|
428
109
|
return apiKey;
|
|
429
110
|
}
|
|
430
111
|
|
|
431
|
-
|
|
432
|
-
provider: StoredProvider;
|
|
433
|
-
apiKey?: string;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
async function promptForProviderEdit(
|
|
437
|
-
ctx: ExtensionCommandContext,
|
|
438
|
-
provider: StoredProvider,
|
|
439
|
-
): Promise<ProviderEdit | undefined> {
|
|
112
|
+
async function promptForProviderEdit(ctx: ExtensionCommandContext, provider: StoredProvider): Promise<{ provider: StoredProvider; apiKey?: string } | undefined> {
|
|
440
113
|
if (!ctx.hasUI) throw new Error("/provider edit requires an interactive UI.");
|
|
441
|
-
|
|
442
114
|
let updatedProvider = { ...provider };
|
|
443
115
|
let updatedApiKey: string | undefined;
|
|
444
|
-
|
|
445
116
|
while (true) {
|
|
446
117
|
const choice = await ctx.ui.select(`Edit provider: ${provider.id}`, [
|
|
447
118
|
`Change display name (${updatedProvider.name})`,
|
|
448
119
|
`Change Base URL (${updatedProvider.baseUrl})`,
|
|
449
120
|
"Change API key",
|
|
121
|
+
"Edit model limits",
|
|
122
|
+
`Reset all model limits (${Object.keys(updatedProvider.modelOverrides ?? {}).length})`,
|
|
450
123
|
"Save changes",
|
|
451
124
|
]);
|
|
452
125
|
if (choice === undefined) return undefined;
|
|
453
126
|
if (choice === "Save changes") return { provider: updatedProvider, apiKey: updatedApiKey };
|
|
454
|
-
|
|
455
127
|
if (choice.startsWith("Change display name")) {
|
|
456
128
|
const name = await ctx.ui.input("Provider display name", `Current value: ${updatedProvider.name}`);
|
|
457
129
|
if (name?.trim()) updatedProvider = { ...updatedProvider, name: name.trim() };
|
|
458
130
|
continue;
|
|
459
131
|
}
|
|
460
|
-
|
|
461
132
|
if (choice.startsWith("Change Base URL")) {
|
|
462
|
-
const
|
|
463
|
-
if (!
|
|
464
|
-
try {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
133
|
+
const baseUrl = await ctx.ui.input("OpenAI-compatible Base URL", `Current value: ${updatedProvider.baseUrl}`);
|
|
134
|
+
if (!baseUrl?.trim()) continue;
|
|
135
|
+
try { updatedProvider = { ...updatedProvider, baseUrl: validateBaseUrl(baseUrl) }; }
|
|
136
|
+
catch (error) { commandNotify(ctx, error instanceof Error ? error.message : String(error), "error"); }
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (choice === "Edit model limits") {
|
|
140
|
+
const edited = await promptForModelOverride(ctx, updatedProvider);
|
|
141
|
+
if (edited) updatedProvider = edited;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (choice.startsWith("Reset all model limits")) {
|
|
145
|
+
if (Object.keys(updatedProvider.modelOverrides ?? {}).length === 0) continue;
|
|
146
|
+
if (await ctx.ui.confirm("Reset model limits", `Remove all model overrides for '${provider.id}'?`)) {
|
|
147
|
+
const { modelOverrides: _old, ...withoutOverrides } = updatedProvider;
|
|
148
|
+
updatedProvider = withoutOverrides;
|
|
468
149
|
}
|
|
469
150
|
continue;
|
|
470
151
|
}
|
|
471
|
-
|
|
472
152
|
const apiKey = await promptForApiKey(ctx);
|
|
473
153
|
if (apiKey !== undefined) updatedApiKey = apiKey;
|
|
474
154
|
}
|
|
475
155
|
}
|
|
476
156
|
|
|
477
|
-
function
|
|
478
|
-
|
|
479
|
-
|
|
157
|
+
async function promptForModelOverride(
|
|
158
|
+
ctx: ExtensionCommandContext,
|
|
159
|
+
provider: StoredProvider,
|
|
160
|
+
requestedModelId?: string,
|
|
161
|
+
): Promise<StoredProvider | undefined> {
|
|
162
|
+
if (!ctx.hasUI) throw new Error("/provider model requires an interactive UI.");
|
|
163
|
+
const loadedModels = ctx.modelRegistry.getProvider(provider.id)?.getModels() ?? [];
|
|
164
|
+
const modelIds = new Set([...loadedModels.map((model) => model.id), ...Object.keys(provider.modelOverrides ?? {})]);
|
|
165
|
+
let modelId = requestedModelId;
|
|
166
|
+
if (!modelId) {
|
|
167
|
+
const modelChoices = [...modelIds].sort().map((id) => {
|
|
168
|
+
const model = loadedModels.find((item) => item.id === id);
|
|
169
|
+
const override = provider.modelOverrides?.[id];
|
|
170
|
+
const context = override?.contextWindow ?? model?.contextWindow;
|
|
171
|
+
const output = override?.maxTokens ?? model?.maxTokens;
|
|
172
|
+
const marker = override ? " · override" : "";
|
|
173
|
+
return `${id} · context ${formatTokenCount(context)} · output ${formatTokenCount(output)}${marker}`;
|
|
174
|
+
});
|
|
175
|
+
const enterModelId = "Enter a model ID…";
|
|
176
|
+
const selected = await ctx.ui.select(`Update model limits: ${provider.id}`, [...modelChoices, enterModelId]);
|
|
177
|
+
if (selected === undefined) return undefined;
|
|
178
|
+
modelId = selected === enterModelId
|
|
179
|
+
? (await ctx.ui.input("Model ID", "e.g. opencode-go/deepseek-v4.1-flash"))?.trim()
|
|
180
|
+
: [...modelIds].sort()[modelChoices.indexOf(selected)];
|
|
181
|
+
}
|
|
182
|
+
if (!modelId) return undefined;
|
|
480
183
|
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
184
|
+
const modelOverrides = { ...provider.modelOverrides };
|
|
185
|
+
let override = { ...modelOverrides[modelId] };
|
|
186
|
+
while (true) {
|
|
187
|
+
const choice = await ctx.ui.select(`Model limits: ${provider.id}/${modelId}`, [
|
|
188
|
+
`Context window: ${formatTokenCount(override.contextWindow)}`,
|
|
189
|
+
`Max output tokens: ${formatTokenCount(override.maxTokens)}`,
|
|
190
|
+
"Clear context window override",
|
|
191
|
+
"Clear max output override",
|
|
192
|
+
"Remove all overrides for this model",
|
|
193
|
+
"Save changes",
|
|
194
|
+
]);
|
|
195
|
+
if (choice === undefined) return undefined;
|
|
196
|
+
if (choice === "Save changes") {
|
|
197
|
+
if (override.contextWindow === undefined && override.maxTokens === undefined) delete modelOverrides[modelId];
|
|
198
|
+
else modelOverrides[modelId] = override;
|
|
199
|
+
const { modelOverrides: _old, ...providerWithoutOverrides } = provider;
|
|
200
|
+
return { ...providerWithoutOverrides, ...(Object.keys(modelOverrides).length ? { modelOverrides } : {}) };
|
|
201
|
+
}
|
|
202
|
+
if (choice === "Clear context window override") { const { contextWindow: _old, ...rest } = override; override = rest; continue; }
|
|
203
|
+
if (choice === "Clear max output override") { const { maxTokens: _old, ...rest } = override; override = rest; continue; }
|
|
204
|
+
if (choice === "Remove all overrides for this model") { override = {}; continue; }
|
|
205
|
+
const field = choice.startsWith("Context window") ? "contextWindow" : "maxTokens";
|
|
206
|
+
const label = field === "contextWindow" ? "Context window" : "Max output tokens";
|
|
207
|
+
const input = await ctx.ui.input(label, "Examples: 128k, 1m, 1048576");
|
|
208
|
+
if (input === undefined) continue;
|
|
209
|
+
const value = parseTokenCount(input);
|
|
210
|
+
if (value === undefined) { commandNotify(ctx, `${label} must be a positive whole token count (e.g. 128k or 1m).`, "error"); continue; }
|
|
211
|
+
override = { ...override, [field]: value };
|
|
484
212
|
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function findManagedProvider(managed: ReadonlyMap<string, StoredProvider>, name: string): StoredProvider | undefined {
|
|
216
|
+
const provider = managed.get(name);
|
|
217
|
+
if (provider) return provider;
|
|
218
|
+
const matches = [...managed.values()].filter((item) => item.name === name);
|
|
219
|
+
if (matches.length > 1) throw new Error(`Multiple providers have the display name '${name}'. Use a provider ID instead.`);
|
|
485
220
|
return matches[0];
|
|
486
221
|
}
|
|
487
222
|
|
|
488
|
-
function providerListMessage(
|
|
489
|
-
providers: readonly StoredProvider[],
|
|
490
|
-
secrets: StoredSecrets,
|
|
491
|
-
ctx: ExtensionCommandContext,
|
|
492
|
-
): string {
|
|
223
|
+
function providerListMessage(providers: readonly StoredProvider[], secrets: StoredSecrets, ctx: ExtensionCommandContext): string {
|
|
493
224
|
if (providers.length === 0) return "No managed providers.";
|
|
494
|
-
|
|
495
225
|
return ["Managed providers:", ...providers.map((provider) => {
|
|
496
226
|
const modelCount = ctx.modelRegistry.getProvider(provider.id)?.getModels().length ?? 0;
|
|
497
227
|
const apiKeyStatus = secrets.apiKeys[provider.id] ? "configured" : "missing";
|
|
498
|
-
|
|
228
|
+
const overrideCount = Object.keys(provider.modelOverrides ?? {}).length;
|
|
229
|
+
return `- ${provider.id} (${provider.name})\n Base URL: ${provider.baseUrl}\n API key: ${apiKeyStatus} · Models: ${modelCount} · Overrides: ${overrideCount}`;
|
|
499
230
|
})].join("\n");
|
|
500
231
|
}
|
|
501
232
|
|
|
@@ -503,49 +234,38 @@ export default async function (pi: ExtensionAPI) {
|
|
|
503
234
|
const state = await loadState();
|
|
504
235
|
const secrets = await loadSecrets();
|
|
505
236
|
const managed = new Map<string, StoredProvider>();
|
|
506
|
-
|
|
507
|
-
for (const provider of state.providers) {
|
|
508
|
-
if (managed.has(provider.id)) continue;
|
|
237
|
+
const register = (provider: StoredProvider) => {
|
|
509
238
|
managed.set(provider.id, provider);
|
|
510
239
|
pi.registerProvider(provider.id, createManagedProvider(provider, () => secrets.apiKeys[provider.id]));
|
|
511
|
-
}
|
|
240
|
+
};
|
|
241
|
+
for (const provider of state.providers) if (!managed.has(provider.id)) register(provider);
|
|
512
242
|
|
|
513
243
|
pi.registerCommand("provider", {
|
|
514
|
-
description: "Add,
|
|
244
|
+
description: "Add, configure models, refresh, and delete OpenAI-compatible providers",
|
|
515
245
|
handler: async (args, ctx) => {
|
|
516
246
|
try {
|
|
517
247
|
const [subcommand, ...rest] = parseArgs(args);
|
|
518
|
-
|
|
519
|
-
if (!subcommand) {
|
|
520
|
-
commandNotify(ctx, usage(), "warning");
|
|
521
|
-
return;
|
|
522
|
-
}
|
|
523
|
-
|
|
248
|
+
if (!subcommand) { commandNotify(ctx, usage(), "warning"); return; }
|
|
524
249
|
if (subcommand === "list") {
|
|
525
|
-
if (rest.length
|
|
250
|
+
if (rest.length) throw new Error("/provider list does not accept arguments.");
|
|
526
251
|
commandNotify(ctx, providerListMessage([...managed.values()], secrets, ctx));
|
|
527
252
|
return;
|
|
528
253
|
}
|
|
529
|
-
|
|
530
254
|
if (subcommand === "add") {
|
|
531
|
-
if (rest.length
|
|
255
|
+
if (rest.length) throw new Error("/provider add does not accept arguments.");
|
|
532
256
|
const provider = await promptForProvider(ctx);
|
|
533
257
|
if (!provider) return;
|
|
534
|
-
if (managed.has(provider.id) || ctx.modelRegistry.getProvider(provider.id)) {
|
|
535
|
-
throw new Error(`Provider '${provider.id}' is already registered.`);
|
|
536
|
-
}
|
|
258
|
+
if (managed.has(provider.id) || ctx.modelRegistry.getProvider(provider.id)) throw new Error(`Provider '${provider.id}' is already registered.`);
|
|
537
259
|
const apiKey = await promptForApiKey(ctx);
|
|
538
260
|
if (!apiKey) return;
|
|
539
|
-
|
|
540
261
|
const nextState: StoredState = { providers: [...state.providers, provider] };
|
|
541
262
|
const previousApiKey = secrets.apiKeys[provider.id];
|
|
542
263
|
secrets.apiKeys[provider.id] = apiKey;
|
|
543
264
|
try {
|
|
544
265
|
await saveState(nextState);
|
|
545
266
|
await saveSecrets(secrets);
|
|
546
|
-
pi.registerProvider(provider.id, createManagedProvider(provider, () => secrets.apiKeys[provider.id]));
|
|
547
267
|
state.providers = nextState.providers;
|
|
548
|
-
|
|
268
|
+
register(provider);
|
|
549
269
|
} catch (error) {
|
|
550
270
|
if (previousApiKey === undefined) delete secrets.apiKeys[provider.id];
|
|
551
271
|
else secrets.apiKeys[provider.id] = previousApiKey;
|
|
@@ -554,87 +274,76 @@ export default async function (pi: ExtensionAPI) {
|
|
|
554
274
|
pi.unregisterProvider(provider.id);
|
|
555
275
|
throw error;
|
|
556
276
|
}
|
|
557
|
-
|
|
558
277
|
commandNotify(ctx, `Provider '${provider.id}' was added. Fetching its model list.`);
|
|
559
278
|
await refreshProviders(ctx, [provider.id]);
|
|
560
279
|
return;
|
|
561
280
|
}
|
|
562
|
-
|
|
563
281
|
if (subcommand === "edit") {
|
|
564
|
-
if (rest.length
|
|
565
|
-
const
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
if (!edit) return;
|
|
570
|
-
|
|
571
|
-
edit.provider.name !== currentProvider.name || edit.provider.baseUrl !== currentProvider.baseUrl;
|
|
572
|
-
if (!providerChanged && edit.apiKey === undefined) {
|
|
573
|
-
commandNotify(ctx, "No provider settings were changed.", "warning");
|
|
574
|
-
return;
|
|
575
|
-
}
|
|
576
|
-
|
|
282
|
+
if (rest.length > 1) throw new Error("Usage: /provider edit [provider_name]");
|
|
283
|
+
const current = rest.length === 1
|
|
284
|
+
? findManagedProvider(managed, rest[0])
|
|
285
|
+
: await chooseManagedProvider(ctx, managed, "Edit provider");
|
|
286
|
+
if (!current) return;
|
|
287
|
+
const edit = await promptForProviderEdit(ctx, current); if (!edit) return;
|
|
288
|
+
if (edit.provider.name === current.name && edit.provider.baseUrl === current.baseUrl && edit.apiKey === undefined) { commandNotify(ctx, "No provider settings were changed.", "warning"); return; }
|
|
577
289
|
const previousState: StoredState = { providers: [...state.providers] };
|
|
578
|
-
const previousApiKey = secrets.apiKeys[
|
|
579
|
-
const nextState: StoredState = {
|
|
580
|
-
|
|
581
|
-
provider.id === currentProvider.id ? edit.provider : provider,
|
|
582
|
-
),
|
|
583
|
-
};
|
|
584
|
-
if (edit.apiKey !== undefined) secrets.apiKeys[currentProvider.id] = edit.apiKey;
|
|
585
|
-
|
|
290
|
+
const previousApiKey = secrets.apiKeys[current.id];
|
|
291
|
+
const nextState: StoredState = { providers: state.providers.map((item) => item.id === current.id ? edit.provider : item) };
|
|
292
|
+
if (edit.apiKey !== undefined) secrets.apiKeys[current.id] = edit.apiKey;
|
|
586
293
|
try {
|
|
587
294
|
await saveState(nextState);
|
|
588
295
|
if (edit.apiKey !== undefined) await saveSecrets(secrets);
|
|
589
|
-
pi.registerProvider(
|
|
590
|
-
currentProvider.id,
|
|
591
|
-
createManagedProvider(edit.provider, () => secrets.apiKeys[currentProvider.id]),
|
|
592
|
-
);
|
|
593
296
|
state.providers = nextState.providers;
|
|
594
|
-
|
|
297
|
+
register(edit.provider);
|
|
595
298
|
} catch (error) {
|
|
596
|
-
if (previousApiKey === undefined) delete secrets.apiKeys[
|
|
597
|
-
else secrets.apiKeys[
|
|
299
|
+
if (previousApiKey === undefined) delete secrets.apiKeys[current.id];
|
|
300
|
+
else secrets.apiKeys[current.id] = previousApiKey;
|
|
598
301
|
await saveState(previousState).catch(() => undefined);
|
|
599
302
|
if (edit.apiKey !== undefined) await saveSecrets(secrets).catch(() => undefined);
|
|
600
|
-
|
|
601
|
-
currentProvider.id,
|
|
602
|
-
createManagedProvider(currentProvider, () => secrets.apiKeys[currentProvider.id]),
|
|
603
|
-
);
|
|
303
|
+
register(current);
|
|
604
304
|
throw error;
|
|
605
305
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
await refreshProviders(ctx, [currentProvider.id]);
|
|
306
|
+
commandNotify(ctx, `Saved settings for provider '${current.id}'. Fetching its model list.`);
|
|
307
|
+
await refreshProviders(ctx, [current.id]);
|
|
609
308
|
return;
|
|
610
309
|
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
310
|
+
if (subcommand === "model") {
|
|
311
|
+
if (rest.length > 2) throw new Error("Usage: /provider model [provider_name] [model_id]");
|
|
312
|
+
const current = rest.length > 0
|
|
313
|
+
? findManagedProvider(managed, rest[0])
|
|
314
|
+
: await chooseManagedProvider(ctx, managed, "Edit model limits");
|
|
315
|
+
if (!current) return;
|
|
316
|
+
const updated = await promptForModelOverride(ctx, current, rest[1]); if (!updated) return;
|
|
317
|
+
const previousState: StoredState = { providers: [...state.providers] };
|
|
318
|
+
const nextState: StoredState = { providers: state.providers.map((item) => item.id === current.id ? updated : item) };
|
|
319
|
+
try {
|
|
320
|
+
await saveState(nextState);
|
|
321
|
+
state.providers = nextState.providers;
|
|
322
|
+
register(updated);
|
|
323
|
+
} catch (error) {
|
|
324
|
+
await saveState(previousState).catch(() => undefined);
|
|
325
|
+
register(current);
|
|
326
|
+
throw error;
|
|
617
327
|
}
|
|
618
|
-
|
|
328
|
+
commandNotify(ctx, `Saved model config overrides for provider '${current.id}'. Fetching its model list.`);
|
|
329
|
+
await refreshProviders(ctx, [current.id]);
|
|
619
330
|
return;
|
|
620
331
|
}
|
|
621
|
-
|
|
332
|
+
if (subcommand === "reload") {
|
|
333
|
+
if (rest.length !== 1) throw new Error("Usage: /provider reload --all | <provider_name>");
|
|
334
|
+
const ids = rest[0] === "--all" ? [...managed.keys()] : [rest[0]];
|
|
335
|
+
for (const id of ids) if (!managed.has(id)) throw new Error(`Managed provider '${id}' was not found.`);
|
|
336
|
+
await refreshProviders(ctx, ids); return;
|
|
337
|
+
}
|
|
622
338
|
if (subcommand === "delete") {
|
|
623
339
|
if (rest.length !== 1) throw new Error("Usage: /provider delete <provider_name>");
|
|
624
340
|
const providerId = rest[0];
|
|
625
341
|
if (!managed.has(providerId)) throw new Error(`Managed provider '${providerId}' was not found.`);
|
|
626
342
|
if (!ctx.hasUI) throw new Error("/provider delete requires an interactive UI.");
|
|
627
|
-
|
|
628
|
-
"Delete provider",
|
|
629
|
-
`Delete provider '${providerId}' and its stored API key?`,
|
|
630
|
-
);
|
|
631
|
-
if (!shouldDelete) return;
|
|
632
|
-
|
|
343
|
+
if (!await ctx.ui.confirm("Delete provider", `Delete provider '${providerId}' and its stored API key?`)) return;
|
|
633
344
|
const previousState: StoredState = { providers: [...state.providers] };
|
|
634
345
|
const previousApiKey = secrets.apiKeys[providerId];
|
|
635
|
-
const nextState: StoredState = {
|
|
636
|
-
providers: state.providers.filter((provider) => provider.id !== providerId),
|
|
637
|
-
};
|
|
346
|
+
const nextState: StoredState = { providers: state.providers.filter((item) => item.id !== providerId) };
|
|
638
347
|
delete secrets.apiKeys[providerId];
|
|
639
348
|
try {
|
|
640
349
|
await saveState(nextState);
|
|
@@ -651,23 +360,14 @@ export default async function (pi: ExtensionAPI) {
|
|
|
651
360
|
commandNotify(ctx, `Provider '${providerId}' was deleted.`);
|
|
652
361
|
return;
|
|
653
362
|
}
|
|
654
|
-
|
|
655
363
|
throw new Error(`Unknown provider command: ${subcommand}\n\n${usage()}`);
|
|
656
|
-
} catch (error) {
|
|
657
|
-
commandNotify(ctx, error instanceof Error ? error.message : String(error), "error");
|
|
658
|
-
}
|
|
364
|
+
} catch (error) { commandNotify(ctx, error instanceof Error ? error.message : String(error), "error"); }
|
|
659
365
|
},
|
|
660
366
|
});
|
|
661
367
|
|
|
662
368
|
pi.on("session_start", async (_event, ctx) => {
|
|
663
369
|
if (managed.size === 0) return;
|
|
664
|
-
const result = await ctx.modelRegistry.refresh({
|
|
665
|
-
|
|
666
|
-
allowNetwork: true,
|
|
667
|
-
signal: AbortSignal.timeout(REFRESH_TIMEOUT_MS),
|
|
668
|
-
});
|
|
669
|
-
if (result.errors.size > 0 && ctx.hasUI) {
|
|
670
|
-
ctx.ui.notify(`Could not refresh model lists for: ${[...result.errors.keys()].join(", ")}`, "warning");
|
|
671
|
-
}
|
|
370
|
+
const result = await ctx.modelRegistry.refresh({ providers: [...managed.keys()], allowNetwork: true, signal: AbortSignal.timeout(REFRESH_TIMEOUT_MS) });
|
|
371
|
+
if (result.errors.size > 0 && ctx.hasUI) ctx.ui.notify(`Could not refresh model lists for: ${[...result.errors.keys()].join(", ")}`, "warning");
|
|
672
372
|
});
|
|
673
373
|
}
|