diff --git a/extensions/chutes/openclaw.plugin.json b/extensions/chutes/openclaw.plugin.json index 6d7775ed13e..26174f31b3a 100644 --- a/extensions/chutes/openclaw.plugin.json +++ b/extensions/chutes/openclaw.plugin.json @@ -1,7 +1,6 @@ { "id": "chutes", "enabledByDefault": true, - "providerDiscoveryEntry": "./provider-discovery.ts", "providers": ["chutes"], "providerAuthEnvVars": { "chutes": ["CHUTES_API_KEY", "CHUTES_OAUTH_TOKEN"] diff --git a/extensions/chutes/provider-discovery.ts b/extensions/chutes/provider-discovery.ts deleted file mode 100644 index 5587291343e..00000000000 --- a/extensions/chutes/provider-discovery.ts +++ /dev/null @@ -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; diff --git a/extensions/kilocode/openclaw.plugin.json b/extensions/kilocode/openclaw.plugin.json index 01f1a43b21d..ef3d29a20df 100644 --- a/extensions/kilocode/openclaw.plugin.json +++ b/extensions/kilocode/openclaw.plugin.json @@ -1,7 +1,6 @@ { "id": "kilocode", "enabledByDefault": true, - "providerDiscoveryEntry": "./provider-discovery.ts", "providers": ["kilocode"], "providerAuthEnvVars": { "kilocode": ["KILOCODE_API_KEY"] diff --git a/extensions/kilocode/provider-discovery.ts b/extensions/kilocode/provider-discovery.ts deleted file mode 100644 index 806200f7398..00000000000 --- a/extensions/kilocode/provider-discovery.ts +++ /dev/null @@ -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; diff --git a/extensions/vercel-ai-gateway/openclaw.plugin.json b/extensions/vercel-ai-gateway/openclaw.plugin.json index a4b576ffba1..865c7d9765c 100644 --- a/extensions/vercel-ai-gateway/openclaw.plugin.json +++ b/extensions/vercel-ai-gateway/openclaw.plugin.json @@ -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"] diff --git a/extensions/vercel-ai-gateway/provider-discovery.ts b/extensions/vercel-ai-gateway/provider-discovery.ts deleted file mode 100644 index 658646ae3bf..00000000000 --- a/extensions/vercel-ai-gateway/provider-discovery.ts +++ /dev/null @@ -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; diff --git a/src/commands/models/list.provider-catalog.test.ts b/src/commands/models/list.provider-catalog.test.ts index a90376372dd..e0925fd3722 100644 --- a/src/commands/models/list.provider-catalog.test.ts +++ b/src/commands/models/list.provider-catalog.test.ts @@ -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", diff --git a/src/plugins/provider-discovery.runtime.test.ts b/src/plugins/provider-discovery.runtime.test.ts index 4b34bb241cd..c091ea6a167 100644 --- a/src/plugins/provider-discovery.runtime.test.ts +++ b/src/plugins/provider-discovery.runtime.test.ts @@ -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); diff --git a/src/plugins/provider-discovery.runtime.ts b/src/plugins/provider-discovery.runtime.ts index 3325c333638..d45c0d7db8f 100644 --- a/src/plugins/provider-discovery.runtime.ts +++ b/src/plugins/provider-discovery.runtime.ts @@ -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,