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