@fanchaozz/provider-manager 0.1.1 → 0.2.1
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 +58 -53
- package/README_EN.md +14 -15
- package/commands.ts +1 -13
- package/components.ts +75 -57
- package/forms.ts +76 -156
- package/package.json +4 -2
- package/store.ts +1 -0
- package/sync.ts +29 -3
- package/test.ts +354 -354
- package/ui.ts +213 -107
package/forms.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
16
16
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
17
17
|
import { readModelsJson, writeModelsJson, backupExists, restoreBackup, type ModelsJson, type ProviderConfig, type ModelConfig, ALLOWED_APIS } from "./store.ts";
|
|
18
|
-
import { fetchListing, inferModel,
|
|
18
|
+
import { fetchListing, inferModel, diffModels } from "./sync.ts";
|
|
19
19
|
import { ModelChecklist, FormEditor, type FormField } from "./components.ts";
|
|
20
20
|
|
|
21
21
|
// ============================================================================
|
|
@@ -27,17 +27,6 @@ const API_OPTIONS: string[] = [
|
|
|
27
27
|
...ALLOWED_APIS,
|
|
28
28
|
"(none / 由 model 字段指定)",
|
|
29
29
|
];
|
|
30
|
-
const INPUT_OPTIONS: string[] = ["text", "image (supports image)"];
|
|
31
|
-
|
|
32
|
-
/** thinking level 预设(避免用户手输 JSON)。
|
|
33
|
-
* null = 禁用该 level,string = 映射到 provider 那个字符串值。 */
|
|
34
|
-
const THINKING_PRESETS: { label: string; map: ModelConfig["thinkingLevelMap"] }[] = [
|
|
35
|
-
{ label: "(不设 / 用 provider 默认)", map: {} },
|
|
36
|
-
{ label: "Anthropic 风格 (low/medium/high/max → 同名 + off=null, minimal=null)", map: { off: null, minimal: null, low: "low", medium: "medium", high: "high", xhigh: null, max: "max" } },
|
|
37
|
-
{ label: "OpenAI o1 风格 (low/medium/high → 同名 + off=null, minimal=null)", map: { off: null, minimal: null, low: "low", medium: "medium", high: "high" } },
|
|
38
|
-
{ label: "只暴露 high+max (其他全 null)", map: { off: null, minimal: null, low: null, medium: null, high: "high", xhigh: null, max: "max" } },
|
|
39
|
-
{ label: "Custom (Enter 自填 JSON)", map: { __custom: true } as any },
|
|
40
|
-
];
|
|
41
30
|
|
|
42
31
|
/** 新 model 的默认配置。调 /providers model <pid> add 或 dashboard n 走 addModelFlow 时
|
|
43
32
|
* 会问 "Use defaults?",回答 yes → 套这里的所有值;回答 no → 逐个问。 */
|
|
@@ -47,6 +36,7 @@ export const DEFAULT_MODEL_CONFIG: {
|
|
|
47
36
|
contextWindow: number;
|
|
48
37
|
maxTokens: number;
|
|
49
38
|
thinkingLevelMap: ModelConfig["thinkingLevelMap"];
|
|
39
|
+
compat: { supportsDeveloperRole: boolean };
|
|
50
40
|
} = {
|
|
51
41
|
reasoning: true,
|
|
52
42
|
input: ["text", "image"],
|
|
@@ -61,6 +51,8 @@ export const DEFAULT_MODEL_CONFIG: {
|
|
|
61
51
|
xhigh: null,
|
|
62
52
|
max: null,
|
|
63
53
|
},
|
|
54
|
+
// Zhipu GLM 等 OpenAI-compat 网关拒收 role:"developer"(会返 422)。默认 false → pi 用 system role。
|
|
55
|
+
compat: { supportsDeveloperRole: false },
|
|
64
56
|
};
|
|
65
57
|
|
|
66
58
|
// ============================================================================
|
|
@@ -99,13 +91,19 @@ export function ensureDefaultConfigFile(): string | null {
|
|
|
99
91
|
}
|
|
100
92
|
|
|
101
93
|
function isValidDefaultModelConfig(v: any): v is typeof DEFAULT_MODEL_CONFIG {
|
|
94
|
+
const compatOk = !v.compat
|
|
95
|
+
|| (typeof v.compat === "object" && !Array.isArray(v.compat) && (
|
|
96
|
+
v.compat.supportsDeveloperRole === undefined
|
|
97
|
+
|| typeof v.compat.supportsDeveloperRole === "boolean"
|
|
98
|
+
));
|
|
102
99
|
return (
|
|
103
100
|
v && typeof v === "object" &&
|
|
104
101
|
typeof v.reasoning === "boolean" &&
|
|
105
102
|
Array.isArray(v.input) && v.input.every((x: any) => x === "text" || x === "image") && v.input.length > 0 &&
|
|
106
103
|
typeof v.contextWindow === "number" && v.contextWindow > 0 && Number.isFinite(v.contextWindow) &&
|
|
107
104
|
typeof v.maxTokens === "number" && v.maxTokens > 0 && Number.isFinite(v.maxTokens) &&
|
|
108
|
-
v.thinkingLevelMap && typeof v.thinkingLevelMap === "object" && !Array.isArray(v.thinkingLevelMap)
|
|
105
|
+
v.thinkingLevelMap && typeof v.thinkingLevelMap === "object" && !Array.isArray(v.thinkingLevelMap) &&
|
|
106
|
+
compatOk
|
|
109
107
|
);
|
|
110
108
|
}
|
|
111
109
|
|
|
@@ -120,7 +118,11 @@ export function loadDefaultModelConfig(): typeof DEFAULT_MODEL_CONFIG {
|
|
|
120
118
|
const raw = readFileSync(p, "utf8");
|
|
121
119
|
const parsed = JSON.parse(raw);
|
|
122
120
|
const cfg = parsed?.defaultModel;
|
|
123
|
-
if (isValidDefaultModelConfig(cfg))
|
|
121
|
+
if (isValidDefaultModelConfig(cfg)) {
|
|
122
|
+
// 补全缺失的 compat(老 config 没有这个字段时默认为 false)
|
|
123
|
+
if (!cfg.compat) cfg.compat = { supportsDeveloperRole: false };
|
|
124
|
+
return cfg;
|
|
125
|
+
}
|
|
124
126
|
} catch {
|
|
125
127
|
// 回退到代码默认
|
|
126
128
|
}
|
|
@@ -161,9 +163,8 @@ async function askSelect(
|
|
|
161
163
|
return result;
|
|
162
164
|
}
|
|
163
165
|
|
|
164
|
-
async function askConfirm(ctx: ExtensionCommandContext, title: string, message: string
|
|
166
|
+
async function askConfirm(ctx: ExtensionCommandContext, title: string, message: string): Promise<boolean | undefined> {
|
|
165
167
|
return ctx.ui.confirm(title, message);
|
|
166
|
-
// 注:confirm 不支持 defaultValue,UI 自带 yes/no
|
|
167
168
|
}
|
|
168
169
|
|
|
169
170
|
/** 包 FormEditor 进 ctx.ui.custom dialog。返回 { saved, values } 或 { saved: false, values: initial }。 */
|
|
@@ -196,164 +197,74 @@ async function runFormEditor<T extends Record<string, unknown>>(
|
|
|
196
197
|
// ============================================================================
|
|
197
198
|
|
|
198
199
|
export async function addProviderFlow(ctx: ExtensionCommandContext, onDone: () => void): Promise<void> {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
return null;
|
|
205
|
-
},
|
|
206
|
-
});
|
|
207
|
-
if (!id) return;
|
|
200
|
+
if (ctx.mode !== "tui") {
|
|
201
|
+
ctx.ui.notify("add provider 需要 TUI 模式。打开 /providers 后按 n", "warning");
|
|
202
|
+
onDone?.();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
208
205
|
|
|
206
|
+
// 与 editProviderFlow 同形:一次性表单采集所有字段(含 id)
|
|
207
|
+
// 错误(id 重复 / id 非法)不走 notify 弹窗 — 留在表单里提示,点 s 后再调
|
|
209
208
|
const json = await readModelsJson();
|
|
209
|
+
const fields: FormField[] = [
|
|
210
|
+
{ key: "id", label: "id", type: "text", hint: "[a-z0-9_-]+", validate: (s) => {
|
|
211
|
+
if (!s) return "id required";
|
|
212
|
+
if (!/^[a-z0-9_-]+$/i.test(s as string)) return "id must match [a-z0-9_-]+";
|
|
213
|
+
if (json.providers[s as string]) return `provider "${s}" already exists`;
|
|
214
|
+
return null;
|
|
215
|
+
} },
|
|
216
|
+
{ key: "name", label: "Display name", type: "text", hint: "(empty = unset)" },
|
|
217
|
+
{ key: "baseUrl", label: "baseUrl", type: "text" },
|
|
218
|
+
{ key: "apiKey", label: "apiKey", type: "secret" },
|
|
219
|
+
{ key: "api", label: "api", type: "select", options: API_OPTIONS, hint: "(empty = unset)" },
|
|
220
|
+
{ key: "authHeader", label: "authHeader", type: "select", options: ["no", "yes"] },
|
|
221
|
+
{ key: "proxy", label: "proxy", type: "text", hint: "(empty = unset; http://host:port)" },
|
|
222
|
+
];
|
|
223
|
+
const initial: Record<string, unknown> = {
|
|
224
|
+
id: "",
|
|
225
|
+
name: "",
|
|
226
|
+
baseUrl: "",
|
|
227
|
+
apiKey: "",
|
|
228
|
+
api: "",
|
|
229
|
+
authHeader: "no",
|
|
230
|
+
proxy: "",
|
|
231
|
+
};
|
|
232
|
+
const result = await runFormEditor(ctx, `Add provider`, fields, initial);
|
|
233
|
+
if (!result.saved) { onDone?.(); return; }
|
|
234
|
+
const v = result.values;
|
|
235
|
+
const id = (v.id as string).trim();
|
|
236
|
+
// 二次校验:表单后还会再查一次(同进程可能别的并发写)
|
|
210
237
|
if (json.providers[id]) {
|
|
211
238
|
ctx.ui.notify(`Provider "${id}" already exists. Use /providers remove ${id} first.`, "error");
|
|
239
|
+
onDone?.();
|
|
212
240
|
return;
|
|
213
241
|
}
|
|
214
|
-
|
|
215
|
-
const name = await askInput(ctx, { message: `Display name (optional):`, placeholder: id });
|
|
216
|
-
if (name === undefined) return;
|
|
217
|
-
|
|
218
|
-
const baseUrl = await askInput(ctx, { message: "baseUrl (e.g. http://localhost:11434/v1):", placeholder: "https://api.example.com/v1" });
|
|
219
|
-
if (baseUrl === undefined) return;
|
|
220
|
-
|
|
221
|
-
const apiKey = await askInput(ctx, { message: "apiKey (empty = no key):", secret: true });
|
|
222
|
-
if (apiKey === undefined) return;
|
|
223
|
-
|
|
224
|
-
const apiChoice = await askSelect(ctx, { message: "API type:", options: API_OPTIONS });
|
|
225
|
-
if (apiChoice === undefined) return;
|
|
226
|
-
|
|
227
242
|
const newProv: ProviderConfig = {
|
|
228
|
-
...(name ? { name } : {}),
|
|
229
|
-
baseUrl,
|
|
230
|
-
apiKey: apiKey || undefined,
|
|
231
|
-
api:
|
|
243
|
+
...((v.name as string) ? { name: v.name as string } : {}),
|
|
244
|
+
baseUrl: ((v.baseUrl as string) || "") || undefined,
|
|
245
|
+
apiKey: ((v.apiKey as string) || "") || undefined,
|
|
246
|
+
api: ((v.api as string) || "") || undefined,
|
|
247
|
+
authHeader: v.authHeader === "yes",
|
|
248
|
+
proxy: ((v.proxy as string) || "") || undefined,
|
|
232
249
|
models: [],
|
|
233
250
|
};
|
|
234
251
|
try {
|
|
235
|
-
|
|
236
|
-
|
|
252
|
+
const fresh = await readModelsJson();
|
|
253
|
+
await writeModelsJson({ ...fresh, providers: { ...fresh.providers, [id]: newProv } });
|
|
254
|
+
ctx.ui.notify(`✓ Provider "${id}" added. Use 'y' to sync models.`, "success");
|
|
237
255
|
} catch (err) {
|
|
238
256
|
ctx.ui.notify(`Write failed: ${err instanceof Error ? err.message : err}`, "error");
|
|
239
257
|
}
|
|
240
258
|
onDone?.();
|
|
241
259
|
}
|
|
242
260
|
|
|
261
|
+
/** @deprecated Models are only added/removed via sync. Kept as a stub so old imports do not crash; logs a notice. */
|
|
243
262
|
export async function addModelFlow(
|
|
244
263
|
ctx: ExtensionCommandContext,
|
|
245
|
-
|
|
264
|
+
_providerId: string,
|
|
246
265
|
onDone?: () => void,
|
|
247
266
|
): Promise<void> {
|
|
248
|
-
|
|
249
|
-
const prov = json.providers[providerId];
|
|
250
|
-
if (!prov) {
|
|
251
|
-
ctx.ui.notify(`Provider "${providerId}" does not exist.`, "error");
|
|
252
|
-
onDone?.();
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const id = await askInput(ctx, { message: "Model id (e.g. gpt-5 / claude-opus-4-7):", validate: (s) => s ? null : "id required" });
|
|
257
|
-
if (!id) return;
|
|
258
|
-
|
|
259
|
-
if ((prov.models ?? []).some((m) => m.id === id)) {
|
|
260
|
-
ctx.ui.notify(`Model "${id}" already exists in "${providerId}".`, "error");
|
|
261
|
-
onDone?.();
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
const name = await askInput(ctx, { message: `Display name (optional):`, placeholder: id });
|
|
266
|
-
if (name === undefined) return;
|
|
267
|
-
|
|
268
|
-
// 用默认配置?reasoning/input/ctx/max/thinkingLevelMap 都用 DEFAULT_MODEL_CONFIG+可被 ~/.pi/agent/provider-manager.json#defaultModel 覆盖
|
|
269
|
-
const DEFAULT_CFG = loadDefaultModelConfig();
|
|
270
|
-
const useDefaults = await askConfirm(
|
|
271
|
-
ctx,
|
|
272
|
-
"Use default config?",
|
|
273
|
-
`reasoning=${DEFAULT_CFG.reasoning ? "yes" : "no"} · input=${DEFAULT_CFG.input.join("+")} · ctx=${DEFAULT_CFG.contextWindow} · max=${DEFAULT_CFG.maxTokens} · thinkingLevelMap: medium=medium, others=null. (After creation, use 'e' to customize.)`,
|
|
274
|
-
);
|
|
275
|
-
if (useDefaults === undefined) return;
|
|
276
|
-
|
|
277
|
-
let reasoning: boolean;
|
|
278
|
-
let input: ("text" | "image")[];
|
|
279
|
-
let ctxWindow: number;
|
|
280
|
-
let maxTokens: number;
|
|
281
|
-
let thinkingLevelMap: ModelConfig["thinkingLevelMap"] | undefined;
|
|
282
|
-
|
|
283
|
-
if (useDefaults) {
|
|
284
|
-
reasoning = DEFAULT_CFG.reasoning;
|
|
285
|
-
input = [...DEFAULT_CFG.input];
|
|
286
|
-
ctxWindow = DEFAULT_CFG.contextWindow;
|
|
287
|
-
maxTokens = DEFAULT_CFG.maxTokens;
|
|
288
|
-
thinkingLevelMap = { ...DEFAULT_CFG.thinkingLevelMap };
|
|
289
|
-
} else {
|
|
290
|
-
reasoning = await askConfirm(ctx, "Supports extended thinking?", "Yes for o1/o3/reasoning models, No otherwise.");
|
|
291
|
-
if (reasoning === undefined) return;
|
|
292
|
-
|
|
293
|
-
const inputType = await askSelect(ctx, { message: "Input type:", options: INPUT_OPTIONS });
|
|
294
|
-
if (inputType === undefined) return;
|
|
295
|
-
input = inputType === "text" ? ["text"] : ["text", "image"];
|
|
296
|
-
|
|
297
|
-
const ctxWindowStr = await askInput(ctx, { message: "context window tokens (empty = 128000):", placeholder: "128000", validate: (s) => !s || /^\d+$/.test(s) ? null : "must be a number" });
|
|
298
|
-
if (ctxWindowStr === undefined) return;
|
|
299
|
-
ctxWindow = ctxWindowStr ? parseInt(ctxWindowStr, 10) : 128000;
|
|
300
|
-
|
|
301
|
-
const maxTokensStr = await askInput(ctx, { message: "max output tokens (empty = 16384):", placeholder: "16384", validate: (s) => !s || /^\d+$/.test(s) ? null : "must be a number" });
|
|
302
|
-
if (maxTokensStr === undefined) return;
|
|
303
|
-
maxTokens = maxTokensStr ? parseInt(maxTokensStr, 10) : 16384;
|
|
304
|
-
|
|
305
|
-
if (reasoning) {
|
|
306
|
-
const presetLabels = THINKING_PRESETS.map((p) => p.label);
|
|
307
|
-
const pick = await askSelect(ctx, {
|
|
308
|
-
message: "Thinking level map (provider-dependent):",
|
|
309
|
-
options: presetLabels,
|
|
310
|
-
defaultValue: presetLabels[0],
|
|
311
|
-
});
|
|
312
|
-
if (pick === undefined) return;
|
|
313
|
-
const preset = THINKING_PRESETS.find((p) => p.label === pick);
|
|
314
|
-
if (preset && Object.keys(preset.map).length > 0) {
|
|
315
|
-
if ((preset.map as any).__custom) {
|
|
316
|
-
const mapStr = await askInput(ctx, {
|
|
317
|
-
message: 'thinking level map (JSON, e.g. {"low":"low","medium":"medium"}):',
|
|
318
|
-
placeholder: '{"off":null,"low":"low"}',
|
|
319
|
-
});
|
|
320
|
-
if (mapStr === undefined) return;
|
|
321
|
-
if (mapStr.trim()) {
|
|
322
|
-
try {
|
|
323
|
-
const parsed = JSON.parse(mapStr);
|
|
324
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
325
|
-
thinkingLevelMap = parsed as ModelConfig["thinkingLevelMap"];
|
|
326
|
-
} else {
|
|
327
|
-
ctx.ui.notify("thinking level map must be a JSON object; skipping", "warning");
|
|
328
|
-
}
|
|
329
|
-
} catch (err) {
|
|
330
|
-
ctx.ui.notify(`thinking level map JSON invalid: ${err instanceof Error ? err.message : err}; skipping`, "warning");
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
} else {
|
|
334
|
-
thinkingLevelMap = preset.map;
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
const model: ModelConfig = {
|
|
341
|
-
id,
|
|
342
|
-
name: name || undefined,
|
|
343
|
-
reasoning,
|
|
344
|
-
input,
|
|
345
|
-
contextWindow: ctxWindow,
|
|
346
|
-
maxTokens,
|
|
347
|
-
...(thinkingLevelMap ? { thinkingLevelMap } : {}),
|
|
348
|
-
};
|
|
349
|
-
|
|
350
|
-
const newProv: ProviderConfig = { ...prov, models: [...(prov.models ?? []), model] };
|
|
351
|
-
try {
|
|
352
|
-
await writeModelsJson({ ...json, providers: { ...json.providers, [providerId]: newProv } });
|
|
353
|
-
ctx.ui.notify(`✓ Model "${id}" added to "${providerId}".`, "success");
|
|
354
|
-
} catch (err) {
|
|
355
|
-
ctx.ui.notify(`Write failed: ${err instanceof Error ? err.message : err}`, "error");
|
|
356
|
-
}
|
|
267
|
+
ctx.ui.notify("新增 model 请用 sync(按 y)。该入口已停用。", "warning");
|
|
357
268
|
onDone?.();
|
|
358
269
|
}
|
|
359
270
|
|
|
@@ -381,6 +292,7 @@ export async function editProviderFlow(
|
|
|
381
292
|
{ key: "apiKey", label: "apiKey", type: "secret" },
|
|
382
293
|
{ key: "api", label: "api", type: "select", options: API_OPTIONS, hint: "1-N 选" },
|
|
383
294
|
{ key: "authHeader", label: "authHeader", type: "select", options: ["no", "yes"] },
|
|
295
|
+
{ key: "proxy", label: "proxy", type: "text", hint: "(empty = unset; http://host:port)" },
|
|
384
296
|
];
|
|
385
297
|
const initial: Record<string, unknown> = {
|
|
386
298
|
name: cur.name ?? "",
|
|
@@ -388,6 +300,7 @@ export async function editProviderFlow(
|
|
|
388
300
|
apiKey: cur.apiKey ?? "",
|
|
389
301
|
api: cur.api ?? "",
|
|
390
302
|
authHeader: cur.authHeader ? "yes" : "no",
|
|
303
|
+
proxy: cur.proxy ?? "",
|
|
391
304
|
};
|
|
392
305
|
const result = await runFormEditor(ctx, `Edit provider "${providerId}"`, fields, initial);
|
|
393
306
|
if (!result.saved) { onDone?.(); return; }
|
|
@@ -399,6 +312,7 @@ export async function editProviderFlow(
|
|
|
399
312
|
apiKey: ((v.apiKey as string) || "") || undefined,
|
|
400
313
|
api: ((v.api as string) || "") || undefined,
|
|
401
314
|
authHeader: v.authHeader === "yes",
|
|
315
|
+
proxy: ((v.proxy as string) || "") || undefined,
|
|
402
316
|
};
|
|
403
317
|
try {
|
|
404
318
|
await writeModelsJson({ ...json, providers: { ...json.providers, [providerId]: next } });
|
|
@@ -462,6 +376,8 @@ export async function editModelFlow(
|
|
|
462
376
|
{ key: "contextWindow", label: "contextWindow", type: "number", validate: (v) => typeof v === "number" && v >= 0 ? null : "must be non-negative" },
|
|
463
377
|
{ key: "maxTokens", label: "maxTokens", type: "number", validate: (v) => typeof v === "number" && v >= 0 ? null : "must be non-negative" },
|
|
464
378
|
{ key: "thinkingLevelMap", label: "thinkingLevelMap", type: "levelmap", hint: "(empty = remove)" },
|
|
379
|
+
// Zhipu GLM 等 OpenAI-compat 网关不接受 role:"developer" (会返 422)。默认 no 用 system role。
|
|
380
|
+
{ key: "supportsDeveloperRole", label: "supportsDeveloperRole (compat)", type: "select", options: ["no", "yes"], hint: "Zhipu GLM 等需 no (用 system role)" },
|
|
465
381
|
];
|
|
466
382
|
const initial: Record<string, unknown> = {
|
|
467
383
|
name: cur.name ?? "",
|
|
@@ -470,6 +386,7 @@ export async function editModelFlow(
|
|
|
470
386
|
contextWindow: cur.contextWindow ?? 0,
|
|
471
387
|
maxTokens: cur.maxTokens ?? 0,
|
|
472
388
|
thinkingLevelMap: cur.thinkingLevelMap ?? null,
|
|
389
|
+
supportsDeveloperRole: (cur.compat as any)?.supportsDeveloperRole === true ? "yes" : "no",
|
|
473
390
|
};
|
|
474
391
|
const result = await runFormEditor(ctx, `Edit model "${providerId}/${modelId}"`, fields, initial);
|
|
475
392
|
if (!result.saved) { onDone?.(); return; }
|
|
@@ -482,6 +399,7 @@ export async function editModelFlow(
|
|
|
482
399
|
contextWindow: (v.contextWindow as number) || undefined,
|
|
483
400
|
maxTokens: (v.maxTokens as number) || undefined,
|
|
484
401
|
thinkingLevelMap: (v.thinkingLevelMap as Record<string, unknown> | null) ?? undefined,
|
|
402
|
+
compat: { ...(cur.compat ?? {}), supportsDeveloperRole: v.supportsDeveloperRole === "yes" },
|
|
485
403
|
};
|
|
486
404
|
const newModels = (prov.models ?? []).map((m) => (m.id === modelId ? next : m));
|
|
487
405
|
const newProv: ProviderConfig = { ...prov, models: newModels };
|
|
@@ -557,7 +475,7 @@ export async function syncFlow(ctx: ExtensionCommandContext, opts: SyncOpts = {}
|
|
|
557
475
|
ctx.ui.notify(`Fetching models from ${prov.baseUrl}...`, "info");
|
|
558
476
|
let result;
|
|
559
477
|
try {
|
|
560
|
-
result = await fetchListing({ baseUrl: prov.baseUrl, apiKey, apiKind, signal: ctx.signal, timeoutMs: 10000 });
|
|
478
|
+
result = await fetchListing({ baseUrl: prov.baseUrl, apiKey, apiKind, proxy: prov.proxy, signal: ctx.signal, timeoutMs: 10000 });
|
|
561
479
|
} catch (err) {
|
|
562
480
|
ctx.ui.notify(`Fetch failed: ${err instanceof Error ? err.message : err}`, "error");
|
|
563
481
|
opts.onDone?.();
|
|
@@ -566,7 +484,9 @@ export async function syncFlow(ctx: ExtensionCommandContext, opts: SyncOpts = {}
|
|
|
566
484
|
if (result.warnings.length) ctx.ui.notify(result.warnings.join("; "), "warning");
|
|
567
485
|
if (result.models.length === 0 && (prov.models ?? []).length === 0) { ctx.ui.notify("No models found. Check baseUrl / api key.", "warning"); opts.onDone?.(); return; }
|
|
568
486
|
const existing = (prov.models ?? []).map((m) => ({ id: m.id }));
|
|
569
|
-
|
|
487
|
+
// 关键修复:传 loadDefaultModelConfig() 作 defaults,使 toAdd 使用用户级 default(不是代码内置默认)
|
|
488
|
+
const userDefaults = loadDefaultModelConfig();
|
|
489
|
+
const { toAdd } = diffModels(result.models, existing, { defaults: userDefaults });
|
|
570
490
|
// wire pi done directly to checklist onConfirm/onCancel (otherwise dialog never closes)
|
|
571
491
|
// checklist shows ALL models in this provider:
|
|
572
492
|
// - existing: label " (existing)", default checked (uncheck = remove)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fanchaozz/provider-manager",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "A pi extension that manages custom providers and models in ~/.pi/agent/models.json via a TUI dashboard, /providers slash command, and remote model sync.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
"ai"
|
|
18
18
|
],
|
|
19
19
|
"pi": {
|
|
20
|
-
"extensions": [
|
|
20
|
+
"extensions": [
|
|
21
|
+
"./"
|
|
22
|
+
]
|
|
21
23
|
},
|
|
22
24
|
"license": "MIT",
|
|
23
25
|
"author": "fanchaozz <fanchaozz@users.noreply.github.com>",
|
package/store.ts
CHANGED
package/sync.ts
CHANGED
|
@@ -107,10 +107,11 @@ export async function fetchListing(opts: {
|
|
|
107
107
|
baseUrl: string;
|
|
108
108
|
apiKey?: string;
|
|
109
109
|
apiKind: ApiKind;
|
|
110
|
+
proxy?: string;
|
|
110
111
|
signal?: AbortSignal;
|
|
111
112
|
timeoutMs?: number;
|
|
112
113
|
}): Promise<FetchResult> {
|
|
113
|
-
const { baseUrl, apiKey, apiKind, signal, timeoutMs = 10000 } = opts;
|
|
114
|
+
const { baseUrl, apiKey, apiKind, proxy, signal, timeoutMs = 10000 } = opts;
|
|
114
115
|
const warnings: string[] = [];
|
|
115
116
|
|
|
116
117
|
// 用 AbortController 双重保护:外部 signal + 超时
|
|
@@ -119,11 +120,30 @@ export async function fetchListing(opts: {
|
|
|
119
120
|
const onAbort = () => ctrl.abort(signal!.reason);
|
|
120
121
|
if (signal) signal.addEventListener("abort", onAbort);
|
|
121
122
|
|
|
123
|
+
// proxy:调 fetch 前设 env,取走后清(Node 18+ 的 undici fetch 读 HTTPS_PROXY/HTTP_PROXY)
|
|
124
|
+
// 限制:env 是进程全局的,sync 一次只 1 个 fetch,其他并发 fetch 会看到同样 proxy
|
|
125
|
+
const envPrev = proxy ? { HTTPS_PROXY: process.env.HTTPS_PROXY, HTTP_PROXY: process.env.HTTP_PROXY } : null;
|
|
126
|
+
if (proxy) {
|
|
127
|
+
process.env.HTTPS_PROXY = proxy;
|
|
128
|
+
process.env.HTTP_PROXY = proxy;
|
|
129
|
+
}
|
|
130
|
+
|
|
122
131
|
try {
|
|
123
132
|
const base = baseUrl.replace(/\/+$/, "");
|
|
133
|
+
// pre-flight: undici 要求 header value 是 Latin-1(每字符 code < 256)。
|
|
134
|
+
// apiKey 从复制粘贴过来常含 •、中文、emoji 等,会让 fetch 抛 "Cannot convert argument to a ByteString"。
|
|
135
|
+
// 这里提前检测并给 actionable 错误。
|
|
136
|
+
if (apiKey) {
|
|
137
|
+
for (let i = 0; i < apiKey.length; i++) {
|
|
138
|
+
if (apiKey.charCodeAt(i) > 255) {
|
|
139
|
+
const code = apiKey.codePointAt(i) ?? 0;
|
|
140
|
+
throw new Error(`apiKey contains non-Latin-1 character at position ${i} (U+${code.toString(16).toUpperCase()}). Re-enter the key in the provider form.`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
124
145
|
let url: string;
|
|
125
146
|
let headers: Record<string, string> = {};
|
|
126
|
-
let body: any;
|
|
127
147
|
|
|
128
148
|
if (apiKind === "google") {
|
|
129
149
|
// GET {base}/models?key=$KEY → { models: [{ name, ... }] }
|
|
@@ -191,6 +211,11 @@ export async function fetchListing(opts: {
|
|
|
191
211
|
} finally {
|
|
192
212
|
clearTimeout(timer);
|
|
193
213
|
if (signal) signal.removeEventListener("abort", onAbort);
|
|
214
|
+
// 还原 env(避免污染后续 fetch)
|
|
215
|
+
if (envPrev) {
|
|
216
|
+
process.env.HTTPS_PROXY = envPrev.HTTPS_PROXY;
|
|
217
|
+
process.env.HTTP_PROXY = envPrev.HTTP_PROXY;
|
|
218
|
+
}
|
|
194
219
|
}
|
|
195
220
|
}
|
|
196
221
|
|
|
@@ -199,7 +224,7 @@ export async function fetchListing(opts: {
|
|
|
199
224
|
// ============================================================================
|
|
200
225
|
|
|
201
226
|
export async function syncExisting(
|
|
202
|
-
providers: Record<string, { baseUrl?: string; apiKey?: string; api?: string }>,
|
|
227
|
+
providers: Record<string, { baseUrl?: string; apiKey?: string; api?: string; proxy?: string }>,
|
|
203
228
|
signal?: AbortSignal,
|
|
204
229
|
timeoutMs?: number,
|
|
205
230
|
): Promise<Array<{ providerId: string; result: FetchResult | { error: string } }>> {
|
|
@@ -212,6 +237,7 @@ export async function syncExisting(
|
|
|
212
237
|
baseUrl: p.baseUrl,
|
|
213
238
|
apiKey: p.apiKey,
|
|
214
239
|
apiKind,
|
|
240
|
+
proxy: p.proxy,
|
|
215
241
|
signal,
|
|
216
242
|
timeoutMs,
|
|
217
243
|
});
|