From 4737a86071e35c409b0af9d7849bad05d7cb387f Mon Sep 17 00:00:00 2001 From: Shakker Date: Fri, 24 Apr 2026 04:47:14 +0100 Subject: [PATCH] fix: preserve provider filtered catalog correctness --- extensions/arcee/provider-discovery.ts | 38 ++++++------------ .../models/list.provider-catalog.test.ts | 39 ++++++++++++++++++- src/commands/models/list.provider-catalog.ts | 35 ++++++++++++++--- src/commands/models/list.row-sources.ts | 10 ++--- src/commands/models/list.rows.ts | 5 +-- src/plugins/provider-discovery.runtime.ts | 5 +++ src/plugins/provider-discovery.ts | 1 + 7 files changed, 92 insertions(+), 41 deletions(-) diff --git a/extensions/arcee/provider-discovery.ts b/extensions/arcee/provider-discovery.ts index 4b0543d0d6b..009ead49bda 100644 --- a/extensions/arcee/provider-discovery.ts +++ b/extensions/arcee/provider-discovery.ts @@ -1,31 +1,17 @@ import type { ProviderPlugin } from "openclaw/plugin-sdk/provider-model-shared"; -import { buildArceeOpenRouterProvider, buildArceeProvider } from "./provider-catalog.js"; +import { buildArceeProvider } from "./provider-catalog.js"; -export const arceeProviderDiscovery: ProviderPlugin[] = [ - { - id: "arcee", - label: "Arcee AI", - docsPath: "/providers/models", - auth: [], - staticCatalog: { - order: "simple", - run: async () => ({ - provider: buildArceeProvider(), - }), - }, +export const arceeProviderDiscovery: ProviderPlugin = { + id: "arcee", + label: "Arcee AI", + docsPath: "/providers/models", + auth: [], + staticCatalog: { + order: "simple", + run: async () => ({ + provider: buildArceeProvider(), + }), }, - { - id: "arcee-openrouter", - label: "Arcee AI via OpenRouter", - docsPath: "/providers/models", - auth: [], - staticCatalog: { - order: "simple", - run: async () => ({ - provider: buildArceeOpenRouterProvider(), - }), - }, - }, -]; +}; export default arceeProviderDiscovery; diff --git a/src/commands/models/list.provider-catalog.test.ts b/src/commands/models/list.provider-catalog.test.ts index 889a3be0686..2a3487d38af 100644 --- a/src/commands/models/list.provider-catalog.test.ts +++ b/src/commands/models/list.provider-catalog.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { + hasProviderStaticCatalogForFilter, loadProviderCatalogModelsForList, resolveProviderCatalogPluginIdsForFilter, } from "./list.provider-catalog.js"; @@ -87,6 +88,18 @@ const openaiProvider = { }, }; +const catalogOnlyProvider = { + id: "ollama", + pluginId: "ollama", + label: "Ollama", + auth: [], + catalog: { + run: async () => ({ + provider: { baseUrl: "http://127.0.0.1:11434", models: [] }, + }), + }, +}; + const defaultProviders = [chutesProvider, moonshotProvider, openaiProvider]; describe("loadProviderCatalogModelsForList", () => { @@ -96,10 +109,13 @@ describe("loadProviderCatalogModelsForList", () => { "chutes", "moonshot", "openai", + "ollama", ]); providerDiscoveryMocks.resolveOwningPluginIdsForProvider.mockImplementation( ({ provider }: { provider: string }) => - defaultProviders.some((entry) => entry.id === provider) ? [provider] : undefined, + [...defaultProviders, catalogOnlyProvider].some((entry) => entry.id === provider) + ? [provider] + : undefined, ); providerDiscoveryMocks.resolveProviderContractPluginIdsForProviderAlias.mockImplementation( (provider: string) => (provider === "azure-openai-responses" ? ["openai"] : undefined), @@ -146,6 +162,27 @@ describe("loadProviderCatalogModelsForList", () => { expect.objectContaining({ onlyPluginIds: ["moonshot"], requireCompleteDiscoveryEntryCoverage: true, + discoveryEntriesOnly: true, + }), + ); + }); + + it("only skips registry for providers with actual static catalogs", async () => { + providerDiscoveryMocks.resolvePluginDiscoveryProviders.mockResolvedValue([catalogOnlyProvider]); + + await expect( + hasProviderStaticCatalogForFilter({ + cfg: baseParams.cfg, + env: baseParams.env, + providerFilter: "ollama", + }), + ).resolves.toBe(false); + + expect(providerDiscoveryMocks.resolvePluginDiscoveryProviders).toHaveBeenCalledWith( + expect.objectContaining({ + onlyPluginIds: ["ollama"], + requireCompleteDiscoveryEntryCoverage: true, + discoveryEntriesOnly: true, }), ); }); diff --git a/src/commands/models/list.provider-catalog.ts b/src/commands/models/list.provider-catalog.ts index 8da1e6dc31f..d06cc097442 100644 --- a/src/commands/models/list.provider-catalog.ts +++ b/src/commands/models/list.provider-catalog.ts @@ -4,7 +4,6 @@ import type { ModelProviderConfig } from "../../config/types.models.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { formatErrorMessage } from "../../infra/errors.js"; import { createSubsystemLogger } from "../../logging/subsystem.js"; -import { loadPluginManifestRegistry } from "../../plugins/manifest-registry.js"; import { groupPluginDiscoveryProvidersByOrder, normalizePluginDiscoveryResult, @@ -15,11 +14,23 @@ import { resolveBundledProviderCompatPluginIds, resolveOwningPluginIdsForProvider, } from "../../plugins/providers.js"; +import type { ProviderPlugin } from "../../plugins/types.js"; const DISCOVERY_ORDERS = ["simple", "profile", "paired", "late"] as const; const SELF_HOSTED_DISCOVERY_PROVIDER_IDS = new Set(["lmstudio", "ollama", "sglang", "vllm"]); const log = createSubsystemLogger("models/list-provider-catalog"); +function providerMatchesFilter(params: { + provider: Pick; + providerFilter: string; +}): boolean { + return [ + params.provider.id, + ...(params.provider.aliases ?? []), + ...(params.provider.hookAliases ?? []), + ].some((providerId) => normalizeProviderId(providerId) === params.providerFilter); +} + export async function resolveProviderCatalogPluginIdsForFilter(params: { cfg: OpenClawConfig; env?: NodeJS.ProcessEnv; @@ -51,13 +62,26 @@ export async function hasProviderStaticCatalogForFilter(params: { env?: NodeJS.ProcessEnv; providerFilter: string; }): Promise { + const providerFilter = normalizeProviderId(params.providerFilter); + if (!providerFilter) { + return false; + } const pluginIds = await resolveProviderCatalogPluginIdsForFilter(params); if (!pluginIds || pluginIds.length === 0) { return false; } - const pluginIdSet = new Set(pluginIds); - return loadPluginManifestRegistry({ config: params.cfg, env: params.env }).plugins.some( - (plugin) => pluginIdSet.has(plugin.id) && typeof plugin.providerDiscoverySource === "string", + const providers = await resolvePluginDiscoveryProviders({ + config: params.cfg, + env: params.env, + onlyPluginIds: pluginIds, + includeUntrustedWorkspacePlugins: false, + requireCompleteDiscoveryEntryCoverage: true, + discoveryEntriesOnly: true, + }); + return providers.some( + (provider) => + typeof provider.staticCatalog?.run === "function" && + providerMatchesFilter({ provider, providerFilter }), ); } @@ -71,7 +95,7 @@ function modelFromProviderCatalog(params: { name: params.model.name || params.model.id, provider: params.provider, api: params.model.api ?? params.providerConfig.api ?? "openai-responses", - baseUrl: params.providerConfig.baseUrl, + baseUrl: params.model.baseUrl ?? params.providerConfig.baseUrl, reasoning: params.model.reasoning, input: params.model.input ?? ["text"], cost: params.model.cost, @@ -122,6 +146,7 @@ export async function loadProviderCatalogModelsForList(params: { onlyPluginIds: scopedPluginIds, includeUntrustedWorkspacePlugins: false, requireCompleteDiscoveryEntryCoverage: params.staticOnly === true, + discoveryEntriesOnly: params.staticOnly === true, }) ).filter( (provider) => diff --git a/src/commands/models/list.row-sources.ts b/src/commands/models/list.row-sources.ts index 9389748e9d8..788ee9930cc 100644 --- a/src/commands/models/list.row-sources.ts +++ b/src/commands/models/list.row-sources.ts @@ -33,6 +33,11 @@ export function modelRowSourcesRequireRegistry(params: { export async function appendAllModelRowSources(params: AllModelRowSources): Promise { if (params.context.filter.provider && params.useProviderCatalogFastPath) { let seenKeys = new Set(); + appendConfiguredProviderRows({ + rows: params.rows, + context: params.context, + seenKeys, + }); const catalogRows = await appendProviderCatalogRows({ rows: params.rows, context: params.context, @@ -46,11 +51,6 @@ export async function appendAllModelRowSources(params: AllModelRowSources): Prom context: params.context, }); } - appendConfiguredProviderRows({ - rows: params.rows, - context: params.context, - seenKeys, - }); return; } diff --git a/src/commands/models/list.rows.ts b/src/commands/models/list.rows.ts index dba142e5642..536a02bbea9 100644 --- a/src/commands/models/list.rows.ts +++ b/src/commands/models/list.rows.ts @@ -108,10 +108,7 @@ function shouldListConfiguredProviderModel(params: { providerConfig: Partial; model: Partial; }): boolean { - return ( - params.providerConfig.apiKey !== undefined && - (params.providerConfig.api !== undefined || params.model.api !== undefined) - ); + return params.providerConfig.api !== undefined || params.model.api !== undefined; } export function appendDiscoveredRows(params: { diff --git a/src/plugins/provider-discovery.runtime.ts b/src/plugins/provider-discovery.runtime.ts index 0adbf77fec7..537fd468649 100644 --- a/src/plugins/provider-discovery.runtime.ts +++ b/src/plugins/provider-discovery.runtime.ts @@ -44,6 +44,7 @@ function resolveProviderDiscoveryEntryPlugins(params: { onlyPluginIds?: string[]; includeUntrustedWorkspacePlugins?: boolean; requireCompleteDiscoveryEntryCoverage?: boolean; + discoveryEntriesOnly?: boolean; }): ProviderPlugin[] { const pluginIds = resolveDiscoveredProviderPluginIds(params); const pluginIdSet = new Set(pluginIds); @@ -82,11 +83,15 @@ export function resolvePluginDiscoveryProvidersRuntime(params: { onlyPluginIds?: string[]; includeUntrustedWorkspacePlugins?: boolean; requireCompleteDiscoveryEntryCoverage?: boolean; + discoveryEntriesOnly?: boolean; }): ProviderPlugin[] { const entryProviders = resolveProviderDiscoveryEntryPlugins(params); if (entryProviders.length > 0) { return entryProviders; } + if (params.discoveryEntriesOnly === true) { + return []; + } return resolvePluginProviders({ ...params, bundledProviderAllowlistCompat: true, diff --git a/src/plugins/provider-discovery.ts b/src/plugins/provider-discovery.ts index 8f632a3fe4a..45c893c5c0a 100644 --- a/src/plugins/provider-discovery.ts +++ b/src/plugins/provider-discovery.ts @@ -35,6 +35,7 @@ export async function resolvePluginDiscoveryProviders(params: { onlyPluginIds?: string[]; includeUntrustedWorkspacePlugins?: boolean; requireCompleteDiscoveryEntryCoverage?: boolean; + discoveryEntriesOnly?: boolean; }): Promise { return (await loadProviderRuntime()) .resolvePluginDiscoveryProvidersRuntime(params)