@fanchaozz/provider-manager 0.1.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/README.md +243 -0
- package/README_EN.md +243 -0
- package/commands.ts +313 -0
- package/components.ts +696 -0
- package/forms.ts +622 -0
- package/index.ts +18 -0
- package/package.json +42 -0
- package/store.ts +282 -0
- package/sync.ts +253 -0
- package/test.ts +354 -0
- package/ui.ts +643 -0
package/commands.ts
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commands.ts — /providers 子命令派发
|
|
3
|
+
*
|
|
4
|
+
* 单一 /providers 命令,按 args 首词派发到子命令。
|
|
5
|
+
* 子命令列表见 printHelp()。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { readModelsJson } from "./store.ts";
|
|
10
|
+
import { openDashboard } from "./ui.ts";
|
|
11
|
+
import {
|
|
12
|
+
addProviderFlow,
|
|
13
|
+
deleteProviderFlow,
|
|
14
|
+
addModelFlow,
|
|
15
|
+
restoreFromBackupFlow,
|
|
16
|
+
syncFlow,
|
|
17
|
+
} from "./forms.ts";
|
|
18
|
+
|
|
19
|
+
// 覆盖范围:仅 models.json 里的自定义 provider。内置 provider 走 pi 的 /model
|
|
20
|
+
const STUBS = new Set(["test", "test-all"]);
|
|
21
|
+
|
|
22
|
+
export function registerCommands(pi: ExtensionAPI): void {
|
|
23
|
+
pi.registerCommand("providers", {
|
|
24
|
+
description: "Manage providers and models in ~/.pi/agent/models.json",
|
|
25
|
+
getArgumentCompletions: (prefix: string) => {
|
|
26
|
+
const subs = ["ls", "add", "remove", "sync", "test", "test-all", "reset", "help"];
|
|
27
|
+
const filtered = subs.filter((s) => s.startsWith(prefix));
|
|
28
|
+
return filtered.length > 0 ? filtered.map((s) => ({ value: s, label: s })) : null;
|
|
29
|
+
},
|
|
30
|
+
handler: async (args, ctx) => {
|
|
31
|
+
const trimmed = (args ?? "").trim();
|
|
32
|
+
const sub = trimmed.split(/\s+/)[0] ?? "";
|
|
33
|
+
const rest = trimmed.slice(sub.length).trim();
|
|
34
|
+
|
|
35
|
+
switch (sub) {
|
|
36
|
+
case "":
|
|
37
|
+
case "ui":
|
|
38
|
+
case "dashboard":
|
|
39
|
+
await openDashboard(ctx);
|
|
40
|
+
return;
|
|
41
|
+
case "ls":
|
|
42
|
+
await cmdLs(rest, ctx);
|
|
43
|
+
return;
|
|
44
|
+
case "help":
|
|
45
|
+
case "-h":
|
|
46
|
+
case "--help":
|
|
47
|
+
cmdHelp(ctx);
|
|
48
|
+
return;
|
|
49
|
+
case "add": {
|
|
50
|
+
if (ctx.mode !== "tui") { ctx.ui.notify("/providers add 需要 TUI 模式。打开 /providers 后按 n", "warning"); return; }
|
|
51
|
+
if (rest) ctx.ui.notify(`将在表单中输入 id="${rest}"`, "info");
|
|
52
|
+
await addProviderFlow(ctx, () => undefined);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
case "remove": {
|
|
56
|
+
const id = rest.trim();
|
|
57
|
+
if (!id) { ctx.ui.notify("用法: /providers remove <id>", "warning"); return; }
|
|
58
|
+
await deleteProviderFlow(ctx, id, () => undefined);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
case "model": {
|
|
62
|
+
const tokens = rest.split(/\s+/);
|
|
63
|
+
const pid = tokens[0];
|
|
64
|
+
if (tokens[1] === "add") {
|
|
65
|
+
if (!pid) { ctx.ui.notify("用法: /providers model <pid> add", "warning"); return; }
|
|
66
|
+
await addModelFlow(ctx, pid, () => undefined);
|
|
67
|
+
} else {
|
|
68
|
+
ctx.ui.notify("用法: /providers model <pid> add", "warning");
|
|
69
|
+
}
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
case "reset": {
|
|
73
|
+
await restoreFromBackupFlow(ctx, () => undefined);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
case "sync": {
|
|
77
|
+
if (ctx.mode !== "tui") { ctx.ui.notify("/providers sync 需要 TUI 模式。打开 /providers 后按 y,或传 <provider-id> 选 source", "warning"); return; }
|
|
78
|
+
await syncFlow(ctx, { sourceProviderId: rest.trim() || undefined });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
case "test-all": {
|
|
82
|
+
await testAllCommand(ctx, rest.trim() || undefined);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
case "test": {
|
|
86
|
+
await testCommand(ctx, rest.trim());
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
default: {
|
|
90
|
+
if (STUBS.has(sub)) {
|
|
91
|
+
ctx.ui.notify(`/providers ${sub} 暂未实现(plan 后续步骤)`, "info");
|
|
92
|
+
} else {
|
|
93
|
+
ctx.ui.notify(`未知子命令: ${sub}。/providers help 查看帮助`, "error");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// /providers ls [filter]
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
async function cmdLs(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
106
|
+
const filter = args.trim().toLowerCase();
|
|
107
|
+
|
|
108
|
+
// 只读 models.json(自定义 provider)
|
|
109
|
+
const json = await readModelsJson();
|
|
110
|
+
const allProviderIds = Object.keys(json.providers).sort();
|
|
111
|
+
|
|
112
|
+
const matchesFilter = (id: string) => !filter || id.toLowerCase().includes(filter);
|
|
113
|
+
const matchedIds = allProviderIds.filter(matchesFilter);
|
|
114
|
+
|
|
115
|
+
if (matchedIds.length === 0) {
|
|
116
|
+
ctx.ui.notify(`没有匹配 "${filter}" 的 provider`, "warning");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 主列表:provider 概要
|
|
121
|
+
const lines: string[] = [];
|
|
122
|
+
lines.push(`Custom providers in models.json (${matchedIds.length}${filter ? `, filter="${filter}"` : ""})`);
|
|
123
|
+
lines.push("─".repeat(60));
|
|
124
|
+
for (const pid of matchedIds) {
|
|
125
|
+
const prov = json.providers[pid];
|
|
126
|
+
const modelCount = prov?.models?.length ?? 0;
|
|
127
|
+
lines.push(` ${pid} — ${modelCount} model(s)`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 详情:每个 provider 的 model 列表
|
|
131
|
+
lines.push("");
|
|
132
|
+
lines.push("Models");
|
|
133
|
+
lines.push("─".repeat(60));
|
|
134
|
+
for (const pid of matchedIds) {
|
|
135
|
+
const prov = json.providers[pid];
|
|
136
|
+
const providerModels = prov?.models ?? [];
|
|
137
|
+
if (providerModels.length === 0) {
|
|
138
|
+
lines.push(` ${pid}: (none)`);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
lines.push(` ${pid}:`);
|
|
142
|
+
for (const m of providerModels) {
|
|
143
|
+
const ctx2 = m.contextWindow ? `${formatNum(m.contextWindow)} ctx` : "? ctx";
|
|
144
|
+
const max2 = m.maxTokens ? `${formatNum(m.maxTokens)} max` : "? max";
|
|
145
|
+
const flags = [m.reasoning && "reasoning", m.input?.includes("image") && "vision"].filter(Boolean).join(" ");
|
|
146
|
+
lines.push(` • ${m.id}${flags ? ` [${flags}]` : ""} (${ctx2}, ${max2})`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const out = lines.join("\n");
|
|
151
|
+
|
|
152
|
+
// print / rpc 模式:走 console.log(headless 下能直接看)
|
|
153
|
+
// TUI 模式:console.log 会破坏 TUI 屏幕,改用 select 列表
|
|
154
|
+
if (ctx.mode === "tui") {
|
|
155
|
+
const selected = await ctx.ui.select(`${matchedIds.length} provider(s)`, lines).catch(() => undefined);
|
|
156
|
+
if (selected) {
|
|
157
|
+
// 尝试解析选中的行:provider 概要行 / model bullet 行
|
|
158
|
+
// provider 概要行: " ${pid} — ${count} model(s)"
|
|
159
|
+
// provider section header: " ${pid}:"
|
|
160
|
+
// model bullet: " • ${modelId} ..."
|
|
161
|
+
const providerHeader = selected.match(/^\s+(\S+):\s*$/);
|
|
162
|
+
const providerSummary = selected.match(/^\s+(\S+)\s+—\s+\d+\s+model\(s\)\s*$/);
|
|
163
|
+
if (providerHeader || providerSummary) {
|
|
164
|
+
const pid = (providerHeader ?? providerSummary)![1]!;
|
|
165
|
+
if (json.providers[pid]) {
|
|
166
|
+
await showProviderModels(ctx, pid);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// model 行 / header 行 / 分隔符:只关闭,do nothing
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
console.log(out);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** 只显示一个 provider 的 models(被 cmdLs drill-down 调用) */
|
|
177
|
+
async function showProviderModels(ctx: ExtensionCommandContext, providerId: string): Promise<void> {
|
|
178
|
+
const json = await readModelsJson();
|
|
179
|
+
const prov = json.providers[providerId];
|
|
180
|
+
if (!prov) { ctx.ui.notify(`Provider "${providerId}" does not exist.`, "error"); return; }
|
|
181
|
+
const models = prov.models ?? [];
|
|
182
|
+
const lines: string[] = [];
|
|
183
|
+
lines.push(`Models of "${providerId}" (${models.length})`);
|
|
184
|
+
lines.push("─".repeat(60));
|
|
185
|
+
if (models.length === 0) {
|
|
186
|
+
lines.push(" (no models)");
|
|
187
|
+
} else {
|
|
188
|
+
for (const m of models) {
|
|
189
|
+
const ctx2 = m.contextWindow ? `${formatNum(m.contextWindow)} ctx` : "? ctx";
|
|
190
|
+
const max2 = m.maxTokens ? `${formatNum(m.maxTokens)} max` : "? max";
|
|
191
|
+
const flags = [m.reasoning && "reasoning", m.input?.includes("image") && "vision"].filter(Boolean).join(" ");
|
|
192
|
+
lines.push(` • ${m.id}${flags ? ` [${flags}]` : ""} (${ctx2}, ${max2})`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (ctx.mode === "tui") {
|
|
196
|
+
await ctx.ui.select(`models of "${providerId}"`, lines).catch(() => undefined);
|
|
197
|
+
} else {
|
|
198
|
+
console.log(lines.join("\n"));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function formatNum(n: number | undefined): string {
|
|
203
|
+
if (n == null) return "?";
|
|
204
|
+
if (n >= 1_000_000) return (n / 1_000_000).toFixed(0) + "M";
|
|
205
|
+
if (n >= 1_000) return (n / 1_000).toFixed(0) + "k";
|
|
206
|
+
return String(n);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
// /providers help
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
function cmdHelp(ctx: ExtensionCommandContext): void {
|
|
214
|
+
const lines = [
|
|
215
|
+
"provider-manager — Manage ~/.pi/agent/models.json",
|
|
216
|
+
"",
|
|
217
|
+
"Usage:",
|
|
218
|
+
" /providers Open TUI dashboard",
|
|
219
|
+
" /providers ls [filter] List providers and their models",
|
|
220
|
+
" /providers add <id> Add a new provider (TUI: open dashboard, press n)",
|
|
221
|
+
" /providers remove <id> Remove a provider (with confirm)",
|
|
222
|
+
" /providers model <pid> add Add a model to a provider (TUI form)",
|
|
223
|
+
" /providers sync [provider-id] Pick a provider (or pass id), fetch remote models, multi-select, write back. TUI: y on selected provider.",
|
|
224
|
+
" /providers test <provider>/<model> Test model (auth + reachable + 1-shot generation). TUI: t on selected model.",
|
|
225
|
+
" /providers test-all [provider] Batch test all models of a provider (concurrency 3). TUI: T on selected provider.",
|
|
226
|
+
" /providers reset Restore models.json.bak (with confirm)",
|
|
227
|
+
" /providers help This help",
|
|
228
|
+
"",
|
|
229
|
+
"Switching models is NOT done by this extension — use Ctrl+L or /model.",
|
|
230
|
+
];
|
|
231
|
+
const out = lines.join("\n");
|
|
232
|
+
console.log(out);
|
|
233
|
+
if (ctx.mode === "tui") {
|
|
234
|
+
// TUI 模式弹 toast + 全屏 select 让用户可滚动
|
|
235
|
+
void ctx.ui.select("provider-manager help", lines).catch(() => undefined);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
// /providers test <provider>/<model> | /providers test-all [provider]
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
import { testModel, testProvider, formatTestResult } from "./test.ts";
|
|
244
|
+
|
|
245
|
+
/** 测单个 model:arg 支持 "<provider>/<model>" 或 "<model>"(缺省走 models.json 里第一个 provider)。 */
|
|
246
|
+
async function testCommand(ctx: ExtensionCommandContext, arg: string): Promise<void> {
|
|
247
|
+
const json = await readModelsJson();
|
|
248
|
+
const m = arg.match(/^([^/\s]+)\/([^/\s]+)$/);
|
|
249
|
+
let provider: string;
|
|
250
|
+
let model: string;
|
|
251
|
+
if (m) {
|
|
252
|
+
provider = m[1];
|
|
253
|
+
model = m[2];
|
|
254
|
+
} else if (arg) {
|
|
255
|
+
// arg 是 model id;provider 用 models.json 里第一个包含该 model 的
|
|
256
|
+
const entry = Object.entries(json.providers).find(([, p]) => (p.models ?? []).some((mm: any) => mm.id === arg));
|
|
257
|
+
if (!entry) {
|
|
258
|
+
ctx.ui.notify(`未找到 model "${arg}"。用法: /providers test <provider>/<model>`, "error");
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
provider = entry[0];
|
|
262
|
+
model = arg;
|
|
263
|
+
} else {
|
|
264
|
+
ctx.ui.notify("用法: /providers test <provider>/<model>,或 /providers test <model>", "warning");
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (!json.providers[provider]) {
|
|
269
|
+
ctx.ui.notify(`provider "${provider}" 不在 models.json 中`, "error");
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const result = await testModel({ ctx: ctx as any, provider, model, mode: "full" });
|
|
274
|
+
ctx.ui.notify(formatTestResult(result), result.ok ? "info" : "warning");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** 批量测某 provider 全部 model;无参数时取第一个 provider。 */
|
|
278
|
+
async function testAllCommand(ctx: ExtensionCommandContext, providerId: string | undefined): Promise<void> {
|
|
279
|
+
const json = await readModelsJson();
|
|
280
|
+
let provider: string;
|
|
281
|
+
if (providerId) {
|
|
282
|
+
if (!json.providers[providerId]) {
|
|
283
|
+
ctx.ui.notify(`provider "${providerId}" 不在 models.json 中`, "error");
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
provider = providerId;
|
|
287
|
+
} else {
|
|
288
|
+
const ids = Object.keys(json.providers);
|
|
289
|
+
if (ids.length === 0) {
|
|
290
|
+
ctx.ui.notify("models.json 中无 provider", "warning");
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
provider = ids[0]!;
|
|
294
|
+
}
|
|
295
|
+
const prov = json.providers[provider]!;
|
|
296
|
+
const modelIds = (prov.models ?? []).map((m: any) => m.id);
|
|
297
|
+
if (modelIds.length === 0) {
|
|
298
|
+
ctx.ui.notify(`provider "${provider}" 无 model`, "warning");
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
ctx.ui.notify(`testing ${modelIds.length} model(s) of "${provider}"...`, "info");
|
|
303
|
+
const results = await testProvider({
|
|
304
|
+
ctx: ctx as any,
|
|
305
|
+
provider,
|
|
306
|
+
modelIds,
|
|
307
|
+
mode: "full",
|
|
308
|
+
concurrency: 3,
|
|
309
|
+
});
|
|
310
|
+
for (const r of results) {
|
|
311
|
+
ctx.ui.notify(formatTestResult(r), r.ok ? "info" : "warning");
|
|
312
|
+
}
|
|
313
|
+
}
|