Skip to content

Commit 4737a86

Browse files
committed
fix: preserve provider filtered catalog correctness
1 parent 3254e96 commit 4737a86

7 files changed

Lines changed: 92 additions & 41 deletions

File tree

Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,17 @@
11
import type { ProviderPlugin } from "openclaw/plugin-sdk/provider-model-shared";
2-
import { buildArceeOpenRouterProvider, buildArceeProvider } from "./provider-catalog.js";
2+
import { buildArceeProvider } from "./provider-catalog.js";
33

4-
export const arceeProviderDiscovery: ProviderPlugin[] = [
5-
{
6-
id: "arcee",
7-
label: "Arcee AI",
8-
docsPath: "/providers/models",
9-
auth: [],
10-
staticCatalog: {
11-
order: "simple",
12-
run: async () => ({
13-
provider: buildArceeProvider(),
14-
}),
15-
},
4+
export const arceeProviderDiscovery: ProviderPlugin = {
5+
id: "arcee",
6+
label: "Arcee AI",
7+
docsPath: "/providers/models",
8+
auth: [],
9+
staticCatalog: {
10+
order: "simple",
11+
run: async () => ({
12+
provider: buildArceeProvider(),
13+
}),
1614
},
17-
{
18-
id: "arcee-openrouter",
19-
label: "Arcee AI via OpenRouter",
20-
docsPath: "/providers/models",
21-
auth: [],
22-
staticCatalog: {
23-
order: "simple",
24-
run: async () => ({
25-
provider: buildArceeOpenRouterProvider(),
26-
}),
27-
},
28-
},
29-
];
15+
};
3016

3117
export default arceeProviderDiscovery;

src/commands/models/list.provider-catalog.test.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { beforeEach, describe, expect, it, vi } from "vitest";
22
import {
3+
hasProviderStaticCatalogForFilter,
34
loadProviderCatalogModelsForList,
45
resolveProviderCatalogPluginIdsForFilter,
56
} from "./list.provider-catalog.js";
@@ -87,6 +88,18 @@ const openaiProvider = {
8788
},
8889
};
8990

91+
const catalogOnlyProvider = {
92+
id: "ollama",
93+
pluginId: "ollama",
94+
label: "Ollama",
95+
auth: [],
96+
catalog: {
97+
run: async () => ({
98+
provider: { baseUrl: "http://127.0.0.1:11434", models: [] },
99+
}),
100+
},
101+
};
102+
90103
const defaultProviders = [chutesProvider, moonshotProvider, openaiProvider];
91104

92105
describe("loadProviderCatalogModelsForList", () => {
@@ -96,10 +109,13 @@ describe("loadProviderCatalogModelsForList", () => {
96109
"chutes",
97110
"moonshot",
98111
"openai",
112+
"ollama",
99113
]);
100114
providerDiscoveryMocks.resolveOwningPluginIdsForProvider.mockImplementation(
101115
({ provider }: { provider: string }) =>
102-
defaultProviders.some((entry) => entry.id === provider) ? [provider] : undefined,
116+
[...defaultProviders, catalogOnlyProvider].some((entry) => entry.id === provider)
117+
? [provider]
118+
: undefined,
103119
);
104120
providerDiscoveryMocks.resolveProviderContractPluginIdsForProviderAlias.mockImplementation(
105121
(provider: string) => (provider === "azure-openai-responses" ? ["openai"] : undefined),
@@ -146,6 +162,27 @@ describe("loadProviderCatalogModelsForList", () => {
146162
expect.objectContaining({
147163
onlyPluginIds: ["moonshot"],
148164
requireCompleteDiscoveryEntryCoverage: true,
165+
discoveryEntriesOnly: true,
166+
}),
167+
);
168+
});
169+
170+
it("only skips registry for providers with actual static catalogs", async () => {
171+
providerDiscoveryMocks.resolvePluginDiscoveryProviders.mockResolvedValue([catalogOnlyProvider]);
172+
173+
await expect(
174+
hasProviderStaticCatalogForFilter({
175+
cfg: baseParams.cfg,
176+
env: baseParams.env,
177+
providerFilter: "ollama",
178+
}),
179+
).resolves.toBe(false);
180+
181+
expect(providerDiscoveryMocks.resolvePluginDiscoveryProviders).toHaveBeenCalledWith(
182+
expect.objectContaining({
183+
onlyPluginIds: ["ollama"],
184+
requireCompleteDiscoveryEntryCoverage: true,
185+
discoveryEntriesOnly: true,
149186
}),
150187
);
151188
});

src/commands/models/list.provider-catalog.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import type { ModelProviderConfig } from "../../config/types.models.js";
44
import type { OpenClawConfig } from "../../config/types.openclaw.js";
55
import { formatErrorMessage } from "../../infra/errors.js";
66
import { createSubsystemLogger } from "../../logging/subsystem.js";
7-
import { loadPluginManifestRegistry } from "../../plugins/manifest-registry.js";
87
import {
98
groupPluginDiscoveryProvidersByOrder,
109
normalizePluginDiscoveryResult,
@@ -15,11 +14,23 @@ import {
1514
resolveBundledProviderCompatPluginIds,
1615
resolveOwningPluginIdsForProvider,
1716
} from "../../plugins/providers.js";
17+
import type { ProviderPlugin } from "../../plugins/types.js";
1818

1919
const DISCOVERY_ORDERS = ["simple", "profile", "paired", "late"] as const;
2020
const SELF_HOSTED_DISCOVERY_PROVIDER_IDS = new Set(["lmstudio", "ollama", "sglang", "vllm"]);
2121
const log = createSubsystemLogger("models/list-provider-catalog");
2222

23+
function providerMatchesFilter(params: {
24+
provider: Pick<ProviderPlugin, "id" | "aliases" | "hookAliases">;
25+
providerFilter: string;
26+
}): boolean {
27+
return [
28+
params.provider.id,
29+
...(params.provider.aliases ?? []),
30+
...(params.provider.hookAliases ?? []),
31+
].some((providerId) => normalizeProviderId(providerId) === params.providerFilter);
32+
}
33+
2334
export async function resolveProviderCatalogPluginIdsForFilter(params: {
2435
cfg: OpenClawConfig;
2536
env?: NodeJS.ProcessEnv;
@@ -51,13 +62,26 @@ export async function hasProviderStaticCatalogForFilter(params: {
5162
env?: NodeJS.ProcessEnv;
5263
providerFilter: string;
5364
}): Promise<boolean> {
65+
const providerFilter = normalizeProviderId(params.providerFilter);
66+
if (!providerFilter) {
67+
return false;
68+
}
5469
const pluginIds = await resolveProviderCatalogPluginIdsForFilter(params);
5570
if (!pluginIds || pluginIds.length === 0) {
5671
return false;
5772
}
58-
const pluginIdSet = new Set(pluginIds);
59-
return loadPluginManifestRegistry({ config: params.cfg, env: params.env }).plugins.some(
60-
(plugin) => pluginIdSet.has(plugin.id) && typeof plugin.providerDiscoverySource === "string",
73+
const providers = await resolvePluginDiscoveryProviders({
74+
config: params.cfg,
75+
env: params.env,
76+
onlyPluginIds: pluginIds,
77+
includeUntrustedWorkspacePlugins: false,
78+
requireCompleteDiscoveryEntryCoverage: true,
79+
discoveryEntriesOnly: true,
80+
});
81+
return providers.some(
82+
(provider) =>
83+
typeof provider.staticCatalog?.run === "function" &&
84+
providerMatchesFilter({ provider, providerFilter }),
6185
);
6286
}
6387

@@ -71,7 +95,7 @@ function modelFromProviderCatalog(params: {
7195
name: params.model.name || params.model.id,
7296
provider: params.provider,
7397
api: params.model.api ?? params.providerConfig.api ?? "openai-responses",
74-
baseUrl: params.providerConfig.baseUrl,
98+
baseUrl: params.model.baseUrl ?? params.providerConfig.baseUrl,
7599
reasoning: params.model.reasoning,
76100
input: params.model.input ?? ["text"],
77101
cost: params.model.cost,
@@ -122,6 +146,7 @@ export async function loadProviderCatalogModelsForList(params: {
122146
onlyPluginIds: scopedPluginIds,
123147
includeUntrustedWorkspacePlugins: false,
124148
requireCompleteDiscoveryEntryCoverage: params.staticOnly === true,
149+
discoveryEntriesOnly: params.staticOnly === true,
125150
})
126151
).filter(
127152
(provider) =>

src/commands/models/list.row-sources.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ export function modelRowSourcesRequireRegistry(params: {
3333
export async function appendAllModelRowSources(params: AllModelRowSources): Promise<void> {
3434
if (params.context.filter.provider && params.useProviderCatalogFastPath) {
3535
let seenKeys = new Set<string>();
36+
appendConfiguredProviderRows({
37+
rows: params.rows,
38+
context: params.context,
39+
seenKeys,
40+
});
3641
const catalogRows = await appendProviderCatalogRows({
3742
rows: params.rows,
3843
context: params.context,
@@ -46,11 +51,6 @@ export async function appendAllModelRowSources(params: AllModelRowSources): Prom
4651
context: params.context,
4752
});
4853
}
49-
appendConfiguredProviderRows({
50-
rows: params.rows,
51-
context: params.context,
52-
seenKeys,
53-
});
5454
return;
5555
}
5656

src/commands/models/list.rows.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,7 @@ function shouldListConfiguredProviderModel(params: {
108108
providerConfig: Partial<ModelProviderConfig>;
109109
model: Partial<ModelDefinitionConfig>;
110110
}): boolean {
111-
return (
112-
params.providerConfig.apiKey !== undefined &&
113-
(params.providerConfig.api !== undefined || params.model.api !== undefined)
114-
);
111+
return params.providerConfig.api !== undefined || params.model.api !== undefined;
115112
}
116113

117114
export function appendDiscoveredRows(params: {

src/plugins/provider-discovery.runtime.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ function resolveProviderDiscoveryEntryPlugins(params: {
4444
onlyPluginIds?: string[];
4545
includeUntrustedWorkspacePlugins?: boolean;
4646
requireCompleteDiscoveryEntryCoverage?: boolean;
47+
discoveryEntriesOnly?: boolean;
4748
}): ProviderPlugin[] {
4849
const pluginIds = resolveDiscoveredProviderPluginIds(params);
4950
const pluginIdSet = new Set(pluginIds);
@@ -82,11 +83,15 @@ export function resolvePluginDiscoveryProvidersRuntime(params: {
8283
onlyPluginIds?: string[];
8384
includeUntrustedWorkspacePlugins?: boolean;
8485
requireCompleteDiscoveryEntryCoverage?: boolean;
86+
discoveryEntriesOnly?: boolean;
8587
}): ProviderPlugin[] {
8688
const entryProviders = resolveProviderDiscoveryEntryPlugins(params);
8789
if (entryProviders.length > 0) {
8890
return entryProviders;
8991
}
92+
if (params.discoveryEntriesOnly === true) {
93+
return [];
94+
}
9095
return resolvePluginProviders({
9196
...params,
9297
bundledProviderAllowlistCompat: true,

src/plugins/provider-discovery.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export async function resolvePluginDiscoveryProviders(params: {
3535
onlyPluginIds?: string[];
3636
includeUntrustedWorkspacePlugins?: boolean;
3737
requireCompleteDiscoveryEntryCoverage?: boolean;
38+
discoveryEntriesOnly?: boolean;
3839
}): Promise<ProviderPlugin[]> {
3940
return (await loadProviderRuntime())
4041
.resolvePluginDiscoveryProvidersRuntime(params)

0 commit comments

Comments
 (0)