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 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.1.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.4.1"
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
- ctx.ui.notify(`${LOG_PREFIX} ${error}`, "error");
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
- ctx.ui.notify(`${LOG_PREFIX} ${error}`, "error");
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
- ctx.ui.notify(i18n.t("refreshEmpty"), "info");
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) ctx.ui.notify(notice.message, notice.level);
545
+ for (const notice of notices) sink(notice, ctx);
544
546
  if (result) {
545
- ctx.ui.notify(
546
- `${LOG_PREFIX} ${i18n.t("discoveredInfo", { id: entry.id, count: result.count, models: result.models.map((m) => m.id).join(", ") })}`,
547
- "info",
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
- ctx.ui.notify(
553
- i18n.t("refreshDone", { ok, total: providers.length }),
554
- ok === providers.length ? "info" : "warning",
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 一次性 flush 到 UI */
573
+ /** 将收集到的 Notice 逐条经来源包装后 flush 到 UI */
566
574
  function flushNotices(ctx: CommandCtx, notices: Notice[]): void {
567
- for (const notice of notices) ctx.ui.notify(notice.message, notice.level);
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
- ctx.ui.notify(i18n.t("invalidId"), "error");
584
+ sink({ level: "error", message: i18n.t("invalidId") }, ctx);
577
585
  return;
578
586
  }
579
587
  if (existingIds.has(id)) {
580
- ctx.ui.notify(i18n.t("exists", { id }), "error");
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
- ctx.ui.notify(i18n.t("invalidUrl"), "error");
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
- ctx.ui.notify(i18n.t("written", { backup }), "info");
613
+ sink({ level: "info", message: i18n.t("written", { backup }) }, ctx);
606
614
  } catch (err) {
607
- ctx.ui.notify(`${LOG_PREFIX} ${err instanceof Error ? err.message : err}`, "error");
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
- ctx.ui.notify(i18n.t("discovered", { id, count: result.count }), "info");
628
+ sink({ level: "info", message: i18n.t("discovered", { id, count: result.count }) }, ctx);
621
629
  } else {
622
- ctx.ui.notify(i18n.t("firstFailed", { id }), "warning");
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
- ctx.ui.notify(i18n.t("rediscovered", { id: entry.id, count: result.count }), "info");
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
- ctx.ui.notify(i18n.t("removed", { id: entry.id, backup }), "info");
673
+ sink({ level: "info", message: i18n.t("removed", { id: entry.id, backup }) }, ctx);
665
674
  } catch (err) {
666
- ctx.ui.notify(`${LOG_PREFIX} ${err instanceof Error ? err.message : err}`, "error");
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.ui.notify(notice.message, notice.level);
719
+ notifyWithSource({ ctx, source: NOTICE_SOURCE, level: notice.level, message: notice.message });
704
720
  }
705
721
  });
706
722
  }