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,386 @@
1
+ // models-json-sync.ts
2
+ //
3
+ // 将插件自有 state 中的接入/模型同步到 Pi 原生配置:
4
+ // - models.json 承载模型定义,供 /model、--list-models、workflow 子代理等原生路径读取。
5
+ // - settings.json 的 enabledModels 只在用户已经启用模型范围过滤时追加新模型;未配置时保持“全部启用”。
6
+
7
+ import {
8
+ CONFIG_DIR_NAME,
9
+ getAgentDir,
10
+ ModelRegistry,
11
+ ModelRuntime,
12
+ SettingsManager,
13
+ type ExtensionCommandContext,
14
+ } from "@earendil-works/pi-coding-agent";
15
+ import { readFile } from "node:fs/promises";
16
+ import { join } from "node:path";
17
+ import { minimatch } from "minimatch";
18
+ import { atomicWriteText } from "./atomic-write.ts";
19
+ import { cloneJson, formatUnknownError, isObjectRecord, stringifyJson, stripJsonNoise } from "./common.ts";
20
+ import {
21
+ deleteProviderInDoc,
22
+ readModelsJsonSnapshot,
23
+ renameModelInDoc,
24
+ renameProviderInDoc,
25
+ setProviderInDoc,
26
+ writeModelsJsonSnapshot,
27
+ type ModelsJsonDocument,
28
+ type ModelsJsonModelEntry,
29
+ type ModelsJsonProviderEntry,
30
+ } from "./models-json-manager.ts";
31
+ import { buildModelRequestHeaders } from "./provider-registrar.ts";
32
+ import type {
33
+ BuiltInClientHeaderProfileId,
34
+ StateDocument,
35
+ StoredClientHeaderCapture,
36
+ StoredModel,
37
+ StoredProvider,
38
+ StoredRequestHeaderProfile,
39
+ } from "./types.ts";
40
+
41
+ const THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
42
+
43
+ export interface NativeModelVerification {
44
+ ok: boolean;
45
+ warnings: string[];
46
+ }
47
+
48
+ export interface EnabledModelUpdate {
49
+ mode: "all-enabled" | "updated" | "unchanged";
50
+ scope?: "global" | "project";
51
+ }
52
+
53
+ function copyRecord(value: unknown): Record<string, unknown> | undefined {
54
+ return isObjectRecord(value) ? cloneJson(value) : undefined;
55
+ }
56
+
57
+ function copyStringRecord(value: unknown): Record<string, string> | undefined {
58
+ if (!isObjectRecord(value)) return undefined;
59
+ const record: Record<string, string> = {};
60
+ for (const [key, item] of Object.entries(value)) {
61
+ if (typeof item === "string") record[key] = item;
62
+ }
63
+ return Object.keys(record).length > 0 ? record : undefined;
64
+ }
65
+
66
+ function copyCost(value: StoredModel["cost"]): ModelsJsonModelEntry["cost"] {
67
+ return cloneJson(value);
68
+ }
69
+
70
+ function copyInput(value: StoredModel["input"]): ModelsJsonModelEntry["input"] {
71
+ return [...value];
72
+ }
73
+
74
+ function buildModelsJsonModelEntry(
75
+ provider: StoredProvider,
76
+ model: StoredModel,
77
+ requestHeaderProfiles: Record<string, StoredRequestHeaderProfile>,
78
+ clientHeaderCaptures: Partial<Record<BuiltInClientHeaderProfileId, StoredClientHeaderCapture>>,
79
+ existing?: ModelsJsonModelEntry,
80
+ ): ModelsJsonModelEntry {
81
+ const next: ModelsJsonModelEntry = existing ? { ...existing } : { id: model.id };
82
+ next.id = model.id;
83
+ if (model.name) next.name = model.name;
84
+ else delete next.name;
85
+ if (model.api) next.api = model.api;
86
+ else delete next.api;
87
+ if (model.baseUrl) next.baseUrl = model.baseUrl;
88
+ else delete next.baseUrl;
89
+ next.reasoning = model.reasoning;
90
+ if (model.thinkingLevelMap) next.thinkingLevelMap = { ...model.thinkingLevelMap };
91
+ else delete next.thinkingLevelMap;
92
+ next.input = copyInput(model.input);
93
+ next.contextWindow = model.contextWindow;
94
+ next.maxTokens = model.maxTokens;
95
+ next.cost = copyCost(model.cost);
96
+ const headers = buildModelRequestHeaders(provider, model, requestHeaderProfiles, clientHeaderCaptures);
97
+ if (headers) next.headers = headers;
98
+ else delete next.headers;
99
+ if (model.compat) next.compat = copyRecord(model.compat);
100
+ else delete next.compat;
101
+ return next;
102
+ }
103
+
104
+ export function buildModelsJsonProviderEntry(
105
+ provider: StoredProvider,
106
+ requestHeaderProfiles: Record<string, StoredRequestHeaderProfile> = {},
107
+ clientHeaderCaptures: Partial<Record<BuiltInClientHeaderProfileId, StoredClientHeaderCapture>> = {},
108
+ existing?: ModelsJsonProviderEntry,
109
+ ): ModelsJsonProviderEntry {
110
+ const next: ModelsJsonProviderEntry = existing ? { ...existing } : {};
111
+ if (provider.name) next.name = provider.name;
112
+ else delete next.name;
113
+ next.baseUrl = provider.baseUrl;
114
+ next.api = provider.api;
115
+ if (provider.apiKey) next.apiKey = provider.apiKey;
116
+ else delete next.apiKey;
117
+ if (provider.authHeader !== undefined) next.authHeader = provider.authHeader;
118
+ else delete next.authHeader;
119
+ const providerHeaders = copyStringRecord(provider.headers);
120
+ if (providerHeaders) next.headers = providerHeaders;
121
+ else delete next.headers;
122
+ if (provider.compat) next.compat = copyRecord(provider.compat);
123
+ else delete next.compat;
124
+ if (provider.modelOverrides) next.modelOverrides = cloneJson(provider.modelOverrides);
125
+ else delete next.modelOverrides;
126
+ const existingModels = new Map((existing?.models ?? []).map((model) => [model.id, model]));
127
+ next.models = provider.models.map((model) => buildModelsJsonModelEntry(
128
+ provider,
129
+ model,
130
+ requestHeaderProfiles,
131
+ clientHeaderCaptures,
132
+ existingModels.get(model.id),
133
+ ));
134
+ return next;
135
+ }
136
+
137
+ export function buildSynchronizedModelsDocument(
138
+ document: StateDocument,
139
+ sourceDocument: ModelsJsonDocument,
140
+ removedProviderIds: string[] = [],
141
+ ): ModelsJsonDocument {
142
+ let nextDocument = sourceDocument;
143
+ for (const providerId of removedProviderIds) {
144
+ nextDocument = deleteProviderInDoc(nextDocument, providerId);
145
+ }
146
+ for (const [providerId, provider] of Object.entries(document.providers)) {
147
+ if (provider.models.length === 0) {
148
+ nextDocument = deleteProviderInDoc(nextDocument, providerId);
149
+ continue;
150
+ }
151
+ const entry = buildModelsJsonProviderEntry(
152
+ provider,
153
+ document.requestHeaderProfiles,
154
+ document.clientHeaderCaptures,
155
+ nextDocument.providers[providerId],
156
+ );
157
+ nextDocument = setProviderInDoc(nextDocument, providerId, entry);
158
+ }
159
+ return nextDocument;
160
+ }
161
+
162
+ export async function syncStateToModelsJson(document: StateDocument, removedProviderIds: string[] = []): Promise<void> {
163
+ const snapshot = await readModelsJsonSnapshot();
164
+ const nextDocument = buildSynchronizedModelsDocument(document, snapshot.document, removedProviderIds);
165
+ await writeModelsJsonSnapshot(snapshot, nextDocument);
166
+ }
167
+
168
+ export async function syncProviderRenameToModelsJson(
169
+ document: StateDocument,
170
+ oldProviderId: string,
171
+ newProviderId: string,
172
+ ): Promise<void> {
173
+ const snapshot = await readModelsJsonSnapshot();
174
+ const renamedDocument = renameProviderInDoc(snapshot.document, oldProviderId, newProviderId);
175
+ const nextDocument = buildSynchronizedModelsDocument(document, renamedDocument);
176
+ await writeModelsJsonSnapshot(snapshot, nextDocument);
177
+ }
178
+
179
+ export async function syncModelRenameToModelsJson(
180
+ document: StateDocument,
181
+ providerId: string,
182
+ oldModelId: string,
183
+ newModelId: string,
184
+ ): Promise<void> {
185
+ const snapshot = await readModelsJsonSnapshot();
186
+ const renamedDocument = renameModelInDoc(snapshot.document, providerId, oldModelId, newModelId);
187
+ const nextDocument = buildSynchronizedModelsDocument(document, renamedDocument);
188
+ await writeModelsJsonSnapshot(snapshot, nextDocument);
189
+ }
190
+
191
+
192
+ function splitThinkingSuffix(pattern: string): { base: string; suffix: string } {
193
+ const index = pattern.lastIndexOf(":");
194
+ if (index < 0) return { base: pattern, suffix: "" };
195
+ const maybeLevel = pattern.slice(index + 1);
196
+ if (!THINKING_LEVELS.has(maybeLevel)) return { base: pattern, suffix: "" };
197
+ return { base: pattern.slice(0, index), suffix: pattern.slice(index) };
198
+ }
199
+
200
+ function dedupeModelPatterns(patterns: string[]): string[] {
201
+ const seen = new Set<string>();
202
+ const next: string[] = [];
203
+ for (const pattern of patterns) {
204
+ const base = splitThinkingSuffix(pattern).base.toLowerCase();
205
+ if (seen.has(base)) continue;
206
+ seen.add(base);
207
+ next.push(pattern);
208
+ }
209
+ return next;
210
+ }
211
+
212
+ function enabledModelPatternMatches(pattern: string, fullModelId: string): boolean {
213
+ const slashIndex = fullModelId.indexOf("/");
214
+ const modelId = slashIndex >= 0 ? fullModelId.slice(slashIndex + 1) : fullModelId;
215
+ return minimatch(fullModelId, pattern, { nocase: true })
216
+ || minimatch(modelId, pattern, { nocase: true });
217
+ }
218
+
219
+ function sameModelPatternBase(left: string, right: string): boolean {
220
+ return left.toLowerCase() === right.toLowerCase();
221
+ }
222
+
223
+ function upsertEnabledModelPattern(patterns: string[], fullModelId: string, replacedFullModelId?: string): string[] {
224
+ let covered = patterns.some((pattern) => enabledModelPatternMatches(splitThinkingSuffix(pattern).base, fullModelId));
225
+ const next = patterns.map((pattern) => {
226
+ const { base, suffix } = splitThinkingSuffix(pattern);
227
+ if (replacedFullModelId && sameModelPatternBase(base, replacedFullModelId)) {
228
+ covered = true;
229
+ return `${fullModelId}${suffix}`;
230
+ }
231
+ return pattern;
232
+ });
233
+ if (!covered) next.push(fullModelId);
234
+ return dedupeModelPatterns(next);
235
+ }
236
+
237
+ function removeEnabledModelPattern(patterns: string[], fullModelId: string): string[] {
238
+ return patterns.filter((pattern) => !sameModelPatternBase(splitThinkingSuffix(pattern).base, fullModelId));
239
+ }
240
+
241
+ async function atomicWriteJson(path: string, value: unknown): Promise<void> {
242
+ await atomicWriteText(path, stringifyJson(value));
243
+ }
244
+
245
+ async function readSettingsFile(path: string): Promise<Record<string, unknown>> {
246
+ const source = await readFile(path, "utf8");
247
+ const parsed = JSON.parse(stripJsonNoise(source));
248
+ if (!isObjectRecord(parsed)) throw new Error(`${path} 根节点必须是对象`);
249
+ return parsed;
250
+ }
251
+
252
+ async function updateProjectEnabledModels(cwd: string, update: (patterns: string[]) => string[]): Promise<EnabledModelUpdate> {
253
+ const path = join(cwd, CONFIG_DIR_NAME, "settings.json");
254
+ const settings = await readSettingsFile(path);
255
+ const current = settings.enabledModels;
256
+ if (!Array.isArray(current)) return { mode: "all-enabled" };
257
+ const patterns = current.filter((item): item is string => typeof item === "string");
258
+ if (patterns.length === 0) return { mode: "all-enabled" };
259
+ const next = update(patterns);
260
+ if (JSON.stringify(next) === JSON.stringify(patterns)) return { mode: "unchanged", scope: "project" };
261
+ settings.enabledModels = next;
262
+ await atomicWriteJson(path, settings);
263
+ return { mode: "updated", scope: "project" };
264
+ }
265
+
266
+ async function flushGlobalSettingsWrite(settings: SettingsManager): Promise<void> {
267
+ await settings.flush();
268
+ const errors = settings.drainErrors();
269
+ if (errors.length > 0) {
270
+ const detail = errors.map((entry) => `${entry.scope}: ${entry.error.message}`).join("; ");
271
+ throw new Error(`settings.json enabledModels 写入失败:${detail}`);
272
+ }
273
+ }
274
+
275
+ async function updateGlobalEnabledModels(cwd: string, update: (patterns: string[]) => string[]): Promise<EnabledModelUpdate> {
276
+ const settings = SettingsManager.create(cwd, getAgentDir());
277
+ const current = settings.getGlobalSettings().enabledModels;
278
+ if (!Array.isArray(current) || current.length === 0) return { mode: "all-enabled" };
279
+ const patterns = current.filter((item): item is string => typeof item === "string");
280
+ if (patterns.length === 0) return { mode: "all-enabled" };
281
+ const next = update(patterns);
282
+ if (JSON.stringify(next) === JSON.stringify(patterns)) return { mode: "unchanged", scope: "global" };
283
+ settings.setEnabledModels(next);
284
+ await flushGlobalSettingsWrite(settings);
285
+ return { mode: "updated", scope: "global" };
286
+ }
287
+
288
+ export async function enableModelForNextPiStart(
289
+ cwd: string,
290
+ fullModelId: string,
291
+ replacedFullModelId?: string,
292
+ ): Promise<EnabledModelUpdate> {
293
+ const settings = SettingsManager.create(cwd, getAgentDir());
294
+ const projectEnabled = settings.getProjectSettings().enabledModels;
295
+ const update = (patterns: string[]) => upsertEnabledModelPattern(patterns, fullModelId, replacedFullModelId);
296
+ if (Array.isArray(projectEnabled)) {
297
+ return updateProjectEnabledModels(cwd, update);
298
+ }
299
+ return updateGlobalEnabledModels(cwd, update);
300
+ }
301
+
302
+ export async function removeModelFromNextPiStart(cwd: string, fullModelId: string): Promise<EnabledModelUpdate> {
303
+ const settings = SettingsManager.create(cwd, getAgentDir());
304
+ const projectEnabled = settings.getProjectSettings().enabledModels;
305
+ const update = (patterns: string[]) => removeEnabledModelPattern(patterns, fullModelId);
306
+ if (Array.isArray(projectEnabled)) {
307
+ return updateProjectEnabledModels(cwd, update);
308
+ }
309
+ return updateGlobalEnabledModels(cwd, update);
310
+ }
311
+
312
+ function replaceEnabledProviderPatterns(
313
+ patterns: string[],
314
+ oldProviderId: string,
315
+ newProviderId: string,
316
+ ): string[] {
317
+ const oldPrefix = `${oldProviderId}/`;
318
+ let changed = false;
319
+ const next = patterns.map((pattern) => {
320
+ const { base, suffix } = splitThinkingSuffix(pattern);
321
+ if (!base.toLowerCase().startsWith(oldPrefix.toLowerCase())) return pattern;
322
+ changed = true;
323
+ return `${newProviderId}/${base.slice(oldPrefix.length)}${suffix}`;
324
+ });
325
+ return changed ? dedupeModelPatterns(next) : patterns;
326
+ }
327
+
328
+
329
+ export async function replaceProviderInEnabledModelsForNextPiStart(
330
+ cwd: string,
331
+ oldProviderId: string,
332
+ newProviderId: string,
333
+ ): Promise<EnabledModelUpdate> {
334
+ if (oldProviderId === newProviderId) return { mode: "unchanged" };
335
+ const settings = SettingsManager.create(cwd, getAgentDir());
336
+ const projectEnabled = settings.getProjectSettings().enabledModels;
337
+ const update = (patterns: string[]) => replaceEnabledProviderPatterns(patterns, oldProviderId, newProviderId);
338
+ if (Array.isArray(projectEnabled)) {
339
+ return updateProjectEnabledModels(cwd, update);
340
+ }
341
+ return updateGlobalEnabledModels(cwd, update);
342
+ }
343
+
344
+ async function verifyRegistryModel(
345
+ label: string,
346
+ registry: ModelRegistry,
347
+ providerId: string,
348
+ modelId: string,
349
+ ): Promise<string[]> {
350
+ const warnings: string[] = [];
351
+ const model = registry.find(providerId, modelId);
352
+ if (!model) {
353
+ warnings.push(`${label} 未找到模型 ${providerId}/${modelId}`);
354
+ return warnings;
355
+ }
356
+ if (!registry.hasConfiguredAuth(model)) {
357
+ warnings.push(`${label} 找到模型 ${providerId}/${modelId},但 API key 未配置或不可解析`);
358
+ return warnings;
359
+ }
360
+ const auth = await registry.getApiKeyAndHeaders(model);
361
+ if (!auth.ok) warnings.push(`${label} 请求认证解析失败:${auth.error}`);
362
+ return warnings;
363
+ }
364
+
365
+ // 当前 registry 已在持久化边界 reload;独立 runtime 校验保证下次启动仍可从原生 models.json 解析。
366
+ export async function verifyNativeModelAvailable(
367
+ ctx: ExtensionCommandContext,
368
+ providerId: string,
369
+ modelId: string,
370
+ ): Promise<NativeModelVerification> {
371
+ const warnings: string[] = [];
372
+ warnings.push(...await verifyRegistryModel("当前会话 registry", ctx.modelRegistry, providerId, modelId));
373
+ try {
374
+ const agentDir = getAgentDir();
375
+ const runtime = await ModelRuntime.create({
376
+ authPath: join(agentDir, "auth.json"),
377
+ modelsPath: join(agentDir, "models.json"),
378
+ allowModelNetwork: false,
379
+ });
380
+ const registry = new ModelRegistry(runtime);
381
+ warnings.push(...await verifyRegistryModel("原生 models.json registry", registry, providerId, modelId));
382
+ } catch (error) {
383
+ warnings.push(`原生 models.json registry 校验失败:${formatUnknownError(error)}`);
384
+ }
385
+ return { ok: warnings.length === 0, warnings };
386
+ }
@@ -0,0 +1,80 @@
1
+ // openai-responses-payload.ts
2
+ //
3
+ // 将 pi 内置 openai-responses 的 system/developer input 形态调整为标准
4
+ // OpenAI Responses wire format:顶层 instructions + 纯对话 input。
5
+
6
+ import { isObjectRecord } from "./common.ts";
7
+ import type { StateDocument } from "./types.ts";
8
+
9
+ type ActiveModelRef = {
10
+ provider: string;
11
+ id: string;
12
+ api: string;
13
+ };
14
+
15
+ type PayloadRecord = Record<string, unknown>;
16
+
17
+ function isPayloadRecord(payload: unknown): payload is PayloadRecord {
18
+ return isObjectRecord(payload);
19
+ }
20
+
21
+ function isPromptRole(role: unknown): role is "developer" | "system" {
22
+ return role === "developer" || role === "system";
23
+ }
24
+
25
+ function extractTextInstruction(content: unknown): string | undefined {
26
+ if (typeof content === "string") {
27
+ const trimmed = content.trim();
28
+ return trimmed ? content : undefined;
29
+ }
30
+ if (!Array.isArray(content)) return undefined;
31
+
32
+ const parts: string[] = [];
33
+ for (const item of content) {
34
+ if (!isObjectRecord(item)) return undefined;
35
+ const type = item.type;
36
+ if (type !== "input_text" && type !== "text") return undefined;
37
+ if (typeof item.text !== "string") return undefined;
38
+ if (item.text.trim()) parts.push(item.text);
39
+ }
40
+ const instruction = parts.join("\n");
41
+ return instruction.trim() ? instruction : undefined;
42
+ }
43
+
44
+ function hasNonEmptyInstructions(payload: PayloadRecord): boolean {
45
+ return typeof payload.instructions === "string" && payload.instructions.trim().length > 0;
46
+ }
47
+
48
+ export function normalizeOpenAIResponsesInstructionsPayload(payload: unknown): PayloadRecord | undefined {
49
+ if (!isPayloadRecord(payload)) return undefined;
50
+ if (hasNonEmptyInstructions(payload)) return undefined;
51
+
52
+ const input = payload.input;
53
+ if (!Array.isArray(input) || input.length === 0) return undefined;
54
+
55
+ const leadingItem = input[0];
56
+ if (!isObjectRecord(leadingItem) || !isPromptRole(leadingItem.role)) return undefined;
57
+
58
+ const instructions = extractTextInstruction(leadingItem.content);
59
+ if (!instructions) return undefined;
60
+
61
+ return {
62
+ ...payload,
63
+ instructions,
64
+ input: input.slice(1),
65
+ };
66
+ }
67
+
68
+ export function normalizeManagedOpenAIResponsesPayload(
69
+ payload: unknown,
70
+ model: ActiveModelRef | undefined,
71
+ state: StateDocument,
72
+ ): PayloadRecord | undefined {
73
+ if (!model || model.api !== "openai-responses") return undefined;
74
+ if (!isPayloadRecord(payload) || payload.model !== model.id) return undefined;
75
+
76
+ const storedModel = state.providers[model.provider]?.models.find((candidate) => candidate.id === model.id);
77
+ if (!storedModel) return undefined;
78
+
79
+ return normalizeOpenAIResponsesInstructionsPayload(payload);
80
+ }
@@ -0,0 +1,36 @@
1
+ // openai-service-tier.ts
2
+ //
3
+ // 模型级 OpenAI Responses service_tier 注入。
4
+
5
+ import type { StateDocument } from "./types.ts";
6
+
7
+ type ActiveModelRef = {
8
+ provider: string;
9
+ id: string;
10
+ api: string;
11
+ };
12
+
13
+ type PayloadRecord = Record<string, unknown>;
14
+
15
+ function isPayloadRecord(payload: unknown): payload is PayloadRecord {
16
+ return typeof payload === "object" && payload !== null && !Array.isArray(payload);
17
+ }
18
+
19
+ export function injectOpenAIServiceTier(
20
+ payload: unknown,
21
+ model: ActiveModelRef | undefined,
22
+ state: StateDocument,
23
+ ): PayloadRecord | undefined {
24
+ if (!model || model.api !== "openai-responses") return undefined;
25
+ if (!isPayloadRecord(payload)) return undefined;
26
+ if (payload.model !== model.id) return undefined;
27
+ if ("service_tier" in payload) return undefined;
28
+
29
+ const storedModel = state.providers[model.provider]?.models.find((candidate) => candidate.id === model.id);
30
+ if (storedModel?.openAIServiceTier !== "priority") return undefined;
31
+
32
+ return {
33
+ ...payload,
34
+ service_tier: "priority",
35
+ };
36
+ }
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "pi-model-manager",
3
+ "version": "0.1.0",
4
+ "description": "A TUI model and provider manager for Pi with native models.json persistence, request-header profiles, proxy routing, and protocol compatibility.",
5
+ "type": "module",
6
+ "main": "./index.ts",
7
+ "files": [
8
+ "NOTICE",
9
+ "README.en.md",
10
+ "index.ts",
11
+ "state-store.ts",
12
+ "state-metadata-store.ts",
13
+ "state-cache.ts",
14
+ "state-document.ts",
15
+ "openai-responses-payload.ts",
16
+ "openai-service-tier.ts",
17
+ "claude-code-compat.ts",
18
+ "builtin-model-catalog.ts",
19
+ "models-json-manager.ts",
20
+ "models-json-sync.ts",
21
+ "provider-registrar.ts",
22
+ "local-proxy-service.ts",
23
+ "request-pipeline.ts",
24
+ "model-mutations.ts",
25
+ "header-profile-mutations.ts",
26
+ "models-json-mutations.ts",
27
+ "rescue.ts",
28
+ "atomic-write.ts",
29
+ "common.ts",
30
+ "config-value-reference.ts",
31
+ "configuration-persistence.ts",
32
+ "compat-settings.ts",
33
+ "runtime-base-url.ts",
34
+ "types.ts",
35
+ "tui/",
36
+ "presets/"
37
+ ],
38
+ "keywords": [
39
+ "pi-package",
40
+ "pi-extension",
41
+ "pi",
42
+ "model-manager",
43
+ "provider-manager",
44
+ "models-json",
45
+ "client-headers",
46
+ "openai",
47
+ "anthropic"
48
+ ],
49
+ "author": "Qihuanxishini",
50
+ "license": "AGPL-3.0-only",
51
+ "repository": {
52
+ "type": "git",
53
+ "url": "git+https://github.com/Qihuanxishini/pi-model-manager.git"
54
+ },
55
+ "homepage": "https://github.com/Qihuanxishini/pi-model-manager#readme",
56
+ "bugs": {
57
+ "url": "https://github.com/Qihuanxishini/pi-model-manager/issues"
58
+ },
59
+ "pi": {
60
+ "extensions": [
61
+ "./index.ts"
62
+ ],
63
+ "image": "https://raw.githubusercontent.com/Qihuanxishini/pi-model-manager/main/assets/pi-model-manager-preview.png"
64
+ },
65
+ "peerDependencies": {
66
+ "@earendil-works/pi-coding-agent": ">=0.82.0",
67
+ "@earendil-works/pi-tui": ">=0.75.0"
68
+ },
69
+ "dependencies": {
70
+ "http-proxy-agent": "9.1.0",
71
+ "https-proxy-agent": "9.1.0",
72
+ "minimatch": "10.2.5"
73
+ }
74
+ }
@@ -0,0 +1,23 @@
1
+ // 由私有 request-capture-service 根据真实客户端请求生成;公开插件仅消费这些脱敏后的内置值。
2
+
3
+ export const CLAUDE_CODE_CLIENT_HEADERS: Record<string, string> = {
4
+ "user-agent": "claude-cli/2.1.219 (external, sdk-cli)",
5
+ "x-stainless-arch": "x64",
6
+ "x-stainless-lang": "js",
7
+ "x-stainless-os": "Windows",
8
+ "x-stainless-package-version": "0.94.0",
9
+ "x-stainless-retry-count": "0",
10
+ "x-stainless-runtime": "node",
11
+ "x-stainless-runtime-version": "v26.3.0",
12
+ "x-stainless-timeout": "300",
13
+ "anthropic-beta": "claude-code-20250219,interleaved-thinking-2025-05-14,thinking-token-count-2026-05-13,context-management-2025-06-27,prompt-caching-scope-2026-01-05,mid-conversation-system-2026-04-07,effort-2025-11-24",
14
+ "anthropic-dangerous-direct-browser-access": "true",
15
+ "anthropic-version": "2023-06-01",
16
+ "x-app": "cli",
17
+ };
18
+
19
+ export const CODEX_CLI_CLIENT_HEADERS: Record<string, string> = {
20
+ "user-agent": "codex-tui/0.145.0 (Windows 10.0.26200; x86_64) WindowsTerminal (codex-tui; 0.145.0)",
21
+ "originator": "codex-tui",
22
+ "x-codex-beta-features": "remote_compaction_v2",
23
+ };