model-utils.ts

 1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
 2//
 3// SPDX-License-Identifier: MIT
 4
 5import {
 6	type ExtensionAPI,
 7	type ExtensionContext,
 8	ModelSelectorComponent,
 9	SettingsManager,
10} from "@earendil-works/pi-coding-agent";
11
12export type SetModelResult =
13	| { ok: true }
14	| { ok: false; reason: "invalid-format" | "unknown-model" | "set-failed"; detail: string };
15
16/**
17 * Parse a "provider/modelId" string, look it up in the model registry, and
18 * set it as the active model. Returns a result describing what happened.
19 */
20export async function parseAndSetModel(spec: string, ctx: ExtensionContext, pi: ExtensionAPI): Promise<SetModelResult> {
21	const slashIdx = spec.indexOf("/");
22	if (slashIdx <= 0 || slashIdx === spec.length - 1) {
23		return { ok: false, reason: "invalid-format", detail: `Invalid model format "${spec}", expected provider/modelId` };
24	}
25
26	const provider = spec.slice(0, slashIdx);
27	const modelId = spec.slice(slashIdx + 1);
28	const model = ctx.modelRegistry.find(provider, modelId);
29
30	if (!model) {
31		return { ok: false, reason: "unknown-model", detail: `Unknown model: ${spec}` };
32	}
33
34	try {
35		await pi.setModel(model);
36	} catch (err) {
37		const msg = err instanceof Error ? err.message : String(err);
38		return { ok: false, reason: "set-failed", detail: `Failed to set model ${spec}: ${msg}` };
39	}
40
41	return { ok: true };
42}
43
44/**
45 * Convenience wrapper: parse and set the model, notifying the user on failure.
46 */
47export async function parseAndSetModelWithNotify(
48	spec: string,
49	ctx: ExtensionContext,
50	pi: ExtensionAPI,
51): Promise<SetModelResult> {
52	const result = await parseAndSetModel(spec, ctx, pi);
53	if (!result.ok) {
54		ctx.ui.notify(result.detail, "warning");
55	}
56	return result;
57}
58
59/**
60 * Show Pi's built-in model picker and return the user's selection without
61 * changing Pi's active model or default model setting.
62 */
63export async function pickModel(
64	ctx: ExtensionContext,
65	currentModel: ExtensionContext["model"] = ctx.model,
66	initialSearchInput?: string,
67): Promise<ExtensionContext["model"]> {
68	return ctx.ui.custom<ExtensionContext["model"]>((tui, _theme, _keybindings, done) => {
69		const selector = new ModelSelectorComponent(
70			tui,
71			currentModel,
72			SettingsManager.inMemory(),
73			ctx.modelRegistry,
74			[],
75			(model) => done(model),
76			() => done(undefined),
77			initialSearchInput,
78		);
79
80		return selector;
81	});
82}