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