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.
- package/LICENSE +661 -0
- package/NOTICE +8 -0
- package/README.en.md +206 -0
- package/README.md +210 -0
- package/atomic-write.ts +100 -0
- package/builtin-model-catalog.ts +92 -0
- package/claude-code-compat.ts +56 -0
- package/common.ts +66 -0
- package/compat-settings.ts +25 -0
- package/config-value-reference.ts +51 -0
- package/configuration-persistence.ts +72 -0
- package/header-profile-mutations.ts +46 -0
- package/index.ts +82 -0
- package/local-proxy-service.ts +313 -0
- package/model-mutations.ts +210 -0
- package/models-json-manager.ts +238 -0
- package/models-json-mutations.ts +90 -0
- package/models-json-sync.ts +386 -0
- package/openai-responses-payload.ts +80 -0
- package/openai-service-tier.ts +36 -0
- package/package.json +74 -0
- package/presets/builtin-client-headers.ts +23 -0
- package/presets/client-headers.ts +126 -0
- package/presets/providers.ts +80 -0
- package/presets/thinking.ts +81 -0
- package/provider-registrar.ts +281 -0
- package/request-pipeline.ts +88 -0
- package/rescue.ts +69 -0
- package/runtime-base-url.ts +54 -0
- package/state-cache.ts +55 -0
- package/state-document.ts +569 -0
- package/state-metadata-store.ts +455 -0
- package/state-store.ts +238 -0
- package/tui/dashboard.ts +376 -0
- package/tui/editor-model.ts +339 -0
- package/tui/editor-provider.ts +219 -0
- package/tui/header-profiles-panel.ts +307 -0
- package/tui/model-list-fetch.ts +259 -0
- package/tui/model-picker.ts +266 -0
- package/tui/models-json-panel.ts +269 -0
- package/tui/persistent-menu.ts +349 -0
- package/tui/ui-helpers.ts +289 -0
- package/types.ts +165 -0
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
// state-metadata-store.ts
|
|
2
|
+
//
|
|
3
|
+
// state.json 持久层:负责插件私有元数据的原子读写和 schema 边界校验。
|
|
4
|
+
// 模型定义的唯一权威源是 Pi 原生 models.json;这里仅保存请求头 profile、
|
|
5
|
+
// profile 库、抓包缓存和 OpenAI Responses service_tier 等原生字段无法表达的元数据。
|
|
6
|
+
|
|
7
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { readFile } from "node:fs/promises";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { atomicWriteText } from "./atomic-write.ts";
|
|
11
|
+
import { cloneJson, isObjectRecord, stringifyJson, stripJsonNoise } from "./common.ts";
|
|
12
|
+
import type {
|
|
13
|
+
ApiKind,
|
|
14
|
+
BuiltInClientHeaderProfileId,
|
|
15
|
+
ClientHeaderProfileId,
|
|
16
|
+
CompatSettings,
|
|
17
|
+
ModelInputKind,
|
|
18
|
+
OpenAIServiceTier,
|
|
19
|
+
StateDocument,
|
|
20
|
+
StoredClientHeaderCapture,
|
|
21
|
+
StoredModel,
|
|
22
|
+
StoredProvider,
|
|
23
|
+
StoredRequestHeaderProfile,
|
|
24
|
+
ThinkingLevelMap,
|
|
25
|
+
TokenCost,
|
|
26
|
+
} from "./types.ts";
|
|
27
|
+
|
|
28
|
+
const STATE_DIR = join(getAgentDir(), "extensions", "pi-model-manager");
|
|
29
|
+
export const STATE_PATH = join(STATE_DIR, "state.json");
|
|
30
|
+
|
|
31
|
+
const API_KINDS = new Set<ApiKind>(["openai-completions", "openai-responses", "anthropic-messages", "google-generative-ai"]);
|
|
32
|
+
const CLIENT_HEADER_PROFILE_IDS = new Set<ClientHeaderProfileId>(["recommended", "disabled", "claude-code", "codex-cli", "custom"]);
|
|
33
|
+
const BUILT_IN_CLIENT_HEADER_PROFILE_IDS = new Set<BuiltInClientHeaderProfileId>(["claude-code", "codex-cli"]);
|
|
34
|
+
const RESERVED_REQUEST_HEADER_PROFILE_IDS = new Set(["claude-code", "codex-cli", "claude-code-live", "codex-cli-live"]);
|
|
35
|
+
const INPUT_KINDS = new Set<ModelInputKind>(["text", "image"]);
|
|
36
|
+
const THINKING_LEVELS = new Set(Object.freeze(["off", "minimal", "low", "medium", "high", "xhigh", "max"]));
|
|
37
|
+
const OPENAI_SERVICE_TIERS = new Set<OpenAIServiceTier>(["priority"]);
|
|
38
|
+
|
|
39
|
+
export interface ProviderMetadata {
|
|
40
|
+
clientHeaderProfile?: ClientHeaderProfileId;
|
|
41
|
+
requestHeaderProfileId?: string;
|
|
42
|
+
customClientHeaders?: Record<string, string>;
|
|
43
|
+
httpProxyEnabled?: boolean;
|
|
44
|
+
httpProxyUrl?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ModelMetadata {
|
|
48
|
+
openAIServiceTier?: OpenAIServiceTier;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface MetadataDocument {
|
|
52
|
+
version: 2;
|
|
53
|
+
providers: Record<string, ProviderMetadata>;
|
|
54
|
+
models: Record<string, ModelMetadata>;
|
|
55
|
+
requestHeaderProfiles: Record<string, StoredRequestHeaderProfile>;
|
|
56
|
+
clientHeaderCaptures: Partial<Record<BuiltInClientHeaderProfileId, StoredClientHeaderCapture>>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ParsedMetadataStateFile {
|
|
60
|
+
metadata: MetadataDocument;
|
|
61
|
+
legacyProviders: StateDocument["providers"];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function createEmptyMetadata(): MetadataDocument {
|
|
65
|
+
return { version: 2, providers: {}, models: {}, requestHeaderProfiles: {}, clientHeaderCaptures: {} };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isNotFound(error: unknown): boolean {
|
|
69
|
+
return error instanceof Error && "code" in error && (error as NodeJS.ErrnoException).code === "ENOENT";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function fail(path: string, message: string): never {
|
|
73
|
+
throw new Error(`state.json ${path}: ${message}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readRequiredString(record: Record<string, unknown>, key: string, path: string): string {
|
|
77
|
+
const value = record[key];
|
|
78
|
+
if (typeof value !== "string" || !value.trim()) fail(`${path}.${key}`, "必须是非空字符串");
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readOptionalString(record: Record<string, unknown>, key: string, path: string): string | undefined {
|
|
83
|
+
const value = record[key];
|
|
84
|
+
if (value === undefined) return undefined;
|
|
85
|
+
if (typeof value !== "string") fail(`${path}.${key}`, "必须是字符串");
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function readOptionalBoolean(record: Record<string, unknown>, key: string, path: string): boolean | undefined {
|
|
90
|
+
const value = record[key];
|
|
91
|
+
if (value === undefined) return undefined;
|
|
92
|
+
if (typeof value !== "boolean") fail(`${path}.${key}`, "必须是 boolean");
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function readPositiveInteger(record: Record<string, unknown>, key: string, path: string): number {
|
|
97
|
+
const value = record[key];
|
|
98
|
+
if (!Number.isInteger(value) || (value as number) <= 0) fail(`${path}.${key}`, "必须是正整数");
|
|
99
|
+
return value as number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function readStringRecord(value: unknown, path: string): Record<string, string> {
|
|
103
|
+
if (!isObjectRecord(value)) fail(path, "必须是对象");
|
|
104
|
+
const record: Record<string, string> = {};
|
|
105
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
106
|
+
if (typeof entry !== "string") fail(`${path}.${key}`, "值必须是字符串");
|
|
107
|
+
record[key] = entry;
|
|
108
|
+
}
|
|
109
|
+
return record;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function readOptionalStringRecord(record: Record<string, unknown>, key: string, path: string): Record<string, string> | undefined {
|
|
113
|
+
const value = record[key];
|
|
114
|
+
if (value === undefined) return undefined;
|
|
115
|
+
return readStringRecord(value, `${path}.${key}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function readObjectRecord(record: Record<string, unknown>, key: string, path: string): Record<string, unknown> | undefined {
|
|
119
|
+
const value = record[key];
|
|
120
|
+
if (value === undefined) return undefined;
|
|
121
|
+
if (!isObjectRecord(value)) fail(`${path}.${key}`, "必须是对象");
|
|
122
|
+
return cloneJson(value);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function readApiKind(record: Record<string, unknown>, key: string, path: string): ApiKind {
|
|
126
|
+
const value = readRequiredString(record, key, path);
|
|
127
|
+
if (!API_KINDS.has(value as ApiKind)) fail(`${path}.${key}`, `未知 API 协议:${value}`);
|
|
128
|
+
return value as ApiKind;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function readOptionalClientHeaderProfile(record: Record<string, unknown>, key: string, path: string): ClientHeaderProfileId | undefined {
|
|
132
|
+
const value = readOptionalString(record, key, path);
|
|
133
|
+
if (value === undefined) return undefined;
|
|
134
|
+
if (!CLIENT_HEADER_PROFILE_IDS.has(value as ClientHeaderProfileId)) fail(`${path}.${key}`, `未知请求头:${value}`);
|
|
135
|
+
return value as ClientHeaderProfileId;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function readOptionalOpenAIServiceTier(record: Record<string, unknown>, key: string, path: string): OpenAIServiceTier | undefined {
|
|
139
|
+
const value = readOptionalString(record, key, path);
|
|
140
|
+
if (value === undefined) return undefined;
|
|
141
|
+
if (!OPENAI_SERVICE_TIERS.has(value as OpenAIServiceTier)) fail(`${path}.${key}`, `未知 OpenAI service tier:${value}`);
|
|
142
|
+
return value as OpenAIServiceTier;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function readInputKinds(record: Record<string, unknown>, key: string, path: string): ModelInputKind[] {
|
|
146
|
+
const value = record[key];
|
|
147
|
+
if (!Array.isArray(value) || value.length === 0) fail(`${path}.${key}`, "必须是非空数组");
|
|
148
|
+
const kinds = value.map((item, index) => {
|
|
149
|
+
if (typeof item !== "string" || !INPUT_KINDS.has(item as ModelInputKind)) fail(`${path}.${key}[${index}]`, "必须是 text 或 image");
|
|
150
|
+
return item as ModelInputKind;
|
|
151
|
+
});
|
|
152
|
+
return [...new Set(kinds)];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function readCost(record: Record<string, unknown>, key: string, path: string): TokenCost {
|
|
156
|
+
const value = record[key];
|
|
157
|
+
if (!isObjectRecord(value)) fail(`${path}.${key}`, "必须是对象");
|
|
158
|
+
const cost: Partial<TokenCost> = {};
|
|
159
|
+
for (const field of ["input", "output", "cacheRead", "cacheWrite"] as const) {
|
|
160
|
+
const amount = value[field];
|
|
161
|
+
if (typeof amount !== "number" || !Number.isFinite(amount) || amount < 0) fail(`${path}.${key}.${field}`, "必须是非负数字");
|
|
162
|
+
cost[field] = amount;
|
|
163
|
+
}
|
|
164
|
+
return cost as TokenCost;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function readThinkingLevelMap(record: Record<string, unknown>, key: string, path: string): ThinkingLevelMap | undefined {
|
|
168
|
+
const value = record[key];
|
|
169
|
+
if (value === undefined) return undefined;
|
|
170
|
+
if (!isObjectRecord(value)) fail(`${path}.${key}`, "必须是对象");
|
|
171
|
+
const map: ThinkingLevelMap = {};
|
|
172
|
+
for (const [level, mapped] of Object.entries(value)) {
|
|
173
|
+
if (!THINKING_LEVELS.has(level)) fail(`${path}.${key}.${level}`, "未知 thinking level");
|
|
174
|
+
if (typeof mapped !== "string" && mapped !== null) fail(`${path}.${key}.${level}`, "必须是字符串或 null");
|
|
175
|
+
map[level as keyof ThinkingLevelMap] = mapped;
|
|
176
|
+
}
|
|
177
|
+
return map;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function readStoredModel(raw: unknown, path: string): StoredModel {
|
|
181
|
+
if (!isObjectRecord(raw)) fail(path, "必须是对象");
|
|
182
|
+
const reasoning = raw.reasoning;
|
|
183
|
+
if (typeof reasoning !== "boolean") fail(`${path}.reasoning`, "必须是 boolean");
|
|
184
|
+
|
|
185
|
+
const model: StoredModel = {
|
|
186
|
+
id: readRequiredString(raw, "id", path),
|
|
187
|
+
reasoning,
|
|
188
|
+
input: readInputKinds(raw, "input", path),
|
|
189
|
+
contextWindow: readPositiveInteger(raw, "contextWindow", path),
|
|
190
|
+
maxTokens: readPositiveInteger(raw, "maxTokens", path),
|
|
191
|
+
cost: readCost(raw, "cost", path),
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const name = readOptionalString(raw, "name", path);
|
|
195
|
+
if (name !== undefined) model.name = name;
|
|
196
|
+
const thinkingLevelMap = readThinkingLevelMap(raw, "thinkingLevelMap", path);
|
|
197
|
+
if (thinkingLevelMap) model.thinkingLevelMap = thinkingLevelMap;
|
|
198
|
+
const clientHeaderProfile = readOptionalClientHeaderProfile(raw, "clientHeaderProfile", path);
|
|
199
|
+
if (clientHeaderProfile) model.clientHeaderProfile = clientHeaderProfile;
|
|
200
|
+
const requestHeaderProfileId = readOptionalString(raw, "requestHeaderProfileId", path);
|
|
201
|
+
if (requestHeaderProfileId !== undefined) model.requestHeaderProfileId = requestHeaderProfileId;
|
|
202
|
+
const customClientHeaders = readOptionalStringRecord(raw, "customClientHeaders", path);
|
|
203
|
+
if (customClientHeaders) model.customClientHeaders = customClientHeaders;
|
|
204
|
+
const openAIServiceTier = readOptionalOpenAIServiceTier(raw, "openAIServiceTier", path);
|
|
205
|
+
if (openAIServiceTier) model.openAIServiceTier = openAIServiceTier;
|
|
206
|
+
const compat = readObjectRecord(raw, "compat", path) as CompatSettings | undefined;
|
|
207
|
+
if (compat) model.compat = compat;
|
|
208
|
+
return model;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function readProviderMetadata(raw: unknown, path: string): ProviderMetadata {
|
|
212
|
+
if (!isObjectRecord(raw)) fail(path, "必须是对象");
|
|
213
|
+
const metadata: ProviderMetadata = {};
|
|
214
|
+
const clientHeaderProfile = readOptionalClientHeaderProfile(raw, "clientHeaderProfile", path);
|
|
215
|
+
if (clientHeaderProfile) metadata.clientHeaderProfile = clientHeaderProfile;
|
|
216
|
+
const requestHeaderProfileId = readOptionalString(raw, "requestHeaderProfileId", path);
|
|
217
|
+
if (requestHeaderProfileId !== undefined) metadata.requestHeaderProfileId = requestHeaderProfileId;
|
|
218
|
+
const customClientHeaders = readOptionalStringRecord(raw, "customClientHeaders", path);
|
|
219
|
+
if (customClientHeaders) metadata.customClientHeaders = customClientHeaders;
|
|
220
|
+
const httpProxyEnabled = readOptionalBoolean(raw, "httpProxyEnabled", path);
|
|
221
|
+
if (httpProxyEnabled !== undefined) metadata.httpProxyEnabled = httpProxyEnabled;
|
|
222
|
+
const httpProxyUrl = readOptionalString(raw, "httpProxyUrl", path);
|
|
223
|
+
if (httpProxyUrl !== undefined) metadata.httpProxyUrl = httpProxyUrl;
|
|
224
|
+
return metadata;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function readModelMetadata(raw: unknown, path: string): ModelMetadata {
|
|
228
|
+
if (!isObjectRecord(raw)) fail(path, "必须是对象");
|
|
229
|
+
const metadata: ModelMetadata = {};
|
|
230
|
+
const openAIServiceTier = readOptionalOpenAIServiceTier(raw, "openAIServiceTier", path);
|
|
231
|
+
if (openAIServiceTier) metadata.openAIServiceTier = openAIServiceTier;
|
|
232
|
+
return metadata;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
interface HeaderProfileSelection {
|
|
236
|
+
clientHeaderProfile: ClientHeaderProfileId;
|
|
237
|
+
requestHeaderProfileId?: string;
|
|
238
|
+
customClientHeaders?: Record<string, string>;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function inferProviderHeaderProfile(models: StoredModel[]): HeaderProfileSelection {
|
|
242
|
+
const selections = models
|
|
243
|
+
.map((model): HeaderProfileSelection | undefined => {
|
|
244
|
+
const legacy = model as StoredModel & ProviderMetadata;
|
|
245
|
+
if (!legacy.clientHeaderProfile) return undefined;
|
|
246
|
+
return {
|
|
247
|
+
clientHeaderProfile: legacy.clientHeaderProfile,
|
|
248
|
+
requestHeaderProfileId: legacy.requestHeaderProfileId,
|
|
249
|
+
customClientHeaders: legacy.customClientHeaders ? cloneJson(legacy.customClientHeaders) : undefined,
|
|
250
|
+
};
|
|
251
|
+
})
|
|
252
|
+
.filter((selection): selection is HeaderProfileSelection => Boolean(selection));
|
|
253
|
+
return selections.find((selection) =>
|
|
254
|
+
selection.clientHeaderProfile !== "recommended"
|
|
255
|
+
|| Boolean(selection.requestHeaderProfileId)
|
|
256
|
+
|| Boolean(selection.customClientHeaders && Object.keys(selection.customClientHeaders).length > 0)
|
|
257
|
+
) ?? selections[0] ?? { clientHeaderProfile: "recommended" };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function readStoredProvider(raw: unknown, path: string): StoredProvider {
|
|
261
|
+
if (!isObjectRecord(raw)) fail(path, "必须是对象");
|
|
262
|
+
const models = raw.models;
|
|
263
|
+
if (!Array.isArray(models)) fail(`${path}.models`, "必须是数组");
|
|
264
|
+
const storedModels = models.map((model, index) => readStoredModel(model, `${path}.models[${index}]`));
|
|
265
|
+
const inferredProfile = inferProviderHeaderProfile(storedModels);
|
|
266
|
+
const clientHeaderProfile = readOptionalClientHeaderProfile(raw, "clientHeaderProfile", path) ?? inferredProfile.clientHeaderProfile;
|
|
267
|
+
|
|
268
|
+
const provider: StoredProvider = {
|
|
269
|
+
name: readRequiredString(raw, "name", path),
|
|
270
|
+
api: readApiKind(raw, "api", path),
|
|
271
|
+
baseUrl: readRequiredString(raw, "baseUrl", path),
|
|
272
|
+
clientHeaderProfile,
|
|
273
|
+
models: storedModels,
|
|
274
|
+
};
|
|
275
|
+
const apiKey = readOptionalString(raw, "apiKey", path);
|
|
276
|
+
if (apiKey) provider.apiKey = apiKey;
|
|
277
|
+
|
|
278
|
+
const authHeader = readOptionalBoolean(raw, "authHeader", path);
|
|
279
|
+
if (authHeader !== undefined) provider.authHeader = authHeader;
|
|
280
|
+
const requestHeaderProfileId = readOptionalString(raw, "requestHeaderProfileId", path) ?? inferredProfile.requestHeaderProfileId;
|
|
281
|
+
if (clientHeaderProfile === "custom" && requestHeaderProfileId !== undefined) provider.requestHeaderProfileId = requestHeaderProfileId;
|
|
282
|
+
const customClientHeaders = readOptionalStringRecord(raw, "customClientHeaders", path) ?? inferredProfile.customClientHeaders;
|
|
283
|
+
if (clientHeaderProfile === "custom" && customClientHeaders) provider.customClientHeaders = customClientHeaders;
|
|
284
|
+
const httpProxyEnabled = readOptionalBoolean(raw, "httpProxyEnabled", path);
|
|
285
|
+
if (httpProxyEnabled !== undefined) provider.httpProxyEnabled = httpProxyEnabled;
|
|
286
|
+
const httpProxyUrl = readOptionalString(raw, "httpProxyUrl", path);
|
|
287
|
+
if (httpProxyUrl !== undefined) provider.httpProxyUrl = httpProxyUrl;
|
|
288
|
+
return provider;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function readRequestHeaderProfile(raw: unknown, path: string): StoredRequestHeaderProfile {
|
|
292
|
+
if (!isObjectRecord(raw)) fail(path, "必须是对象");
|
|
293
|
+
return {
|
|
294
|
+
name: readRequiredString(raw, "name", path),
|
|
295
|
+
headers: readStringRecord(raw.headers, `${path}.headers`),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function readClientHeaderCapture(raw: unknown, path: string): StoredClientHeaderCapture {
|
|
300
|
+
if (!isObjectRecord(raw)) fail(path, "必须是对象");
|
|
301
|
+
return {
|
|
302
|
+
capturedAt: readRequiredString(raw, "capturedAt", path),
|
|
303
|
+
headers: readStringRecord(raw.headers, `${path}.headers`),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function readCommonMetadataFields(parsed: Record<string, unknown>, base: MetadataDocument): MetadataDocument {
|
|
308
|
+
const requestHeaderProfiles: MetadataDocument["requestHeaderProfiles"] = {};
|
|
309
|
+
const rawProfiles = parsed.requestHeaderProfiles;
|
|
310
|
+
if (rawProfiles !== undefined) {
|
|
311
|
+
if (!isObjectRecord(rawProfiles)) fail(".requestHeaderProfiles", "必须是对象");
|
|
312
|
+
for (const [profileId, profile] of Object.entries(rawProfiles)) {
|
|
313
|
+
if (RESERVED_REQUEST_HEADER_PROFILE_IDS.has(profileId)) {
|
|
314
|
+
fail(`.requestHeaderProfiles.${profileId}`, "不能使用插件内置请求头保留名");
|
|
315
|
+
}
|
|
316
|
+
requestHeaderProfiles[profileId] = readRequestHeaderProfile(profile, `.requestHeaderProfiles.${profileId}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const clientHeaderCaptures: MetadataDocument["clientHeaderCaptures"] = {};
|
|
321
|
+
const rawCaptures = parsed.clientHeaderCaptures;
|
|
322
|
+
if (rawCaptures !== undefined) {
|
|
323
|
+
if (!isObjectRecord(rawCaptures)) fail(".clientHeaderCaptures", "必须是对象");
|
|
324
|
+
for (const [profileId, capture] of Object.entries(rawCaptures)) {
|
|
325
|
+
if (!BUILT_IN_CLIENT_HEADER_PROFILE_IDS.has(profileId as BuiltInClientHeaderProfileId)) {
|
|
326
|
+
fail(`.clientHeaderCaptures.${profileId}`, "只能保存 ClaudeCode/Codex 内置 profile 的抓包数据");
|
|
327
|
+
}
|
|
328
|
+
clientHeaderCaptures[profileId as BuiltInClientHeaderProfileId] = readClientHeaderCapture(
|
|
329
|
+
capture,
|
|
330
|
+
`.clientHeaderCaptures.${profileId}`,
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
...base,
|
|
337
|
+
requestHeaderProfiles,
|
|
338
|
+
clientHeaderCaptures,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function getFullModelId(providerId: string, modelId: string): string {
|
|
343
|
+
return `${providerId}/${modelId}`;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function extractProviderMetadata(provider: StoredProvider): ProviderMetadata {
|
|
347
|
+
const metadata: ProviderMetadata = {};
|
|
348
|
+
if (provider.clientHeaderProfile !== "recommended") metadata.clientHeaderProfile = provider.clientHeaderProfile;
|
|
349
|
+
if (provider.clientHeaderProfile === "custom" && provider.requestHeaderProfileId) {
|
|
350
|
+
metadata.clientHeaderProfile = "custom";
|
|
351
|
+
metadata.requestHeaderProfileId = provider.requestHeaderProfileId;
|
|
352
|
+
}
|
|
353
|
+
if (provider.clientHeaderProfile === "custom" && provider.customClientHeaders && Object.keys(provider.customClientHeaders).length > 0) {
|
|
354
|
+
metadata.clientHeaderProfile = "custom";
|
|
355
|
+
metadata.customClientHeaders = cloneJson(provider.customClientHeaders);
|
|
356
|
+
}
|
|
357
|
+
if (provider.httpProxyEnabled) metadata.httpProxyEnabled = true;
|
|
358
|
+
const httpProxyUrl = provider.httpProxyUrl?.trim();
|
|
359
|
+
if (httpProxyUrl) metadata.httpProxyUrl = httpProxyUrl;
|
|
360
|
+
return metadata;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function extractModelMetadata(model: StoredModel): ModelMetadata {
|
|
364
|
+
const metadata: ModelMetadata = {};
|
|
365
|
+
if (model.openAIServiceTier) metadata.openAIServiceTier = model.openAIServiceTier;
|
|
366
|
+
return metadata;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function extractMetadataFromLegacyState(legacy: StateDocument): MetadataDocument {
|
|
370
|
+
const metadata = createEmptyMetadata();
|
|
371
|
+
metadata.requestHeaderProfiles = cloneJson(legacy.requestHeaderProfiles);
|
|
372
|
+
metadata.clientHeaderCaptures = cloneJson(legacy.clientHeaderCaptures);
|
|
373
|
+
for (const [providerId, provider] of Object.entries(legacy.providers)) {
|
|
374
|
+
metadata.providers[providerId] = extractProviderMetadata(provider);
|
|
375
|
+
for (const model of provider.models) {
|
|
376
|
+
const modelMetadata = extractModelMetadata(model);
|
|
377
|
+
if (Object.keys(modelMetadata).length > 0) metadata.models[getFullModelId(providerId, model.id)] = modelMetadata;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return metadata;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function parseLegacyState(parsed: Record<string, unknown>): ParsedMetadataStateFile {
|
|
384
|
+
const providers: StateDocument["providers"] = {};
|
|
385
|
+
const rawProviders = parsed.providers;
|
|
386
|
+
if (rawProviders !== undefined) {
|
|
387
|
+
if (!isObjectRecord(rawProviders)) fail(".providers", "必须是对象");
|
|
388
|
+
for (const [providerId, provider] of Object.entries(rawProviders)) {
|
|
389
|
+
providers[providerId] = readStoredProvider(provider, `.providers.${providerId}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const common = readCommonMetadataFields(parsed, createEmptyMetadata());
|
|
393
|
+
const legacy = { version: 1, providers, requestHeaderProfiles: common.requestHeaderProfiles, clientHeaderCaptures: common.clientHeaderCaptures } satisfies StateDocument;
|
|
394
|
+
return { metadata: extractMetadataFromLegacyState(legacy), legacyProviders: providers };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function parseMetadataState(parsed: Record<string, unknown>): ParsedMetadataStateFile {
|
|
398
|
+
const metadata = readCommonMetadataFields(parsed, createEmptyMetadata());
|
|
399
|
+
const rawProviders = parsed.providers;
|
|
400
|
+
if (rawProviders !== undefined) {
|
|
401
|
+
if (!isObjectRecord(rawProviders)) fail(".providers", "必须是对象");
|
|
402
|
+
for (const [providerId, provider] of Object.entries(rawProviders)) {
|
|
403
|
+
metadata.providers[providerId] = readProviderMetadata(provider, `.providers.${providerId}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const rawModels = parsed.models;
|
|
407
|
+
if (rawModels !== undefined) {
|
|
408
|
+
if (!isObjectRecord(rawModels)) fail(".models", "必须是对象");
|
|
409
|
+
for (const [fullModelId, model] of Object.entries(rawModels)) {
|
|
410
|
+
metadata.models[fullModelId] = readModelMetadata(model, `.models.${fullModelId}`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return { metadata, legacyProviders: {} };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function parseStateFile(source: string): ParsedMetadataStateFile {
|
|
417
|
+
const parsed = JSON.parse(stripJsonNoise(source));
|
|
418
|
+
if (!isObjectRecord(parsed)) fail("", "根节点必须是对象");
|
|
419
|
+
if (parsed.version === 2) return parseMetadataState(parsed);
|
|
420
|
+
if (parsed.version === undefined || parsed.version === 1) return parseLegacyState(parsed);
|
|
421
|
+
fail(".version", `不支持的版本:${String(parsed.version)}`);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export async function readMetadataStateFile(): Promise<ParsedMetadataStateFile> {
|
|
425
|
+
try {
|
|
426
|
+
const source = await readFile(STATE_PATH, "utf8");
|
|
427
|
+
return parseStateFile(source);
|
|
428
|
+
} catch (error) {
|
|
429
|
+
if (isNotFound(error)) return { metadata: createEmptyMetadata(), legacyProviders: {} };
|
|
430
|
+
throw error;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function extractMetadata(state: StateDocument): MetadataDocument {
|
|
435
|
+
const metadata = createEmptyMetadata();
|
|
436
|
+
metadata.requestHeaderProfiles = cloneJson(state.requestHeaderProfiles);
|
|
437
|
+
metadata.clientHeaderCaptures = cloneJson(state.clientHeaderCaptures);
|
|
438
|
+
for (const [providerId, provider] of Object.entries(state.providers)) {
|
|
439
|
+
const providerMetadata = extractProviderMetadata(provider);
|
|
440
|
+
if (Object.keys(providerMetadata).length > 0) metadata.providers[providerId] = providerMetadata;
|
|
441
|
+
for (const model of provider.models) {
|
|
442
|
+
const modelMetadata = extractModelMetadata(model);
|
|
443
|
+
if (Object.keys(modelMetadata).length > 0) metadata.models[getFullModelId(providerId, model.id)] = modelMetadata;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return metadata;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export async function writeMetadataState(state: StateDocument): Promise<void> {
|
|
450
|
+
await atomicWriteText(STATE_PATH, stringifyJson(extractMetadata(state)));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export function getStatePath(): string {
|
|
454
|
+
return STATE_PATH;
|
|
455
|
+
}
|
package/state-store.ts
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// state-store.ts
|
|
2
|
+
//
|
|
3
|
+
// StateDocument 合成层:从 Pi 原生 models.json 读取模型定义,再叠加
|
|
4
|
+
// state.json 中的插件私有元数据,提供 TUI 与运行时注册使用的统一视图。
|
|
5
|
+
|
|
6
|
+
import { getBuiltinProviderDefaults } from "./builtin-model-catalog.ts";
|
|
7
|
+
import { mergeCompatSettings } from "./compat-settings.ts";
|
|
8
|
+
import { cloneJson, hasStringRecordEntries, isObjectRecord } from "./common.ts";
|
|
9
|
+
import { readModelsJson, type ModelsJsonDocument, type ModelsJsonModelEntry, type ModelsJsonProviderEntry } from "./models-json-manager.ts";
|
|
10
|
+
import {
|
|
11
|
+
getClientHeadersForProfile,
|
|
12
|
+
stripManagedClientHeaders,
|
|
13
|
+
} from "./presets/client-headers.ts";
|
|
14
|
+
import { normalizeThinkingLevelMap } from "./presets/thinking.ts";
|
|
15
|
+
import {
|
|
16
|
+
getStatePath as getMetadataStatePath,
|
|
17
|
+
readMetadataStateFile,
|
|
18
|
+
writeMetadataState,
|
|
19
|
+
type MetadataDocument,
|
|
20
|
+
} from "./state-metadata-store.ts";
|
|
21
|
+
import type {
|
|
22
|
+
ApiKind,
|
|
23
|
+
CompatSettings,
|
|
24
|
+
ModelInputKind,
|
|
25
|
+
StateDocument,
|
|
26
|
+
StoredModel,
|
|
27
|
+
StoredProvider,
|
|
28
|
+
ThinkingLevelMap,
|
|
29
|
+
TokenCost,
|
|
30
|
+
TokenCostTier,
|
|
31
|
+
} from "./types.ts";
|
|
32
|
+
import { ZERO_COST } from "./types.ts";
|
|
33
|
+
|
|
34
|
+
export { STATE_PATH } from "./state-metadata-store.ts";
|
|
35
|
+
|
|
36
|
+
const API_KINDS = new Set<ApiKind>(["openai-completions", "openai-responses", "anthropic-messages", "google-generative-ai"]);
|
|
37
|
+
const INPUT_KINDS = new Set<ModelInputKind>(["text", "image"]);
|
|
38
|
+
|
|
39
|
+
function createEmptyStateDocument(): StateDocument {
|
|
40
|
+
return { version: 1, providers: {}, requestHeaderProfiles: {}, clientHeaderCaptures: {} };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function asApiKind(value: unknown): ApiKind | undefined {
|
|
44
|
+
return typeof value === "string" && API_KINDS.has(value as ApiKind) ? value as ApiKind : undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getFullModelId(providerId: string, modelId: string): string {
|
|
48
|
+
return `${providerId}/${modelId}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readNonNegativeNumber(value: unknown): number {
|
|
52
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readCostTier(value: unknown): TokenCostTier | undefined {
|
|
56
|
+
if (!isObjectRecord(value)) return undefined;
|
|
57
|
+
const inputTokensAbove = value.inputTokensAbove;
|
|
58
|
+
if (typeof inputTokensAbove !== "number" || !Number.isFinite(inputTokensAbove) || inputTokensAbove < 0) return undefined;
|
|
59
|
+
return {
|
|
60
|
+
inputTokensAbove,
|
|
61
|
+
input: readNonNegativeNumber(value.input),
|
|
62
|
+
output: readNonNegativeNumber(value.output),
|
|
63
|
+
cacheRead: readNonNegativeNumber(value.cacheRead),
|
|
64
|
+
cacheWrite: readNonNegativeNumber(value.cacheWrite),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readModelCost(model: ModelsJsonModelEntry): TokenCost {
|
|
69
|
+
const cost = model.cost;
|
|
70
|
+
if (!cost) return { ...ZERO_COST };
|
|
71
|
+
const stored: TokenCost = {
|
|
72
|
+
input: readNonNegativeNumber(cost.input),
|
|
73
|
+
output: readNonNegativeNumber(cost.output),
|
|
74
|
+
cacheRead: readNonNegativeNumber(cost.cacheRead),
|
|
75
|
+
cacheWrite: readNonNegativeNumber(cost.cacheWrite),
|
|
76
|
+
};
|
|
77
|
+
const tiers = cost.tiers?.map(readCostTier).filter((tier): tier is TokenCostTier => Boolean(tier));
|
|
78
|
+
if (tiers && tiers.length > 0) stored.tiers = tiers;
|
|
79
|
+
return stored;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readModelInput(model: ModelsJsonModelEntry): ModelInputKind[] {
|
|
83
|
+
const input = model.input?.filter((item): item is ModelInputKind => INPUT_KINDS.has(item as ModelInputKind));
|
|
84
|
+
return input && input.length > 0 ? [...new Set(input)] : ["text"];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function resolveProviderCustomHeaders(
|
|
88
|
+
providerMetadata: MetadataDocument["providers"][string] | undefined,
|
|
89
|
+
metadata: MetadataDocument,
|
|
90
|
+
): Record<string, string> {
|
|
91
|
+
const profileId = providerMetadata?.requestHeaderProfileId;
|
|
92
|
+
if (profileId && metadata.requestHeaderProfiles[profileId]) return metadata.requestHeaderProfiles[profileId].headers;
|
|
93
|
+
return providerMetadata?.customClientHeaders ?? {};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function buildStoredModelFromModelsJson(
|
|
97
|
+
providerId: string,
|
|
98
|
+
providerApi: ApiKind,
|
|
99
|
+
providerBaseUrl: string,
|
|
100
|
+
providerCompat: CompatSettings | undefined,
|
|
101
|
+
clientHeaderProfile: StoredProvider["clientHeaderProfile"],
|
|
102
|
+
customClientHeaders: Record<string, string>,
|
|
103
|
+
model: ModelsJsonModelEntry,
|
|
104
|
+
metadata: MetadataDocument,
|
|
105
|
+
): StoredModel | undefined {
|
|
106
|
+
if (!model.id || typeof model.id !== "string") return undefined;
|
|
107
|
+
const explicitApi = typeof model.api === "string" && model.api ? model.api : undefined;
|
|
108
|
+
const effectiveApi = asApiKind(explicitApi) ?? providerApi;
|
|
109
|
+
const modelCompat = model.compat ? cloneJson(model.compat) : undefined;
|
|
110
|
+
const effectiveCompat = mergeCompatSettings(providerCompat, modelCompat);
|
|
111
|
+
const profileHeaders = getClientHeadersForProfile(
|
|
112
|
+
clientHeaderProfile,
|
|
113
|
+
effectiveApi,
|
|
114
|
+
customClientHeaders,
|
|
115
|
+
metadata.clientHeaderCaptures,
|
|
116
|
+
effectiveCompat,
|
|
117
|
+
);
|
|
118
|
+
const nativeHeaders = stripManagedClientHeaders(model.headers, profileHeaders);
|
|
119
|
+
const stored: StoredModel = {
|
|
120
|
+
id: model.id,
|
|
121
|
+
reasoning: model.reasoning ?? false,
|
|
122
|
+
input: readModelInput(model),
|
|
123
|
+
contextWindow: model.contextWindow && model.contextWindow > 0 ? model.contextWindow : 128000,
|
|
124
|
+
maxTokens: model.maxTokens && model.maxTokens > 0 ? model.maxTokens : 16384,
|
|
125
|
+
cost: readModelCost(model),
|
|
126
|
+
};
|
|
127
|
+
if (explicitApi && explicitApi !== providerApi) stored.api = explicitApi;
|
|
128
|
+
if (model.baseUrl && model.baseUrl !== providerBaseUrl) stored.baseUrl = model.baseUrl;
|
|
129
|
+
if (nativeHeaders) stored.headers = nativeHeaders;
|
|
130
|
+
if (model.name) stored.name = model.name;
|
|
131
|
+
const storedThinkingLevelMap = model.thinkingLevelMap ? cloneJson(model.thinkingLevelMap) as ThinkingLevelMap : undefined;
|
|
132
|
+
const thinkingLevelMap = normalizeThinkingLevelMap(effectiveApi, stored.reasoning, storedThinkingLevelMap);
|
|
133
|
+
if (thinkingLevelMap) stored.thinkingLevelMap = thinkingLevelMap;
|
|
134
|
+
if (modelCompat) stored.compat = modelCompat;
|
|
135
|
+
const modelMetadata = metadata.models[getFullModelId(providerId, model.id)];
|
|
136
|
+
if (effectiveApi === "openai-responses" && modelMetadata?.openAIServiceTier) {
|
|
137
|
+
stored.openAIServiceTier = modelMetadata.openAIServiceTier;
|
|
138
|
+
}
|
|
139
|
+
return stored;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function buildStoredProviderFromModelsJson(
|
|
143
|
+
providerId: string,
|
|
144
|
+
entry: ModelsJsonProviderEntry,
|
|
145
|
+
metadata: MetadataDocument,
|
|
146
|
+
): Promise<StoredProvider | undefined> {
|
|
147
|
+
const rawModels = entry.models ?? [];
|
|
148
|
+
if (rawModels.length === 0) return undefined;
|
|
149
|
+
|
|
150
|
+
const firstModel = rawModels[0];
|
|
151
|
+
const builtInDefaults = await getBuiltinProviderDefaults(providerId);
|
|
152
|
+
const api = asApiKind(entry.api) ?? asApiKind(firstModel?.api) ?? asApiKind(builtInDefaults?.api);
|
|
153
|
+
const baseUrl = entry.baseUrl ?? firstModel?.baseUrl ?? builtInDefaults?.baseUrl;
|
|
154
|
+
if (!api || !baseUrl) return undefined;
|
|
155
|
+
|
|
156
|
+
const providerMetadata = metadata.providers[providerId];
|
|
157
|
+
const clientHeaderProfile = providerMetadata?.clientHeaderProfile ?? "recommended";
|
|
158
|
+
const customClientHeaders = clientHeaderProfile === "custom"
|
|
159
|
+
? resolveProviderCustomHeaders(providerMetadata, metadata)
|
|
160
|
+
: {};
|
|
161
|
+
const providerCompat = entry.compat ? cloneJson(entry.compat) : undefined;
|
|
162
|
+
const models = rawModels
|
|
163
|
+
.map((model) => buildStoredModelFromModelsJson(
|
|
164
|
+
providerId,
|
|
165
|
+
api,
|
|
166
|
+
baseUrl,
|
|
167
|
+
providerCompat,
|
|
168
|
+
clientHeaderProfile,
|
|
169
|
+
customClientHeaders,
|
|
170
|
+
model,
|
|
171
|
+
metadata,
|
|
172
|
+
))
|
|
173
|
+
.filter((model): model is StoredModel => Boolean(model));
|
|
174
|
+
if (models.length === 0) return undefined;
|
|
175
|
+
|
|
176
|
+
const provider: StoredProvider = {
|
|
177
|
+
name: entry.name || providerId,
|
|
178
|
+
api,
|
|
179
|
+
baseUrl,
|
|
180
|
+
clientHeaderProfile,
|
|
181
|
+
models,
|
|
182
|
+
};
|
|
183
|
+
if (entry.apiKey) provider.apiKey = entry.apiKey;
|
|
184
|
+
if (entry.authHeader !== undefined) provider.authHeader = entry.authHeader;
|
|
185
|
+
if (entry.headers !== undefined) provider.headers = cloneJson(entry.headers);
|
|
186
|
+
if (providerCompat) provider.compat = providerCompat;
|
|
187
|
+
if (entry.modelOverrides !== undefined) provider.modelOverrides = cloneJson(entry.modelOverrides);
|
|
188
|
+
if (provider.clientHeaderProfile === "custom" && providerMetadata?.requestHeaderProfileId) {
|
|
189
|
+
provider.requestHeaderProfileId = providerMetadata.requestHeaderProfileId;
|
|
190
|
+
}
|
|
191
|
+
if (provider.clientHeaderProfile === "custom" && hasStringRecordEntries(providerMetadata?.customClientHeaders)) {
|
|
192
|
+
provider.customClientHeaders = cloneJson(providerMetadata!.customClientHeaders!);
|
|
193
|
+
}
|
|
194
|
+
if (providerMetadata?.httpProxyEnabled !== undefined) provider.httpProxyEnabled = providerMetadata.httpProxyEnabled;
|
|
195
|
+
if (providerMetadata?.httpProxyUrl !== undefined) provider.httpProxyUrl = providerMetadata.httpProxyUrl;
|
|
196
|
+
return provider;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function buildStateDocumentFromModelsJson(
|
|
200
|
+
document: ModelsJsonDocument,
|
|
201
|
+
metadata: MetadataDocument,
|
|
202
|
+
): Promise<StateDocument> {
|
|
203
|
+
const providers: StateDocument["providers"] = {};
|
|
204
|
+
for (const [providerId, entry] of Object.entries(document.providers)) {
|
|
205
|
+
const provider = await buildStoredProviderFromModelsJson(providerId, entry, metadata);
|
|
206
|
+
if (provider) providers[providerId] = provider;
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
version: 1,
|
|
210
|
+
providers,
|
|
211
|
+
requestHeaderProfiles: metadata.requestHeaderProfiles,
|
|
212
|
+
clientHeaderCaptures: metadata.clientHeaderCaptures,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export async function readState(): Promise<StateDocument> {
|
|
217
|
+
const { metadata, legacyProviders } = await readMetadataStateFile();
|
|
218
|
+
const state = await buildStateDocumentFromModelsJson(
|
|
219
|
+
await readModelsJson(),
|
|
220
|
+
metadata,
|
|
221
|
+
);
|
|
222
|
+
for (const [providerId, provider] of Object.entries(legacyProviders)) {
|
|
223
|
+
if (!state.providers[providerId]) state.providers[providerId] = provider;
|
|
224
|
+
}
|
|
225
|
+
return state;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export async function writeState(state: StateDocument): Promise<void> {
|
|
229
|
+
await writeMetadataState(state);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function getStatePath(): string {
|
|
233
|
+
return getMetadataStatePath();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function createEmptyState(): StateDocument {
|
|
237
|
+
return createEmptyStateDocument();
|
|
238
|
+
}
|