pi-model-manager 0.1.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.
Files changed (43) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +8 -0
  3. package/README.en.md +206 -0
  4. package/README.md +210 -0
  5. package/atomic-write.ts +100 -0
  6. package/builtin-model-catalog.ts +92 -0
  7. package/claude-code-compat.ts +56 -0
  8. package/common.ts +66 -0
  9. package/compat-settings.ts +25 -0
  10. package/config-value-reference.ts +51 -0
  11. package/configuration-persistence.ts +72 -0
  12. package/header-profile-mutations.ts +46 -0
  13. package/index.ts +82 -0
  14. package/local-proxy-service.ts +313 -0
  15. package/model-mutations.ts +210 -0
  16. package/models-json-manager.ts +238 -0
  17. package/models-json-mutations.ts +90 -0
  18. package/models-json-sync.ts +386 -0
  19. package/openai-responses-payload.ts +80 -0
  20. package/openai-service-tier.ts +36 -0
  21. package/package.json +74 -0
  22. package/presets/builtin-client-headers.ts +23 -0
  23. package/presets/client-headers.ts +126 -0
  24. package/presets/providers.ts +80 -0
  25. package/presets/thinking.ts +81 -0
  26. package/provider-registrar.ts +281 -0
  27. package/request-pipeline.ts +88 -0
  28. package/rescue.ts +69 -0
  29. package/runtime-base-url.ts +54 -0
  30. package/state-cache.ts +55 -0
  31. package/state-document.ts +569 -0
  32. package/state-metadata-store.ts +455 -0
  33. package/state-store.ts +238 -0
  34. package/tui/dashboard.ts +376 -0
  35. package/tui/editor-model.ts +339 -0
  36. package/tui/editor-provider.ts +219 -0
  37. package/tui/header-profiles-panel.ts +307 -0
  38. package/tui/model-list-fetch.ts +259 -0
  39. package/tui/model-picker.ts +266 -0
  40. package/tui/models-json-panel.ts +269 -0
  41. package/tui/persistent-menu.ts +349 -0
  42. package/tui/ui-helpers.ts +289 -0
  43. package/types.ts +165 -0
package/rescue.ts ADDED
@@ -0,0 +1,69 @@
1
+ // rescue.ts
2
+ //
3
+ // 当前会话模型在以下场景可能失效:
4
+ // 1) 模型被删除(unregister)
5
+ // 2) 模型被改名(同接入内 id 变更)
6
+ // 3) 接入被改名 / 删除
7
+ // 4) 接入的 baseUrl/apiKey 变更后 in-memory 模型 stale
8
+ //
9
+ // withModelRescue 统一处理:
10
+ // - 优先切换到调用方指定的 preferred(rename 场景)
11
+ // - 否则切到 modelRegistry.getAvailable()[0]
12
+ // - 都不行则只 notify warning
13
+ // 调用方负责先同步 registry/runtime,避免救援阶段再次触发全目录刷新。
14
+
15
+ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
16
+
17
+ interface AffectedRef {
18
+ providerId: string;
19
+ modelId?: string;
20
+ }
21
+
22
+ export interface RescueOptions {
23
+ reason: string;
24
+ preferred?: { providerId: string; modelId: string };
25
+ }
26
+
27
+ export async function withModelRescue(
28
+ ctx: ExtensionCommandContext,
29
+ pi: ExtensionAPI,
30
+ affected: AffectedRef,
31
+ options: RescueOptions,
32
+ ): Promise<void> {
33
+ const current = ctx.model;
34
+ if (!current) return;
35
+
36
+ const isAffected = current.provider === affected.providerId
37
+ && (affected.modelId === undefined || current.id === affected.modelId);
38
+ const isUsable = !!ctx.modelRegistry.find(current.provider, current.id)
39
+ && ctx.modelRegistry.hasConfiguredAuth(current);
40
+
41
+ if (!isAffected && isUsable) return;
42
+
43
+ if (options.preferred) {
44
+ const replacement = ctx.modelRegistry.find(options.preferred.providerId, options.preferred.modelId);
45
+ if (replacement && ctx.modelRegistry.hasConfiguredAuth(replacement)) {
46
+ if (await pi.setModel(replacement)) {
47
+ ctx.ui.notify(
48
+ `${options.reason},已切换到 ${options.preferred.providerId}/${options.preferred.modelId}`,
49
+ "info",
50
+ );
51
+ return;
52
+ }
53
+ }
54
+ }
55
+
56
+ const fallback = ctx.modelRegistry.getAvailable()[0];
57
+ if (fallback && await pi.setModel(fallback)) {
58
+ ctx.ui.notify(
59
+ `${options.reason},已自动切换到 ${fallback.provider}/${fallback.id}`,
60
+ "info",
61
+ );
62
+ return;
63
+ }
64
+
65
+ ctx.ui.notify(
66
+ `${options.reason},但当前没有其它可用模型。请使用 /model-manager 添加模型或 /login 配置认证。`,
67
+ "warning",
68
+ );
69
+ }
@@ -0,0 +1,54 @@
1
+ // runtime-base-url.ts
2
+ //
3
+ // 将用户在 TUI 中填写的 Base URL 转成 pi 各内置 SDK 实际需要的 baseUrl。
4
+ // 约定:state.json 保存用户输入;注册到 pi 和 bootstrap 到 models.json 时使用运行时 URL。
5
+
6
+ import type { ApiKind } from "./types.ts";
7
+
8
+ function trimTrailingSlashes(value: string): string {
9
+ return value.trim().replace(/\/+$/, "");
10
+ }
11
+
12
+ function stripTrailingV1(baseUrl: string): string {
13
+ const trimmed = trimTrailingSlashes(baseUrl);
14
+ return trimmed.toLowerCase().endsWith("/v1") ? trimmed.slice(0, -3) : trimmed;
15
+ }
16
+
17
+ function hasRootPathWithoutQueryOrHash(url: URL): boolean {
18
+ return (url.pathname === "" || url.pathname === "/") && !url.search && !url.hash;
19
+ }
20
+
21
+ function appendV1ForRootUrl(baseUrl: string): string {
22
+ const trimmed = trimTrailingSlashes(baseUrl);
23
+ try {
24
+ const parsed = new URL(trimmed);
25
+ if (hasRootPathWithoutQueryOrHash(parsed)) {
26
+ return `${trimmed}/v1`;
27
+ }
28
+ } catch {
29
+ return trimmed;
30
+ }
31
+ return trimmed;
32
+ }
33
+
34
+ function appendGoogleGenerativeApiVersionForRootUrl(baseUrl: string): string {
35
+ const trimmed = trimTrailingSlashes(baseUrl);
36
+ try {
37
+ const parsed = new URL(trimmed);
38
+ // Google Generative Language 的根域名不是可直接请求的模型 API 根路径;
39
+ // pi 的 google-generative-ai 适配器需要带版本段的 baseUrl。
40
+ if (parsed.hostname === "generativelanguage.googleapis.com" && hasRootPathWithoutQueryOrHash(parsed)) {
41
+ return `${trimmed}/v1beta`;
42
+ }
43
+ } catch {
44
+ return trimmed;
45
+ }
46
+ return trimmed;
47
+ }
48
+
49
+ export function resolveRuntimeBaseUrl(api: ApiKind, baseUrl: string): string {
50
+ if (api === "anthropic-messages") return stripTrailingV1(baseUrl);
51
+ if (api === "openai-completions" || api === "openai-responses") return appendV1ForRootUrl(baseUrl);
52
+ if (api === "google-generative-ai") return appendGoogleGenerativeApiVersionForRootUrl(baseUrl);
53
+ return trimTrailingSlashes(baseUrl);
54
+ }
package/state-cache.ts ADDED
@@ -0,0 +1,55 @@
1
+ // state-cache.ts
2
+ //
3
+ // 请求路径上的轻量 StateDocument 缓存。外部改动由文件签名检测,插件自身 mutation 还会主动失效。
4
+
5
+ import { getModelsJsonPath, readFileSignature, sameFileSignature, type FileSignature } from "./models-json-manager.ts";
6
+ import { readState, getStatePath } from "./state-store.ts";
7
+ import type { StateDocument } from "./types.ts";
8
+
9
+
10
+ interface StateSignature {
11
+ modelsJson: FileSignature;
12
+ metadataState: FileSignature;
13
+ }
14
+
15
+ let cachedState: { signature: StateSignature; state: StateDocument } | undefined;
16
+
17
+
18
+ async function readStateSignature(): Promise<StateSignature> {
19
+ const [modelsJson, metadataState] = await Promise.all([
20
+ readFileSignature(getModelsJsonPath()),
21
+ readFileSignature(getStatePath()),
22
+ ]);
23
+ return { modelsJson, metadataState };
24
+ }
25
+
26
+
27
+ function sameStateSignature(a: StateSignature, b: StateSignature): boolean {
28
+ return sameFileSignature(a.modelsJson, b.modelsJson)
29
+ && sameFileSignature(a.metadataState, b.metadataState);
30
+ }
31
+
32
+ export function invalidateStateCache(): void {
33
+ cachedState = undefined;
34
+ }
35
+
36
+ export async function readCachedState(): Promise<StateDocument> {
37
+ const signature = await readStateSignature();
38
+ if (cachedState && sameStateSignature(cachedState.signature, signature)) {
39
+ return cachedState.state;
40
+ }
41
+
42
+ // StateDocument 横跨两个文件;只缓存同一稳定读取窗口内得到的组合。
43
+ for (let attempt = 0; attempt < 3; attempt += 1) {
44
+ const before = await readStateSignature();
45
+ const state = await readState();
46
+ const after = await readStateSignature();
47
+ if (sameStateSignature(before, after)) {
48
+ cachedState = { signature: after, state };
49
+ return state;
50
+ }
51
+ }
52
+
53
+ // 文件持续变化时返回最后一次读取结果但不缓存,下一请求会重新尝试。
54
+ return readState();
55
+ }