diff --git a/docs/cli/models.md b/docs/cli/models.md index bc0422e6296..8e7e1bbf2be 100644 --- a/docs/cli/models.md +++ b/docs/cli/models.md @@ -51,6 +51,10 @@ Notes: rows from plugin manifests or bundled provider catalog metadata even when you have not authenticated with that provider yet. Those rows still show as unavailable until matching auth is configured. +- Broad `models list --all` merges manifest catalog rows over registry rows + without loading provider runtime supplement hooks. Provider-filtered manifest + fast paths only use providers marked `static`, so partial `refreshable` + manifest rows do not hide registry-backed provider lists. - `models list` keeps native model metadata and runtime caps distinct. In table output, `Ctx` shows `contextTokens/contextWindow` when an effective runtime cap differs from the native context window; JSON rows include `contextTokens` 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 be36882bef5..9363e003bdf 100644 --- a/src/commands/models/list.list-command.forward-compat.test.ts +++ b/src/commands/models/list.list-command.forward-compat.test.ts @@ -49,7 +49,7 @@ const mocks = vi.hoisted(() => { loadModelRegistry: vi.fn(), loadModelCatalog: vi.fn(), loadProviderCatalogModelsForList: vi.fn(), - loadStaticManifestCatalogRowsForList: vi.fn(), + loadManifestCatalogRowsForList: vi.fn(), loadProviderIndexCatalogRowsForList: vi.fn(), hasProviderStaticCatalogForFilter: vi.fn(), resolveConfiguredEntries: vi.fn(), @@ -78,7 +78,7 @@ function resetMocks() { }); mocks.loadModelCatalog.mockResolvedValue([]); mocks.loadProviderCatalogModelsForList.mockResolvedValue([]); - mocks.loadStaticManifestCatalogRowsForList.mockReturnValue([]); + mocks.loadManifestCatalogRowsForList.mockReturnValue([]); mocks.loadProviderIndexCatalogRowsForList.mockReturnValue([]); mocks.hasProviderStaticCatalogForFilter.mockResolvedValue(false); mocks.resolveConfiguredEntries.mockReturnValue({ @@ -143,7 +143,7 @@ function installModelsListCommandForwardCompatMocks() { })); vi.doMock("./list.manifest-catalog.js", () => ({ - loadStaticManifestCatalogRowsForList: mocks.loadStaticManifestCatalogRowsForList, + loadManifestCatalogRowsForList: mocks.loadManifestCatalogRowsForList, })); vi.doMock("./list.provider-index-catalog.js", () => ({ @@ -525,7 +525,7 @@ describe("modelsListCommand forward-compat", () => { it("uses manifest catalog rows before provider runtime catalog rows", async () => { mocks.resolveConfiguredEntries.mockReturnValueOnce({ entries: [] }); - mocks.loadStaticManifestCatalogRowsForList.mockReturnValueOnce([ + mocks.loadManifestCatalogRowsForList.mockReturnValueOnce([ { provider: "moonshot", id: "kimi-k2.6", @@ -596,7 +596,7 @@ describe("modelsListCommand forward-compat", () => { getAll: () => [{ ...OPENAI_CODEX_MODEL }], }, }); - mocks.loadStaticManifestCatalogRowsForList.mockReturnValueOnce([ + mocks.loadManifestCatalogRowsForList.mockReturnValueOnce([ { provider: "moonshot", id: "kimi-k2.6", @@ -622,6 +622,7 @@ describe("modelsListCommand forward-compat", () => { }); expect(mocks.loadProviderCatalogModelsForList).not.toHaveBeenCalled(); expect(mocks.resolveModelWithRegistry).not.toHaveBeenCalled(); + expect(mocks.loadModelCatalog).not.toHaveBeenCalled(); expect(lastPrintedRows<{ key: string }>()).toEqual([ expect.objectContaining({ key: "openai-codex/gpt-5.4", diff --git a/src/commands/models/list.registry.ts b/src/commands/models/list.registry.ts index f2d3b39a7e0..999b7075b02 100644 --- a/src/commands/models/list.registry.ts +++ b/src/commands/models/list.registry.ts @@ -8,7 +8,10 @@ import { resolveAwsSdkEnvVarName, resolveEnvApiKey, } from "../../agents/model-auth.js"; -import { shouldSuppressBuiltInModel } from "../../agents/model-suppression.js"; +import { + shouldSuppressBuiltInModel, + shouldSuppressBuiltInModelFromManifest, +} from "../../agents/model-suppression.js"; import { discoverAuthStorage, discoverModels } from "../../agents/pi-model-discovery.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { resolveRuntimeSyntheticAuthProviderRefs } from "../../plugins/synthetic-auth.runtime.js"; @@ -85,7 +88,11 @@ function validateAvailableModels(availableModels: unknown): Model[] { return availableModels as Model[]; } -function loadAvailableModels(registry: ModelRegistry, cfg: OpenClawConfig): Model[] { +function loadAvailableModels( + registry: ModelRegistry, + cfg: OpenClawConfig, + opts?: { runtimeSuppression?: boolean }, +): Model[] { let availableModels: unknown; try { availableModels = registry.getAvailable(); @@ -93,14 +100,19 @@ function loadAvailableModels(registry: ModelRegistry, cfg: OpenClawConfig): Mode throw normalizeAvailabilityError(err); } try { - return validateAvailableModels(availableModels).filter( - (model) => - !shouldSuppressBuiltInModel({ - provider: model.provider, - id: model.id, - baseUrl: model.baseUrl, - config: cfg, - }), + return validateAvailableModels(availableModels).filter((model) => + opts?.runtimeSuppression === false + ? !shouldSuppressBuiltInModelFromManifest({ + provider: model.provider, + id: model.id, + config: cfg, + }) + : !shouldSuppressBuiltInModel({ + provider: model.provider, + id: model.id, + baseUrl: model.baseUrl, + config: cfg, + }), ); } catch (err) { throw normalizeAvailabilityError(err); @@ -111,26 +123,32 @@ export async function loadModelRegistry( cfg: OpenClawConfig, opts?: { providerFilter?: string; normalizeModels?: boolean }, ) { + const runtimeSuppression = opts?.normalizeModels !== false; const agentDir = resolveOpenClawAgentDir(); const authStorage = discoverAuthStorage(agentDir, { readOnly: true }); const registry = discoverModels(authStorage, agentDir, { providerFilter: opts?.providerFilter, normalizeModels: opts?.normalizeModels, }); - const models = registry.getAll().filter( - (model) => - !shouldSuppressBuiltInModel({ - provider: model.provider, - id: model.id, - baseUrl: model.baseUrl, - config: cfg, - }), + const models = registry.getAll().filter((model) => + runtimeSuppression + ? !shouldSuppressBuiltInModel({ + provider: model.provider, + id: model.id, + baseUrl: model.baseUrl, + config: cfg, + }) + : !shouldSuppressBuiltInModelFromManifest({ + provider: model.provider, + id: model.id, + config: cfg, + }), ); let availableKeys: Set | undefined; let availabilityErrorMessage: string | undefined; try { - const availableModels = loadAvailableModels(registry, cfg); + const availableModels = loadAvailableModels(registry, cfg, { runtimeSuppression }); availableKeys = new Set(availableModels.map((model) => modelKey(model.provider, model.id))); } catch (err) { if (!shouldFallbackToAuthHeuristics(err)) { diff --git a/src/commands/models/list.row-sources.ts b/src/commands/models/list.row-sources.ts index 573cba8fc1c..89f551f4b3f 100644 --- a/src/commands/models/list.row-sources.ts +++ b/src/commands/models/list.row-sources.ts @@ -109,13 +109,15 @@ export async function appendAllModelRowSources( }); } - if (params.modelRegistry) { + if (params.modelRegistry && params.context.filter.provider) { await appendCatalogSupplementRows({ rows: params.rows, modelRegistry: params.modelRegistry, context: params.context, seenKeys, }); + } + if (params.modelRegistry) { return { requiresRegistryFallback: false }; }