From 4e4f9204d75cd87d98ff70511447fa93e27a6608 Mon Sep 17 00:00:00 2001 From: Shakker Date: Wed, 29 Apr 2026 11:21:10 +0100 Subject: [PATCH] fix: restore provider-filtered model registry rows --- CHANGELOG.md | 1 + src/agents/pi-auth-discovery.ts | 1 + src/agents/pi-model-discovery.ts | 3 +- .../list.list-command.forward-compat.test.ts | 53 +++++++++++++++++-- src/commands/models/list.list-command.ts | 24 +++++++-- src/commands/models/list.registry-load.ts | 2 +- src/commands/models/list.registry.ts | 33 +++++++----- src/commands/models/list.row-sources.ts | 2 + src/commands/models/list.source-plan.test.ts | 4 +- src/commands/models/list.source-plan.ts | 1 + 10 files changed, 96 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0277701beda..b6ac907db3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- CLI/models: restore provider-filtered `models list --all --provider ` rows for providers without manifest/static catalog coverage, including Anthropic and Amazon Bedrock, while keeping the compatibility fallback off expensive availability and resolver paths. Thanks @shakkernerd. - Plugins/runtime-deps: memoize packaged bundled runtime dist-mirror preparation after the first successful pass while keeping source-checkout mirrors refreshable, so constrained Docker/VPS installs avoid repeated root scans before chat turns. Refs #73428, #73421, #73532, and #73477. Thanks @Dimaoggg, @oromeis, @oadiazp, @jmfraga, @bstanbury, @antoniusfelix, and @jkobject. - Channels/Discord: treat bare numeric outbound targets that match the effective Discord DM allowlist as user DMs while preserving account-specific legacy `dm.allowFrom` precedence over inherited root `allowFrom`. (#74303) Thanks @Squirbie. - Control UI: make the chat sidebar split divider focusable, keyboard-resizable, ARIA-described, and pointer-event based so sidebar resizing works without a mouse. Thanks @BunsDev. diff --git a/src/agents/pi-auth-discovery.ts b/src/agents/pi-auth-discovery.ts index a2643cb696d..fe567c027ed 100644 --- a/src/agents/pi-auth-discovery.ts +++ b/src/agents/pi-auth-discovery.ts @@ -9,6 +9,7 @@ import { addEnvBackedPiCredentials } from "./pi-auth-discovery-core.js"; export type DiscoverAuthStorageOptions = { readOnly?: boolean; + skipCredentials?: boolean; }; export function resolvePiCredentialsForDiscovery( diff --git a/src/agents/pi-model-discovery.ts b/src/agents/pi-model-discovery.ts index 1744305b117..e40736d5acb 100644 --- a/src/agents/pi-model-discovery.ts +++ b/src/agents/pi-model-discovery.ts @@ -222,7 +222,8 @@ export function discoverAuthStorage( agentDir: string, options?: DiscoverAuthStorageOptions, ): PiAuthStorage { - const credentials = resolvePiCredentialsForDiscovery(agentDir, options); + const credentials = + options?.skipCredentials === true ? {} : resolvePiCredentialsForDiscovery(agentDir, options); const authPath = path.join(agentDir, "auth.json"); if (options?.readOnly !== true) { scrubLegacyStaticAuthJsonEntriesForDiscovery(authPath); 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 f7ec57d337c..394c08cb1f9 100644 --- a/src/commands/models/list.list-command.forward-compat.test.ts +++ b/src/commands/models/list.list-command.forward-compat.test.ts @@ -125,6 +125,9 @@ function installModelsListCommandForwardCompatMocks() { vi.doMock("../../agents/model-suppression.js", () => ({ shouldSuppressBuiltInModel: suppressOpenAiSpark, shouldSuppressBuiltInModelFromManifest: suppressOpenAiSpark, + createManifestBuiltInModelSuppressor: vi.fn( + () => (model: { provider?: string | null; id?: string | null }) => suppressOpenAiSpark(model), + ), })); vi.doMock("./load-config.js", () => ({ @@ -156,7 +159,7 @@ function installModelsListCommandForwardCompatMocks() { vi.doMock("./list.registry-load.js", () => ({ loadListModelRegistry: async ( cfg: unknown, - opts?: { providerFilter?: string; normalizeModels?: boolean }, + opts?: { providerFilter?: string; normalizeModels?: boolean; loadAvailability?: boolean }, ): Promise<{ models: Array<{ provider: string; id: string }>; availableKeys?: Set; @@ -747,15 +750,55 @@ describe("modelsListCommand forward-compat", () => { ]); }); - it("does not fall back to the registry for provider filters without catalog coverage", async () => { + it("falls back to registry rows for provider filters without catalog coverage", async () => { mocks.resolveConfiguredEntries.mockReturnValueOnce({ entries: [] }); mocks.hasProviderStaticCatalogForFilter.mockResolvedValueOnce(false); + mocks.loadModelRegistry.mockResolvedValueOnce({ + models: [ + { + provider: "anthropic", + id: "claude-opus-4-7", + name: "Claude Opus 4.7", + api: "anthropic-messages", + baseUrl: "https://api.anthropic.com/v1", + input: ["text", "image"], + contextWindow: 1_000_000, + maxTokens: 64_000, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + }, + ], + availableKeys: undefined, + registry: { + getAll: () => [ + { + provider: "anthropic", + id: "claude-opus-4-7", + name: "Claude Opus 4.7", + api: "anthropic-messages", + baseUrl: "https://api.anthropic.com/v1", + input: ["text", "image"], + contextWindow: 1_000_000, + maxTokens: 64_000, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + }, + ], + }, + }); const runtime = createRuntime(); - await modelsListCommand({ all: true, provider: "openrouter", json: true }, runtime as never); + await modelsListCommand({ all: true, provider: "anthropic", json: true }, runtime as never); - expect(mocks.loadModelRegistry).not.toHaveBeenCalled(); - expect(runtime.log).toHaveBeenCalledWith("No models found."); + expect(mocks.loadModelRegistry).toHaveBeenCalledWith(mocks.resolvedConfig, { + providerFilter: "anthropic", + normalizeModels: false, + loadAvailability: false, + }); + expect(lastPrintedRows<{ key: string; available: boolean }>()).toEqual([ + expect.objectContaining({ + key: "anthropic/claude-opus-4-7", + available: false, + }), + ]); }); it("includes provider-owned supplemental catalog rows with provider filters", async () => { diff --git a/src/commands/models/list.list-command.ts b/src/commands/models/list.list-command.ts index ecaebfbc795..8b74566f052 100644 --- a/src/commands/models/list.list-command.ts +++ b/src/commands/models/list.list-command.ts @@ -92,11 +92,15 @@ export async function modelsListCommand( }) : undefined; const shouldLoadRegistry = sourcePlan?.requiresInitialRegistry ?? false; - const loadRegistryState = async () => { + const loadRegistryState = async (opts?: { + normalizeModels?: boolean; + loadAvailability?: boolean; + }) => { const { loadListModelRegistry } = await loadRegistryLoadModule(); const loaded = await loadListModelRegistry(cfg, { providerFilter, - normalizeModels: Boolean(providerFilter), + normalizeModels: opts?.normalizeModels ?? Boolean(providerFilter), + loadAvailability: opts?.loadAvailability, }); modelRegistry = loaded.registry; registryModels = loaded.models; @@ -148,21 +152,31 @@ export async function modelsListCommand( sourcePlan, }); if (initialAppend.requiresRegistryFallback) { + const useScopedRegistryFallback = sourcePlan.kind === "provider-runtime-scoped"; try { - await loadRegistryState(); + await loadRegistryState( + useScopedRegistryFallback + ? { + normalizeModels: false, + loadAvailability: false, + } + : undefined, + ); } catch (err) { runtime.error(`Model registry unavailable:\n${formatErrorWithStack(err)}`); process.exitCode = 1; return; } rows.length = 0; - rowContext = buildRowContext(false); + rowContext = buildRowContext(useScopedRegistryFallback); await appendAllModelRowSources({ rows, context: rowContext, modelRegistry, registryModels, - sourcePlan: sourcePlanModule.createRegistryModelListSourcePlan(), + sourcePlan: useScopedRegistryFallback + ? sourcePlan + : sourcePlanModule.createRegistryModelListSourcePlan(), }); } } else { diff --git a/src/commands/models/list.registry-load.ts b/src/commands/models/list.registry-load.ts index b6b65f7c674..86fcf0498d1 100644 --- a/src/commands/models/list.registry-load.ts +++ b/src/commands/models/list.registry-load.ts @@ -10,7 +10,7 @@ import { modelKey } from "./shared.js"; export async function loadListModelRegistry( cfg: OpenClawConfig, - opts?: { providerFilter?: string; normalizeModels?: boolean }, + opts?: { providerFilter?: string; normalizeModels?: boolean; loadAvailability?: boolean }, ) { const loaded = await loadModelRegistry(cfg, opts); return { diff --git a/src/commands/models/list.registry.ts b/src/commands/models/list.registry.ts index 999b7075b02..53bb4672c9b 100644 --- a/src/commands/models/list.registry.ts +++ b/src/commands/models/list.registry.ts @@ -121,11 +121,14 @@ function loadAvailableModels( export async function loadModelRegistry( cfg: OpenClawConfig, - opts?: { providerFilter?: string; normalizeModels?: boolean }, + opts?: { providerFilter?: string; normalizeModels?: boolean; loadAvailability?: boolean }, ) { const runtimeSuppression = opts?.normalizeModels !== false; const agentDir = resolveOpenClawAgentDir(); - const authStorage = discoverAuthStorage(agentDir, { readOnly: true }); + const authStorage = discoverAuthStorage(agentDir, { + readOnly: true, + skipCredentials: opts?.loadAvailability === false, + }); const registry = discoverModels(authStorage, agentDir, { providerFilter: opts?.providerFilter, normalizeModels: opts?.normalizeModels, @@ -147,19 +150,21 @@ export async function loadModelRegistry( let availableKeys: Set | undefined; let availabilityErrorMessage: string | undefined; - try { - const availableModels = loadAvailableModels(registry, cfg, { runtimeSuppression }); - availableKeys = new Set(availableModels.map((model) => modelKey(model.provider, model.id))); - } catch (err) { - if (!shouldFallbackToAuthHeuristics(err)) { - throw err; - } + if (opts?.loadAvailability !== false) { + try { + const availableModels = loadAvailableModels(registry, cfg, { runtimeSuppression }); + availableKeys = new Set(availableModels.map((model) => modelKey(model.provider, model.id))); + } catch (err) { + if (!shouldFallbackToAuthHeuristics(err)) { + throw err; + } - // Some providers can report model-level availability as unavailable. - // Fall back to provider-level auth heuristics when availability is undefined. - availableKeys = undefined; - if (!availabilityErrorMessage) { - availabilityErrorMessage = formatErrorWithStack(err); + // Some providers can report model-level availability as unavailable. + // Fall back to provider-level auth heuristics when availability is undefined. + availableKeys = undefined; + if (!availabilityErrorMessage) { + availabilityErrorMessage = formatErrorWithStack(err); + } } } return { registry, models, availableKeys, availabilityErrorMessage }; diff --git a/src/commands/models/list.row-sources.ts b/src/commands/models/list.row-sources.ts index bfa5d8aecc9..de5e5170b60 100644 --- a/src/commands/models/list.row-sources.ts +++ b/src/commands/models/list.row-sources.ts @@ -72,6 +72,8 @@ export async function appendAllModelRowSources( models: params.modelRegistry.getAll(), modelRegistry: params.modelRegistry, context: params.context, + resolveWithRegistry: false, + skipSuppression: true, }); } return { requiresRegistryFallback: false }; diff --git a/src/commands/models/list.source-plan.test.ts b/src/commands/models/list.source-plan.test.ts index 22e0ca2d55c..966b553dc96 100644 --- a/src/commands/models/list.source-plan.test.ts +++ b/src/commands/models/list.source-plan.test.ts @@ -113,7 +113,7 @@ describe("planAllModelListSources", () => { expect(mocks.loadProviderIndexCatalogRowsForList).not.toHaveBeenCalled(); }); - it("keeps scoped runtime catalog fallback separate from broad registry loading", async () => { + it("allows scoped runtime catalog plans to fall back to registry rows", async () => { const { planAllModelListSources } = await import("./list.source-plan.js"); await expect( @@ -126,7 +126,7 @@ describe("planAllModelListSources", () => { kind: "provider-runtime-scoped", requiresInitialRegistry: false, skipRuntimeModelSuppression: false, - fallbackToRegistryWhenEmpty: false, + fallbackToRegistryWhenEmpty: true, }); }); diff --git a/src/commands/models/list.source-plan.ts b/src/commands/models/list.source-plan.ts index 293f246c067..2582d5334ca 100644 --- a/src/commands/models/list.source-plan.ts +++ b/src/commands/models/list.source-plan.ts @@ -123,5 +123,6 @@ export async function planAllModelListSources(params: { return createSourcePlan({ kind: "provider-runtime-scoped", + fallbackToRegistryWhenEmpty: true, }); }