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
@@ -0,0 +1,210 @@
1
+ // model-mutations.ts
2
+ //
3
+ // /model-manager 的模型配置事务层。这里集中处理 models.json/state.json 持久化、
4
+ // Pi runtime provider 注册同步、enabledModels 同步和模型救援;TUI 只负责收集用户意图。
5
+
6
+ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
7
+ import { formatUnknownError } from "./common.ts";
8
+ import {
9
+ persistManagedConfiguration,
10
+ persistModelRenameConfiguration,
11
+ persistProviderRenameConfiguration,
12
+ } from "./configuration-persistence.ts";
13
+ import {
14
+ enableModelForNextPiStart,
15
+ removeModelFromNextPiStart,
16
+ replaceProviderInEnabledModelsForNextPiStart,
17
+ verifyNativeModelAvailable,
18
+ } from "./models-json-sync.ts";
19
+ import { reconcileProvider, unregisterManagedProvider } from "./provider-registrar.ts";
20
+ import { withModelRescue } from "./rescue.ts";
21
+ import { deleteModelFromDocument, deleteProviderFromDocument, getModelFullId, upsertModelInDocument, upsertProviderInDocument } from "./state-document.ts";
22
+ import { readState } from "./state-store.ts";
23
+ import type { StateDocument, StoredProvider } from "./types.ts";
24
+ import type { createModelDraftForStoredProvider, createProviderDraft } from "./state-document.ts";
25
+
26
+ type ProviderDraft = ReturnType<typeof createProviderDraft>;
27
+ type ModelDraft = ReturnType<typeof createModelDraftForStoredProvider>;
28
+
29
+ function formatEnableNote(mode: "all-enabled" | "updated" | "unchanged", scope?: "global" | "project"): string {
30
+ if (mode === "all-enabled") return "当前未限制 enabledModels,重启后默认可选";
31
+ if (mode === "updated") return `已写入${scope === "project" ? "项目" : "全局"} enabledModels`;
32
+ return "已在 enabledModels 中";
33
+ }
34
+
35
+ async function notifyModelAvailability(
36
+ ctx: ExtensionCommandContext,
37
+ providerId: string,
38
+ modelId: string,
39
+ replacedFullId?: string,
40
+ messagePrefix = "已保存并启用模型",
41
+ ): Promise<void> {
42
+ const fullId = getModelFullId(providerId, modelId);
43
+ try {
44
+ const enableOutcome = await enableModelForNextPiStart(ctx.cwd, fullId, replacedFullId);
45
+ const verification = await verifyNativeModelAvailable(ctx, providerId, modelId);
46
+ const enableNote = formatEnableNote(enableOutcome.mode, enableOutcome.scope);
47
+ if (verification.ok) {
48
+ ctx.ui.notify(`${messagePrefix} ${fullId}(${enableNote})`, "info");
49
+ } else {
50
+ ctx.ui.notify(`已保存模型 ${fullId},但启用校验有警告:\n- ${verification.warnings.join("\n- ")}`, "warning");
51
+ }
52
+ } catch (error) {
53
+ ctx.ui.notify(`已保存模型 ${fullId},但启用同步/校验失败:${formatUnknownError(error)}`, "warning");
54
+ }
55
+ }
56
+
57
+ async function reconcilePersistedProviderRuntime(
58
+ pi: ExtensionAPI,
59
+ providerId: string,
60
+ provider: StoredProvider,
61
+ document: StateDocument,
62
+ ): Promise<void> {
63
+ try {
64
+ await reconcileProvider(pi, providerId, provider, document.requestHeaderProfiles, document.clientHeaderCaptures);
65
+ } catch (error) {
66
+ throw new Error(`models.json/state.json 已保存,但当前会话接入刷新失败:${formatUnknownError(error)}。可执行 /reload 重试。`);
67
+ }
68
+ }
69
+
70
+ export async function saveProviderConfiguration(
71
+ pi: ExtensionAPI,
72
+ ctx: ExtensionCommandContext,
73
+ state: StateDocument,
74
+ draft: ProviderDraft,
75
+ oldProviderId: string | undefined,
76
+ ): Promise<void> {
77
+ const nextState = upsertProviderInDocument(state, oldProviderId, draft);
78
+ const stored = nextState.providers[draft.providerId]!;
79
+ const renamedCurrentModelId = oldProviderId
80
+ && oldProviderId !== draft.providerId
81
+ && ctx.model?.provider === oldProviderId
82
+ && stored.models.some((model) => model.id === ctx.model?.id)
83
+ ? ctx.model.id
84
+ : undefined;
85
+ if (oldProviderId && oldProviderId !== draft.providerId) {
86
+ await persistProviderRenameConfiguration(ctx, nextState, oldProviderId, draft.providerId);
87
+ } else {
88
+ await persistManagedConfiguration(ctx, nextState);
89
+ }
90
+ await reconcilePersistedProviderRuntime(pi, draft.providerId, stored, nextState);
91
+ if (oldProviderId && oldProviderId !== draft.providerId) unregisterManagedProvider(pi, oldProviderId);
92
+
93
+ ctx.ui.notify(`已保存接入 ${draft.providerId}`, "info");
94
+
95
+ if (oldProviderId && oldProviderId !== draft.providerId) {
96
+ try {
97
+ const outcome = await replaceProviderInEnabledModelsForNextPiStart(
98
+ ctx.cwd,
99
+ oldProviderId,
100
+ draft.providerId,
101
+ );
102
+ if (outcome.mode === "updated") {
103
+ ctx.ui.notify(`已同步${outcome.scope === "project" ? "项目" : "全局"} enabledModels 中的接入重命名`, "info");
104
+ }
105
+ } catch (error) {
106
+ ctx.ui.notify(`接入已保存,但 enabledModels 同步失败:${formatUnknownError(error)}`, "warning");
107
+ }
108
+ await withModelRescue(ctx, pi, { providerId: oldProviderId }, {
109
+ reason: `接入 ${oldProviderId} 已重命名为 ${draft.providerId}`,
110
+ preferred: renamedCurrentModelId
111
+ ? { providerId: draft.providerId, modelId: renamedCurrentModelId }
112
+ : undefined,
113
+ });
114
+ }
115
+ }
116
+
117
+ export async function saveModelConfiguration(
118
+ pi: ExtensionAPI,
119
+ ctx: ExtensionCommandContext,
120
+ state: StateDocument,
121
+ draft: ModelDraft,
122
+ replacedModelId: string | undefined,
123
+ ): Promise<void> {
124
+ const nextState = upsertModelInDocument(state, draft, { replacedModelId });
125
+ const stored = nextState.providers[draft.providerId]!;
126
+ const newModelId = draft.modelId.trim();
127
+ const oldFullId = replacedModelId && replacedModelId !== newModelId
128
+ ? getModelFullId(draft.providerId, replacedModelId)
129
+ : undefined;
130
+ if (oldFullId && replacedModelId) {
131
+ await persistModelRenameConfiguration(ctx, nextState, draft.providerId, replacedModelId, newModelId);
132
+ } else {
133
+ await persistManagedConfiguration(ctx, nextState);
134
+ }
135
+ await reconcilePersistedProviderRuntime(pi, draft.providerId, stored, nextState);
136
+ const newFullId = getModelFullId(draft.providerId, newModelId);
137
+ await notifyModelAvailability(ctx, draft.providerId, newModelId, oldFullId);
138
+
139
+ if (oldFullId && replacedModelId) {
140
+ await withModelRescue(
141
+ ctx,
142
+ pi,
143
+ { providerId: draft.providerId, modelId: replacedModelId },
144
+ {
145
+ reason: `模型 ${oldFullId} 已重命名为 ${newFullId}`,
146
+ preferred: { providerId: draft.providerId, modelId: draft.modelId.trim() },
147
+ },
148
+ );
149
+ }
150
+ }
151
+
152
+ export async function saveNewProviderWithModelConfiguration(
153
+ pi: ExtensionAPI,
154
+ ctx: ExtensionCommandContext,
155
+ providerState: StateDocument,
156
+ providerDraft: ProviderDraft,
157
+ modelDraft: ModelDraft,
158
+ ): Promise<void> {
159
+ const nextState = upsertModelInDocument(providerState, modelDraft);
160
+ const stored = nextState.providers[providerDraft.providerId]!;
161
+ await persistManagedConfiguration(ctx, nextState);
162
+ await reconcilePersistedProviderRuntime(pi, providerDraft.providerId, stored, nextState);
163
+ await notifyModelAvailability(ctx, providerDraft.providerId, modelDraft.modelId.trim(), undefined, "已创建并启用模型");
164
+ }
165
+
166
+ export async function deleteProviderConfiguration(
167
+ pi: ExtensionAPI,
168
+ ctx: ExtensionCommandContext,
169
+ providerId: string,
170
+ provider: StoredProvider,
171
+ ): Promise<void> {
172
+ const state = await readState();
173
+ const nextState = deleteProviderFromDocument(state, providerId);
174
+ await persistManagedConfiguration(ctx, nextState, [providerId]);
175
+ try {
176
+ for (const model of provider.models) {
177
+ await removeModelFromNextPiStart(ctx.cwd, getModelFullId(providerId, model.id));
178
+ }
179
+ } catch (error) {
180
+ ctx.ui.notify(`接入已删除,但 enabledModels 清理失败:${formatUnknownError(error)}`, "warning");
181
+ }
182
+ unregisterManagedProvider(pi, providerId);
183
+ ctx.ui.notify(`已删除接入 ${providerId}`, "info");
184
+ await withModelRescue(ctx, pi, { providerId }, { reason: `接入 ${providerId} 已删除` });
185
+ }
186
+
187
+ export async function deleteModelConfiguration(
188
+ pi: ExtensionAPI,
189
+ ctx: ExtensionCommandContext,
190
+ providerId: string,
191
+ modelId: string,
192
+ ): Promise<void> {
193
+ const fullId = getModelFullId(providerId, modelId);
194
+ const state = await readState();
195
+ const nextState = deleteModelFromDocument(state, providerId, modelId);
196
+ if ((nextState.providers[providerId]?.models.length ?? 0) === 0) {
197
+ delete nextState.providers[providerId];
198
+ }
199
+ const stored = nextState.providers[providerId];
200
+ await persistManagedConfiguration(ctx, nextState, stored ? [] : [providerId]);
201
+ try {
202
+ await removeModelFromNextPiStart(ctx.cwd, fullId);
203
+ } catch (error) {
204
+ ctx.ui.notify(`模型已删除,但 enabledModels 清理失败:${formatUnknownError(error)}`, "warning");
205
+ }
206
+ if (stored) await reconcilePersistedProviderRuntime(pi, providerId, stored, nextState);
207
+ else unregisterManagedProvider(pi, providerId);
208
+ ctx.ui.notify(`已删除模型 ${fullId}`, "info");
209
+ await withModelRescue(ctx, pi, { providerId, modelId }, { reason: `模型 ${fullId} 已删除` });
210
+ }
@@ -0,0 +1,238 @@
1
+ // models-json-manager.ts
2
+ //
3
+ // 管理 pi 的 ~/.pi/agent/models.json:读 / 写 / 增 / 删 / 改。
4
+ //
5
+ // 主保存流程通过 models-json-sync.ts 同步 StateDocument;原始节点 mutation 在这里保留未知字段。
6
+ // 写入后由调用方执行 ctx.modelRegistry.refresh(),保持 pi 原生 models.json 语义。
7
+
8
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
9
+ import { readFile, stat } from "node:fs/promises";
10
+ import { join } from "node:path";
11
+ import { atomicWriteText } from "./atomic-write.ts";
12
+ import { cloneJson, isObjectRecord, stringifyJson, stripJsonNoise } from "./common.ts";
13
+ import type { TokenCost } from "./types.ts";
14
+
15
+ export const MODELS_JSON_PATH = join(getAgentDir(), "models.json");
16
+
17
+ // ========== schema ==========
18
+
19
+ export interface ModelsJsonModelEntry {
20
+ id: string;
21
+ name?: string;
22
+ api?: string;
23
+ baseUrl?: string;
24
+ reasoning?: boolean;
25
+ thinkingLevelMap?: Record<string, string | null>;
26
+ input?: ("text" | "image")[];
27
+ contextWindow?: number;
28
+ maxTokens?: number;
29
+ cost?: TokenCost;
30
+ headers?: Record<string, string>;
31
+ compat?: Record<string, unknown>;
32
+ [key: string]: unknown;
33
+ }
34
+
35
+ export interface ModelsJsonProviderEntry {
36
+ name?: string;
37
+ baseUrl?: string;
38
+ api?: string;
39
+ apiKey?: string;
40
+ authHeader?: boolean;
41
+ headers?: Record<string, string>;
42
+ models?: ModelsJsonModelEntry[];
43
+ modelOverrides?: Record<string, unknown>;
44
+ compat?: Record<string, unknown>;
45
+ [key: string]: unknown;
46
+ }
47
+
48
+ export interface ModelsJsonDocument {
49
+ providers: Record<string, ModelsJsonProviderEntry>;
50
+ [key: string]: unknown;
51
+ }
52
+
53
+ export interface FileSignature {
54
+ exists: boolean;
55
+ mtimeMs: number;
56
+ ctimeMs: number;
57
+ size: number;
58
+ ino: number;
59
+ }
60
+
61
+ export interface ModelsJsonSnapshot {
62
+ document: ModelsJsonDocument;
63
+ signature: FileSignature;
64
+ }
65
+
66
+ export interface StableTextFileSnapshot {
67
+ source: string | undefined;
68
+ signature: FileSignature;
69
+ }
70
+
71
+ // ========== IO ==========
72
+
73
+ function createEmpty(): ModelsJsonDocument {
74
+ return { providers: {} };
75
+ }
76
+
77
+ const MISSING_FILE_SIGNATURE: FileSignature = { exists: false, mtimeMs: 0, ctimeMs: 0, size: -1, ino: 0 };
78
+
79
+ function isNotFound(error: unknown): boolean {
80
+ return error instanceof Error && "code" in error && (error as NodeJS.ErrnoException).code === "ENOENT";
81
+ }
82
+
83
+ export async function readFileSignature(path: string): Promise<FileSignature> {
84
+ try {
85
+ const stats = await stat(path);
86
+ return { exists: true, mtimeMs: stats.mtimeMs, ctimeMs: stats.ctimeMs, size: stats.size, ino: stats.ino };
87
+ } catch (error) {
88
+ if (isNotFound(error)) return MISSING_FILE_SIGNATURE;
89
+ throw error;
90
+ }
91
+ }
92
+
93
+ export function sameFileSignature(a: FileSignature, b: FileSignature): boolean {
94
+ return a.exists === b.exists
95
+ && a.mtimeMs === b.mtimeMs
96
+ && a.ctimeMs === b.ctimeMs
97
+ && a.size === b.size
98
+ && a.ino === b.ino;
99
+ }
100
+
101
+ /** 文件在读取窗口内变化时重试,避免把旧内容和新签名组合成同一个 snapshot。 */
102
+ export async function readStableTextFileSnapshot(path: string): Promise<StableTextFileSnapshot> {
103
+ for (let attempt = 0; attempt < 3; attempt += 1) {
104
+ const before = await readFileSignature(path);
105
+ if (!before.exists) {
106
+ const after = await readFileSignature(path);
107
+ if (sameFileSignature(before, after)) return { source: undefined, signature: after };
108
+ continue;
109
+ }
110
+ let source: string;
111
+ try {
112
+ source = await readFile(path, "utf8");
113
+ } catch (error) {
114
+ if (isNotFound(error)) continue;
115
+ throw error;
116
+ }
117
+ const after = await readFileSignature(path);
118
+ if (sameFileSignature(before, after)) return { source, signature: after };
119
+ }
120
+ throw new Error(`${path} 在读取期间持续变化;请停止其它写入后重试。`);
121
+ }
122
+
123
+ function parseModelsJson(source: string): ModelsJsonDocument {
124
+ const parsed = JSON.parse(stripJsonNoise(source));
125
+ if (!isObjectRecord(parsed)) throw new Error("models.json 根节点必须是对象");
126
+ const providers = isObjectRecord(parsed.providers) ? parsed.providers : {};
127
+ return { ...parsed, providers: providers as Record<string, ModelsJsonProviderEntry> };
128
+ }
129
+
130
+ export async function readModelsJsonSnapshot(): Promise<ModelsJsonSnapshot> {
131
+ const snapshot = await readStableTextFileSnapshot(MODELS_JSON_PATH);
132
+ return {
133
+ document: snapshot.source === undefined ? createEmpty() : parseModelsJson(snapshot.source),
134
+ signature: snapshot.signature,
135
+ };
136
+ }
137
+
138
+ export async function readModelsJson(): Promise<ModelsJsonDocument> {
139
+ return (await readModelsJsonSnapshot()).document;
140
+ }
141
+
142
+ export async function writeModelsJson(doc: ModelsJsonDocument): Promise<void> {
143
+ await atomicWriteText(MODELS_JSON_PATH, stringifyJson(doc));
144
+ }
145
+
146
+ export async function writeModelsJsonSnapshot(snapshot: ModelsJsonSnapshot, doc: ModelsJsonDocument): Promise<void> {
147
+ const current = await readFileSignature(MODELS_JSON_PATH);
148
+ if (!sameFileSignature(snapshot.signature, current)) {
149
+ throw new Error("models.json 已被其它进程或编辑器修改;请重新打开 /model-manager 后再保存。");
150
+ }
151
+ await writeModelsJson(doc);
152
+ }
153
+
154
+ export function getModelsJsonPath(): string {
155
+ return MODELS_JSON_PATH;
156
+ }
157
+
158
+ // ========== 文档操作(纯函数,深拷贝后改) ==========
159
+
160
+ export function setProviderInDoc(
161
+ doc: ModelsJsonDocument,
162
+ providerId: string,
163
+ entry: ModelsJsonProviderEntry,
164
+ ): ModelsJsonDocument {
165
+ const next = cloneJson(doc);
166
+ next.providers[providerId] = entry;
167
+ return next;
168
+ }
169
+
170
+ export function deleteProviderInDoc(doc: ModelsJsonDocument, providerId: string): ModelsJsonDocument {
171
+ const next = cloneJson(doc);
172
+ delete next.providers[providerId];
173
+ return next;
174
+ }
175
+
176
+ /** [喵喵喵]: 重命名直接移动原始节点,避免未知原生字段在 StateDocument 往返时丢失 (2026-07-17) */
177
+ export function renameProviderInDoc(
178
+ doc: ModelsJsonDocument,
179
+ oldProviderId: string,
180
+ newProviderId: string,
181
+ ): ModelsJsonDocument {
182
+ const next = cloneJson(doc);
183
+ if (oldProviderId === newProviderId) return next;
184
+ const source = next.providers[oldProviderId];
185
+ if (!source) throw new Error(`models.json 中不存在待重命名接入:${oldProviderId}`);
186
+ if (next.providers[newProviderId]) throw new Error(`models.json 中已存在接入:${newProviderId}`);
187
+ next.providers[newProviderId] = source;
188
+ delete next.providers[oldProviderId];
189
+ return next;
190
+ }
191
+
192
+ export function setModelInDoc(
193
+ doc: ModelsJsonDocument,
194
+ providerId: string,
195
+ model: ModelsJsonModelEntry,
196
+ replacedModelId?: string,
197
+ ): ModelsJsonDocument {
198
+ const next = cloneJson(doc);
199
+ const provider = next.providers[providerId];
200
+ if (!provider) throw new Error(`models.json 中不存在接入:${providerId}`);
201
+ const retained = (provider.models ?? []).filter(
202
+ (m) => m.id !== model.id && m.id !== replacedModelId,
203
+ );
204
+ provider.models = [...retained, model];
205
+ return next;
206
+ }
207
+
208
+ export function renameModelInDoc(
209
+ doc: ModelsJsonDocument,
210
+ providerId: string,
211
+ oldModelId: string,
212
+ newModelId: string,
213
+ ): ModelsJsonDocument {
214
+ const next = cloneJson(doc);
215
+ if (oldModelId === newModelId) return next;
216
+ const provider = next.providers[providerId];
217
+ if (!provider) throw new Error(`models.json 中不存在接入:${providerId}`);
218
+ const models = provider.models ?? [];
219
+ const source = models.find((model) => model.id === oldModelId);
220
+ if (!source) throw new Error(`models.json 中不存在待重命名模型:${providerId}/${oldModelId}`);
221
+ if (models.some((model) => model.id === newModelId)) {
222
+ throw new Error(`models.json 中已存在模型:${providerId}/${newModelId}`);
223
+ }
224
+ source.id = newModelId;
225
+ return next;
226
+ }
227
+
228
+ export function deleteModelInDoc(
229
+ doc: ModelsJsonDocument,
230
+ providerId: string,
231
+ modelId: string,
232
+ ): ModelsJsonDocument {
233
+ const next = cloneJson(doc);
234
+ const provider = next.providers[providerId];
235
+ if (!provider) return next;
236
+ provider.models = (provider.models ?? []).filter((m) => m.id !== modelId);
237
+ return next;
238
+ }
@@ -0,0 +1,90 @@
1
+ // models-json-mutations.ts
2
+ //
3
+ // 原生 models.json 面板的事务层:集中处理 models.json 写入、插件 metadata 回写、
4
+ // runtime registry 刷新、enabledModels 清理和模型救援。
5
+
6
+ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
7
+ import { formatUnknownError } from "./common.ts";
8
+ import { deleteModelInDoc, deleteProviderInDoc, readModelsJsonSnapshot, writeModelsJsonSnapshot } from "./models-json-manager.ts";
9
+ import { removeModelFromNextPiStart } from "./models-json-sync.ts";
10
+ import { reconcileProvider, unregisterManagedProvider } from "./provider-registrar.ts";
11
+ import { withModelRescue } from "./rescue.ts";
12
+ import { getModelFullId } from "./state-document.ts";
13
+ import { invalidateStateCache } from "./state-cache.ts";
14
+ import { readState, writeState } from "./state-store.ts";
15
+ import type { StateDocument } from "./types.ts";
16
+
17
+ async function refreshMetadataAfterNativeWrite(operation: string): Promise<StateDocument> {
18
+ try {
19
+ const state = await readState();
20
+ await writeState(state);
21
+ invalidateStateCache();
22
+ return state;
23
+ } catch (error) {
24
+ throw new Error(`${operation} 已写入 models.json,但 state.json metadata 清理失败:${formatUnknownError(error)}`);
25
+ }
26
+ }
27
+
28
+ async function refreshNativeMutationRuntime(
29
+ pi: ExtensionAPI,
30
+ ctx: ExtensionCommandContext,
31
+ operation: string,
32
+ state: StateDocument,
33
+ providerId: string,
34
+ ): Promise<void> {
35
+ try {
36
+ await ctx.modelRegistry.refresh();
37
+ const provider = state.providers[providerId];
38
+ if (provider) await reconcileProvider(pi, providerId, provider, state.requestHeaderProfiles, state.clientHeaderCaptures);
39
+ else unregisterManagedProvider(pi, providerId);
40
+ } catch (error) {
41
+ throw new Error(`${operation} 已写入 models.json/state.json,但当前会话 registry 刷新失败:${formatUnknownError(error)}。可执行 /reload 重试。`);
42
+ }
43
+ }
44
+
45
+ export async function deleteModelsJsonProviderConfiguration(
46
+ pi: ExtensionAPI,
47
+ ctx: ExtensionCommandContext,
48
+ providerId: string,
49
+ ): Promise<void> {
50
+ const snapshot = await readModelsJsonSnapshot();
51
+ const doc = snapshot.document;
52
+ const removedModels = [...(doc.providers[providerId]?.models ?? [])].map((model) => model.id);
53
+ await writeModelsJsonSnapshot(snapshot, deleteProviderInDoc(doc, providerId));
54
+ invalidateStateCache();
55
+ const state = await refreshMetadataAfterNativeWrite(`删除 ${providerId}`);
56
+ await refreshNativeMutationRuntime(pi, ctx, `删除 ${providerId}`, state, providerId);
57
+ try {
58
+ for (const modelId of removedModels) {
59
+ await removeModelFromNextPiStart(ctx.cwd, getModelFullId(providerId, modelId));
60
+ }
61
+ } catch (error) {
62
+ ctx.ui.notify(`已删除 ${providerId},但 enabledModels 清理失败:${formatUnknownError(error)}`, "warning");
63
+ }
64
+ ctx.ui.notify(`已删除 ${providerId}`, "info");
65
+ await withModelRescue(ctx, pi, { providerId }, { reason: `models.json 中的 ${providerId} 已删除` });
66
+ }
67
+
68
+ export async function deleteModelsJsonModelConfiguration(
69
+ pi: ExtensionAPI,
70
+ ctx: ExtensionCommandContext,
71
+ providerId: string,
72
+ modelId: string,
73
+ ): Promise<void> {
74
+ const fullId = getModelFullId(providerId, modelId);
75
+ const snapshot = await readModelsJsonSnapshot();
76
+ const doc = snapshot.document;
77
+ let nextDoc = deleteModelInDoc(doc, providerId, modelId);
78
+ if ((nextDoc.providers[providerId]?.models?.length ?? 0) === 0) nextDoc = deleteProviderInDoc(nextDoc, providerId);
79
+ await writeModelsJsonSnapshot(snapshot, nextDoc);
80
+ invalidateStateCache();
81
+ const state = await refreshMetadataAfterNativeWrite(`删除 ${fullId}`);
82
+ await refreshNativeMutationRuntime(pi, ctx, `删除 ${fullId}`, state, providerId);
83
+ try {
84
+ await removeModelFromNextPiStart(ctx.cwd, fullId);
85
+ } catch (error) {
86
+ ctx.ui.notify(`已删除 ${fullId},但 enabledModels 清理失败:${formatUnknownError(error)}`, "warning");
87
+ }
88
+ ctx.ui.notify(`已删除 ${fullId}`, "info");
89
+ await withModelRescue(ctx, pi, { providerId, modelId }, { reason: `models.json 中的 ${fullId} 已删除` });
90
+ }