pi-models-discovery 1.1.1 → 1.3.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 +1 -0
- package/README.zh-CN.md +1 -0
- package/package.json +2 -2
- package/src/index.ts +55 -39
package/README.md
CHANGED
|
@@ -59,6 +59,7 @@ Forces a rediscovery of every discovery provider and updates the local cache, no
|
|
|
59
59
|
- **Offline / fetch failure**: handwritten `models` in models.json (if any) are kept as a fallback, and an explicit warning is surfaced via in-session notify — never a silent degradation. One provider failing does not affect the others.
|
|
60
60
|
- **apiKey resolution** (discovery request only): supports literals and `$ENV_VAR` / `${ENV_VAR}` interpolation; `!command` values skip discovery with an explicit warning (chat requests are still resolved by pi itself and are unaffected).
|
|
61
61
|
- Default parameters for discovered models: `reasoning: true`, `input: ["text", "image"]`, zero cost, `contextWindow` 1M, `maxTokens` 64K, `compat.supportsDeveloperRole: false`. Provider-level `compat` is merged into every discovered model.
|
|
62
|
+
- Discovered models do not declare `thinkingLevelMap`, so pi applies the provider's default thinking-level mapping (standard levels through `high`; the extended `xhigh` / `max` levels are not exposed). `/models` cannot report per-model thinking capability, and the extension does not guess it — declare `thinkingLevelMap` yourself in a handwritten `models` entry if a provider needs something different.
|
|
62
63
|
- Model metadata may carry `name` / `context_window` (or `contextWindow`) / `max_tokens` (or `maxTokens`); defaults are used when absent.
|
|
63
64
|
|
|
64
65
|
The old `/model-discovery`, `/model-discovery-refresh`, `/pi-model-discovery`, and `/pi-model-discovery-refresh` names remain available as compatibility aliases.
|
package/README.zh-CN.md
CHANGED
|
@@ -59,6 +59,7 @@ pi install npm:pi-models-discovery
|
|
|
59
59
|
- **离线 / 拉取失败**:保留 models.json 里手写的 `models`(如有,作为回退),并通过会话内 notify 显式警告,不静默降级;单个 provider 失败不影响其他 provider。
|
|
60
60
|
- **apiKey 解析**(仅发现请求):支持字面量与 `$ENV_VAR` / `${ENV_VAR}` 插值;`!command` 形式跳过发现并显式警告(聊天请求仍由 pi 自身解析执行,不受影响)。
|
|
61
61
|
- 发现的模型默认参数:`reasoning: true`、`input: ["text", "image"]`、cost 全 0、`contextWindow` 1M、`maxTokens` 64K、`compat.supportsDeveloperRole: false`;provider 级 `compat` 会合并进每个发现的模型。
|
|
62
|
+
- 发现的模型不声明 `thinkingLevelMap`,由 pi 按 provider 默认映射决定可选思考等级(标准等级直至 `high`,不提供扩展的 `xhigh` / `max`)。`/models` 无法逐个模型返回思考能力,扩展也不替它猜;个别 provider 需要不同映射时,请在 models.json 里手写该模型的 `models` 条目自行声明。
|
|
62
63
|
- 模型元数据可携带 `name` / `context_window`(或 `contextWindow`)/ `max_tokens`(或 `maxTokens`),缺失时用默认值。
|
|
63
64
|
|
|
64
65
|
旧的 `/model-discovery`、`/model-discovery-refresh`、`/pi-model-discovery` 和 `/pi-model-discovery-refresh` 名称仍作为兼容别名保留。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-models-discovery",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Pi extension: discover models from {baseUrl}/models for providers marked with discoverModels in models.json",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.ts",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"@earendil-works/pi-coding-agent": ">=0.80.0"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"pi-extensions-i18n": "^0.
|
|
60
|
+
"pi-extensions-i18n": "^0.6.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@earendil-works/pi-ai": "0.85.1",
|
package/src/index.ts
CHANGED
|
@@ -48,21 +48,30 @@ import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
|
48
48
|
import type { ExtensionAPI, ProviderModelConfig } from "@earendil-works/pi-coding-agent";
|
|
49
49
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
50
50
|
import { dirname, join } from "node:path";
|
|
51
|
-
import { createTranslator, loadCatalog } from "pi-extensions-i18n";
|
|
51
|
+
import { createTranslator, installNoticeRenderer, loadCatalog, notifyWithSource, type NoticeColor, type NoticeSource } from "pi-extensions-i18n";
|
|
52
52
|
|
|
53
53
|
const i18n = createTranslator(loadCatalog(new URL("../locales/index.json", import.meta.url)));
|
|
54
54
|
|
|
55
55
|
const FETCH_TIMEOUT_MS = 5000;
|
|
56
56
|
const DISCOVERY_MARKER = "discoverModels";
|
|
57
57
|
const LOG_PREFIX = "[model-discovery]";
|
|
58
|
+
/** 本扩展的提示标签;短且唯一,便于在会话里定位来源。 */
|
|
59
|
+
const NOTICE_TAG = "models";
|
|
60
|
+
/** 提示标签颜色;与其它扩展错开,避免看起来像同一条消息。 */
|
|
61
|
+
const NOTICE_COLOR: NoticeColor = "accent";
|
|
62
|
+
/** 本扩展的提示来源。 */
|
|
63
|
+
const NOTICE_SOURCE: NoticeSource = { tag: NOTICE_TAG, color: NOTICE_COLOR };
|
|
58
64
|
const API_CHOICES = ["openai-completions", "anthropic-messages", "openai-responses", "google-generative-ai"] as const;
|
|
59
65
|
|
|
60
|
-
/** 用户可见消息(级别与 ctx.ui.notify 的 type
|
|
66
|
+
/** 用户可见消息(级别与 ctx.ui.notify 的 type 对齐);供通知中继与无 ctx 的收集队列共用 */
|
|
61
67
|
interface Notice {
|
|
62
68
|
level: "info" | "warning" | "error";
|
|
63
69
|
message: string;
|
|
64
70
|
}
|
|
65
71
|
|
|
72
|
+
/** 用户可见消息的统一出口:带来源标签与颜色(有 UI 传入 ctx 时),否则交给外部回调转发 */
|
|
73
|
+
type NoticeSink = (notice: Notice, ctx?: CommandCtx) => void;
|
|
74
|
+
|
|
66
75
|
/** models.json 中 provider 条目的读取形态(含发现标记) */
|
|
67
76
|
interface DiscoveryProviderEntry {
|
|
68
77
|
id: string;
|
|
@@ -294,13 +303,6 @@ function buildModel(
|
|
|
294
303
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
295
304
|
contextWindow: contextWindow ?? 1_000_000,
|
|
296
305
|
maxTokens: maxTokens ?? 65_536,
|
|
297
|
-
thinkingLevelMap: {
|
|
298
|
-
minimal: null,
|
|
299
|
-
low: null,
|
|
300
|
-
medium: null,
|
|
301
|
-
high: "high",
|
|
302
|
-
xhigh: "max",
|
|
303
|
-
},
|
|
304
306
|
compat: {
|
|
305
307
|
supportsDeveloperRole: false,
|
|
306
308
|
...providerCompat,
|
|
@@ -477,7 +479,7 @@ function registerFromCache(
|
|
|
477
479
|
}
|
|
478
480
|
|
|
479
481
|
/** /config:model-discovery 交互式配置命令;旧名称保留为兼容别名。 */
|
|
480
|
-
function registerDiscoveryCommand(pi: ExtensionAPI, fetchCache: FetchCache) {
|
|
482
|
+
function registerDiscoveryCommand(pi: ExtensionAPI, fetchCache: FetchCache, sink: NoticeSink) {
|
|
481
483
|
const command = {
|
|
482
484
|
description: i18n.t("commandDescription"),
|
|
483
485
|
handler: async (_args, ctx) => {
|
|
@@ -485,7 +487,7 @@ function registerDiscoveryCommand(pi: ExtensionAPI, fetchCache: FetchCache) {
|
|
|
485
487
|
while (true) {
|
|
486
488
|
const { data, error } = await readModelsFile();
|
|
487
489
|
if (error) {
|
|
488
|
-
|
|
490
|
+
sink({ level: "error", message: `${LOG_PREFIX} ${error}` }, ctx);
|
|
489
491
|
return;
|
|
490
492
|
}
|
|
491
493
|
const providers = pickDiscoveryProviders(data);
|
|
@@ -503,12 +505,12 @@ function registerDiscoveryCommand(pi: ExtensionAPI, fetchCache: FetchCache) {
|
|
|
503
505
|
if (choice === undefined || choice === EXIT) return;
|
|
504
506
|
|
|
505
507
|
if (choice === ADD) {
|
|
506
|
-
await addProviderFlow(pi, ctx, data, fetchCache);
|
|
508
|
+
await addProviderFlow(pi, ctx, data, fetchCache, sink);
|
|
507
509
|
continue;
|
|
508
510
|
}
|
|
509
511
|
const selected = providers[choices.indexOf(choice)];
|
|
510
512
|
if (selected) {
|
|
511
|
-
await manageProviderFlow(pi, ctx, data, selected, fetchCache);
|
|
513
|
+
await manageProviderFlow(pi, ctx, data, selected, fetchCache, sink);
|
|
512
514
|
}
|
|
513
515
|
}
|
|
514
516
|
},
|
|
@@ -519,19 +521,19 @@ function registerDiscoveryCommand(pi: ExtensionAPI, fetchCache: FetchCache) {
|
|
|
519
521
|
}
|
|
520
522
|
|
|
521
523
|
/** /config:model-discovery-refresh 强制刷新命令;旧名称保留为兼容别名。 */
|
|
522
|
-
function registerRefreshCommand(pi: ExtensionAPI) {
|
|
524
|
+
function registerRefreshCommand(pi: ExtensionAPI, sink: NoticeSink) {
|
|
523
525
|
const command = {
|
|
524
526
|
description: i18n.t("refreshDescription"),
|
|
525
527
|
handler: async (_args, ctx) => {
|
|
526
528
|
if (!ctx.hasUI) return;
|
|
527
529
|
const { data, error } = await readModelsFile();
|
|
528
530
|
if (error) {
|
|
529
|
-
|
|
531
|
+
sink({ level: "error", message: `${LOG_PREFIX} ${error}` }, ctx);
|
|
530
532
|
return;
|
|
531
533
|
}
|
|
532
534
|
const providers = pickDiscoveryProviders(data);
|
|
533
535
|
if (providers.length === 0) {
|
|
534
|
-
|
|
536
|
+
sink({ level: "info", message: i18n.t("refreshEmpty") }, ctx);
|
|
535
537
|
return;
|
|
536
538
|
}
|
|
537
539
|
// 每次刷新用新的请求级缓存:同 baseUrl 的 provider 仍共享一次请求,但不复用启动期结果
|
|
@@ -540,18 +542,24 @@ function registerRefreshCommand(pi: ExtensionAPI) {
|
|
|
540
542
|
for (const entry of providers) {
|
|
541
543
|
const notices: Notice[] = [];
|
|
542
544
|
const result = await discoverAndRegister(pi, entry, fetchCache, notices);
|
|
543
|
-
for (const notice of notices)
|
|
545
|
+
for (const notice of notices) sink(notice, ctx);
|
|
544
546
|
if (result) {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
547
|
+
sink(
|
|
548
|
+
{
|
|
549
|
+
level: "info",
|
|
550
|
+
message: `${LOG_PREFIX} ${i18n.t("discoveredInfo", { id: entry.id, count: result.count, models: result.models.map((m) => m.id).join(", ") })}`,
|
|
551
|
+
},
|
|
552
|
+
ctx,
|
|
548
553
|
);
|
|
549
554
|
ok++;
|
|
550
555
|
}
|
|
551
556
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
557
|
+
sink(
|
|
558
|
+
{
|
|
559
|
+
level: ok === providers.length ? "info" : "warning",
|
|
560
|
+
message: i18n.t("refreshDone", { ok, total: providers.length }),
|
|
561
|
+
},
|
|
562
|
+
ctx,
|
|
555
563
|
);
|
|
556
564
|
},
|
|
557
565
|
};
|
|
@@ -562,28 +570,28 @@ function registerRefreshCommand(pi: ExtensionAPI) {
|
|
|
562
570
|
|
|
563
571
|
type CommandCtx = Parameters<Parameters<ExtensionAPI["registerCommand"]>[1]["handler"]>[1];
|
|
564
572
|
|
|
565
|
-
/** 将收集到的 Notice
|
|
573
|
+
/** 将收集到的 Notice 逐条经来源包装后 flush 到 UI */
|
|
566
574
|
function flushNotices(ctx: CommandCtx, notices: Notice[]): void {
|
|
567
|
-
for (const notice of notices) ctx
|
|
575
|
+
for (const notice of notices) notifyWithSource({ ctx, source: NOTICE_SOURCE, level: notice.level, message: notice.message });
|
|
568
576
|
}
|
|
569
577
|
|
|
570
578
|
/** /config:model-discovery 添加 provider 交互流程 */
|
|
571
|
-
async function addProviderFlow(pi: ExtensionAPI, ctx: CommandCtx, data: Record<string, unknown>, fetchCache: FetchCache) {
|
|
579
|
+
async function addProviderFlow(pi: ExtensionAPI, ctx: CommandCtx, data: Record<string, unknown>, fetchCache: FetchCache, sink: NoticeSink) {
|
|
572
580
|
const existingIds = new Set(Object.keys((data.providers ?? {}) as Record<string, unknown>));
|
|
573
581
|
const id = (await ctx.ui.input(i18n.t("providerId")))?.trim();
|
|
574
582
|
if (!id) return;
|
|
575
583
|
if (!/^[a-z0-9][a-z0-9-]*$/i.test(id)) {
|
|
576
|
-
|
|
584
|
+
sink({ level: "error", message: i18n.t("invalidId") }, ctx);
|
|
577
585
|
return;
|
|
578
586
|
}
|
|
579
587
|
if (existingIds.has(id)) {
|
|
580
|
-
|
|
588
|
+
sink({ level: "error", message: i18n.t("exists", { id }) }, ctx);
|
|
581
589
|
return;
|
|
582
590
|
}
|
|
583
591
|
const baseUrl = (await ctx.ui.input(i18n.t("baseUrl")))?.trim();
|
|
584
592
|
if (!baseUrl) return;
|
|
585
593
|
if (!/^https?:\/\//.test(baseUrl)) {
|
|
586
|
-
|
|
594
|
+
sink({ level: "error", message: i18n.t("invalidUrl") }, ctx);
|
|
587
595
|
return;
|
|
588
596
|
}
|
|
589
597
|
const api = await ctx.ui.select(i18n.t("api"), [...API_CHOICES]);
|
|
@@ -602,9 +610,9 @@ async function addProviderFlow(pi: ExtensionAPI, ctx: CommandCtx, data: Record<s
|
|
|
602
610
|
data.providers = providers;
|
|
603
611
|
try {
|
|
604
612
|
const { backup } = await writeModelsFile(data);
|
|
605
|
-
|
|
613
|
+
sink({ level: "info", message: i18n.t("written", { backup }) }, ctx);
|
|
606
614
|
} catch (err) {
|
|
607
|
-
|
|
615
|
+
sink({ level: "error", message: `${LOG_PREFIX} ${err instanceof Error ? err.message : err}` }, ctx);
|
|
608
616
|
return;
|
|
609
617
|
}
|
|
610
618
|
|
|
@@ -617,9 +625,9 @@ async function addProviderFlow(pi: ExtensionAPI, ctx: CommandCtx, data: Record<s
|
|
|
617
625
|
);
|
|
618
626
|
flushNotices(ctx, notices);
|
|
619
627
|
if (result) {
|
|
620
|
-
|
|
628
|
+
sink({ level: "info", message: i18n.t("discovered", { id, count: result.count }) }, ctx);
|
|
621
629
|
} else {
|
|
622
|
-
|
|
630
|
+
sink({ level: "warning", message: i18n.t("firstFailed", { id }) }, ctx);
|
|
623
631
|
}
|
|
624
632
|
}
|
|
625
633
|
|
|
@@ -630,6 +638,7 @@ async function manageProviderFlow(
|
|
|
630
638
|
data: Record<string, unknown>,
|
|
631
639
|
entry: DiscoveryProviderEntry,
|
|
632
640
|
fetchCache: FetchCache,
|
|
641
|
+
sink: NoticeSink,
|
|
633
642
|
) {
|
|
634
643
|
const REDISCOVER = i18n.t("rediscover");
|
|
635
644
|
const REMOVE = i18n.t("remove");
|
|
@@ -645,7 +654,7 @@ async function manageProviderFlow(
|
|
|
645
654
|
const result = await discoverAndRegister(pi, entry, fetchCache, notices);
|
|
646
655
|
flushNotices(ctx, notices);
|
|
647
656
|
if (result) {
|
|
648
|
-
|
|
657
|
+
sink({ level: "info", message: i18n.t("rediscovered", { id: entry.id, count: result.count }) }, ctx);
|
|
649
658
|
}
|
|
650
659
|
return;
|
|
651
660
|
}
|
|
@@ -661,15 +670,22 @@ async function manageProviderFlow(
|
|
|
661
670
|
const notices: Notice[] = [];
|
|
662
671
|
await removeCachedModels(entry.id, notices);
|
|
663
672
|
flushNotices(ctx, notices);
|
|
664
|
-
|
|
673
|
+
sink({ level: "info", message: i18n.t("removed", { id: entry.id, backup }) }, ctx);
|
|
665
674
|
} catch (err) {
|
|
666
|
-
|
|
675
|
+
sink({ level: "error", message: `${LOG_PREFIX} ${err instanceof Error ? err.message : err}` }, ctx);
|
|
667
676
|
}
|
|
668
677
|
}
|
|
669
678
|
|
|
670
679
|
export default async function (pi: ExtensionAPI) {
|
|
680
|
+
// 提示画成会话区里的带底色消息块;渲染器在本包这个模块实例里注册一次。
|
|
681
|
+
installNoticeRenderer(pi);
|
|
671
682
|
// 加载期没有 ctx,消息统一收集,session_start 时 flush(运行期后续追加的也会在下个 session 补发)
|
|
672
683
|
const pendingNotices: Notice[] = [];
|
|
684
|
+
// 有 UI 时走统一来源包装;无 UI 时退到日志前缀,不静默丢掉提示
|
|
685
|
+
const sink: NoticeSink = (notice, ctx) => {
|
|
686
|
+
if (ctx) notifyWithSource({ ctx, source: NOTICE_SOURCE, level: notice.level, message: notice.message });
|
|
687
|
+
else pendingNotices.push(notice);
|
|
688
|
+
};
|
|
673
689
|
const { data, error } = await readModelsFile();
|
|
674
690
|
if (error) {
|
|
675
691
|
pendingNotices.push({ level: "warning", message: `${LOG_PREFIX} ${error}` });
|
|
@@ -695,12 +711,12 @@ export default async function (pi: ExtensionAPI) {
|
|
|
695
711
|
}
|
|
696
712
|
}
|
|
697
713
|
|
|
698
|
-
registerDiscoveryCommand(pi, fetchCache);
|
|
699
|
-
registerRefreshCommand(pi);
|
|
714
|
+
registerDiscoveryCommand(pi, fetchCache, sink);
|
|
715
|
+
registerRefreshCommand(pi, sink);
|
|
700
716
|
|
|
701
717
|
pi.on("session_start", (_event, ctx) => {
|
|
702
718
|
for (const notice of pendingNotices.splice(0)) {
|
|
703
|
-
ctx
|
|
719
|
+
notifyWithSource({ ctx, source: NOTICE_SOURCE, level: notice.level, message: notice.message });
|
|
704
720
|
}
|
|
705
721
|
});
|
|
706
722
|
}
|