























@@ -1,3 +1,4 @@
1+import { readFile } from "node:fs/promises";
12import { join } from "node:path";
23import { getRuntimeConfig } from "../config/config.js";
34import type { OpenClawConfig } from "../config/types.openclaw.js";
@@ -20,6 +21,7 @@ import { ensureOpenClawModelsJson } from "./models-config.js";
2021import { normalizeProviderId } from "./provider-id.js";
21222223const log = createSubsystemLogger("model-catalog");
24+const PI_CUSTOM_MODEL_DEFAULT_CONTEXT_WINDOW = 128_000;
23252426export type { ModelCatalogEntry, ModelInputType } from "./model-catalog.types.js";
2527export {
@@ -161,12 +163,106 @@ export function loadManifestModelCatalog(params: {
161163});
162164}
163165166+function sortModelCatalogEntries(entries: ModelCatalogEntry[]): ModelCatalogEntry[] {
167+return entries.toSorted((a, b) => {
168+const p = a.provider.localeCompare(b.provider);
169+if (p !== 0) {
170+return p;
171+}
172+return a.name.localeCompare(b.name);
173+});
174+}
175+176+function normalizePersistedModelCatalogEntry(
177+providerRaw: string,
178+entry: Record<string, unknown>,
179+defaults?: {
180+contextWindow?: number;
181+},
182+): ModelCatalogEntry | undefined {
183+const id = normalizeOptionalString(entry.id) ?? "";
184+if (!id) {
185+return undefined;
186+}
187+const provider = normalizeProviderId(providerRaw);
188+if (!provider) {
189+return undefined;
190+}
191+const name = normalizeOptionalString(entry.name ?? id) || id;
192+const contextWindow =
193+typeof entry?.contextWindow === "number" && entry.contextWindow > 0
194+ ? entry.contextWindow
195+ : defaults?.contextWindow !== undefined
196+ ? defaults.contextWindow
197+ : PI_CUSTOM_MODEL_DEFAULT_CONTEXT_WINDOW;
198+const reasoning = typeof entry?.reasoning === "boolean" ? entry.reasoning : false;
199+const parsedInput = Array.isArray(entry?.input)
200+ ? entry.input.filter((value): value is ModelInputType =>
201+["text", "image", "audio", "video", "document"].includes(String(value)),
202+)
203+ : undefined;
204+const input: ModelInputType[] = parsedInput?.length ? parsedInput : ["text"];
205+const compat =
206+entry?.compat && typeof entry.compat === "object"
207+ ? (entry.compat as ModelCatalogEntry["compat"])
208+ : undefined;
209+return { id, name, provider, contextWindow, reasoning, input, compat };
210+}
211+212+async function loadReadOnlyPersistedModelCatalog(params?: {
213+config?: OpenClawConfig;
214+}): Promise<ModelCatalogEntry[]> {
215+const cfg = params?.config ?? getRuntimeConfig();
216+const agentDir = resolveOpenClawAgentDir();
217+const raw = await readFile(join(agentDir, "models.json"), "utf8");
218+const parsed = JSON.parse(raw) as Record<string, unknown>;
219+const models: ModelCatalogEntry[] = [];
220+const { buildShouldSuppressBuiltInModel } = await loadModelSuppression();
221+const shouldSuppressBuiltInModel = buildShouldSuppressBuiltInModel({ config: cfg });
222+const providers =
223+parsed?.providers && typeof parsed.providers === "object"
224+ ? (parsed.providers as Record<string, Record<string, unknown>>)
225+ : {};
226+for (const [providerRaw, providerConfig] of Object.entries(providers)) {
227+if (!Array.isArray(providerConfig?.models)) {
228+continue;
229+}
230+const providerContextWindow =
231+typeof providerConfig?.contextWindow === "number" && providerConfig.contextWindow > 0
232+ ? providerConfig.contextWindow
233+ : undefined;
234+for (const entry of providerConfig.models as Record<string, unknown>[]) {
235+const normalized = normalizePersistedModelCatalogEntry(providerRaw, entry, {
236+contextWindow: providerContextWindow,
237+});
238+if (normalized && !shouldSuppressBuiltInModel(normalized)) {
239+models.push(normalized);
240+}
241+}
242+}
243+if (models.length === 0) {
244+throw new Error("persisted model catalog has no usable model rows");
245+}
246+const configuredModels = buildConfiguredModelCatalog({ cfg });
247+if (configuredModels.length > 0) {
248+appendCatalogEntriesIfAbsent(models, configuredModels);
249+}
250+return sortModelCatalogEntries(models);
251+}
252+164253export async function loadModelCatalog(params?: {
165254config?: OpenClawConfig;
166255useCache?: boolean;
167256readOnly?: boolean;
168257}): Promise<ModelCatalogEntry[]> {
169258const readOnly = params?.readOnly === true;
259+if (readOnly) {
260+try {
261+return await loadReadOnlyPersistedModelCatalog(params);
262+} catch {
263+// fall through to full catalog path
264+}
265+}
170266if (!readOnly && params?.useCache === false) {
171267modelCatalogPromise = null;
172268}
@@ -185,14 +281,7 @@ export async function loadModelCatalog(params?: {
185281const suffix = extra ? ` ${extra}` : "";
186282log.info(`model-catalog stage=${stage} elapsedMs=${Date.now() - startMs}${suffix}`);
187283};
188-const sortModels = (entries: ModelCatalogEntry[]) =>
189-entries.sort((a, b) => {
190-const p = a.provider.localeCompare(b.provider);
191-if (p !== 0) {
192-return p;
193-}
194-return a.name.localeCompare(b.name);
195-});
284+const sortModels = sortModelCatalogEntries;
196285try {
197286const cfg = params?.config ?? getRuntimeConfig();
198287if (!readOnly) {
@@ -247,18 +336,20 @@ export async function loadModelCatalog(params?: {
247336const compat = entry?.compat && typeof entry.compat === "object" ? entry.compat : undefined;
248337models.push({ id, name, provider, contextWindow, reasoning, input, compat });
249338}
250-const supplemental = await augmentModelCatalogWithProviderPlugins({
251-config: cfg,
252-env: process.env,
253-context: {
339+if (!readOnly) {
340+const supplemental = await augmentModelCatalogWithProviderPlugins({
254341config: cfg,
255- agentDir,
256342env: process.env,
257-entries: [...models],
258-},
259-});
260-if (supplemental.length > 0) {
261-appendCatalogEntriesIfAbsent(models, supplemental);
343+context: {
344+config: cfg,
345+ agentDir,
346+env: process.env,
347+entries: [...models],
348+},
349+});
350+if (supplemental.length > 0) {
351+appendCatalogEntriesIfAbsent(models, supplemental);
352+}
262353}
263354logStage("plugin-models-merged", `entries=${models.length}`);
264355此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。