feat(models): read provider owners from installed index

This commit is contained in:
Vincent Koc
2026-04-24 23:16:26 -07:00
parent ea3e390346
commit 0b2bc8c5f6
2 changed files with 103 additions and 0 deletions

View File

@@ -6,12 +6,19 @@ import {
} from "./list.provider-catalog.js";
const providerDiscoveryMocks = vi.hoisted(() => ({
loadInstalledPluginIndex: vi.fn(),
resolveBundledProviderCompatPluginIds: vi.fn(),
resolveInstalledPluginContributions: vi.fn(),
resolveOwningPluginIdsForProvider: vi.fn(),
resolvePluginDiscoveryProviders: vi.fn(),
resolveProviderContractPluginIdsForProviderAlias: vi.fn(),
}));
vi.mock("../../plugins/installed-plugin-index.js", () => ({
loadInstalledPluginIndex: providerDiscoveryMocks.loadInstalledPluginIndex,
resolveInstalledPluginContributions: providerDiscoveryMocks.resolveInstalledPluginContributions,
}));
vi.mock("../../plugins/providers.js", () => ({
resolveBundledProviderCompatPluginIds:
providerDiscoveryMocks.resolveBundledProviderCompatPluginIds,
@@ -102,9 +109,34 @@ const catalogOnlyProvider = {
const defaultProviders = [chutesProvider, moonshotProvider, openaiProvider];
function createContributionMaps(params: {
providers?: ReadonlyMap<string, readonly string[]>;
cliBackends?: ReadonlyMap<string, readonly string[]>;
}) {
return {
providers: params.providers ?? new Map(),
channels: new Map(),
channelConfigs: new Map(),
setupProviders: new Map(),
cliBackends: params.cliBackends ?? new Map(),
modelCatalogProviders: new Map(),
commandAliases: new Map(),
contracts: new Map(),
};
}
describe("loadProviderCatalogModelsForList", () => {
beforeEach(() => {
vi.clearAllMocks();
providerDiscoveryMocks.loadInstalledPluginIndex.mockReturnValue({
plugins: [],
diagnostics: [],
});
providerDiscoveryMocks.resolveInstalledPluginContributions.mockReturnValue(
createContributionMaps({
providers: new Map(defaultProviders.map((provider) => [provider.id, [provider.pluginId]])),
}),
);
providerDiscoveryMocks.resolveBundledProviderCompatPluginIds.mockReturnValue([
"chutes",
"moonshot",
@@ -167,6 +199,22 @@ describe("loadProviderCatalogModelsForList", () => {
);
});
it("resolves provider owners from the installed plugin index before manifest fallback", async () => {
await expect(
resolveProviderCatalogPluginIdsForFilter({
cfg: baseParams.cfg,
env: baseParams.env,
providerFilter: "moonshot",
}),
).resolves.toEqual(["moonshot"]);
expect(providerDiscoveryMocks.loadInstalledPluginIndex).toHaveBeenCalledWith({
config: baseParams.cfg,
env: baseParams.env,
});
expect(providerDiscoveryMocks.resolveOwningPluginIdsForProvider).not.toHaveBeenCalled();
});
it("returns an empty catalog when a static provider catalog throws", async () => {
providerDiscoveryMocks.resolvePluginDiscoveryProviders.mockResolvedValueOnce([
{
@@ -224,6 +272,9 @@ describe("loadProviderCatalogModelsForList", () => {
});
it("does not skip registry for non-bundled static catalog owners", async () => {
providerDiscoveryMocks.resolveInstalledPluginContributions.mockReturnValueOnce(
createContributionMaps({}),
);
providerDiscoveryMocks.resolveOwningPluginIdsForProvider.mockReturnValueOnce([
"workspace-static-provider",
]);
@@ -241,6 +292,10 @@ describe("loadProviderCatalogModelsForList", () => {
});
it("recognizes bundled provider hook aliases before the unknown-provider short-circuit", async () => {
providerDiscoveryMocks.resolveInstalledPluginContributions.mockReturnValueOnce(
createContributionMaps({}),
);
await expect(
resolveProviderCatalogPluginIdsForFilter({
cfg: baseParams.cfg,
@@ -291,6 +346,10 @@ describe("loadProviderCatalogModelsForList", () => {
});
it("keeps unknown provider filters eligible for early empty results", async () => {
providerDiscoveryMocks.resolveInstalledPluginContributions.mockReturnValueOnce(
createContributionMaps({}),
);
await expect(
resolveProviderCatalogPluginIdsForFilter({
cfg: baseParams.cfg,

View File

@@ -4,6 +4,10 @@ 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 {
loadInstalledPluginIndex,
resolveInstalledPluginContributions,
} from "../../plugins/installed-plugin-index.js";
import {
groupPluginDiscoveryProvidersByOrder,
normalizePluginDiscoveryResult,
@@ -31,6 +35,38 @@ function providerMatchesFilter(params: {
].some((providerId) => normalizeProviderId(providerId) === params.providerFilter);
}
function collectMatchingContributionPluginIds(
contributions: ReadonlyMap<string, readonly string[]>,
providerFilter: string,
): string[] {
const pluginIds: string[] = [];
for (const [contributionId, ownerPluginIds] of contributions) {
if (normalizeProviderId(contributionId) === providerFilter) {
pluginIds.push(...ownerPluginIds);
}
}
return [...new Set(pluginIds)].toSorted((left, right) => left.localeCompare(right));
}
function resolveInstalledIndexPluginIdsForProviderFilter(params: {
cfg: OpenClawConfig;
env?: NodeJS.ProcessEnv;
providerFilter: string;
}): string[] | undefined {
const index = loadInstalledPluginIndex({
config: params.cfg,
env: params.env,
});
const contributions = resolveInstalledPluginContributions(index);
const pluginIds = [
...collectMatchingContributionPluginIds(contributions.providers, params.providerFilter),
...collectMatchingContributionPluginIds(contributions.cliBackends, params.providerFilter),
];
return pluginIds.length > 0
? [...new Set(pluginIds)].toSorted((left, right) => left.localeCompare(right))
: undefined;
}
export async function resolveProviderCatalogPluginIdsForFilter(params: {
cfg: OpenClawConfig;
env?: NodeJS.ProcessEnv;
@@ -40,6 +76,14 @@ export async function resolveProviderCatalogPluginIdsForFilter(params: {
if (!providerFilter) {
return undefined;
}
const installedIndexPluginIds = resolveInstalledIndexPluginIdsForProviderFilter({
cfg: params.cfg,
env: params.env,
providerFilter,
});
if (installedIndexPluginIds) {
return installedIndexPluginIds;
}
const manifestPluginIds = resolveOwningPluginIdsForProvider({
provider: providerFilter,
config: params.cfg,