fix: keep live catalog providers on registry path

This commit is contained in:
Shakker
2026-04-24 05:12:05 +01:00
committed by Shakker
parent 9941393c7a
commit 2e45218ae8
9 changed files with 52 additions and 65 deletions

View File

@@ -1,7 +1,6 @@
{
"id": "chutes",
"enabledByDefault": true,
"providerDiscoveryEntry": "./provider-discovery.ts",
"providers": ["chutes"],
"providerAuthEnvVars": {
"chutes": ["CHUTES_API_KEY", "CHUTES_OAUTH_TOKEN"]

View File

@@ -1,17 +0,0 @@
import type { ProviderPlugin } from "openclaw/plugin-sdk/provider-model-shared";
import { buildStaticChutesProvider } from "./provider-catalog.js";
export const chutesProviderDiscovery: ProviderPlugin = {
id: "chutes",
label: "Chutes",
docsPath: "/providers/models",
auth: [],
staticCatalog: {
order: "profile",
run: async () => ({
provider: buildStaticChutesProvider(),
}),
},
};
export default chutesProviderDiscovery;

View File

@@ -1,7 +1,6 @@
{
"id": "kilocode",
"enabledByDefault": true,
"providerDiscoveryEntry": "./provider-discovery.ts",
"providers": ["kilocode"],
"providerAuthEnvVars": {
"kilocode": ["KILOCODE_API_KEY"]

View File

@@ -1,17 +0,0 @@
import type { ProviderPlugin } from "openclaw/plugin-sdk/provider-model-shared";
import { buildKilocodeProvider } from "./provider-catalog.js";
export const kilocodeProviderDiscovery: ProviderPlugin = {
id: "kilocode",
label: "Kilo Code",
docsPath: "/providers/models",
auth: [],
staticCatalog: {
order: "simple",
run: async () => ({
provider: buildKilocodeProvider(),
}),
},
};
export default kilocodeProviderDiscovery;

View File

@@ -1,7 +1,6 @@
{
"id": "vercel-ai-gateway",
"enabledByDefault": true,
"providerDiscoveryEntry": "./provider-discovery.ts",
"providers": ["vercel-ai-gateway"],
"providerAuthEnvVars": {
"vercel-ai-gateway": ["AI_GATEWAY_API_KEY"]

View File

@@ -1,17 +0,0 @@
import type { ProviderPlugin } from "openclaw/plugin-sdk/provider-model-shared";
import { buildStaticVercelAiGatewayProvider } from "./provider-catalog.js";
export const vercelAiGatewayProviderDiscovery: ProviderPlugin = {
id: "vercel-ai-gateway",
label: "Vercel AI Gateway",
docsPath: "/providers/models",
auth: [],
staticCatalog: {
order: "simple",
run: async () => ({
provider: buildStaticVercelAiGatewayProvider(),
}),
},
};
export default vercelAiGatewayProviderDiscovery;

View File

@@ -187,6 +187,18 @@ describe("loadProviderCatalogModelsForList", () => {
);
});
it("does not skip registry when a bundled provider has no lightweight static entry", async () => {
providerDiscoveryMocks.resolvePluginDiscoveryProviders.mockResolvedValueOnce([]);
await expect(
hasProviderStaticCatalogForFilter({
cfg: baseParams.cfg,
env: baseParams.env,
providerFilter: "chutes",
}),
).resolves.toBe(false);
});
it("does not skip registry for non-bundled static catalog owners", async () => {
providerDiscoveryMocks.resolveOwningPluginIdsForProvider.mockReturnValueOnce([
"workspace-static-provider",

View File

@@ -84,6 +84,26 @@ describe("resolvePluginDiscoveryProvidersRuntime", () => {
);
});
it("falls back to full provider plugins for mixed live and static-only entries", () => {
const fullProviders = [
createProvider({ id: "codex", mode: "catalog" }),
createProvider({ id: "deepseek", mode: "catalog" }),
];
mocks.resolveDiscoveredProviderPluginIds.mockReturnValue(["codex", "deepseek"]);
mocks.loadPluginManifestRegistry.mockReturnValue({
plugins: [createManifestPlugin("codex"), createManifestPlugin("deepseek")],
diagnostics: [],
});
mocks.loadSource.mockImplementation((modulePath: string) =>
modulePath.includes("/codex/")
? createProvider({ id: "codex", mode: "catalog" })
: createProvider({ id: "deepseek", mode: "static" }),
);
mocks.resolvePluginProviders.mockReturnValue(fullProviders);
expect(resolvePluginDiscoveryProvidersRuntime({})).toEqual(fullProviders);
});
it("returns static-only discovery entries for callers that explicitly request them", () => {
const staticProvider = createProvider({ id: "deepseek", mode: "static" });
mocks.loadSource.mockReturnValue(staticProvider);

View File

@@ -14,6 +14,11 @@ type ProviderDiscoveryModule =
provider?: ProviderPlugin;
};
type ProviderDiscoveryEntryResult = {
providers: ProviderPlugin[];
complete: boolean;
};
function normalizeDiscoveryModule(value: ProviderDiscoveryModule): ProviderPlugin[] {
const resolved =
value && typeof value === "object" && "default" in value && value.default !== undefined
@@ -51,17 +56,18 @@ function resolveProviderDiscoveryEntryPlugins(params: {
includeUntrustedWorkspacePlugins?: boolean;
requireCompleteDiscoveryEntryCoverage?: boolean;
discoveryEntriesOnly?: boolean;
}): ProviderPlugin[] {
}): ProviderDiscoveryEntryResult {
const pluginIds = resolveDiscoveredProviderPluginIds(params);
const pluginIdSet = new Set(pluginIds);
const records = loadPluginManifestRegistry(params).plugins.filter(
(plugin) => plugin.providerDiscoverySource && pluginIdSet.has(plugin.id),
);
if (records.length === 0) {
return [];
return { providers: [], complete: false };
}
if (params.requireCompleteDiscoveryEntryCoverage && records.length < pluginIdSet.size) {
return [];
const complete = records.length === pluginIdSet.size;
if (params.requireCompleteDiscoveryEntryCoverage && !complete) {
return { providers: [], complete: false };
}
const loadSource = createPluginSourceLoader();
const providers: ProviderPlugin[] = [];
@@ -76,10 +82,10 @@ function resolveProviderDiscoveryEntryPlugins(params: {
} catch {
// Discovery fast path is optional. Fall back to the full plugin loader
// below so existing plugin diagnostics/load behavior remains canonical.
return [];
return { providers: [], complete: false };
}
}
return providers;
return { providers, complete };
}
export function resolvePluginDiscoveryProvidersRuntime(params: {
@@ -91,13 +97,16 @@ export function resolvePluginDiscoveryProvidersRuntime(params: {
requireCompleteDiscoveryEntryCoverage?: boolean;
discoveryEntriesOnly?: boolean;
}): ProviderPlugin[] {
const entryProviders = resolveProviderDiscoveryEntryPlugins(params);
const entryResult = resolveProviderDiscoveryEntryPlugins(params);
if (params.discoveryEntriesOnly === true) {
return entryProviders;
return entryResult.providers;
}
const liveEntryProviders = entryProviders.filter(hasLiveProviderDiscoveryHook);
if (liveEntryProviders.length > 0) {
return liveEntryProviders;
if (
entryResult.complete &&
entryResult.providers.length > 0 &&
entryResult.providers.every(hasLiveProviderDiscoveryHook)
) {
return entryResult.providers;
}
return resolvePluginProviders({
...params,