@maheidem/model-discovery 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +49 -12
- package/application.ts +104 -0
- package/commands.ts +70 -0
- package/index.ts +475 -452
- package/package.json +28 -5
- package/storage.ts +265 -0
- package/ui/wizard-shell.ts +442 -0
- package/ui-model.ts +127 -0
- package/scripts/bisect-grammar.ts +0 -65
- package/scripts/live-schema-repair-check.ts +0 -86
package/index.ts
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
18
|
-
import { BorderedLoader
|
|
19
|
-
import {
|
|
18
|
+
import { BorderedLoader } from "@earendil-works/pi-coding-agent";
|
|
19
|
+
import type { SelectItem } from "@earendil-works/pi-tui";
|
|
20
20
|
import { Type } from "typebox";
|
|
21
21
|
import {
|
|
22
22
|
analyzeExplicitProfileRouting,
|
|
@@ -26,7 +26,6 @@ import {
|
|
|
26
26
|
describeProfileSampling,
|
|
27
27
|
expandAdaptiveProfileRouters,
|
|
28
28
|
expandModelProfiles,
|
|
29
|
-
migrateLegacyProfileRouting,
|
|
30
29
|
profileModelId,
|
|
31
30
|
REASONING_EFFORTS,
|
|
32
31
|
repetitionPenaltyKeyForServer,
|
|
@@ -52,205 +51,40 @@ import {
|
|
|
52
51
|
repairRequestToolSchemas,
|
|
53
52
|
type ToolSchemaRepairReport,
|
|
54
53
|
} from "./schema-repair.ts";
|
|
55
|
-
import {
|
|
56
|
-
import {
|
|
57
|
-
import
|
|
54
|
+
import { createDiscoveryApplication } from "./application.ts";
|
|
55
|
+
import { completeDiscoverArgs, DISCOVER_USAGE, parseDiscoverArgs } from "./commands.ts";
|
|
56
|
+
import {
|
|
57
|
+
errorMessage,
|
|
58
|
+
recordFailedScan,
|
|
59
|
+
recordSuccessfulScan,
|
|
60
|
+
STORAGE_PATH,
|
|
61
|
+
type DiscoveredProvider,
|
|
62
|
+
type ModelOverride,
|
|
63
|
+
} from "./storage.ts";
|
|
64
|
+
import {
|
|
65
|
+
buildDiagnosticsLines,
|
|
66
|
+
buildHomeItems,
|
|
67
|
+
buildHomeSummary,
|
|
68
|
+
formatDiscoveryStatus,
|
|
69
|
+
} from "./ui-model.ts";
|
|
70
|
+
import { WizardInput, WizardSecretInput, WizardSelect, WizardTextView } from "./ui/wizard-shell.ts";
|
|
58
71
|
|
|
59
72
|
// ---------------------------------------------------------------------------
|
|
60
73
|
// Types
|
|
61
74
|
// ---------------------------------------------------------------------------
|
|
62
75
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
interface DiscoveredProvider {
|
|
70
|
-
name: string;
|
|
71
|
-
baseUrl: string;
|
|
72
|
-
apiKey?: string;
|
|
73
|
-
serverType?: string;
|
|
74
|
-
defaultContextWindow?: number;
|
|
75
|
-
defaultMaxTokens?: number;
|
|
76
|
-
modelOverrides?: Record<string, ModelOverride>;
|
|
77
|
-
modelProfiles?: Record<string, ModelProfile[]>;
|
|
78
|
-
modelProfileRouting?: Record<string, ModelProfileRouting>;
|
|
79
|
-
profileSchemaVersion?: number;
|
|
80
|
-
cachedModels?: Record<string, unknown>[];
|
|
81
|
-
compat?: Record<string, unknown>;
|
|
82
|
-
/**
|
|
83
|
-
* Inline $defs/$ref in outgoing tool schemas for this endpoint (default: true for
|
|
84
|
-
* local/self-hosted endpoints, where llama.cpp-style grammar converters reject any
|
|
85
|
-
* $ref that is not resolvable at the document root). Set false to send verbatim.
|
|
86
|
-
*/
|
|
87
|
-
repairToolSchemas?: boolean;
|
|
88
|
-
/** Last successful live catalogue refresh (legacy name retained in storage). */
|
|
89
|
-
lastScanned?: number;
|
|
90
|
-
lastScanAttempt?: number;
|
|
91
|
-
lastScanError?: string;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// ---------------------------------------------------------------------------
|
|
95
|
-
// Storage
|
|
96
|
-
// ---------------------------------------------------------------------------
|
|
97
|
-
|
|
98
|
-
const STORAGE_PATH = join(os.homedir(), ".pi", "agent", "model-discovery.json");
|
|
99
|
-
|
|
100
|
-
function writeProvidersAtomic(providers: DiscoveredProvider[]): void {
|
|
101
|
-
const tempPath = `${STORAGE_PATH}.${process.pid}.${Date.now()}.tmp`;
|
|
102
|
-
try {
|
|
103
|
-
writeFileSync(tempPath, JSON.stringify(providers, null, 2), { encoding: "utf-8", mode: 0o600 });
|
|
104
|
-
renameSync(tempPath, STORAGE_PATH);
|
|
105
|
-
} catch (error) {
|
|
106
|
-
try {
|
|
107
|
-
if (existsSync(tempPath)) unlinkSync(tempPath);
|
|
108
|
-
} catch {
|
|
109
|
-
/* best-effort cleanup */
|
|
110
|
-
}
|
|
111
|
-
throw error;
|
|
76
|
+
function normalizeEndpointUrl(rawUrl: string): string {
|
|
77
|
+
let url = rawUrl.trim();
|
|
78
|
+
if (!url) throw new Error("Endpoint URL cannot be blank.");
|
|
79
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(url) && !/^https?:\/\//i.test(url)) {
|
|
80
|
+
throw new Error("Endpoint URL must use HTTP or HTTPS.");
|
|
112
81
|
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (existsSync(STORAGE_PATH)) {
|
|
118
|
-
const providers = JSON.parse(readFileSync(STORAGE_PATH, "utf-8")) as DiscoveredProvider[];
|
|
119
|
-
let migrated = false;
|
|
120
|
-
for (const provider of providers) {
|
|
121
|
-
if ((provider.profileSchemaVersion ?? 0) >= 2) continue;
|
|
122
|
-
for (const [modelId, rawProfiles] of Object.entries(provider.modelProfiles ?? {})) {
|
|
123
|
-
if (!Array.isArray(rawProfiles)) continue;
|
|
124
|
-
const result = migrateLegacyProfileRouting(rawProfiles, provider.modelProfileRouting?.[modelId]);
|
|
125
|
-
if (!result.changed || !result.routing) continue;
|
|
126
|
-
provider.modelProfiles = { ...provider.modelProfiles, [modelId]: result.profiles };
|
|
127
|
-
provider.modelProfileRouting = { ...provider.modelProfileRouting, [modelId]: result.routing };
|
|
128
|
-
migrated = true;
|
|
129
|
-
}
|
|
130
|
-
provider.profileSchemaVersion = 2;
|
|
131
|
-
migrated = true;
|
|
132
|
-
}
|
|
133
|
-
if (migrated) writeProvidersAtomic(providers);
|
|
134
|
-
return providers;
|
|
135
|
-
}
|
|
136
|
-
} catch {
|
|
137
|
-
/* ignore */
|
|
82
|
+
if (!/^https?:\/\//i.test(url)) url = `http://${url}`;
|
|
83
|
+
const parsed = new URL(url);
|
|
84
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
85
|
+
throw new Error("Endpoint URL must use HTTP or HTTPS.");
|
|
138
86
|
}
|
|
139
|
-
return
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function saveProviders(providers: DiscoveredProvider[]): void {
|
|
143
|
-
writeProvidersAtomic(providers);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function upsertProvider(provider: DiscoveredProvider): void {
|
|
147
|
-
const all = loadProviders();
|
|
148
|
-
const idx = all.findIndex((p) => p.name === provider.name);
|
|
149
|
-
if (idx >= 0) all[idx] = provider;
|
|
150
|
-
else all.push(provider);
|
|
151
|
-
saveProviders(all);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function deleteProvider(name: string): void {
|
|
155
|
-
saveProviders(loadProviders().filter((p) => p.name !== name));
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function errorMessage(error: unknown): string {
|
|
159
|
-
return error instanceof Error ? error.message : String(error);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function persistProviderScanState(provider: DiscoveredProvider): void {
|
|
163
|
-
try {
|
|
164
|
-
const providers = loadProviders();
|
|
165
|
-
const stored = providers.find((candidate) => candidate.name === provider.name && candidate.baseUrl === provider.baseUrl);
|
|
166
|
-
if (!stored) return;
|
|
167
|
-
stored.serverType = provider.serverType;
|
|
168
|
-
stored.cachedModels = provider.cachedModels;
|
|
169
|
-
stored.lastScanned = provider.lastScanned;
|
|
170
|
-
stored.lastScanAttempt = provider.lastScanAttempt;
|
|
171
|
-
stored.lastScanError = provider.lastScanError;
|
|
172
|
-
saveProviders(providers);
|
|
173
|
-
} catch (error) {
|
|
174
|
-
// Runtime registration must not fail merely because scan metadata could not be persisted.
|
|
175
|
-
console.error(`[model-discovery] ${provider.name}: could not persist catalogue state (${errorMessage(error)}).`);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function recordSuccessfulScan(
|
|
180
|
-
provider: DiscoveredProvider,
|
|
181
|
-
models: Record<string, unknown>[],
|
|
182
|
-
serverType: string,
|
|
183
|
-
persist = true,
|
|
184
|
-
): void {
|
|
185
|
-
const now = Date.now();
|
|
186
|
-
provider.serverType = serverType;
|
|
187
|
-
provider.cachedModels = models;
|
|
188
|
-
provider.lastScanned = now;
|
|
189
|
-
provider.lastScanAttempt = now;
|
|
190
|
-
provider.lastScanError = undefined;
|
|
191
|
-
if (persist) persistProviderScanState(provider);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function recordFailedScan(provider: DiscoveredProvider, error: unknown, persist = true): void {
|
|
195
|
-
provider.lastScanAttempt = Date.now();
|
|
196
|
-
provider.lastScanError = redactSecret(errorMessage(error), provider.apiKey);
|
|
197
|
-
if (persist) persistProviderScanState(provider);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function renameProvider(oldName: string, newName: string): boolean {
|
|
201
|
-
const all = loadProviders();
|
|
202
|
-
const idx = all.findIndex((p) => p.name === oldName);
|
|
203
|
-
if (idx < 0) return false;
|
|
204
|
-
if (all.some((p) => p.name === newName)) return false; // name already taken
|
|
205
|
-
all[idx].name = newName;
|
|
206
|
-
saveProviders(all);
|
|
207
|
-
return true;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function getModelProfiles(provider: DiscoveredProvider, modelId: string): ModelProfile[] {
|
|
211
|
-
const profiles: unknown = provider.modelProfiles?.[modelId];
|
|
212
|
-
if (!Array.isArray(profiles)) return [];
|
|
213
|
-
return profiles.filter((profile): profile is ModelProfile => validateModelProfile(profile) === null);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function saveModelProfile(
|
|
217
|
-
provider: DiscoveredProvider,
|
|
218
|
-
modelId: string,
|
|
219
|
-
profile: ModelProfile,
|
|
220
|
-
previousSlug?: string,
|
|
221
|
-
): void {
|
|
222
|
-
const profiles = getModelProfiles(provider, modelId);
|
|
223
|
-
const index = previousSlug === undefined ? -1 : profiles.findIndex((item) => item.slug === previousSlug);
|
|
224
|
-
const next = [...profiles];
|
|
225
|
-
if (index >= 0) next[index] = profile;
|
|
226
|
-
else next.push(profile);
|
|
227
|
-
provider.modelProfiles = { ...provider.modelProfiles, [modelId]: next };
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function deleteModelProfile(provider: DiscoveredProvider, modelId: string, slug: string): void {
|
|
231
|
-
const nextProfiles = getModelProfiles(provider, modelId).filter((profile) => profile.slug !== slug);
|
|
232
|
-
const modelProfiles = { ...provider.modelProfiles };
|
|
233
|
-
if (nextProfiles.length > 0) modelProfiles[modelId] = nextProfiles;
|
|
234
|
-
else delete modelProfiles[modelId];
|
|
235
|
-
provider.modelProfiles = Object.keys(modelProfiles).length > 0 ? modelProfiles : undefined;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function getModelProfileRouting(provider: DiscoveredProvider, modelId: string): ModelProfileRouting | undefined {
|
|
239
|
-
return provider.modelProfileRouting?.[modelId];
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function saveModelProfileRouting(
|
|
243
|
-
provider: DiscoveredProvider,
|
|
244
|
-
modelId: string,
|
|
245
|
-
routing: ModelProfileRouting,
|
|
246
|
-
): void {
|
|
247
|
-
provider.modelProfileRouting = { ...provider.modelProfileRouting, [modelId]: routing };
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function deleteModelProfileRouting(provider: DiscoveredProvider, modelId: string): void {
|
|
251
|
-
const routing = { ...provider.modelProfileRouting };
|
|
252
|
-
delete routing[modelId];
|
|
253
|
-
provider.modelProfileRouting = Object.keys(routing).length > 0 ? routing : undefined;
|
|
87
|
+
return url.replace(/\/+$/, "");
|
|
254
88
|
}
|
|
255
89
|
|
|
256
90
|
function generateProviderName(url: string): string {
|
|
@@ -273,6 +107,8 @@ function fmt(n: number | null | undefined): string {
|
|
|
273
107
|
// ---------------------------------------------------------------------------
|
|
274
108
|
|
|
275
109
|
export default async function (pi: ExtensionAPI) {
|
|
110
|
+
const app = createDiscoveryApplication();
|
|
111
|
+
|
|
276
112
|
type RuntimeThinkingRoutes = {
|
|
277
113
|
routes: ThinkingProfileRoutes;
|
|
278
114
|
repetitionPenaltyKey: ReturnType<typeof repetitionPenaltyKeyForServer>;
|
|
@@ -316,8 +152,9 @@ export default async function (pi: ExtensionAPI) {
|
|
|
316
152
|
async function registerProvider(
|
|
317
153
|
provider: DiscoveredProvider,
|
|
318
154
|
prefetched?: { models: Record<string, unknown>[]; serverType: string },
|
|
155
|
+
signal: AbortSignal = AbortSignal.timeout(2_000),
|
|
319
156
|
): Promise<{ models: ModelConfig[]; rawModels: Record<string, unknown>[]; serverType: string; profileCount: number }> {
|
|
320
|
-
const { models, serverType } = prefetched ?? (await fetchModels(provider.baseUrl, provider.apiKey,
|
|
157
|
+
const { models, serverType } = prefetched ?? (await fetchModels(provider.baseUrl, provider.apiKey, signal));
|
|
321
158
|
if (models.length === 0) throw new Error("No models found at this endpoint.");
|
|
322
159
|
|
|
323
160
|
const routePrefix = `${provider.name}/`;
|
|
@@ -383,7 +220,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
383
220
|
thinkingRoutes.set(routeKey(provider.name, modelId), { routes, repetitionPenaltyKey });
|
|
384
221
|
}
|
|
385
222
|
for (const base of baseModels) {
|
|
386
|
-
for (const profile of
|
|
223
|
+
for (const profile of app.profiles(provider, base.id)) {
|
|
387
224
|
if (profile.exposeAsModel !== false) {
|
|
388
225
|
fixedProfileLabels.set(routeKey(provider.name, profileModelId(base.id, profile.slug)), profile.slug);
|
|
389
226
|
}
|
|
@@ -410,8 +247,40 @@ export default async function (pi: ExtensionAPI) {
|
|
|
410
247
|
};
|
|
411
248
|
}
|
|
412
249
|
|
|
250
|
+
async function discoverAndRegisterSource(options: {
|
|
251
|
+
url: string;
|
|
252
|
+
providerName?: string;
|
|
253
|
+
apiKey?: string;
|
|
254
|
+
signal?: AbortSignal;
|
|
255
|
+
}): Promise<{
|
|
256
|
+
provider: DiscoveredProvider;
|
|
257
|
+
models: ModelConfig[];
|
|
258
|
+
rawModels: Record<string, unknown>[];
|
|
259
|
+
serverType: string;
|
|
260
|
+
profileCount: number;
|
|
261
|
+
}> {
|
|
262
|
+
const url = normalizeEndpointUrl(options.url);
|
|
263
|
+
const providerName = options.providerName?.trim() || generateProviderName(url);
|
|
264
|
+
const live = await fetchModels(url, options.apiKey, options.signal);
|
|
265
|
+
if (live.models.length === 0) throw new Error("Endpoint is online but reports no models.");
|
|
266
|
+
const existing = app.findSource(providerName);
|
|
267
|
+
const provider: DiscoveredProvider = existing
|
|
268
|
+
? { ...existing, baseUrl: url, apiKey: options.apiKey ?? existing.apiKey }
|
|
269
|
+
: { name: providerName, baseUrl: url, apiKey: options.apiKey };
|
|
270
|
+
const registered = await registerProvider(provider, live);
|
|
271
|
+
recordSuccessfulScan(provider, live.models, live.serverType, false);
|
|
272
|
+
app.saveSource(provider);
|
|
273
|
+
return {
|
|
274
|
+
provider,
|
|
275
|
+
models: registered.models,
|
|
276
|
+
rawModels: live.models,
|
|
277
|
+
serverType: live.serverType,
|
|
278
|
+
profileCount: registered.profileCount,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
413
282
|
// Register saved providers at startup (concurrent — one dead endpoint can't block the others)
|
|
414
|
-
const providers =
|
|
283
|
+
const providers = app.listSources();
|
|
415
284
|
if (providers.length > 0) {
|
|
416
285
|
const results = await Promise.allSettled(
|
|
417
286
|
providers.map(async (provider) => {
|
|
@@ -501,42 +370,67 @@ export default async function (pi: ExtensionAPI) {
|
|
|
501
370
|
items: SelectItem[],
|
|
502
371
|
headerLines: string[] = [],
|
|
503
372
|
): Promise<string | null> {
|
|
504
|
-
return await ctx.ui.custom<string | null>(
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
container.addChild(new Text(theme.fg("muted", line), 1, 0));
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
const selectList = new SelectList(
|
|
373
|
+
return await ctx.ui.custom<string | null>(
|
|
374
|
+
(tui, theme, keybindings, done) => new WizardSelect({
|
|
375
|
+
theme,
|
|
376
|
+
keybindings,
|
|
377
|
+
title,
|
|
513
378
|
items,
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
selectList.onSelect = (item) => done(item.value);
|
|
525
|
-
selectList.onCancel = () => done(null);
|
|
526
|
-
container.addChild(selectList);
|
|
379
|
+
headerLines,
|
|
380
|
+
requestRender: () => tui.requestRender(),
|
|
381
|
+
done,
|
|
382
|
+
}),
|
|
383
|
+
{
|
|
384
|
+
overlay: true,
|
|
385
|
+
overlayOptions: { anchor: "center", width: 88, minWidth: 36, maxHeight: "90%", margin: 1 },
|
|
386
|
+
},
|
|
387
|
+
);
|
|
388
|
+
}
|
|
527
389
|
|
|
528
|
-
|
|
529
|
-
|
|
390
|
+
async function runTextView(
|
|
391
|
+
ctx: ExtensionCommandContext,
|
|
392
|
+
title: string,
|
|
393
|
+
lines: string[],
|
|
394
|
+
): Promise<void> {
|
|
395
|
+
await ctx.ui.custom<void>(
|
|
396
|
+
(tui, theme, keybindings, done) => new WizardTextView({
|
|
397
|
+
theme,
|
|
398
|
+
keybindings,
|
|
399
|
+
title,
|
|
400
|
+
lines,
|
|
401
|
+
requestRender: () => tui.requestRender(),
|
|
402
|
+
done: () => done(),
|
|
403
|
+
}),
|
|
404
|
+
{
|
|
405
|
+
overlay: true,
|
|
406
|
+
overlayOptions: { anchor: "center", width: 88, minWidth: 36, maxHeight: "90%", margin: 1 },
|
|
407
|
+
},
|
|
408
|
+
);
|
|
409
|
+
}
|
|
530
410
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
411
|
+
async function runInput(
|
|
412
|
+
ctx: ExtensionCommandContext,
|
|
413
|
+
title: string,
|
|
414
|
+
initialValue = "",
|
|
415
|
+
description?: string,
|
|
416
|
+
validate?: (value: string) => string | null,
|
|
417
|
+
): Promise<string | undefined> {
|
|
418
|
+
return await ctx.ui.custom<string | undefined>(
|
|
419
|
+
(tui, theme, keybindings, done) => new WizardInput({
|
|
420
|
+
theme,
|
|
421
|
+
keybindings,
|
|
422
|
+
title,
|
|
423
|
+
description,
|
|
424
|
+
initialValue,
|
|
425
|
+
validate,
|
|
426
|
+
requestRender: () => tui.requestRender(),
|
|
427
|
+
done,
|
|
428
|
+
}),
|
|
429
|
+
{
|
|
430
|
+
overlay: true,
|
|
431
|
+
overlayOptions: { anchor: "center", width: 72, minWidth: 36, maxHeight: "90%", margin: 1 },
|
|
432
|
+
},
|
|
433
|
+
);
|
|
540
434
|
}
|
|
541
435
|
|
|
542
436
|
async function runLoader<T>(
|
|
@@ -545,18 +439,31 @@ export default async function (pi: ExtensionAPI) {
|
|
|
545
439
|
work: (signal: AbortSignal) => Promise<T>,
|
|
546
440
|
onError?: (error: unknown) => void,
|
|
547
441
|
): Promise<T | null> {
|
|
548
|
-
return await ctx.ui.custom<T | null>(
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
.
|
|
553
|
-
|
|
554
|
-
onError?.(err);
|
|
555
|
-
ctx.ui.notify(errorMessage(err), "error");
|
|
442
|
+
return await ctx.ui.custom<T | null>(
|
|
443
|
+
(tui, theme, _keybindings, done) => {
|
|
444
|
+
const loader = new BorderedLoader(tui, theme, message);
|
|
445
|
+
let settled = false;
|
|
446
|
+
loader.onAbort = () => {
|
|
447
|
+
settled = true;
|
|
556
448
|
done(null);
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
|
|
449
|
+
};
|
|
450
|
+
work(loader.signal)
|
|
451
|
+
.then((result) => {
|
|
452
|
+
if (!settled) done(result);
|
|
453
|
+
})
|
|
454
|
+
.catch((err) => {
|
|
455
|
+
if (settled) return;
|
|
456
|
+
onError?.(err);
|
|
457
|
+
ctx.ui.notify(errorMessage(err), "error");
|
|
458
|
+
done(null);
|
|
459
|
+
});
|
|
460
|
+
return loader;
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
overlay: true,
|
|
464
|
+
overlayOptions: { anchor: "center", width: 72, minWidth: 36, maxHeight: "90%", margin: 1 },
|
|
465
|
+
},
|
|
466
|
+
);
|
|
560
467
|
}
|
|
561
468
|
|
|
562
469
|
async function askSecret(
|
|
@@ -564,54 +471,41 @@ export default async function (pi: ExtensionAPI) {
|
|
|
564
471
|
title: string,
|
|
565
472
|
description: string,
|
|
566
473
|
): Promise<string | undefined> {
|
|
567
|
-
return await ctx.ui.custom<string | undefined>(
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
const marker = input.focused ? CURSOR_MARKER : "";
|
|
582
|
-
return [truncateToWidth(`> ${masked}${marker}\x1b[7m \x1b[27m`, width, "")];
|
|
583
|
-
},
|
|
584
|
-
invalidate: () => {},
|
|
585
|
-
});
|
|
586
|
-
container.addChild(new Text(theme.fg("dim", "enter submit • esc cancel • value is masked"), 1, 0));
|
|
587
|
-
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
588
|
-
|
|
589
|
-
return {
|
|
590
|
-
get focused() {
|
|
591
|
-
return input.focused;
|
|
592
|
-
},
|
|
593
|
-
set focused(value: boolean) {
|
|
594
|
-
input.focused = value;
|
|
595
|
-
},
|
|
596
|
-
render: (width: number) => container.render(width),
|
|
597
|
-
invalidate: () => container.invalidate(),
|
|
598
|
-
handleInput: (data: string) => {
|
|
599
|
-
input.handleInput(data);
|
|
600
|
-
tui.requestRender();
|
|
601
|
-
},
|
|
602
|
-
};
|
|
603
|
-
});
|
|
474
|
+
return await ctx.ui.custom<string | undefined>(
|
|
475
|
+
(tui, theme, keybindings, done) => new WizardSecretInput({
|
|
476
|
+
theme,
|
|
477
|
+
keybindings,
|
|
478
|
+
title,
|
|
479
|
+
description,
|
|
480
|
+
requestRender: () => tui.requestRender(),
|
|
481
|
+
done,
|
|
482
|
+
}),
|
|
483
|
+
{
|
|
484
|
+
overlay: true,
|
|
485
|
+
overlayOptions: { anchor: "center", width: 72, minWidth: 36, maxHeight: "90%", margin: 1 },
|
|
486
|
+
},
|
|
487
|
+
);
|
|
604
488
|
}
|
|
605
489
|
|
|
606
490
|
async function askNumber(
|
|
607
491
|
ctx: ExtensionCommandContext,
|
|
608
492
|
title: string,
|
|
609
|
-
|
|
493
|
+
initialValue: string,
|
|
610
494
|
): Promise<number | undefined> {
|
|
611
|
-
const raw =
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
495
|
+
const raw = await runInput(
|
|
496
|
+
ctx,
|
|
497
|
+
title,
|
|
498
|
+
initialValue,
|
|
499
|
+
"Enter a positive whole number. Clear the field and submit to keep the current value.",
|
|
500
|
+
(value) => {
|
|
501
|
+
const normalized = value.trim().replace(/[,._\s]/g, "");
|
|
502
|
+
if (!normalized) return null;
|
|
503
|
+
if (!/^\d+$/.test(normalized) || Number(normalized) <= 0) return "Enter a positive whole number greater than zero.";
|
|
504
|
+
return null;
|
|
505
|
+
},
|
|
506
|
+
);
|
|
507
|
+
const normalized = raw?.trim().replace(/[,._\s]/g, "");
|
|
508
|
+
return normalized ? Number(normalized) : undefined;
|
|
615
509
|
}
|
|
616
510
|
|
|
617
511
|
function modelFlags(c: ModelConfig, ov?: ModelOverride): string {
|
|
@@ -721,19 +615,18 @@ export default async function (pi: ExtensionAPI) {
|
|
|
721
615
|
if (action === null) return null;
|
|
722
616
|
if (action === "omit") return undefined;
|
|
723
617
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
}
|
|
618
|
+
const raw = await runInput(
|
|
619
|
+
ctx,
|
|
620
|
+
`Value for ${field.label}`,
|
|
621
|
+
String(current ?? field.example),
|
|
622
|
+
field.description,
|
|
623
|
+
(value) => {
|
|
624
|
+
const trimmed = value.trim();
|
|
625
|
+
if (!trimmed) return "Enter a numeric value, or return and choose Omit.";
|
|
626
|
+
return validateProfileSampling({ [field.key]: Number(trimmed) });
|
|
627
|
+
},
|
|
628
|
+
);
|
|
629
|
+
return raw === undefined ? null : Number(raw.trim());
|
|
737
630
|
}
|
|
738
631
|
|
|
739
632
|
function profileDescription(
|
|
@@ -782,7 +675,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
782
675
|
candidate: ModelProfile,
|
|
783
676
|
previousSlug?: string,
|
|
784
677
|
): ModelProfile[] {
|
|
785
|
-
const profiles =
|
|
678
|
+
const profiles = app.profiles(provider, modelId);
|
|
786
679
|
const index = previousSlug === undefined ? -1 : profiles.findIndex((profile) => profile.slug === previousSlug);
|
|
787
680
|
if (index < 0) return [...profiles, candidate];
|
|
788
681
|
return profiles.map((profile, profileIndex) => (profileIndex === index ? candidate : profile));
|
|
@@ -792,14 +685,14 @@ export default async function (pi: ExtensionAPI) {
|
|
|
792
685
|
ctx: ExtensionCommandContext,
|
|
793
686
|
initial: string,
|
|
794
687
|
): Promise<string | null> {
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
688
|
+
const answer = await runInput(
|
|
689
|
+
ctx,
|
|
690
|
+
"Preset name",
|
|
691
|
+
initial || "thinking-medium",
|
|
692
|
+
"Use a short slug for the fixed model alias and routing map.",
|
|
693
|
+
(value) => validateProfileSlug(value.trim()),
|
|
694
|
+
);
|
|
695
|
+
return answer === undefined ? null : answer.trim();
|
|
803
696
|
}
|
|
804
697
|
|
|
805
698
|
function validateProfileForProvider(
|
|
@@ -813,7 +706,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
813
706
|
if (profileError) return profileError;
|
|
814
707
|
|
|
815
708
|
const aliasId = profileModelId(config.id, profile.slug);
|
|
816
|
-
const routing =
|
|
709
|
+
const routing = app.profileRouting(provider, config.id);
|
|
817
710
|
if (routing?.aliasSlug === profile.slug && profile.slug !== previousSlug) {
|
|
818
711
|
return `Preset name "${profile.slug}" collides with the adaptive model alias.`;
|
|
819
712
|
}
|
|
@@ -865,7 +758,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
865
758
|
const sampling = profile.sampling ?? {};
|
|
866
759
|
const samplingFields = profileSamplingFields(serverType);
|
|
867
760
|
const repetitionPenaltyKey = repetitionPenaltyKeyForServer(serverType);
|
|
868
|
-
const currentRouting =
|
|
761
|
+
const currentRouting = app.profileRouting(provider, config.id);
|
|
869
762
|
const previewRouting = currentRouting
|
|
870
763
|
? { ...currentRouting, levels: { ...currentRouting.levels } }
|
|
871
764
|
: undefined;
|
|
@@ -904,10 +797,10 @@ export default async function (pi: ExtensionAPI) {
|
|
|
904
797
|
label: field.label,
|
|
905
798
|
description: `${sampling[field.key] ?? "omitted (server/model default)"} · ${field.description}`,
|
|
906
799
|
})),
|
|
907
|
-
{ value: "save", label: "
|
|
800
|
+
{ value: "save", label: "Save preset", description: profileModelId(config.id, profile.slug) },
|
|
908
801
|
];
|
|
909
|
-
if (existing) items.push({ value: "delete", label: "
|
|
910
|
-
items.push({ value: "cancel", label: "
|
|
802
|
+
if (existing) items.push({ value: "delete", label: "Delete preset", description: "Requires confirmation" });
|
|
803
|
+
items.push({ value: "cancel", label: "Cancel" });
|
|
911
804
|
|
|
912
805
|
const action = await runSelect(ctx, `Preset: ${profile.slug}`, items, [
|
|
913
806
|
`model id: ${profileModelId(config.id, profile.slug)} → ${config.id}`,
|
|
@@ -966,7 +859,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
966
859
|
}
|
|
967
860
|
return { action: "save", profile };
|
|
968
861
|
} else if (action === "delete" && existing) {
|
|
969
|
-
const routing =
|
|
862
|
+
const routing = app.profileRouting(provider, config.id);
|
|
970
863
|
const routed = routing && Object.values(routing.levels).includes(existing.slug);
|
|
971
864
|
const confirmed = await ctx.ui.confirm(
|
|
972
865
|
"Delete preset",
|
|
@@ -982,10 +875,10 @@ export default async function (pi: ExtensionAPI) {
|
|
|
982
875
|
provider: DiscoveredProvider,
|
|
983
876
|
prefetched: { models: Record<string, unknown>[]; serverType: string },
|
|
984
877
|
): Promise<boolean> {
|
|
985
|
-
|
|
878
|
+
app.saveSource(provider);
|
|
986
879
|
try {
|
|
987
880
|
await registerProvider(provider, prefetched);
|
|
988
|
-
|
|
881
|
+
app.saveSource(provider);
|
|
989
882
|
return true;
|
|
990
883
|
} catch (err) {
|
|
991
884
|
ctx.ui.notify(
|
|
@@ -1012,12 +905,12 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1012
905
|
provider: DiscoveredProvider,
|
|
1013
906
|
modelId: string,
|
|
1014
907
|
): Promise<void> {
|
|
1015
|
-
const routing =
|
|
908
|
+
const routing = app.profileRouting(provider, modelId);
|
|
1016
909
|
if (!routing || ctx.model?.provider !== provider.name) return;
|
|
1017
910
|
const adaptiveId = profileModelId(modelId, routing.aliasSlug);
|
|
1018
911
|
if (ctx.model.id !== adaptiveId) return;
|
|
1019
912
|
const valid =
|
|
1020
|
-
routing.enabled && analyzeExplicitProfileRouting(routing,
|
|
913
|
+
routing.enabled && analyzeExplicitProfileRouting(routing, app.profiles(provider, modelId)).errors.length === 0;
|
|
1021
914
|
const refreshed = ctx.modelRegistry.find(provider.name, valid ? adaptiveId : modelId);
|
|
1022
915
|
if (refreshed) await pi.setModel(refreshed);
|
|
1023
916
|
}
|
|
@@ -1088,7 +981,10 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1088
981
|
profile,
|
|
1089
982
|
repetitionPenaltyKeyForServer(serverType),
|
|
1090
983
|
);
|
|
1091
|
-
await
|
|
984
|
+
await runTextView(ctx, `${level} → ${profile.slug}`, [
|
|
985
|
+
`Adaptive model: ${profileModelId(config.id, routing.aliasSlug)}`,
|
|
986
|
+
`Base model: ${config.id}`,
|
|
987
|
+
"",
|
|
1092
988
|
...JSON.stringify(payload, null, 2).split("\n"),
|
|
1093
989
|
]);
|
|
1094
990
|
}
|
|
@@ -1107,7 +1003,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1107
1003
|
ctx.ui.notify("Create at least one preset before configuring adaptive routing.", "warning");
|
|
1108
1004
|
return null;
|
|
1109
1005
|
}
|
|
1110
|
-
const existing =
|
|
1006
|
+
const existing = app.profileRouting(provider, config.id);
|
|
1111
1007
|
const routing: ModelProfileRouting = existing
|
|
1112
1008
|
? { ...existing, levels: { ...existing.levels } }
|
|
1113
1009
|
: defaultProfileRouting(profiles);
|
|
@@ -1135,11 +1031,11 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1135
1031
|
label: `Pi ${level}`,
|
|
1136
1032
|
description: `→ ${routing.levels[level] || "not selected"}`,
|
|
1137
1033
|
})),
|
|
1138
|
-
{ value: "preview", label: "Preview exact requests", description: "
|
|
1139
|
-
{ value: "save", label: "
|
|
1034
|
+
{ value: "preview", label: "Preview exact requests", description: "Inspect the payload preset for each Pi level" },
|
|
1035
|
+
{ value: "save", label: "Review and save", description: analysis.errors.length ? `${analysis.errors.length} issue(s)` : "Valid mapping" },
|
|
1140
1036
|
];
|
|
1141
|
-
if (existing) items.push({ value: "remove", label: "
|
|
1142
|
-
items.push({ value: "cancel", label: "
|
|
1037
|
+
if (existing) items.push({ value: "remove", label: "Remove adaptive routing", description: "Fixed presets remain unchanged · requires confirmation" });
|
|
1038
|
+
items.push({ value: "cancel", label: "Cancel" });
|
|
1143
1039
|
|
|
1144
1040
|
const action = await runSelect(ctx, "Adaptive Shift-Tab routing", items, [
|
|
1145
1041
|
"Explicit router: only this alias changes complete presets when Shift-Tab is pressed.",
|
|
@@ -1212,13 +1108,13 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1212
1108
|
prefetched: { models: Record<string, unknown>[]; serverType: string },
|
|
1213
1109
|
): Promise<void> {
|
|
1214
1110
|
for (;;) {
|
|
1215
|
-
const profiles =
|
|
1216
|
-
const routing =
|
|
1111
|
+
const profiles = app.profiles(provider, config.id);
|
|
1112
|
+
const routing = app.profileRouting(provider, config.id);
|
|
1217
1113
|
const routeAnalysis = routing ? analyzeExplicitProfileRouting(routing, profiles) : undefined;
|
|
1218
1114
|
const items: SelectItem[] = [
|
|
1219
1115
|
{
|
|
1220
1116
|
value: "routing",
|
|
1221
|
-
label:
|
|
1117
|
+
label: "Configure adaptive routing",
|
|
1222
1118
|
description: !routing
|
|
1223
1119
|
? "explicitly map all seven Pi levels to complete presets"
|
|
1224
1120
|
: routeAnalysis?.errors.length
|
|
@@ -1236,8 +1132,8 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1236
1132
|
});
|
|
1237
1133
|
}
|
|
1238
1134
|
items.push(
|
|
1239
|
-
{ value: "add", label: "
|
|
1240
|
-
{ value: "clone", label: "
|
|
1135
|
+
{ value: "add", label: "Create preset", description: "Create a complete thinking/sampling parameter bundle" },
|
|
1136
|
+
{ value: "clone", label: "Clone preset", description: "Copy an existing preset, then edit only what differs" },
|
|
1241
1137
|
);
|
|
1242
1138
|
for (const profile of profiles) {
|
|
1243
1139
|
items.push({
|
|
@@ -1246,7 +1142,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1246
1142
|
description: profileDescription(profile, prefetched.serverType, routing),
|
|
1247
1143
|
});
|
|
1248
1144
|
}
|
|
1249
|
-
items.push({ value: "back", label: "
|
|
1145
|
+
items.push({ value: "back", label: "Back" });
|
|
1250
1146
|
|
|
1251
1147
|
const action = await runSelect(ctx, `Thinking & presets: ${config.id}`, items, [
|
|
1252
1148
|
`${profiles.length} preset(s) · base model behavior is never changed by presets`,
|
|
@@ -1265,8 +1161,8 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1265
1161
|
prefetched.serverType,
|
|
1266
1162
|
);
|
|
1267
1163
|
if (!result) continue;
|
|
1268
|
-
if (result.action === "save")
|
|
1269
|
-
else
|
|
1164
|
+
if (result.action === "save") app.saveRouting(provider, config.id, result.routing);
|
|
1165
|
+
else app.removeRouting(provider, config.id);
|
|
1270
1166
|
const registered = await persistProfileChange(ctx, provider, prefetched);
|
|
1271
1167
|
if (registered) {
|
|
1272
1168
|
const nextAlias = result.action === "save" && result.routing.enabled ? result.routing.aliasSlug : undefined;
|
|
@@ -1318,7 +1214,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1318
1214
|
);
|
|
1319
1215
|
if (!result) continue;
|
|
1320
1216
|
if (result.action === "save") {
|
|
1321
|
-
const currentRouting =
|
|
1217
|
+
const currentRouting = app.profileRouting(provider, config.id);
|
|
1322
1218
|
const nextRouting = currentRouting
|
|
1323
1219
|
? { ...currentRouting, levels: { ...currentRouting.levels } }
|
|
1324
1220
|
: undefined;
|
|
@@ -1344,8 +1240,8 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1344
1240
|
);
|
|
1345
1241
|
if (!confirmed) continue;
|
|
1346
1242
|
}
|
|
1347
|
-
|
|
1348
|
-
if (nextRouting)
|
|
1243
|
+
app.saveProfile(provider, config.id, result.profile, existing?.slug);
|
|
1244
|
+
if (nextRouting) app.saveRouting(provider, config.id, nextRouting);
|
|
1349
1245
|
const registered = await persistProfileChange(ctx, provider, prefetched);
|
|
1350
1246
|
if (registered) {
|
|
1351
1247
|
if (existing && existing.exposeAsModel !== false) {
|
|
@@ -1363,7 +1259,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1363
1259
|
ctx.ui.notify(`${existing ? "Updated" : "Created"} preset "${result.profile.slug}".`, "info");
|
|
1364
1260
|
}
|
|
1365
1261
|
} else if (existing) {
|
|
1366
|
-
|
|
1262
|
+
app.removeProfile(provider, config.id, existing.slug);
|
|
1367
1263
|
const registered = await persistProfileChange(ctx, provider, prefetched);
|
|
1368
1264
|
if (registered) {
|
|
1369
1265
|
await refreshSelectedProfile(
|
|
@@ -1407,8 +1303,8 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1407
1303
|
} · input ${effInput.join("+")}`,
|
|
1408
1304
|
];
|
|
1409
1305
|
|
|
1410
|
-
const configuredProfiles =
|
|
1411
|
-
const configuredRouting =
|
|
1306
|
+
const configuredProfiles = app.profiles(provider, config.id);
|
|
1307
|
+
const configuredRouting = app.profileRouting(provider, config.id);
|
|
1412
1308
|
const routingAnalysis = configuredRouting
|
|
1413
1309
|
? analyzeExplicitProfileRouting(configuredRouting, configuredProfiles)
|
|
1414
1310
|
: undefined;
|
|
@@ -1441,7 +1337,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1441
1337
|
if (Object.keys(ov).length > 0) {
|
|
1442
1338
|
items.push({ value: "clear", label: "Clear overrides", description: "revert to server-reported values" });
|
|
1443
1339
|
}
|
|
1444
|
-
items.push({ value: "back", label: "
|
|
1340
|
+
items.push({ value: "back", label: "Back" });
|
|
1445
1341
|
|
|
1446
1342
|
const action = await runSelect(ctx, `Model: ${config.id}${modelFlags(config, ov)}`, items, header);
|
|
1447
1343
|
if (!action || action === "back") return;
|
|
@@ -1451,21 +1347,24 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1451
1347
|
continue;
|
|
1452
1348
|
}
|
|
1453
1349
|
|
|
1350
|
+
let feedback: string | undefined;
|
|
1454
1351
|
if (action === "ctx") {
|
|
1455
1352
|
const n = await askNumber(ctx, `Context window for ${config.id}`, String(effCtx ?? 128000));
|
|
1456
|
-
if (n
|
|
1457
|
-
|
|
1458
|
-
}
|
|
1353
|
+
if (n === undefined) continue;
|
|
1354
|
+
provider.modelOverrides = { ...provider.modelOverrides, [config.id]: { ...ov, contextWindow: n } };
|
|
1355
|
+
feedback = `Context window saved as ${fmt(n)}.`;
|
|
1459
1356
|
} else if (action === "max") {
|
|
1460
1357
|
const n = await askNumber(ctx, `Max output tokens for ${config.id}`, String(effMax ?? 16384));
|
|
1461
|
-
if (n
|
|
1462
|
-
|
|
1463
|
-
}
|
|
1358
|
+
if (n === undefined) continue;
|
|
1359
|
+
provider.modelOverrides = { ...provider.modelOverrides, [config.id]: { ...ov, maxTokens: n } };
|
|
1360
|
+
feedback = `Max output tokens saved as ${fmt(n)}.`;
|
|
1464
1361
|
} else if (action === "reasoning") {
|
|
1362
|
+
const reasoning = !(effReasoning ?? false);
|
|
1465
1363
|
provider.modelOverrides = {
|
|
1466
1364
|
...provider.modelOverrides,
|
|
1467
|
-
[config.id]: { ...ov, reasoning
|
|
1365
|
+
[config.id]: { ...ov, reasoning },
|
|
1468
1366
|
};
|
|
1367
|
+
feedback = `Reasoning ${reasoning ? "enabled" : "disabled"}.`;
|
|
1469
1368
|
} else if (action === "input") {
|
|
1470
1369
|
// Toggle vision: add/remove "image" from input modalities
|
|
1471
1370
|
const hasVision = effInput.includes("image");
|
|
@@ -1476,19 +1375,23 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1476
1375
|
...provider.modelOverrides,
|
|
1477
1376
|
[config.id]: { ...ov, input: newInput },
|
|
1478
1377
|
};
|
|
1378
|
+
feedback = `Vision input ${hasVision ? "disabled" : "enabled"}.`;
|
|
1479
1379
|
} else if (action === "clear") {
|
|
1480
1380
|
if (provider.modelOverrides) {
|
|
1481
1381
|
delete provider.modelOverrides[config.id];
|
|
1482
1382
|
if (Object.keys(provider.modelOverrides).length === 0) provider.modelOverrides = undefined;
|
|
1483
1383
|
}
|
|
1384
|
+
feedback = "Model overrides cleared.";
|
|
1484
1385
|
}
|
|
1386
|
+
if (!feedback) continue;
|
|
1485
1387
|
|
|
1486
|
-
// Persist + re-register with new values
|
|
1487
|
-
|
|
1388
|
+
// Persist + re-register with new values.
|
|
1389
|
+
app.saveSource(provider);
|
|
1488
1390
|
try {
|
|
1489
1391
|
await registerProvider(provider, prefetched);
|
|
1490
|
-
|
|
1491
|
-
|
|
1392
|
+
ctx.ui.notify(feedback, "info");
|
|
1393
|
+
} catch (error) {
|
|
1394
|
+
ctx.ui.notify(`${feedback} Provider registration remains on its previous state: ${errorMessage(error)}`, "warning");
|
|
1492
1395
|
}
|
|
1493
1396
|
}
|
|
1494
1397
|
}
|
|
@@ -1532,7 +1435,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1532
1435
|
header.push(`last successful scan: ${new Date(provider.lastScanned).toLocaleString()}`);
|
|
1533
1436
|
}
|
|
1534
1437
|
if (!live && provider.lastScanError) {
|
|
1535
|
-
header.push(`latest live scan failed: ${provider.lastScanError}`);
|
|
1438
|
+
header.push(`latest live scan failed: ${redactSecret(provider.lastScanError, provider.apiKey)}`);
|
|
1536
1439
|
header.push("Last known-good models and all saved presets remain available.");
|
|
1537
1440
|
}
|
|
1538
1441
|
|
|
@@ -1541,26 +1444,26 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1541
1444
|
label: `${c.id}${modelFlags(c, provider.modelOverrides?.[c.id])}`,
|
|
1542
1445
|
description: modelDescription(c, provider),
|
|
1543
1446
|
}));
|
|
1544
|
-
items.push({ value: "rescan", label: "
|
|
1447
|
+
items.push({ value: "rescan", label: "Re-scan source", description: "Fetch a fresh model list and re-register" });
|
|
1545
1448
|
items.push({
|
|
1546
1449
|
value: "rename",
|
|
1547
|
-
label: "
|
|
1548
|
-
description: `
|
|
1450
|
+
label: "Rename source",
|
|
1451
|
+
description: `Current: ${provider.name}`,
|
|
1549
1452
|
});
|
|
1550
1453
|
items.push({
|
|
1551
1454
|
value: "auth",
|
|
1552
|
-
label: "
|
|
1553
|
-
description: provider.apiKey ? "API key configured · replace or clear" : "
|
|
1455
|
+
label: "Authentication",
|
|
1456
|
+
description: provider.apiKey ? "API key configured · replace or clear" : "Anonymous · add an API key",
|
|
1554
1457
|
});
|
|
1555
1458
|
items.push({
|
|
1556
1459
|
value: "defaults",
|
|
1557
|
-
label: "
|
|
1558
|
-
description: `
|
|
1460
|
+
label: "Fallback defaults",
|
|
1461
|
+
description: `Used when the server reports nothing · ctx ${fmt(provider.defaultContextWindow ?? null)} · max ${fmt(provider.defaultMaxTokens ?? null)}`,
|
|
1559
1462
|
});
|
|
1560
|
-
items.push({ value: "remove", label: "
|
|
1561
|
-
items.push({ value: "back", label: "
|
|
1463
|
+
items.push({ value: "remove", label: "Remove source", description: "Unregister the provider and delete its saved configuration" });
|
|
1464
|
+
items.push({ value: "back", label: "Back" });
|
|
1562
1465
|
|
|
1563
|
-
const action = await runSelect(ctx, `
|
|
1466
|
+
const action = await runSelect(ctx, `Source: ${provider.name}`, items, header);
|
|
1564
1467
|
if (!action || action === "back") return;
|
|
1565
1468
|
|
|
1566
1469
|
if (action.startsWith("model:")) {
|
|
@@ -1582,7 +1485,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1582
1485
|
try {
|
|
1583
1486
|
const registered = await registerProvider(provider, live);
|
|
1584
1487
|
recordSuccessfulScan(provider, live.models, live.serverType, false);
|
|
1585
|
-
|
|
1488
|
+
app.saveSource(provider);
|
|
1586
1489
|
ctx.ui.notify(
|
|
1587
1490
|
`Re-registered ${live.models.length} base model(s)${
|
|
1588
1491
|
registered.profileCount ? ` + ${registered.profileCount} profile(s)` : ""
|
|
@@ -1596,13 +1499,26 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1596
1499
|
}
|
|
1597
1500
|
}
|
|
1598
1501
|
} else if (action === "rename") {
|
|
1599
|
-
const
|
|
1502
|
+
const enteredName = await runInput(
|
|
1503
|
+
ctx,
|
|
1504
|
+
"New source name",
|
|
1505
|
+
provider.name,
|
|
1506
|
+
"This name identifies the provider and its models in /model.",
|
|
1507
|
+
(value) => {
|
|
1508
|
+
const name = value.trim();
|
|
1509
|
+
if (!name) return "Source name cannot be blank.";
|
|
1510
|
+
if (app.listSources().some((candidate) => candidate.name === name && candidate.name !== provider.name)) {
|
|
1511
|
+
return `Source "${name}" already exists.`;
|
|
1512
|
+
}
|
|
1513
|
+
return null;
|
|
1514
|
+
},
|
|
1515
|
+
);
|
|
1516
|
+
const newName = enteredName?.trim();
|
|
1600
1517
|
if (newName && newName !== provider.name) {
|
|
1601
|
-
const
|
|
1602
|
-
if (!
|
|
1603
|
-
ctx.ui.notify(`Cannot rename — "${newName}" already exists or "${oldName}" not found.`, "error");
|
|
1518
|
+
const renamed = app.renameSource(provider, newName);
|
|
1519
|
+
if (!renamed.ok) {
|
|
1520
|
+
ctx.ui.notify(`Cannot rename — "${newName}" already exists or "${renamed.oldName}" was not found.`, "error");
|
|
1604
1521
|
} else {
|
|
1605
|
-
provider.name = newName;
|
|
1606
1522
|
const catalog = live ??
|
|
1607
1523
|
(provider.cachedModels?.length
|
|
1608
1524
|
? {
|
|
@@ -1612,12 +1528,11 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1612
1528
|
: undefined);
|
|
1613
1529
|
try {
|
|
1614
1530
|
await registerProvider(provider, catalog);
|
|
1615
|
-
pi.unregisterProvider(oldName);
|
|
1616
|
-
|
|
1531
|
+
pi.unregisterProvider(renamed.oldName);
|
|
1532
|
+
app.saveSource(provider);
|
|
1617
1533
|
ctx.ui.notify(`Renamed to "${newName}"${live ? "" : " using the cached catalogue"}.`, "info");
|
|
1618
1534
|
} catch (error) {
|
|
1619
|
-
|
|
1620
|
-
provider.name = oldName;
|
|
1535
|
+
app.renameSource(provider, renamed.oldName);
|
|
1621
1536
|
ctx.ui.notify(`Rename rolled back: ${errorMessage(error)}`, "error");
|
|
1622
1537
|
}
|
|
1623
1538
|
}
|
|
@@ -1635,9 +1550,9 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1635
1550
|
...(provider.apiKey
|
|
1636
1551
|
? [{ value: "clear", label: "Clear API key", description: "remove the saved bearer credential" }]
|
|
1637
1552
|
: []),
|
|
1638
|
-
{ value: "back", label: "
|
|
1553
|
+
{ value: "back", label: "Back" },
|
|
1639
1554
|
],
|
|
1640
|
-
[provider.baseUrl, `
|
|
1555
|
+
[provider.baseUrl, `Current: ${provider.apiKey ? "API key configured" : "anonymous"}`],
|
|
1641
1556
|
);
|
|
1642
1557
|
if (!authAction || authAction === "back") continue;
|
|
1643
1558
|
|
|
@@ -1658,8 +1573,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1658
1573
|
if (!confirmed) continue;
|
|
1659
1574
|
}
|
|
1660
1575
|
|
|
1661
|
-
provider
|
|
1662
|
-
upsertProvider(provider);
|
|
1576
|
+
app.setCredential(provider, nextApiKey);
|
|
1663
1577
|
const checked = await runLoader(
|
|
1664
1578
|
ctx,
|
|
1665
1579
|
`Validating ${provider.name} authentication...`,
|
|
@@ -1670,7 +1584,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1670
1584
|
try {
|
|
1671
1585
|
await registerProvider(provider, checked);
|
|
1672
1586
|
recordSuccessfulScan(provider, checked.models, checked.serverType, false);
|
|
1673
|
-
|
|
1587
|
+
app.saveSource(provider);
|
|
1674
1588
|
live = checked;
|
|
1675
1589
|
ctx.ui.notify(`Authentication saved and validated for ${provider.name}.`, "info");
|
|
1676
1590
|
} catch (error) {
|
|
@@ -1698,11 +1612,19 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1698
1612
|
ctx.ui.notify("Authentication saved but could not be validated; the last known-good catalogue was retained.", "warning");
|
|
1699
1613
|
}
|
|
1700
1614
|
} else if (action === "defaults") {
|
|
1615
|
+
const changed: string[] = [];
|
|
1701
1616
|
const cw = await askNumber(ctx, "Default context window (blank = keep)", String(provider.defaultContextWindow ?? 128000));
|
|
1702
|
-
if (cw !== undefined)
|
|
1617
|
+
if (cw !== undefined) {
|
|
1618
|
+
provider.defaultContextWindow = cw;
|
|
1619
|
+
changed.push(`context ${fmt(cw)}`);
|
|
1620
|
+
}
|
|
1703
1621
|
const mt = await askNumber(ctx, "Default max output tokens (blank = keep)", String(provider.defaultMaxTokens ?? 16384));
|
|
1704
|
-
if (mt !== undefined)
|
|
1705
|
-
|
|
1622
|
+
if (mt !== undefined) {
|
|
1623
|
+
provider.defaultMaxTokens = mt;
|
|
1624
|
+
changed.push(`max output ${fmt(mt)}`);
|
|
1625
|
+
}
|
|
1626
|
+
if (!changed.length) continue;
|
|
1627
|
+
app.saveSource(provider);
|
|
1706
1628
|
const catalog = live ??
|
|
1707
1629
|
(provider.cachedModels?.length
|
|
1708
1630
|
? {
|
|
@@ -1712,14 +1634,18 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1712
1634
|
: undefined);
|
|
1713
1635
|
try {
|
|
1714
1636
|
await registerProvider(provider, catalog);
|
|
1637
|
+
ctx.ui.notify(`Fallback defaults saved: ${changed.join(" · ")}.`, "info");
|
|
1715
1638
|
} catch (error) {
|
|
1716
1639
|
ctx.ui.notify(`Defaults saved; provider remains on its last registered catalogue: ${errorMessage(error)}`, "warning");
|
|
1717
1640
|
}
|
|
1718
1641
|
} else if (action === "remove") {
|
|
1719
|
-
const sure = await ctx.ui.confirm(
|
|
1642
|
+
const sure = await ctx.ui.confirm(
|
|
1643
|
+
"Remove source",
|
|
1644
|
+
`Unregister "${provider.name}" and delete its saved configuration, cached catalogue, presets, and routing?`,
|
|
1645
|
+
);
|
|
1720
1646
|
if (sure) {
|
|
1721
1647
|
pi.unregisterProvider(provider.name);
|
|
1722
|
-
|
|
1648
|
+
app.removeSource(provider.name);
|
|
1723
1649
|
ctx.ui.notify(`Removed "${provider.name}".`, "info");
|
|
1724
1650
|
return;
|
|
1725
1651
|
}
|
|
@@ -1731,23 +1657,45 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1731
1657
|
// Screen: add endpoint
|
|
1732
1658
|
// -----------------------------------------------------------------------
|
|
1733
1659
|
|
|
1734
|
-
async function showAddScreen(ctx: ExtensionCommandContext, presetUrl?: string): Promise<void> {
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1660
|
+
async function showAddScreen(ctx: ExtensionCommandContext, presetUrl?: string, presetName?: string): Promise<void> {
|
|
1661
|
+
const enteredUrl = presetUrl ?? await runInput(
|
|
1662
|
+
ctx,
|
|
1663
|
+
"Endpoint URL",
|
|
1664
|
+
"http://192.168.1.100:8080",
|
|
1665
|
+
"Enter the base URL of an OpenAI-compatible model server.",
|
|
1666
|
+
(value) => {
|
|
1667
|
+
if (!value.trim()) return "Endpoint URL cannot be blank.";
|
|
1668
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(value.trim()) && !/^https?:\/\//i.test(value.trim())) {
|
|
1669
|
+
return "Endpoint URL must use HTTP or HTTPS.";
|
|
1670
|
+
}
|
|
1671
|
+
try {
|
|
1672
|
+
new URL(normalizeEndpointUrl(value));
|
|
1673
|
+
return null;
|
|
1674
|
+
} catch {
|
|
1675
|
+
return "Enter a valid HTTP or HTTPS endpoint URL.";
|
|
1676
|
+
}
|
|
1677
|
+
},
|
|
1678
|
+
);
|
|
1679
|
+
if (!enteredUrl) return;
|
|
1680
|
+
let baseUrl: string;
|
|
1681
|
+
try {
|
|
1682
|
+
baseUrl = normalizeEndpointUrl(enteredUrl);
|
|
1683
|
+
} catch (error) {
|
|
1684
|
+
ctx.ui.notify(errorMessage(error), "error");
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1739
1687
|
|
|
1740
1688
|
const authMode = await runSelect(
|
|
1741
1689
|
ctx,
|
|
1742
1690
|
"Endpoint authentication",
|
|
1743
1691
|
[
|
|
1744
|
-
{ value: "none", label: "No API key", description: "
|
|
1692
|
+
{ value: "none", label: "No API key", description: "Connect without a configured bearer credential" },
|
|
1745
1693
|
{
|
|
1746
1694
|
value: "api-key",
|
|
1747
1695
|
label: "Enter API key",
|
|
1748
|
-
description: "
|
|
1696
|
+
description: "Masked while typing · saved only in the private model-discovery configuration",
|
|
1749
1697
|
},
|
|
1750
|
-
{ value: "cancel", label: "
|
|
1698
|
+
{ value: "cancel", label: "Cancel" },
|
|
1751
1699
|
],
|
|
1752
1700
|
[baseUrl, "The key is sent as an Authorization: Bearer header for discovery and inference."],
|
|
1753
1701
|
);
|
|
@@ -1789,7 +1737,21 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1789
1737
|
return;
|
|
1790
1738
|
}
|
|
1791
1739
|
|
|
1792
|
-
const
|
|
1740
|
+
const suggestedName = presetName?.trim() || generateProviderName(baseUrl);
|
|
1741
|
+
const enteredName = await runInput(
|
|
1742
|
+
ctx,
|
|
1743
|
+
"Source name",
|
|
1744
|
+
suggestedName,
|
|
1745
|
+
"This name identifies the provider and its models in /model.",
|
|
1746
|
+
(value) => {
|
|
1747
|
+
const name = value.trim();
|
|
1748
|
+
if (!name) return "Source name cannot be blank.";
|
|
1749
|
+
if (app.findSource(name)) return `Source "${name}" already exists. Open it from the home screen instead.`;
|
|
1750
|
+
return null;
|
|
1751
|
+
},
|
|
1752
|
+
);
|
|
1753
|
+
if (enteredName === undefined) return;
|
|
1754
|
+
const name = enteredName.trim();
|
|
1793
1755
|
|
|
1794
1756
|
const provider: DiscoveredProvider = { name, baseUrl, apiKey };
|
|
1795
1757
|
const configs = live.models.map(extractModelConfig);
|
|
@@ -1810,8 +1772,8 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1810
1772
|
label: `${c.id}${modelFlags(c, provider.modelOverrides?.[c.id])}`,
|
|
1811
1773
|
description: modelDescription(c, provider),
|
|
1812
1774
|
}));
|
|
1813
|
-
items.push({ value: "register", label: "
|
|
1814
|
-
items.push({ value: "cancel", label: "
|
|
1775
|
+
items.push({ value: "register", label: "Register source", description: `Save as "${name}" and make models available in /model` });
|
|
1776
|
+
items.push({ value: "cancel", label: "Cancel" });
|
|
1815
1777
|
|
|
1816
1778
|
const action = await runSelect(ctx, `Review: ${name}`, items, header);
|
|
1817
1779
|
if (!action || action === "cancel") {
|
|
@@ -1854,7 +1816,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1854
1816
|
try {
|
|
1855
1817
|
await registerProvider(provider, live);
|
|
1856
1818
|
recordSuccessfulScan(provider, live.models, live.serverType, false);
|
|
1857
|
-
|
|
1819
|
+
app.saveSource(provider);
|
|
1858
1820
|
ctx.ui.notify(
|
|
1859
1821
|
`Registered ${configs.length} model(s) from ${live.serverType} as "${name}". Use /model to select.`,
|
|
1860
1822
|
"info",
|
|
@@ -1873,41 +1835,33 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1873
1835
|
|
|
1874
1836
|
async function showMainScreen(ctx: ExtensionCommandContext): Promise<void> {
|
|
1875
1837
|
for (;;) {
|
|
1876
|
-
const providers =
|
|
1877
|
-
const
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
: `live scan ${p.lastScanned ? new Date(p.lastScanned).toLocaleString() : "never completed"}`
|
|
1884
|
-
}`,
|
|
1885
|
-
}));
|
|
1886
|
-
items.push({ value: "add", label: "+ Add endpoint", description: "discover models from an OpenAI-compatible server" });
|
|
1887
|
-
if (providers.length > 0) {
|
|
1888
|
-
items.push({ value: "rescan-all", label: "⟳ Re-scan all", description: "refresh model lists from every endpoint" });
|
|
1889
|
-
}
|
|
1890
|
-
items.push({ value: "quit", label: "✗ Close" });
|
|
1891
|
-
|
|
1892
|
-
const action = await runSelect(ctx, "Model Discovery", items, [
|
|
1893
|
-
providers.length === 0 ? "No endpoints yet — add your first one." : `${providers.length} endpoint(s) registered`,
|
|
1894
|
-
]);
|
|
1838
|
+
const providers = app.listSources();
|
|
1839
|
+
const action = await runSelect(
|
|
1840
|
+
ctx,
|
|
1841
|
+
"Model Discovery",
|
|
1842
|
+
buildHomeItems(providers),
|
|
1843
|
+
buildHomeSummary(providers),
|
|
1844
|
+
);
|
|
1895
1845
|
if (!action || action === "quit") return;
|
|
1896
1846
|
|
|
1897
1847
|
if (action === "add") {
|
|
1898
1848
|
await showAddScreen(ctx);
|
|
1849
|
+
} else if (action === "diagnostics") {
|
|
1850
|
+
await runTextView(ctx, "Model Discovery diagnostics", buildDiagnosticsLines(app.listSources()));
|
|
1899
1851
|
} else if (action === "rescan-all") {
|
|
1900
|
-
const results = await runLoader(ctx, "Re-scanning all
|
|
1852
|
+
const results = await runLoader(ctx, "Re-scanning all sources...", async (signal) => {
|
|
1901
1853
|
let live = 0;
|
|
1902
1854
|
let cached = 0;
|
|
1903
1855
|
let failed = 0;
|
|
1904
|
-
for (const provider of
|
|
1856
|
+
for (const provider of app.listSources()) {
|
|
1857
|
+
if (signal.aborted) break;
|
|
1905
1858
|
try {
|
|
1906
|
-
const registered = await registerProvider(provider);
|
|
1859
|
+
const registered = await registerProvider(provider, undefined, signal);
|
|
1907
1860
|
recordSuccessfulScan(provider, registered.rawModels, registered.serverType, false);
|
|
1908
|
-
|
|
1861
|
+
app.saveSource(provider);
|
|
1909
1862
|
live++;
|
|
1910
1863
|
} catch (error) {
|
|
1864
|
+
if (signal.aborted) break;
|
|
1911
1865
|
recordFailedScan(provider, error, false);
|
|
1912
1866
|
if (provider.cachedModels?.length) {
|
|
1913
1867
|
try {
|
|
@@ -1915,14 +1869,14 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1915
1869
|
models: provider.cachedModels,
|
|
1916
1870
|
serverType: provider.serverType ?? "OpenAI-compatible",
|
|
1917
1871
|
});
|
|
1918
|
-
|
|
1872
|
+
app.saveSource(provider);
|
|
1919
1873
|
cached++;
|
|
1920
1874
|
continue;
|
|
1921
1875
|
} catch {
|
|
1922
1876
|
/* report below without removing the previously registered provider */
|
|
1923
1877
|
}
|
|
1924
1878
|
}
|
|
1925
|
-
|
|
1879
|
+
app.saveSource(provider);
|
|
1926
1880
|
failed++;
|
|
1927
1881
|
}
|
|
1928
1882
|
}
|
|
@@ -1937,7 +1891,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1937
1891
|
}
|
|
1938
1892
|
} else if (action.startsWith("provider:")) {
|
|
1939
1893
|
const name = action.slice("provider:".length);
|
|
1940
|
-
const provider =
|
|
1894
|
+
const provider = app.findSource(name);
|
|
1941
1895
|
if (provider) await showEndpointScreen(ctx, provider);
|
|
1942
1896
|
}
|
|
1943
1897
|
}
|
|
@@ -1947,18 +1901,100 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1947
1901
|
// Command: /discover — single entry point
|
|
1948
1902
|
// -----------------------------------------------------------------------
|
|
1949
1903
|
|
|
1904
|
+
type ReportLevel = "info" | "warning" | "error";
|
|
1905
|
+
function emitText(ctx: ExtensionCommandContext, text: string, level: ReportLevel = "info"): void {
|
|
1906
|
+
if (ctx.hasUI) ctx.ui.notify(text, level);
|
|
1907
|
+
else console.log(text);
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
async function showReport(ctx: ExtensionCommandContext, title: string, text: string): Promise<void> {
|
|
1911
|
+
if (ctx.mode === "tui") await runTextView(ctx, title, text.split("\n"));
|
|
1912
|
+
else emitText(ctx, text);
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
async function addSourceHeadlessly(
|
|
1916
|
+
ctx: ExtensionCommandContext,
|
|
1917
|
+
url: string,
|
|
1918
|
+
providerName?: string,
|
|
1919
|
+
): Promise<void> {
|
|
1920
|
+
try {
|
|
1921
|
+
const result = await discoverAndRegisterSource({ url, providerName });
|
|
1922
|
+
emitText(
|
|
1923
|
+
ctx,
|
|
1924
|
+
`Registered source "${result.provider.name}" (${result.serverType}): ${result.models.length} base model(s)${result.profileCount ? ` + ${result.profileCount} preset model(s)` : ""}.`,
|
|
1925
|
+
);
|
|
1926
|
+
} catch (error) {
|
|
1927
|
+
emitText(ctx, `Could not add source: ${errorMessage(error)}`, "error");
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1950
1931
|
pi.registerCommand("discover", {
|
|
1951
|
-
description: "
|
|
1932
|
+
description: "Open model-source discovery or inspect it with /discover status",
|
|
1933
|
+
getArgumentCompletions: (prefix) => completeDiscoverArgs(
|
|
1934
|
+
prefix,
|
|
1935
|
+
app.listSources().map((provider) => provider.name),
|
|
1936
|
+
),
|
|
1952
1937
|
handler: async (args, ctx) => {
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1938
|
+
const intent = parseDiscoverArgs(args);
|
|
1939
|
+
switch (intent.kind) {
|
|
1940
|
+
case "open":
|
|
1941
|
+
if (ctx.mode === "tui") await showMainScreen(ctx);
|
|
1942
|
+
else await showReport(ctx, "Model Discovery status", formatDiscoveryStatus(app.listSources()));
|
|
1943
|
+
return;
|
|
1944
|
+
case "status":
|
|
1945
|
+
await showReport(ctx, "Model Discovery status", formatDiscoveryStatus(app.listSources()));
|
|
1946
|
+
return;
|
|
1947
|
+
case "doctor": {
|
|
1948
|
+
const lines = buildDiagnosticsLines(app.listSources());
|
|
1949
|
+
lines.push(
|
|
1950
|
+
"",
|
|
1951
|
+
"Actions",
|
|
1952
|
+
"- Re-scan from the wizard to refresh live catalogues.",
|
|
1953
|
+
"- Authentication secrets are configured only through the masked TUI.",
|
|
1954
|
+
);
|
|
1955
|
+
await showReport(ctx, "Model Discovery diagnostics", lines.join("\n"));
|
|
1956
|
+
return;
|
|
1957
|
+
}
|
|
1958
|
+
case "paths":
|
|
1959
|
+
await showReport(ctx, "Model Discovery paths", `Configuration: ${STORAGE_PATH}`);
|
|
1960
|
+
return;
|
|
1961
|
+
case "help":
|
|
1962
|
+
await showReport(ctx, "Model Discovery help", DISCOVER_USAGE);
|
|
1963
|
+
return;
|
|
1964
|
+
case "add":
|
|
1965
|
+
if (!intent.url) {
|
|
1966
|
+
if (ctx.mode === "tui") await showAddScreen(ctx);
|
|
1967
|
+
else emitText(ctx, `Missing source URL.\n${DISCOVER_USAGE}`, "error");
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
if (ctx.mode === "tui") {
|
|
1971
|
+
await showAddScreen(ctx, intent.url, intent.providerName);
|
|
1972
|
+
} else {
|
|
1973
|
+
await addSourceHeadlessly(ctx, intent.url, intent.providerName);
|
|
1974
|
+
}
|
|
1975
|
+
return;
|
|
1976
|
+
case "remove": {
|
|
1977
|
+
const provider = app.findSource(intent.name);
|
|
1978
|
+
if (!provider) {
|
|
1979
|
+
emitText(ctx, `Unknown source: ${intent.name}`, "error");
|
|
1980
|
+
return;
|
|
1981
|
+
}
|
|
1982
|
+
if (!intent.confirmed && ctx.mode !== "tui") {
|
|
1983
|
+
emitText(ctx, `Refusing to remove "${provider.name}" without --yes.`, "error");
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
const confirmed = intent.confirmed || await ctx.ui.confirm(
|
|
1987
|
+
"Remove source",
|
|
1988
|
+
`Unregister "${provider.name}" and delete its saved configuration, cached catalogue, presets, and routing?`,
|
|
1989
|
+
);
|
|
1990
|
+
if (!confirmed) return;
|
|
1991
|
+
pi.unregisterProvider(provider.name);
|
|
1992
|
+
app.removeSource(provider.name);
|
|
1993
|
+
emitText(ctx, `Removed source "${provider.name}".`);
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
case "invalid":
|
|
1997
|
+
emitText(ctx, `${intent.message}\n${DISCOVER_USAGE}`, "error");
|
|
1962
1998
|
}
|
|
1963
1999
|
},
|
|
1964
2000
|
});
|
|
@@ -1986,36 +2022,23 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1986
2022
|
"Discover and register models from an OpenAI-compatible endpoint (llama.cpp, oMLX, Ollama, vLLM). Reads actual server config. Use when the user asks to add a local model server.",
|
|
1987
2023
|
parameters: discoverModelsParameters,
|
|
1988
2024
|
async execute(_toolCallId, params) {
|
|
1989
|
-
let
|
|
1990
|
-
if (!url.startsWith("http")) url = `http://${url}`;
|
|
1991
|
-
url = url.replace(/\/+$/, "");
|
|
1992
|
-
providerName = providerName || generateProviderName(url);
|
|
1993
|
-
|
|
1994
|
-
let live: { models: Record<string, unknown>[]; serverType: string };
|
|
2025
|
+
let result: Awaited<ReturnType<typeof discoverAndRegisterSource>>;
|
|
1995
2026
|
try {
|
|
1996
|
-
|
|
1997
|
-
} catch (
|
|
2027
|
+
result = await discoverAndRegisterSource(params);
|
|
2028
|
+
} catch (error) {
|
|
2029
|
+
const message = errorMessage(error);
|
|
2030
|
+
if (message === "Endpoint is online but reports no models.") {
|
|
2031
|
+
return { content: [{ type: "text", text: message }], details: {} };
|
|
2032
|
+
}
|
|
1998
2033
|
return {
|
|
1999
|
-
content: [
|
|
2000
|
-
{ type: "text", text: `Endpoint unavailable: ${err instanceof Error ? err.message : String(err)}` },
|
|
2001
|
-
],
|
|
2034
|
+
content: [{ type: "text", text: `Endpoint unavailable or registration failed: ${message}` }],
|
|
2002
2035
|
details: {},
|
|
2003
2036
|
isError: true,
|
|
2004
2037
|
};
|
|
2005
2038
|
}
|
|
2006
|
-
if (live.models.length === 0) {
|
|
2007
|
-
return { content: [{ type: "text", text: "Endpoint online but reports no models." }], details: {} };
|
|
2008
|
-
}
|
|
2009
2039
|
|
|
2010
|
-
const
|
|
2011
|
-
const provider: DiscoveredProvider = existing
|
|
2012
|
-
? { ...existing, baseUrl: url, apiKey: apiKey ?? existing.apiKey }
|
|
2013
|
-
: { name: providerName, baseUrl: url, apiKey };
|
|
2040
|
+
const { provider, models: configs, serverType, profileCount } = result;
|
|
2014
2041
|
try {
|
|
2015
|
-
const { models: configs, profileCount } = await registerProvider(provider, live);
|
|
2016
|
-
recordSuccessfulScan(provider, live.models, live.serverType, false);
|
|
2017
|
-
upsertProvider(provider);
|
|
2018
|
-
|
|
2019
2042
|
const lines = configs.map(
|
|
2020
2043
|
(c) =>
|
|
2021
2044
|
`- ${c.id}${modelFlags(c, provider.modelOverrides?.[c.id])}: ${modelDescription(c, provider)}`,
|
|
@@ -2030,12 +2053,12 @@ export default async function (pi: ExtensionAPI) {
|
|
|
2030
2053
|
content: [
|
|
2031
2054
|
{
|
|
2032
2055
|
type: "text",
|
|
2033
|
-
text: `Endpoint online (${
|
|
2056
|
+
text: `Endpoint online (${serverType}). Registered ${configs.length} base model(s)${
|
|
2034
2057
|
profileCount ? ` + ${profileCount} profile(s)` : ""
|
|
2035
|
-
} as "${
|
|
2058
|
+
} as "${provider.name}":\n${lines.join("\n")}${note}\n\nModels are now selectable via /model.`,
|
|
2036
2059
|
},
|
|
2037
2060
|
],
|
|
2038
|
-
details: { providerName
|
|
2061
|
+
details: { providerName: provider.name, serverType, modelCount: configs.length, profileCount },
|
|
2039
2062
|
};
|
|
2040
2063
|
} catch (err) {
|
|
2041
2064
|
return {
|