fix: preserve provider filtered catalog correctness

This commit is contained in:
Shakker
2026-04-24 04:47:14 +01:00
committed by Shakker
parent 3254e961e9
commit 4737a86071
7 changed files with 92 additions and 41 deletions

View File

@@ -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;

View File

@@ -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,
}),
);
});

View File

@@ -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<ProviderPlugin, "id" | "aliases" | "hookAliases">;
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<boolean> {
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) =>

View File

@@ -33,6 +33,11 @@ export function modelRowSourcesRequireRegistry(params: {
export async function appendAllModelRowSources(params: AllModelRowSources): Promise<void> {
if (params.context.filter.provider && params.useProviderCatalogFastPath) {
let seenKeys = new Set<string>();
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;
}

View File

@@ -108,10 +108,7 @@ function shouldListConfiguredProviderModel(params: {
providerConfig: Partial<ModelProviderConfig>;
model: Partial<ModelDefinitionConfig>;
}): 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: {

View File

@@ -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,

View File

@@ -35,6 +35,7 @@ export async function resolvePluginDiscoveryProviders(params: {
onlyPluginIds?: string[];
includeUntrustedWorkspacePlugins?: boolean;
requireCompleteDiscoveryEntryCoverage?: boolean;
discoveryEntriesOnly?: boolean;
}): Promise<ProviderPlugin[]> {
return (await loadProviderRuntime())
.resolvePluginDiscoveryProvidersRuntime(params)