mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:20:43 +00:00
fix: keep model catalog reads read-only
This commit is contained in:
@@ -9,6 +9,7 @@ let findModelInCatalog: typeof import("./model-catalog.js").findModelInCatalog;
|
||||
let loadModelCatalog: typeof import("./model-catalog.js").loadModelCatalog;
|
||||
let resetModelCatalogCacheForTest: typeof import("./model-catalog.js").resetModelCatalogCacheForTest;
|
||||
let augmentCatalogMock: ReturnType<typeof vi.fn>;
|
||||
let ensureOpenClawModelsJsonMock: ReturnType<typeof vi.fn>;
|
||||
|
||||
vi.mock("./model-suppression.runtime.js", () => ({
|
||||
shouldSuppressBuiltInModel: (params: { provider?: string; id?: string }) =>
|
||||
@@ -59,8 +60,9 @@ function mockSingleOpenAiCatalogModel() {
|
||||
|
||||
describe("loadModelCatalog", () => {
|
||||
beforeAll(async () => {
|
||||
ensureOpenClawModelsJsonMock = vi.fn().mockResolvedValue({ agentDir: "/tmp", wrote: false });
|
||||
vi.doMock("./models-config.js", () => ({
|
||||
ensureOpenClawModelsJson: vi.fn().mockResolvedValue({ agentDir: "/tmp", wrote: false }),
|
||||
ensureOpenClawModelsJson: ensureOpenClawModelsJsonMock,
|
||||
}));
|
||||
vi.doMock("./agent-paths.js", () => ({
|
||||
resolveOpenClawAgentDir: () => "/tmp/openclaw",
|
||||
@@ -81,6 +83,7 @@ describe("loadModelCatalog", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
resetModelCatalogCacheForTest();
|
||||
ensureOpenClawModelsJsonMock.mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -146,6 +149,28 @@ describe("loadModelCatalog", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("does not prepare models.json when loading catalog in read-only mode", async () => {
|
||||
const discoverAuthStorage = vi.fn(() => ({}));
|
||||
__setModelCatalogImportForTest(
|
||||
async () =>
|
||||
({
|
||||
discoverAuthStorage,
|
||||
AuthStorage: function AuthStorage() {},
|
||||
ModelRegistry: class {
|
||||
getAll() {
|
||||
return [{ id: "gpt-4.1", name: "GPT-4.1", provider: "openai" }];
|
||||
}
|
||||
},
|
||||
}) as unknown as PiSdkModule,
|
||||
);
|
||||
|
||||
const result = await loadModelCatalog({ config: {} as OpenClawConfig, readOnly: true });
|
||||
|
||||
expect(result).toEqual([{ id: "gpt-4.1", name: "GPT-4.1", provider: "openai" }]);
|
||||
expect(ensureOpenClawModelsJsonMock).not.toHaveBeenCalled();
|
||||
expect(discoverAuthStorage).toHaveBeenCalledWith("/tmp/openclaw", { readOnly: true });
|
||||
});
|
||||
|
||||
it("does not synthesize stale openai-codex/gpt-5.3-codex-spark entries from gpt-5.4", async () => {
|
||||
mockPiDiscoveryModels([
|
||||
{
|
||||
|
||||
@@ -81,15 +81,17 @@ function instantiatePiModelRegistry(
|
||||
export async function loadModelCatalog(params?: {
|
||||
config?: OpenClawConfig;
|
||||
useCache?: boolean;
|
||||
readOnly?: boolean;
|
||||
}): Promise<ModelCatalogEntry[]> {
|
||||
if (params?.useCache === false) {
|
||||
const readOnly = params?.readOnly === true;
|
||||
if (!readOnly && params?.useCache === false) {
|
||||
modelCatalogPromise = null;
|
||||
}
|
||||
if (modelCatalogPromise) {
|
||||
if (!readOnly && modelCatalogPromise) {
|
||||
return modelCatalogPromise;
|
||||
}
|
||||
|
||||
modelCatalogPromise = (async () => {
|
||||
const loadCatalog = async () => {
|
||||
const models: ModelCatalogEntry[] = [];
|
||||
const timingEnabled = shouldLogModelCatalogTiming();
|
||||
const startMs = timingEnabled ? Date.now() : 0;
|
||||
@@ -110,8 +112,10 @@ export async function loadModelCatalog(params?: {
|
||||
});
|
||||
try {
|
||||
const cfg = params?.config ?? loadConfig();
|
||||
await ensureOpenClawModelsJson(cfg);
|
||||
logStage("models-json-ready");
|
||||
if (!readOnly) {
|
||||
await ensureOpenClawModelsJson(cfg);
|
||||
logStage("models-json-ready");
|
||||
}
|
||||
// IMPORTANT: keep the dynamic import *inside* the try/catch.
|
||||
// If this fails once (e.g. during a pnpm install that temporarily swaps node_modules),
|
||||
// we must not poison the cache with a rejected promise (otherwise all channel handlers
|
||||
@@ -121,7 +125,10 @@ export async function loadModelCatalog(params?: {
|
||||
const agentDir = resolveOpenClawAgentDir();
|
||||
const { shouldSuppressBuiltInModel } = await loadModelSuppression();
|
||||
logStage("catalog-deps-ready");
|
||||
const authStorage = piSdk.discoverAuthStorage(agentDir);
|
||||
const authStorage = piSdk.discoverAuthStorage(
|
||||
agentDir,
|
||||
readOnly ? { readOnly: true } : undefined,
|
||||
);
|
||||
logStage("auth-storage-ready");
|
||||
const registry = instantiatePiModelRegistry(
|
||||
piSdk,
|
||||
@@ -182,7 +189,9 @@ export async function loadModelCatalog(params?: {
|
||||
|
||||
if (models.length === 0) {
|
||||
// If we found nothing, don't cache this result so we can try again.
|
||||
modelCatalogPromise = null;
|
||||
if (!readOnly) {
|
||||
modelCatalogPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
const sorted = sortModels(models);
|
||||
@@ -194,14 +203,21 @@ export async function loadModelCatalog(params?: {
|
||||
log.warn(`Failed to load model catalog: ${String(error)}`);
|
||||
}
|
||||
// Don't poison the cache on transient dependency/filesystem issues.
|
||||
modelCatalogPromise = null;
|
||||
if (!readOnly) {
|
||||
modelCatalogPromise = null;
|
||||
}
|
||||
if (models.length > 0) {
|
||||
return sortModels(models);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
if (readOnly) {
|
||||
return loadCatalog();
|
||||
}
|
||||
|
||||
modelCatalogPromise = loadCatalog();
|
||||
return modelCatalogPromise;
|
||||
}
|
||||
|
||||
|
||||
@@ -193,7 +193,7 @@ export async function appendCatalogSupplementRows(params: {
|
||||
context: RowBuilderContext;
|
||||
seenKeys: Set<string>;
|
||||
}): Promise<void> {
|
||||
const catalog = await loadModelCatalog({ config: params.context.cfg });
|
||||
const catalog = await loadModelCatalog({ config: params.context.cfg, readOnly: true });
|
||||
for (const entry of catalog) {
|
||||
if (
|
||||
params.context.filter.provider &&
|
||||
|
||||
Reference in New Issue
Block a user