@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/ui.ts
CHANGED
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
addProviderFlow,
|
|
15
15
|
editProviderFlow,
|
|
16
16
|
deleteProviderFlow,
|
|
17
|
-
addModelFlow,
|
|
18
17
|
editModelFlow,
|
|
19
18
|
deleteModelFlow,
|
|
20
19
|
syncFlow,
|
|
@@ -33,6 +32,10 @@ type ModelRow = {
|
|
|
33
32
|
reasoning: boolean;
|
|
34
33
|
input: string[];
|
|
35
34
|
hasApiKey: boolean;
|
|
35
|
+
// 详情面板需要从 raw ModelConfig 透传
|
|
36
|
+
thinkingLevelMap?: Partial<Record<"off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max", string | null>>;
|
|
37
|
+
cost?: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
38
|
+
compat?: Record<string, unknown>;
|
|
36
39
|
};
|
|
37
40
|
|
|
38
41
|
type ProviderRow = {
|
|
@@ -182,17 +185,13 @@ function buildProviders(ctx: ExtensionCommandContext, json: ModelsJson): { provi
|
|
|
182
185
|
// 只看 models.json 里的自定义 provider;内置 provider 走 pi 的 /model,不在插件覆盖范围
|
|
183
186
|
const customIds = Object.keys(json.providers).sort();
|
|
184
187
|
|
|
188
|
+
// 本插件只管理 models.json 里的 url+apiKey 自定义 provider(无 OAuth)。
|
|
189
|
+
// 直接从 json.providers[pid].apiKey 自检,避开 pi runtime 的多路径判断。
|
|
190
|
+
// source 标识 key 来源:models.json_key(明文)、models.json_env($ENV)、models.json_command(!cmd)、empty(未设)。
|
|
185
191
|
const auth = new Map<string, { hasKey: boolean; source?: string }>();
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
try {
|
|
190
|
-
const s = authStatus(pid);
|
|
191
|
-
auth.set(pid, { hasKey: !!s?.ok, source: s?.source });
|
|
192
|
-
} catch {
|
|
193
|
-
auth.set(pid, { hasKey: false });
|
|
194
|
-
}
|
|
195
|
-
}
|
|
192
|
+
for (const pid of customIds) {
|
|
193
|
+
const apiKey = json.providers[pid]?.apiKey;
|
|
194
|
+
auth.set(pid, inspectApiKey(apiKey));
|
|
196
195
|
}
|
|
197
196
|
|
|
198
197
|
const providers: ProviderRow[] = customIds.map((pid) => {
|
|
@@ -208,6 +207,10 @@ function buildProviders(ctx: ExtensionCommandContext, json: ModelsJson): { provi
|
|
|
208
207
|
reasoning: !!m.reasoning,
|
|
209
208
|
input: m.input ?? ["text"],
|
|
210
209
|
hasApiKey: auth.get(pid)?.hasKey ?? false,
|
|
210
|
+
// 详情面板需要这些字段
|
|
211
|
+
thinkingLevelMap: m.thinkingLevelMap,
|
|
212
|
+
cost: m.cost,
|
|
213
|
+
compat: m.compat,
|
|
211
214
|
})),
|
|
212
215
|
};
|
|
213
216
|
});
|
|
@@ -278,7 +281,7 @@ class Dashboard {
|
|
|
278
281
|
this.onClose();
|
|
279
282
|
return;
|
|
280
283
|
}
|
|
281
|
-
if (matchesKey(data, "
|
|
284
|
+
if (matchesKey(data, "left") || matchesKey(data, "right")) {
|
|
282
285
|
this.pane = this.pane === "provider" ? "model" : "provider";
|
|
283
286
|
this.invalidate();
|
|
284
287
|
return;
|
|
@@ -300,22 +303,8 @@ class Dashboard {
|
|
|
300
303
|
this.setIndex(0);
|
|
301
304
|
} else if (data === "G") {
|
|
302
305
|
this.setIndex(items.length - 1);
|
|
303
|
-
} else if (data === "n") {
|
|
304
|
-
//
|
|
305
|
-
if (this.pane === "provider") {
|
|
306
|
-
void this.runForm(addProviderFlow);
|
|
307
|
-
} else {
|
|
308
|
-
const cur = this.providers[this.providerIndex];
|
|
309
|
-
if (cur) {
|
|
310
|
-
void this.runForm(addModelFlow, cur.id);
|
|
311
|
-
} else {
|
|
312
|
-
// 无 provider:切到 provider pane 再走 add
|
|
313
|
-
this.pane = "provider";
|
|
314
|
-
this.invalidate();
|
|
315
|
-
this.ctx.ui.notify("先新建 provider:按 n 添加", "info");
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
} else if (data === "e") {
|
|
306
|
+
} else if (matchesKey(data, "enter") || data === "\r" || data === "\n") {
|
|
307
|
+
// Enter → 选中项的编辑(与原 'e' 行为一致)。model 仅允许 edit,不允许 new。
|
|
319
308
|
if (this.pane === "provider" && this.providers[this.providerIndex]) {
|
|
320
309
|
const id = this.providers[this.providerIndex].id;
|
|
321
310
|
void this.runForm(editProviderFlow, id);
|
|
@@ -324,6 +313,13 @@ class Dashboard {
|
|
|
324
313
|
const m = prov?.models[this.modelIndex];
|
|
325
314
|
if (prov && m) void this.runForm(editModelFlow, prov.id, m.id);
|
|
326
315
|
}
|
|
316
|
+
} else if (data === "n") {
|
|
317
|
+
// n: 新增 provider。仅 provider 面板支持;model 面板的 n 跳到 sync 提示。
|
|
318
|
+
if (this.pane === "provider") {
|
|
319
|
+
void this.runForm(addProviderFlow);
|
|
320
|
+
} else {
|
|
321
|
+
this.ctx.ui.notify("model 不能直接新增,请用 sync(按 y)", "info");
|
|
322
|
+
}
|
|
327
323
|
} else if (data === "d") {
|
|
328
324
|
const prov = this.providers[this.providerIndex];
|
|
329
325
|
if (this.pane === "provider" && prov) {
|
|
@@ -357,16 +353,8 @@ class Dashboard {
|
|
|
357
353
|
const json = await readModelsJson();
|
|
358
354
|
this.json = json;
|
|
359
355
|
this.auth = new Map();
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
for (const pid of Object.keys(json.providers)) {
|
|
363
|
-
try {
|
|
364
|
-
const s = (authStatus as any)(pid);
|
|
365
|
-
this.auth.set(pid, { hasKey: !!s?.ok, source: s?.source });
|
|
366
|
-
} catch {
|
|
367
|
-
this.auth.set(pid, { hasKey: false });
|
|
368
|
-
}
|
|
369
|
-
}
|
|
356
|
+
for (const pid of Object.keys(json.providers)) {
|
|
357
|
+
this.auth.set(pid, inspectApiKey(json.providers[pid]?.apiKey));
|
|
370
358
|
}
|
|
371
359
|
this.invalidate();
|
|
372
360
|
}
|
|
@@ -457,19 +445,18 @@ class Dashboard {
|
|
|
457
445
|
const th = this.theme;
|
|
458
446
|
const lines: string[] = [];
|
|
459
447
|
|
|
460
|
-
// 1. Header
|
|
461
|
-
lines.push(
|
|
462
|
-
lines.push("");
|
|
448
|
+
// 1. Header (title + stats)
|
|
449
|
+
lines.push(this.renderTitleBar(width, th));
|
|
463
450
|
|
|
464
451
|
if (this.initError) {
|
|
465
452
|
lines.push(th.fg("error", ` ⚠ ${this.initError}`));
|
|
466
453
|
lines.push(th.fg("dim", " 按 q 退出,修复 models.json 后 /providers 重开"));
|
|
467
454
|
} else if (this.providers.length === 0) {
|
|
468
|
-
lines.push(
|
|
469
|
-
lines.push(th.fg("dim", " 按 n 新建 provider,或检查 ~/.pi/agent/models.json"));
|
|
455
|
+
lines.push(...this.renderEmptyState(th));
|
|
470
456
|
} else {
|
|
471
457
|
// 2. Body: 两栏
|
|
472
|
-
|
|
458
|
+
lines.push("");
|
|
459
|
+
const colWidth = Math.max(24, Math.floor((width - 3) / 2));
|
|
473
460
|
const leftLines = this.renderProviderColumn(colWidth, th);
|
|
474
461
|
const rightLines = this.renderModelColumn(colWidth, th);
|
|
475
462
|
const rows = Math.max(leftLines.length, rightLines.length);
|
|
@@ -477,13 +464,11 @@ class Dashboard {
|
|
|
477
464
|
for (let r = 0; r < rows; r++) {
|
|
478
465
|
const l = leftLines[r] ?? "";
|
|
479
466
|
const rr = rightLines[r] ?? "";
|
|
480
|
-
// 关键:visiblePad 只看可见宽度(剥掉 [tag]...[/tag]),不再被主题标签吃掉 padding
|
|
481
467
|
lines.push(visiblePad(l, colWidth) + sep + rr);
|
|
482
468
|
}
|
|
483
|
-
lines.push("");
|
|
484
469
|
|
|
485
470
|
// 3. Detail
|
|
486
|
-
lines.push(
|
|
471
|
+
lines.push("");
|
|
487
472
|
lines.push(...this.renderDetail(width, th));
|
|
488
473
|
}
|
|
489
474
|
|
|
@@ -492,24 +477,76 @@ class Dashboard {
|
|
|
492
477
|
if (this.help) {
|
|
493
478
|
lines.push(...this.renderHelp(width, th));
|
|
494
479
|
} else {
|
|
495
|
-
|
|
480
|
+
const parts = ["↑↓/jk nav", "←→ pane"];
|
|
481
|
+
if (this.pane === "provider") parts.push("n new", "Enter edit", "y sync");
|
|
482
|
+
else parts.push("Enter edit", "y sync", "t test", "T test-all");
|
|
483
|
+
parts.push("d del", "? help", "q close");
|
|
484
|
+
lines.push(th.fg("dim", " " + parts.join(" · ")));
|
|
496
485
|
}
|
|
497
486
|
this.cachedWidth = width;
|
|
498
487
|
this.cachedLines = lines;
|
|
499
488
|
return lines;
|
|
500
489
|
}
|
|
501
490
|
|
|
491
|
+
/** title bar:左侧包名+粗体,右侧 stats(providers/models/authed) */
|
|
492
|
+
private renderTitleBar(width: number, th: any): string {
|
|
493
|
+
const totalModels = this.providers.reduce((s, p) => s + p.models.length, 0);
|
|
494
|
+
const authed = Array.from(this.auth.values()).filter(a => a?.hasKey).length;
|
|
495
|
+
const stats = this.providers.length === 0
|
|
496
|
+
? "no providers"
|
|
497
|
+
: `${this.providers.length}P · ${totalModels}M${authed > 0 ? ` · ${authed}✓` : ""}`;
|
|
498
|
+
const title = th.fg("accent", th.bold(" provider-manager "));
|
|
499
|
+
const right = th.fg("dim", " " + stats + " ");
|
|
500
|
+
const titleW = 18; // " provider-manager " visible length
|
|
501
|
+
const rightW = visibleWidthStrippingTheme(right);
|
|
502
|
+
const fill = Math.max(2, width - titleW - rightW);
|
|
503
|
+
return title + th.fg("borderMuted", "─".repeat(fill)) + right;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** 无 provider 时的空态提示 */
|
|
507
|
+
private renderEmptyState(th: any): string[] {
|
|
508
|
+
const out: string[] = [];
|
|
509
|
+
out.push("");
|
|
510
|
+
out.push(th.fg("dim", " ┌──────────────────────────────────────────────────┐"));
|
|
511
|
+
out.push(th.fg("dim", " │ (no providers found) │"));
|
|
512
|
+
out.push(th.fg("dim", " │ │"));
|
|
513
|
+
out.push(th.fg("dim", " │ Press ") + th.fg("accent", "n") + th.fg("dim", " to add the first provider. │"));
|
|
514
|
+
out.push(th.fg("dim", " │ Or check ~/.pi/agent/models.json. │"));
|
|
515
|
+
out.push(th.fg("dim", " └──────────────────────────────────────────────────┘"));
|
|
516
|
+
return out;
|
|
517
|
+
}
|
|
518
|
+
|
|
502
519
|
private renderProviderColumn(width: number, th: any): string[] {
|
|
503
520
|
const lines: string[] = [];
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
|
|
521
|
+
const totalModels = this.providers.reduce((s, p) => s + p.models.length, 0);
|
|
522
|
+
const authed = Array.from(this.auth.values()).filter(a => a?.hasKey).length;
|
|
523
|
+
const stats = ` ${this.providers.length}·${authed}✓ ${totalModels}m `;
|
|
524
|
+
// ▸ 之前硬编码在 headText 里,inactive 时 trimStart() 不能去掉它(不是空白),导致头部 2 空格+▸ 与下面
|
|
525
|
+
// 非 cursor 行的 2 空格+内容 错 1 个字符。现在按 pane 动态生成。
|
|
526
|
+
const headActive = this.pane === "provider";
|
|
527
|
+
const headPrefix = headActive ? "▸ " : " ";
|
|
528
|
+
const headBase = "Providers";
|
|
529
|
+
// 先按 plain 文本 truncate,再 th.fg 整行包色(同 model 列)
|
|
530
|
+
const headPlain = truncateToWidth(headPrefix + headBase + stats, width);
|
|
531
|
+
const head = (headActive ? th.fg("accent", th.bold(headPlain)) : th.fg("muted", th.bold(headPlain)));
|
|
532
|
+
lines.push(head);
|
|
533
|
+
// 下划线长度 = head 实际可见宽度
|
|
534
|
+
lines.push(th.fg("borderMuted", "─".repeat(Math.min(width, headPrefix.length + headBase.length + stats.length))));
|
|
507
535
|
this.providers.forEach((p, i) => {
|
|
508
536
|
const sel = i === this.providerIndex;
|
|
509
|
-
const
|
|
537
|
+
const isActivePane = sel && this.pane === "provider";
|
|
538
|
+
const arrow = isActivePane ? th.fg("accent", "▸ ") : " ";
|
|
510
539
|
const nameTh = sel ? th.bold(p.id) : p.id;
|
|
511
|
-
|
|
512
|
-
|
|
540
|
+
// 认证状态图标:✓ (有 key) / ✗ (无 key) / 空格 (无 status)
|
|
541
|
+
const auth = this.auth.get(p.id);
|
|
542
|
+
let authIcon = " ";
|
|
543
|
+
if (auth) authIcon = auth.hasKey ? th.fg("success", "✓ ") : th.fg("error", "✗ ");
|
|
544
|
+
// model 数量
|
|
545
|
+
const cnt = th.fg("dim", ` ${p.models.length}m`);
|
|
546
|
+
// 0 model 提示
|
|
547
|
+
const warn = p.models.length === 0 ? th.fg("warning", " ⚠") : "";
|
|
548
|
+
const line = arrow + nameTh + authIcon + cnt + warn;
|
|
549
|
+
lines.push(visiblePad(line, width));
|
|
513
550
|
});
|
|
514
551
|
return lines;
|
|
515
552
|
}
|
|
@@ -518,19 +555,35 @@ class Dashboard {
|
|
|
518
555
|
const lines: string[] = [];
|
|
519
556
|
const provider = this.providers[this.providerIndex];
|
|
520
557
|
const models = provider?.models ?? [];
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
558
|
+
const rCount = models.filter(m => m.reasoning).length;
|
|
559
|
+
const iCount = models.filter(m => m.input.includes("image")).length;
|
|
560
|
+
const stats = models.length > 0 ? ` ${models.length}m · ${rCount}R · ${iCount}I ` : " 0m ";
|
|
561
|
+
// ▸ 由 pane 决定,不在 headPlain 里。同 provider 列。
|
|
562
|
+
const isHeadActive = this.pane === "model" && !!provider;
|
|
563
|
+
const headPrefix = isHeadActive ? "▸ " : " ";
|
|
564
|
+
const headBase = provider ? `Models (${provider.id})` : "Models";
|
|
565
|
+
// 先按 plain 文本 truncate(避免 ANSI 字符撑爆宽度),最后整行包色
|
|
566
|
+
const headPlain = truncateToWidth(headPrefix + headBase + stats, width);
|
|
567
|
+
const headColored = isHeadActive ? th.fg("accent", th.bold(headPlain)) : th.fg("muted", th.bold(headPlain));
|
|
568
|
+
lines.push(headColored);
|
|
569
|
+
// 下划线长度 = head 可见宽度
|
|
570
|
+
lines.push(th.fg("borderMuted", "─".repeat(Math.min(width, headPrefix.length + headBase.length + stats.length))));
|
|
571
|
+
|
|
524
572
|
if (models.length === 0) {
|
|
525
573
|
lines.push(th.fg("dim", " (no models)"));
|
|
574
|
+
lines.push(th.fg("dim", " Press ") + th.fg("accent", "y") + th.fg("dim", " to sync from remote"));
|
|
575
|
+
return lines;
|
|
526
576
|
}
|
|
527
577
|
models.forEach((m, i) => {
|
|
528
578
|
const sel = i === this.modelIndex;
|
|
529
|
-
const
|
|
530
|
-
|
|
579
|
+
const isActivePane = sel && this.pane === "model";
|
|
580
|
+
// 全 plain text,末尾才 th.fg 整行包色(避免 ANSI 被 truncateToWidth 计入宽度)
|
|
581
|
+
const arrow = isActivePane ? "▸ " : " ";
|
|
582
|
+
const rFlag = m.reasoning ? "R" : "-";
|
|
583
|
+
const iFlag = m.input.includes("image") ? "I" : "-";
|
|
584
|
+
const flagStr = ` [${rFlag}${iFlag}]`;
|
|
531
585
|
const ctx2 = m.contextWindow ? ` ${formatNum(m.contextWindow)}c` : "";
|
|
532
586
|
const max2 = m.maxTokens ? ` ${formatNum(m.maxTokens)}m` : "";
|
|
533
|
-
const flagStr = flags ? ` [${flags}]` : "";
|
|
534
587
|
const raw = arrow + m.id + flagStr + ctx2 + max2;
|
|
535
588
|
const line = truncateToWidth(raw, width);
|
|
536
589
|
lines.push(sel ? th.fg("accent", line) : line);
|
|
@@ -543,53 +596,106 @@ class Dashboard {
|
|
|
543
596
|
if (this.pane === "provider") {
|
|
544
597
|
const p = this.providers[this.providerIndex];
|
|
545
598
|
if (!p) return [th.fg("dim", " (no provider selected)")];
|
|
546
|
-
lines.push(th.fg("accent", th.bold("Provider: ")) + p.id);
|
|
547
|
-
lines.push(` displayName: ${p.displayName}`);
|
|
548
|
-
lines.push(` source: models.json (custom)`);
|
|
549
|
-
lines.push(` models: ${p.models.length}`);
|
|
550
599
|
const auth = this.auth.get(p.id);
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
600
|
+
const authIcon = auth
|
|
601
|
+
? (auth.hasKey ? th.fg("success", "✓ ") : th.fg("error", "✗ "))
|
|
602
|
+
: th.fg("dim", " ");
|
|
603
|
+
// 大标题
|
|
604
|
+
lines.push(th.fg("accent", th.bold(` ${authIcon} Provider: `)) + th.bold(p.id));
|
|
605
|
+
lines.push("");
|
|
606
|
+
// Identity
|
|
607
|
+
lines.push(th.fg("muted", " Identity"));
|
|
608
|
+
lines.push(` displayName: ${p.displayName || th.fg("dim", "(unset)")}`);
|
|
609
|
+
lines.push(` source: models.json (custom)`);
|
|
610
|
+
lines.push(` models: ${p.models.length}`);
|
|
611
|
+
// raw config
|
|
612
|
+
const raw = this.json?.providers?.[p.id] as any;
|
|
554
613
|
if (raw) {
|
|
555
|
-
lines.push(
|
|
556
|
-
|
|
557
|
-
lines.push(
|
|
614
|
+
lines.push("");
|
|
615
|
+
lines.push(th.fg("muted", " Endpoint"));
|
|
616
|
+
lines.push(` baseUrl: ${raw.baseUrl || th.fg("dim", "(unset)")}`);
|
|
617
|
+
lines.push(` api: ${raw.api || th.fg("dim", "(unset)")}`);
|
|
618
|
+
if (raw.proxy) lines.push(` proxy: ${raw.proxy}`);
|
|
619
|
+
lines.push("");
|
|
620
|
+
lines.push(th.fg("muted", " Auth"));
|
|
621
|
+
lines.push(` apiKey: ${maskApiKey(raw.apiKey)}`);
|
|
622
|
+
lines.push(` authHeader: ${raw.authHeader ? "yes" : "no"}`);
|
|
623
|
+
if (auth) {
|
|
624
|
+
// 自检:仅描述 models.json 里 apiKey 字段状态(不是 pi 的认证是否有效;那是 t/T 测的)
|
|
625
|
+
const statusText = auth.hasKey ? "set" : "empty";
|
|
626
|
+
const statusColor = auth.hasKey ? th.fg("success", "✓ set") : th.fg("warning", "✗ empty");
|
|
627
|
+
lines.push(` apiKey status: ${statusColor}${auth.source && auth.source !== "empty" ? th.fg("dim", " (" + auth.source + ")") : ""}`);
|
|
628
|
+
}
|
|
558
629
|
}
|
|
559
630
|
} else {
|
|
560
631
|
const p = this.providers[this.providerIndex];
|
|
561
632
|
const m = p?.models[this.modelIndex];
|
|
562
633
|
if (!m) return [th.fg("dim", " (no model selected)")];
|
|
563
|
-
lines.push(th.fg("accent", th.bold(
|
|
564
|
-
lines.push(
|
|
565
|
-
lines.push(
|
|
566
|
-
lines.push(`
|
|
567
|
-
lines.push(`
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
634
|
+
lines.push(th.fg("accent", th.bold(` Model: `)) + `${p.id} / ${m.id}`);
|
|
635
|
+
lines.push("");
|
|
636
|
+
lines.push(th.fg("muted", " Capabilities"));
|
|
637
|
+
lines.push(` reasoning: ${m.reasoning ? th.fg("accent", "yes") : th.fg("dim", "no")}`);
|
|
638
|
+
lines.push(` input: ${m.input.join(", ") || th.fg("dim", "(none)")}`);
|
|
639
|
+
lines.push("");
|
|
640
|
+
lines.push(th.fg("muted", " Limits"));
|
|
641
|
+
lines.push(` context: ${m.contextWindow?.toLocaleString() ?? th.fg("dim", "?")}`);
|
|
642
|
+
lines.push(` max output: ${m.maxTokens?.toLocaleString() ?? th.fg("dim", "?")}`);
|
|
643
|
+
// thinking level map:单行显示 enabled 的 level 名字(`low, medium, max`)。无任何 enabled 时跳过
|
|
644
|
+
// 详情面板真值来自 m.thinkingLevelMap(buildProviders 已从 ModelConfig 透传)
|
|
645
|
+
const tlm = m.thinkingLevelMap;
|
|
646
|
+
if (tlm && typeof tlm === "object") {
|
|
647
|
+
const enabled = (Object.entries(tlm) as [string, string | null][])
|
|
648
|
+
.filter(([, v]) => v !== null && v !== undefined)
|
|
649
|
+
.map(([k]) => k);
|
|
650
|
+
if (enabled.length) {
|
|
651
|
+
lines.push("");
|
|
652
|
+
lines.push(` Thinking levels: ${th.fg("text", enabled.join(", "))}`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
// cost
|
|
656
|
+
const cost = m.cost;
|
|
657
|
+
if (cost) {
|
|
658
|
+
lines.push("");
|
|
659
|
+
lines.push(th.fg("muted", " Cost"));
|
|
660
|
+
lines.push(` input: $${cost.input}/M`);
|
|
661
|
+
lines.push(` output: $${cost.output}/M`);
|
|
662
|
+
if (cost.cacheRead) lines.push(` cache read: $${cost.cacheRead}/M`);
|
|
663
|
+
if (cost.cacheWrite) lines.push(` cache write: $${cost.cacheWrite}/M`);
|
|
664
|
+
}
|
|
665
|
+
// compat:Zhipu GLM 等 OpenAI-compat 网关拒收 role:"developer"(会返 422)。为 false 时 pi 用 system role。
|
|
666
|
+
const compat = m.compat;
|
|
667
|
+
if (compat && typeof compat === "object") {
|
|
668
|
+
lines.push("");
|
|
669
|
+
lines.push(th.fg("muted", " Compat"));
|
|
670
|
+
if (typeof (compat as any).supportsDeveloperRole === "boolean") {
|
|
671
|
+
const sdr = (compat as any).supportsDeveloperRole;
|
|
672
|
+
lines.push(` supportsDeveloperRole: ${sdr ? th.fg("success", "yes") : th.fg("warning", "no")}`);
|
|
673
|
+
}
|
|
573
674
|
}
|
|
574
675
|
}
|
|
575
676
|
return lines.map((l) => truncateToWidth(l, width));
|
|
576
677
|
}
|
|
577
678
|
|
|
578
679
|
private renderHelp(width: number, th: any): string[] {
|
|
579
|
-
|
|
680
|
+
const lines: string[] = [
|
|
580
681
|
th.fg("accent", "Key bindings"),
|
|
581
682
|
" ↑/↓ or j/k navigate in current pane",
|
|
582
683
|
" g / G jump to top / bottom",
|
|
583
|
-
"
|
|
584
|
-
"
|
|
585
|
-
" e edit selected provider / model",
|
|
684
|
+
" ← / → switch between Providers and Models pane",
|
|
685
|
+
" Enter edit selected provider / model",
|
|
586
686
|
" d delete (with confirm)",
|
|
587
687
|
" y sync — fetch remote models for selected provider",
|
|
588
688
|
" ? toggle this help",
|
|
589
689
|
" q / Esc close dashboard",
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
690
|
+
];
|
|
691
|
+
// 按面板增补特有项
|
|
692
|
+
if (this.pane === "provider") {
|
|
693
|
+
lines.splice(5, 0, " n new provider (models are added via sync)");
|
|
694
|
+
} else {
|
|
695
|
+
lines.splice(5, 0, " t / T test current model / test all in provider");
|
|
696
|
+
}
|
|
697
|
+
lines.push("", th.fg("dim", " y sync"));
|
|
698
|
+
return lines.map((l) => truncateToWidth(l, width));
|
|
593
699
|
}
|
|
594
700
|
}
|
|
595
701
|
|
|
@@ -599,27 +705,27 @@ function formatNum(n: number): string {
|
|
|
599
705
|
return String(n);
|
|
600
706
|
}
|
|
601
707
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
if (
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
708
|
+
/**
|
|
709
|
+
* 检查 models.json 里 provider.apiKey 的状态。
|
|
710
|
+
* 返回 { hasKey, source }:source 标识 key 的来源类型。
|
|
711
|
+
* - "models_json_key" 明文 API key
|
|
712
|
+
* - "models_json_env" $ENV_VAR 或 ${ENV_VAR} 插值
|
|
713
|
+
* - "models.json_command" !shell-command 动态取 key
|
|
714
|
+
* - "empty" 未设
|
|
715
|
+
*/
|
|
716
|
+
function inspectApiKey(apiKey: unknown): { hasKey: boolean; source?: string } {
|
|
717
|
+
if (typeof apiKey !== "string" || apiKey.length === 0) {
|
|
718
|
+
return { hasKey: false, source: "empty" };
|
|
719
|
+
}
|
|
720
|
+
// !command 动态取 key
|
|
721
|
+
if (apiKey.startsWith("!")) {
|
|
722
|
+
return { hasKey: true, source: "models.json_command" };
|
|
723
|
+
}
|
|
724
|
+
// $ENV 或 ${ENV}
|
|
725
|
+
if (/^\$\{?[A-Za-z_][A-Za-z0-9_]*\}?$/.test(apiKey)) {
|
|
726
|
+
return { hasKey: true, source: "models.json_env" };
|
|
621
727
|
}
|
|
622
|
-
return
|
|
728
|
+
return { hasKey: true, source: "models_json_key" };
|
|
623
729
|
}
|
|
624
730
|
|
|
625
731
|
// ============================================================================
|