@fanchaozz/provider-manager 0.1.0 → 0.2.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 +60 -59
- package/README_EN.md +16 -22
- package/commands.ts +1 -13
- package/components.ts +75 -57
- package/forms.ts +57 -154
- package/package.json +5 -2
- package/store.ts +1 -0
- package/sync.ts +29 -3
- package/ui.ts +201 -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 → 逐个问。 */
|
|
@@ -161,9 +150,8 @@ async function askSelect(
|
|
|
161
150
|
return result;
|
|
162
151
|
}
|
|
163
152
|
|
|
164
|
-
async function askConfirm(ctx: ExtensionCommandContext, title: string, message: string
|
|
153
|
+
async function askConfirm(ctx: ExtensionCommandContext, title: string, message: string): Promise<boolean | undefined> {
|
|
165
154
|
return ctx.ui.confirm(title, message);
|
|
166
|
-
// 注:confirm 不支持 defaultValue,UI 自带 yes/no
|
|
167
155
|
}
|
|
168
156
|
|
|
169
157
|
/** 包 FormEditor 进 ctx.ui.custom dialog。返回 { saved, values } 或 { saved: false, values: initial }。 */
|
|
@@ -196,164 +184,74 @@ async function runFormEditor<T extends Record<string, unknown>>(
|
|
|
196
184
|
// ============================================================================
|
|
197
185
|
|
|
198
186
|
export async function addProviderFlow(ctx: ExtensionCommandContext, onDone: () => void): Promise<void> {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
return null;
|
|
205
|
-
},
|
|
206
|
-
});
|
|
207
|
-
if (!id) return;
|
|
187
|
+
if (ctx.mode !== "tui") {
|
|
188
|
+
ctx.ui.notify("add provider 需要 TUI 模式。打开 /providers 后按 n", "warning");
|
|
189
|
+
onDone?.();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
208
192
|
|
|
193
|
+
// 与 editProviderFlow 同形:一次性表单采集所有字段(含 id)
|
|
194
|
+
// 错误(id 重复 / id 非法)不走 notify 弹窗 — 留在表单里提示,点 s 后再调
|
|
209
195
|
const json = await readModelsJson();
|
|
196
|
+
const fields: FormField[] = [
|
|
197
|
+
{ key: "id", label: "id", type: "text", hint: "[a-z0-9_-]+", validate: (s) => {
|
|
198
|
+
if (!s) return "id required";
|
|
199
|
+
if (!/^[a-z0-9_-]+$/i.test(s as string)) return "id must match [a-z0-9_-]+";
|
|
200
|
+
if (json.providers[s as string]) return `provider "${s}" already exists`;
|
|
201
|
+
return null;
|
|
202
|
+
} },
|
|
203
|
+
{ key: "name", label: "Display name", type: "text", hint: "(empty = unset)" },
|
|
204
|
+
{ key: "baseUrl", label: "baseUrl", type: "text" },
|
|
205
|
+
{ key: "apiKey", label: "apiKey", type: "secret" },
|
|
206
|
+
{ key: "api", label: "api", type: "select", options: API_OPTIONS, hint: "(empty = unset)" },
|
|
207
|
+
{ key: "authHeader", label: "authHeader", type: "select", options: ["no", "yes"] },
|
|
208
|
+
{ key: "proxy", label: "proxy", type: "text", hint: "(empty = unset; http://host:port)" },
|
|
209
|
+
];
|
|
210
|
+
const initial: Record<string, unknown> = {
|
|
211
|
+
id: "",
|
|
212
|
+
name: "",
|
|
213
|
+
baseUrl: "",
|
|
214
|
+
apiKey: "",
|
|
215
|
+
api: "",
|
|
216
|
+
authHeader: "no",
|
|
217
|
+
proxy: "",
|
|
218
|
+
};
|
|
219
|
+
const result = await runFormEditor(ctx, `Add provider`, fields, initial);
|
|
220
|
+
if (!result.saved) { onDone?.(); return; }
|
|
221
|
+
const v = result.values;
|
|
222
|
+
const id = (v.id as string).trim();
|
|
223
|
+
// 二次校验:表单后还会再查一次(同进程可能别的并发写)
|
|
210
224
|
if (json.providers[id]) {
|
|
211
225
|
ctx.ui.notify(`Provider "${id}" already exists. Use /providers remove ${id} first.`, "error");
|
|
226
|
+
onDone?.();
|
|
212
227
|
return;
|
|
213
228
|
}
|
|
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
229
|
const newProv: ProviderConfig = {
|
|
228
|
-
...(name ? { name } : {}),
|
|
229
|
-
baseUrl,
|
|
230
|
-
apiKey: apiKey || undefined,
|
|
231
|
-
api:
|
|
230
|
+
...((v.name as string) ? { name: v.name as string } : {}),
|
|
231
|
+
baseUrl: ((v.baseUrl as string) || "") || undefined,
|
|
232
|
+
apiKey: ((v.apiKey as string) || "") || undefined,
|
|
233
|
+
api: ((v.api as string) || "") || undefined,
|
|
234
|
+
authHeader: v.authHeader === "yes",
|
|
235
|
+
proxy: ((v.proxy as string) || "") || undefined,
|
|
232
236
|
models: [],
|
|
233
237
|
};
|
|
234
238
|
try {
|
|
235
|
-
|
|
236
|
-
|
|
239
|
+
const fresh = await readModelsJson();
|
|
240
|
+
await writeModelsJson({ ...fresh, providers: { ...fresh.providers, [id]: newProv } });
|
|
241
|
+
ctx.ui.notify(`✓ Provider "${id}" added. Use 'y' to sync models.`, "success");
|
|
237
242
|
} catch (err) {
|
|
238
243
|
ctx.ui.notify(`Write failed: ${err instanceof Error ? err.message : err}`, "error");
|
|
239
244
|
}
|
|
240
245
|
onDone?.();
|
|
241
246
|
}
|
|
242
247
|
|
|
248
|
+
/** @deprecated Models are only added/removed via sync. Kept as a stub so old imports do not crash; logs a notice. */
|
|
243
249
|
export async function addModelFlow(
|
|
244
250
|
ctx: ExtensionCommandContext,
|
|
245
|
-
|
|
251
|
+
_providerId: string,
|
|
246
252
|
onDone?: () => void,
|
|
247
253
|
): 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
|
-
}
|
|
254
|
+
ctx.ui.notify("新增 model 请用 sync(按 y)。该入口已停用。", "warning");
|
|
357
255
|
onDone?.();
|
|
358
256
|
}
|
|
359
257
|
|
|
@@ -381,6 +279,7 @@ export async function editProviderFlow(
|
|
|
381
279
|
{ key: "apiKey", label: "apiKey", type: "secret" },
|
|
382
280
|
{ key: "api", label: "api", type: "select", options: API_OPTIONS, hint: "1-N 选" },
|
|
383
281
|
{ key: "authHeader", label: "authHeader", type: "select", options: ["no", "yes"] },
|
|
282
|
+
{ key: "proxy", label: "proxy", type: "text", hint: "(empty = unset; http://host:port)" },
|
|
384
283
|
];
|
|
385
284
|
const initial: Record<string, unknown> = {
|
|
386
285
|
name: cur.name ?? "",
|
|
@@ -388,6 +287,7 @@ export async function editProviderFlow(
|
|
|
388
287
|
apiKey: cur.apiKey ?? "",
|
|
389
288
|
api: cur.api ?? "",
|
|
390
289
|
authHeader: cur.authHeader ? "yes" : "no",
|
|
290
|
+
proxy: cur.proxy ?? "",
|
|
391
291
|
};
|
|
392
292
|
const result = await runFormEditor(ctx, `Edit provider "${providerId}"`, fields, initial);
|
|
393
293
|
if (!result.saved) { onDone?.(); return; }
|
|
@@ -399,6 +299,7 @@ export async function editProviderFlow(
|
|
|
399
299
|
apiKey: ((v.apiKey as string) || "") || undefined,
|
|
400
300
|
api: ((v.api as string) || "") || undefined,
|
|
401
301
|
authHeader: v.authHeader === "yes",
|
|
302
|
+
proxy: ((v.proxy as string) || "") || undefined,
|
|
402
303
|
};
|
|
403
304
|
try {
|
|
404
305
|
await writeModelsJson({ ...json, providers: { ...json.providers, [providerId]: next } });
|
|
@@ -557,7 +458,7 @@ export async function syncFlow(ctx: ExtensionCommandContext, opts: SyncOpts = {}
|
|
|
557
458
|
ctx.ui.notify(`Fetching models from ${prov.baseUrl}...`, "info");
|
|
558
459
|
let result;
|
|
559
460
|
try {
|
|
560
|
-
result = await fetchListing({ baseUrl: prov.baseUrl, apiKey, apiKind, signal: ctx.signal, timeoutMs: 10000 });
|
|
461
|
+
result = await fetchListing({ baseUrl: prov.baseUrl, apiKey, apiKind, proxy: prov.proxy, signal: ctx.signal, timeoutMs: 10000 });
|
|
561
462
|
} catch (err) {
|
|
562
463
|
ctx.ui.notify(`Fetch failed: ${err instanceof Error ? err.message : err}`, "error");
|
|
563
464
|
opts.onDone?.();
|
|
@@ -566,7 +467,9 @@ export async function syncFlow(ctx: ExtensionCommandContext, opts: SyncOpts = {}
|
|
|
566
467
|
if (result.warnings.length) ctx.ui.notify(result.warnings.join("; "), "warning");
|
|
567
468
|
if (result.models.length === 0 && (prov.models ?? []).length === 0) { ctx.ui.notify("No models found. Check baseUrl / api key.", "warning"); opts.onDone?.(); return; }
|
|
568
469
|
const existing = (prov.models ?? []).map((m) => ({ id: m.id }));
|
|
569
|
-
|
|
470
|
+
// 关键修复:传 loadDefaultModelConfig() 作 defaults,使 toAdd 使用用户级 default(不是代码内置默认)
|
|
471
|
+
const userDefaults = loadDefaultModelConfig();
|
|
472
|
+
const { toAdd } = diffModels(result.models, existing, { defaults: userDefaults });
|
|
570
473
|
// wire pi done directly to checklist onConfirm/onCancel (otherwise dialog never closes)
|
|
571
474
|
// checklist shows ALL models in this provider:
|
|
572
475
|
// - 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.0",
|
|
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",
|
|
@@ -10,12 +10,15 @@
|
|
|
10
10
|
"README_EN.md"
|
|
11
11
|
],
|
|
12
12
|
"keywords": [
|
|
13
|
-
"pi",
|
|
13
|
+
"pi-package",
|
|
14
14
|
"pi-extension",
|
|
15
15
|
"provider",
|
|
16
16
|
"model",
|
|
17
17
|
"ai"
|
|
18
18
|
],
|
|
19
|
+
"pi": {
|
|
20
|
+
"extensions": ["./"]
|
|
21
|
+
},
|
|
19
22
|
"license": "MIT",
|
|
20
23
|
"author": "fanchaozz <fanchaozz@users.noreply.github.com>",
|
|
21
24
|
"homepage": "https://github.com/fanchaozz/provider-manager#readme",
|
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
|
});
|