diff --git a/src/commands/models/list.list-command.forward-compat.test.ts b/src/commands/models/list.list-command.forward-compat.test.ts index ccc75cc5a83..1cc1247d025 100644 --- a/src/commands/models/list.list-command.forward-compat.test.ts +++ b/src/commands/models/list.list-command.forward-compat.test.ts @@ -587,6 +587,49 @@ describe("modelsListCommand forward-compat", () => { ]); }); + it("does not load broad provider runtime catalogs for unfiltered all-model lists", async () => { + mocks.resolveConfiguredEntries.mockReturnValueOnce({ entries: [] }); + mocks.loadModelRegistry.mockResolvedValueOnce({ + models: [{ ...OPENAI_CODEX_MODEL }], + availableKeys: new Set(["openai-codex/gpt-5.4"]), + registry: { + getAll: () => [{ ...OPENAI_CODEX_MODEL }], + }, + }); + mocks.loadStaticManifestCatalogRowsForList.mockReturnValueOnce([ + { + provider: "moonshot", + id: "kimi-k2.6", + ref: "moonshot/kimi-k2.6", + mergeKey: "moonshot::kimi-k2.6", + name: "Kimi K2.6", + source: "manifest", + input: ["text", "image"], + reasoning: false, + status: "available", + baseUrl: "https://api.moonshot.ai/v1", + contextWindow: 262_144, + }, + ]); + mocks.loadModelCatalog.mockResolvedValueOnce([]); + const runtime = createRuntime(); + + await modelsListCommand({ all: true, json: true }, runtime as never); + + expect(mocks.loadModelRegistry).toHaveBeenCalledWith(mocks.resolvedConfig, { + providerFilter: undefined, + }); + expect(mocks.loadProviderCatalogModelsForList).not.toHaveBeenCalled(); + expect(lastPrintedRows<{ key: string }>()).toEqual([ + expect.objectContaining({ + key: "openai-codex/gpt-5.4", + }), + expect.objectContaining({ + key: "moonshot/kimi-k2.6", + }), + ]); + }); + it("falls back to registry-backed rows when the fast-path catalog is empty", async () => { mocks.resolveConfiguredEntries.mockReturnValueOnce({ entries: [] }); mocks.hasProviderStaticCatalogForFilter.mockResolvedValueOnce(true); diff --git a/src/commands/models/list.row-sources.ts b/src/commands/models/list.row-sources.ts index 8a50abc96e7..5c274880da5 100644 --- a/src/commands/models/list.row-sources.ts +++ b/src/commands/models/list.row-sources.ts @@ -89,6 +89,24 @@ export async function appendAllModelRowSources( seenKeys, }); + if (params.sourcePlan.manifestCatalogRows.length > 0) { + await appendManifestCatalogRows({ + rows: params.rows, + context: { ...params.context, skipRuntimeModelSuppression: true }, + seenKeys, + manifestRows: params.sourcePlan.manifestCatalogRows, + }); + } + + if (params.sourcePlan.providerIndexCatalogRows.length > 0) { + await appendModelCatalogRows({ + rows: params.rows, + context: { ...params.context, skipRuntimeModelSuppression: true }, + seenKeys, + catalogRows: params.sourcePlan.providerIndexCatalogRows, + }); + } + if (params.modelRegistry) { await appendCatalogSupplementRows({ rows: params.rows, diff --git a/src/commands/models/list.rows.ts b/src/commands/models/list.rows.ts index f8ea35ab2e4..fabc9f39320 100644 --- a/src/commands/models/list.rows.ts +++ b/src/commands/models/list.rows.ts @@ -412,7 +412,7 @@ export async function appendCatalogSupplementRows(params: { }); } - if (params.context.filter.local) { + if (params.context.filter.local || !params.context.filter.provider) { return; } diff --git a/src/commands/models/list.source-plan.test.ts b/src/commands/models/list.source-plan.test.ts index 4d7a9bbe935..b28d33532c0 100644 --- a/src/commands/models/list.source-plan.test.ts +++ b/src/commands/models/list.source-plan.test.ts @@ -95,6 +95,27 @@ describe("planAllModelListSources", () => { }); }); + it("keeps broad all-model lists on the registry path with cheap catalog supplements", async () => { + const { planAllModelListSources } = await import("./list.source-plan.js"); + const providerIndexRow = { ...catalogRow, source: "provider-index" }; + mocks.loadStaticManifestCatalogRowsForList.mockReturnValueOnce([catalogRow]); + mocks.loadProviderIndexCatalogRowsForList.mockReturnValueOnce([providerIndexRow]); + + const plan = await planAllModelListSources({ + all: true, + cfg: {}, + }); + + expect(plan).toMatchObject({ + kind: "registry", + requiresInitialRegistry: true, + skipRuntimeModelSuppression: false, + }); + expect(plan.manifestCatalogRows).toEqual([catalogRow]); + expect(plan.providerIndexCatalogRows).toEqual([providerIndexRow]); + expect(mocks.hasProviderStaticCatalogForFilter).not.toHaveBeenCalled(); + }); + it("falls back to registry only for provider static fast paths that return no rows", async () => { const { planAllModelListSources } = await import("./list.source-plan.js"); mocks.hasProviderStaticCatalogForFilter.mockResolvedValueOnce(true); diff --git a/src/commands/models/list.source-plan.ts b/src/commands/models/list.source-plan.ts index d4baac67917..f434fdc69a2 100644 --- a/src/commands/models/list.source-plan.ts +++ b/src/commands/models/list.source-plan.ts @@ -47,15 +47,28 @@ export async function planAllModelListSources(params: { providerFilter?: string; cfg: OpenClawConfig; }): Promise { - if (!params.all || !params.providerFilter) { + if (!params.all) { return createRegistryModelListSourcePlan(); } const { loadStaticManifestCatalogRowsForList } = await import("./list.manifest-catalog.js"); const manifestCatalogRows = loadStaticManifestCatalogRowsForList({ cfg: params.cfg, - providerFilter: params.providerFilter, + ...(params.providerFilter ? { providerFilter: params.providerFilter } : {}), }); + if (!params.providerFilter) { + const { loadProviderIndexCatalogRowsForList } = + await import("./list.provider-index-catalog.js"); + return createSourcePlan({ + kind: "registry", + manifestCatalogRows, + providerIndexCatalogRows: loadProviderIndexCatalogRowsForList({ + cfg: params.cfg, + }), + requiresInitialRegistry: true, + }); + } + if (manifestCatalogRows.length > 0) { return createSourcePlan({ kind: "manifest",