From 20c7a98fb8b34bcc71caee029ff5d27ab82cc7a0 Mon Sep 17 00:00:00 2001 From: brokemac79 Date: Wed, 29 Apr 2026 07:52:32 +0100 Subject: [PATCH] fix(plugins): keep provider discovery metadata-only Fix startup and per-turn provider registry hot paths by keeping primary-model startup discovery on metadata-only provider entries and by keeping capability provider fallback loads scoped to manifest-derived owners, including explicit empty scopes when no bundled owner exists. Evidence: - Reproduces the reported code paths from #73729, #73835, and #73793: startup prewarm was able to enter provider/model discovery that loaded plugin runtime, and capability lookups could bypass active registry reuse or broaden fallback registry loads. - Fix threads providerDiscoveryEntriesOnly through models-config planning into plugin discovery. - Fix reuses active non-memory/non-speech capability providers even with explicit plugins.entries. - Fix keeps fallback registry loads scoped with onlyPluginIds, including [] for no-owner media capability checks. - Local targeted tests passed for gateway startup, models config, provider discovery, capability providers, and web provider runtimes. - Testbox pnpm check:changed passed. - Testbox pnpm build passed. - GitHub CI required checks passed on e5e6fe1d52d8224a3979359feee402e0bd836d5d. Fixes #73729. Fixes #73835. Fixes #73793. Supersedes #73794. --- CHANGELOG.md | 1 + ...els-config.applies-config-env-vars.test.ts | 5 + src/agents/models-config.plan.ts | 7 ++ ...providers.implicit.discovery-scope.test.ts | 16 +++ .../models-config.providers.implicit.ts | 2 + src/agents/models-config.ts | 12 ++ .../server-startup-post-attach.test.ts | 10 ++ src/gateway/server-startup-post-attach.ts | 11 ++ src/gateway/server-startup.test.ts | 9 ++ .../capability-provider-runtime.test.ts | 118 +++++++++++++++--- src/plugins/capability-provider-runtime.ts | 4 +- .../provider-discovery.runtime.test.ts | 11 ++ 12 files changed, 189 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a27813b9fd6..b44712e3620 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ Docs: https://docs.openclaw.ai - iMessage: normalize known leading attributedBody corruption markers on sent-message echo text keys so delayed reflected echoes with U+FFFD/U+FFFE/U+FFFF/FEFF prefixes are dropped without collapsing interior text. Fixes #59973; carries forward #59980 and #62191. Thanks @neeravmakwana and @maguilar631697. - Security/audit: recognize dangerous node command IDs as valid `gateway.nodes.denyCommands` entries, so audit only warns on real typos or unsupported patterns. (#56923) Thanks @chziyue. - Telegram/exec approvals: stop treating general Telegram chat allowlists and `defaultTo` routes as native exec approvers; Telegram now uses explicit `execApprovals.approvers` or owner identity from `commands.ownerAllowFrom`, matching the first-pairing owner bootstrap path. Thanks @pashpashpash. +- Plugins/providers: keep Gateway startup primary-model discovery on metadata-only provider entries and reuse active non-speech capability providers even with explicit plugin entries, avoiding unnecessary provider registry loads during startup and media capability checks. Fixes #73729, #73835, and #73793; carries forward #73853 and #73794. Thanks @sg1416-zg, @brokemac79, and @poolside-ventures. - Chat commands: route sensitive group `/diagnostics` and `/export-trajectory` approvals and results to a private owner route, preferring same-surface DMs before falling back to the first configured owner route, so Discord group invocations can land in Telegram when that is the primary owner interface. Thanks @pashpashpash. - Plugin SDK/Discord: restore a deprecated `openclaw/plugin-sdk/discord` compatibility facade and the legacy compat group-policy warning export for the published `@openclaw/discord@2026.3.13` package, covering its config, account, directory, status, and thread-binding imports while keeping new plugins on generic SDK subpaths. Fixes #73685; supersedes #73703. Thanks @rderickson9 and @SymbolStar. - Channels/Discord: suppress duplicate gateway monitors when multiple enabled accounts resolve to the same bot token, preferring config tokens over default env fallback and reporting skipped duplicates as disabled. Supersedes #73608. Thanks @kagura-agent. diff --git a/src/agents/models-config.applies-config-env-vars.test.ts b/src/agents/models-config.applies-config-env-vars.test.ts index 544cf8d4688..bd036dcfb2d 100644 --- a/src/agents/models-config.applies-config-env-vars.test.ts +++ b/src/agents/models-config.applies-config-env-vars.test.ts @@ -127,6 +127,7 @@ describe("models-config", () => { it("threads startup provider discovery scope into implicit provider discovery", async () => { let observedProviderIds: readonly string[] | undefined; + let observedEntriesOnly: boolean | undefined; let observedTimeoutMs: number | undefined; await resolveProvidersForModelsJsonWithDeps( @@ -135,14 +136,17 @@ describe("models-config", () => { agentDir: "/tmp/openclaw-models-config-env-vars-test", env: {}, providerDiscoveryProviderIds: ["openai"], + providerDiscoveryEntriesOnly: true, providerDiscoveryTimeoutMs: 5000, }, { resolveImplicitProviders: async ({ providerDiscoveryProviderIds, + providerDiscoveryEntriesOnly, providerDiscoveryTimeoutMs, }) => { observedProviderIds = providerDiscoveryProviderIds; + observedEntriesOnly = providerDiscoveryEntriesOnly; observedTimeoutMs = providerDiscoveryTimeoutMs; return {}; }, @@ -150,6 +154,7 @@ describe("models-config", () => { ); expect(observedProviderIds).toEqual(["openai"]); + expect(observedEntriesOnly).toBe(true); expect(observedTimeoutMs).toBe(5000); }); diff --git a/src/agents/models-config.plan.ts b/src/agents/models-config.plan.ts index 9218692699d..f5fdefb3a64 100644 --- a/src/agents/models-config.plan.ts +++ b/src/agents/models-config.plan.ts @@ -24,6 +24,7 @@ export type ResolveImplicitProvidersForModelsJson = (params: { pluginMetadataSnapshot?: Pick; providerDiscoveryProviderIds?: readonly string[]; providerDiscoveryTimeoutMs?: number; + providerDiscoveryEntriesOnly?: boolean; }) => Promise>; export type ModelsJsonPlan = @@ -47,6 +48,7 @@ export async function resolveProvidersForModelsJsonWithDeps( pluginMetadataSnapshot?: Pick; providerDiscoveryProviderIds?: readonly string[]; providerDiscoveryTimeoutMs?: number; + providerDiscoveryEntriesOnly?: boolean; }, deps?: { resolveImplicitProviders?: ResolveImplicitProvidersForModelsJson; @@ -70,6 +72,7 @@ export async function resolveProvidersForModelsJsonWithDeps( ...(params.providerDiscoveryTimeoutMs !== undefined ? { providerDiscoveryTimeoutMs: params.providerDiscoveryTimeoutMs } : {}), + ...(params.providerDiscoveryEntriesOnly === true ? { providerDiscoveryEntriesOnly: true } : {}), }); return mergeProviders({ implicit: implicitProviders, @@ -113,6 +116,7 @@ export async function planOpenClawModelsJsonWithDeps( pluginMetadataSnapshot?: Pick; providerDiscoveryProviderIds?: readonly string[]; providerDiscoveryTimeoutMs?: number; + providerDiscoveryEntriesOnly?: boolean; }, deps?: { resolveImplicitProviders?: ResolveImplicitProvidersForModelsJson; @@ -134,6 +138,9 @@ export async function planOpenClawModelsJsonWithDeps( ...(params.providerDiscoveryTimeoutMs !== undefined ? { providerDiscoveryTimeoutMs: params.providerDiscoveryTimeoutMs } : {}), + ...(params.providerDiscoveryEntriesOnly === true + ? { providerDiscoveryEntriesOnly: true } + : {}), }, deps, ); diff --git a/src/agents/models-config.providers.implicit.discovery-scope.test.ts b/src/agents/models-config.providers.implicit.discovery-scope.test.ts index 47c16ce0cde..b987675ec0c 100644 --- a/src/agents/models-config.providers.implicit.discovery-scope.test.ts +++ b/src/agents/models-config.providers.implicit.discovery-scope.test.ts @@ -98,4 +98,20 @@ describe("resolveImplicitProviders startup discovery scope", () => { }), ); }); + + it("can keep startup discovery on provider discovery entries only", async () => { + await resolveImplicitProviders({ + agentDir: "/tmp/openclaw-agent", + config: {}, + env: {} as NodeJS.ProcessEnv, + explicitProviders: {}, + providerDiscoveryEntriesOnly: true, + }); + + expect(mocks.resolveRuntimePluginDiscoveryProviders).toHaveBeenCalledWith( + expect.objectContaining({ + discoveryEntriesOnly: true, + }), + ); + }); }); diff --git a/src/agents/models-config.providers.implicit.ts b/src/agents/models-config.providers.implicit.ts index 475b8613c06..8d769baace5 100644 --- a/src/agents/models-config.providers.implicit.ts +++ b/src/agents/models-config.providers.implicit.ts @@ -47,6 +47,7 @@ type ImplicitProviderParams = { pluginMetadataSnapshot?: Pick; providerDiscoveryProviderIds?: readonly string[]; providerDiscoveryTimeoutMs?: number; + providerDiscoveryEntriesOnly?: boolean; }; type ImplicitProviderContext = ImplicitProviderParams & { @@ -483,6 +484,7 @@ export async function resolveImplicitProviders( ...(params.pluginMetadataSnapshot ? { pluginMetadataSnapshot: params.pluginMetadataSnapshot } : {}), + ...(params.providerDiscoveryEntriesOnly === true ? { discoveryEntriesOnly: true } : {}), }); for (const order of PLUGIN_DISCOVERY_ORDERS) { diff --git a/src/agents/models-config.ts b/src/agents/models-config.ts index 141e4b48a40..cfba621280a 100644 --- a/src/agents/models-config.ts +++ b/src/agents/models-config.ts @@ -49,6 +49,7 @@ async function buildModelsJsonFingerprint(params: { pluginMetadataSnapshot?: Pick; providerDiscoveryProviderIds?: readonly string[]; providerDiscoveryTimeoutMs?: number; + providerDiscoveryEntriesOnly?: boolean; }): Promise { const authProfilesMtimeMs = await readFileMtimeMs( path.join(params.agentDir, "auth-profiles.json"), @@ -68,6 +69,7 @@ async function buildModelsJsonFingerprint(params: { pluginMetadataSnapshotIndexFingerprint, providerDiscoveryProviderIds: params.providerDiscoveryProviderIds, providerDiscoveryTimeoutMs: params.providerDiscoveryTimeoutMs, + providerDiscoveryEntriesOnly: params.providerDiscoveryEntriesOnly === true, }); } @@ -162,6 +164,7 @@ export async function ensureOpenClawModelsJson( workspaceDir?: string; providerDiscoveryProviderIds?: readonly string[]; providerDiscoveryTimeoutMs?: number; + providerDiscoveryEntriesOnly?: boolean; } = {}, ): Promise<{ agentDir: string; wrote: boolean }> { const resolved = resolveModelsConfigInput(config); @@ -191,6 +194,9 @@ export async function ensureOpenClawModelsJson( ...(options.providerDiscoveryTimeoutMs !== undefined ? { providerDiscoveryTimeoutMs: options.providerDiscoveryTimeoutMs } : {}), + ...(options.providerDiscoveryEntriesOnly === true + ? { providerDiscoveryEntriesOnly: true } + : {}), }); const cacheKey = modelsJsonReadyCacheKey(targetPath, fingerprint); const cached = MODELS_JSON_STATE.readyCache.get(cacheKey); @@ -220,6 +226,9 @@ export async function ensureOpenClawModelsJson( ...(options.providerDiscoveryTimeoutMs !== undefined ? { providerDiscoveryTimeoutMs: options.providerDiscoveryTimeoutMs } : {}), + ...(options.providerDiscoveryEntriesOnly === true + ? { providerDiscoveryEntriesOnly: true } + : {}), }); if (plan.action === "skip") { @@ -251,6 +260,9 @@ export async function ensureOpenClawModelsJson( ...(options.providerDiscoveryTimeoutMs !== undefined ? { providerDiscoveryTimeoutMs: options.providerDiscoveryTimeoutMs } : {}), + ...(options.providerDiscoveryEntriesOnly === true + ? { providerDiscoveryEntriesOnly: true } + : {}), }); const refreshedCacheKey = modelsJsonReadyCacheKey(targetPath, refreshedFingerprint); if (refreshedCacheKey !== cacheKey) { diff --git a/src/gateway/server-startup-post-attach.test.ts b/src/gateway/server-startup-post-attach.test.ts index eb59edf8af1..31dea54f722 100644 --- a/src/gateway/server-startup-post-attach.test.ts +++ b/src/gateway/server-startup-post-attach.test.ts @@ -153,6 +153,11 @@ vi.mock("../agents/agent-paths.js", () => ({ resolveOpenClawAgentDir: hoisted.resolveOpenClawAgentDir, })); +vi.mock("../agents/agent-scope.js", () => ({ + resolveAgentWorkspaceDir: vi.fn(() => "/tmp/openclaw-workspace"), + resolveDefaultAgentId: vi.fn(() => "default"), +})); + vi.mock("../agents/defaults.js", () => ({ DEFAULT_MODEL: "gpt-5.4", DEFAULT_PROVIDER: "openai", @@ -357,6 +362,11 @@ describe("startGatewayPostAttachRuntime", () => { await vi.waitFor( () => { expect(prewarmPrimaryModel).toHaveBeenCalledTimes(1); + expect(prewarmPrimaryModel).toHaveBeenCalledWith( + expect.objectContaining({ + workspaceDir: "/tmp/openclaw-workspace", + }), + ); expect(startChannels).toHaveBeenCalledTimes(1); }, { timeout: 2_000 }, diff --git a/src/gateway/server-startup-post-attach.ts b/src/gateway/server-startup-post-attach.ts index 3b533438b1e..130ed15d054 100644 --- a/src/gateway/server-startup-post-attach.ts +++ b/src/gateway/server-startup-post-attach.ts @@ -106,6 +106,7 @@ async function waitForAcpRuntimeBackendReady(params: { async function prewarmConfiguredPrimaryModel(params: { cfg: OpenClawConfig; + workspaceDir?: string; log: { warn: (msg: string) => void }; }): Promise { const { resolveAgentModelPrimaryValue } = await import("../config/model-input.js"); @@ -125,11 +126,13 @@ async function prewarmConfiguredPrimaryModel(params: { } const [ { resolveOpenClawAgentDir }, + { resolveAgentWorkspaceDir, resolveDefaultAgentId }, { DEFAULT_MODEL, DEFAULT_PROVIDER }, { isCliProvider, resolveConfiguredModelRef }, { resolveEmbeddedAgentRuntime }, ] = await Promise.all([ import("../agents/agent-paths.js"), + import("../agents/agent-scope.js"), import("../agents/defaults.js"), import("../agents/model-selection.js"), import("../agents/pi-embedded-runner/runtime.js"), @@ -149,10 +152,14 @@ async function prewarmConfiguredPrimaryModel(params: { // Keep startup prewarm metadata-only; resolving models can import provider runtimes and block readiness. const { ensureOpenClawModelsJson } = await import("../agents/models-config.js"); const agentDir = resolveOpenClawAgentDir(); + const workspaceDir = + params.workspaceDir ?? resolveAgentWorkspaceDir(params.cfg, resolveDefaultAgentId(params.cfg)); try { await ensureOpenClawModelsJson(params.cfg, agentDir, { + workspaceDir, providerDiscoveryProviderIds: [provider], providerDiscoveryTimeoutMs: STARTUP_PROVIDER_DISCOVERY_TIMEOUT_MS, + providerDiscoveryEntriesOnly: true, }); } catch (err) { params.log.warn(`startup model warmup failed for ${provider}/${model}: ${String(err)}`); @@ -162,6 +169,7 @@ async function prewarmConfiguredPrimaryModel(params: { async function prewarmConfiguredPrimaryModelWithTimeout( params: { cfg: OpenClawConfig; + workspaceDir?: string; log: { warn: (msg: string) => void }; timeoutMs?: number; }, @@ -190,6 +198,7 @@ async function prewarmConfiguredPrimaryModelWithTimeout( function schedulePrimaryModelPrewarm( params: { cfg: OpenClawConfig; + workspaceDir?: string; log: { warn: (msg: string) => void }; startupTrace?: GatewayStartupTrace; }, @@ -202,6 +211,7 @@ function schedulePrimaryModelPrewarm( prewarmConfiguredPrimaryModelWithTimeout( { cfg: params.cfg, + ...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}), log: params.log, }, prewarm, @@ -342,6 +352,7 @@ export async function startGatewaySidecars(params: { schedulePrimaryModelPrewarm( { cfg: params.cfg, + workspaceDir: params.defaultWorkspaceDir, log: params.log, startupTrace: params.startupTrace, }, diff --git a/src/gateway/server-startup.test.ts b/src/gateway/server-startup.test.ts index 21b7dc2fa02..f33464e5d11 100644 --- a/src/gateway/server-startup.test.ts +++ b/src/gateway/server-startup.test.ts @@ -15,6 +15,11 @@ vi.mock("../agents/agent-paths.js", () => ({ resolveOpenClawAgentDir: () => "/tmp/agent", })); +vi.mock("../agents/agent-scope.js", () => ({ + resolveAgentWorkspaceDir: () => "/tmp/workspace", + resolveDefaultAgentId: () => "default", +})); + vi.mock("../agents/models-config.js", () => ({ ensureOpenClawModelsJson: (config: unknown, agentDir: unknown, options?: unknown) => ensureOpenClawModelsJsonMock(config, agentDir, options), @@ -68,8 +73,10 @@ describe("gateway startup primary model warmup", () => { cfg, "/tmp/agent", expect.objectContaining({ + workspaceDir: "/tmp/workspace", providerDiscoveryProviderIds: ["openai-codex"], providerDiscoveryTimeoutMs: 5000, + providerDiscoveryEntriesOnly: true, }), ); expect(piModelModuleLoadedMock).not.toHaveBeenCalled(); @@ -163,8 +170,10 @@ describe("gateway startup primary model warmup", () => { cfg, "/tmp/agent", expect.objectContaining({ + workspaceDir: "/tmp/workspace", providerDiscoveryProviderIds: ["openai-codex"], providerDiscoveryTimeoutMs: 5000, + providerDiscoveryEntriesOnly: true, }), ); expect(piModelModuleLoadedMock).not.toHaveBeenCalled(); diff --git a/src/plugins/capability-provider-runtime.test.ts b/src/plugins/capability-provider-runtime.test.ts index dca838250e5..33655de6562 100644 --- a/src/plugins/capability-provider-runtime.test.ts +++ b/src/plugins/capability-provider-runtime.test.ts @@ -122,13 +122,17 @@ function createCompatChainConfig() { return { cfg, allowlistCompat, enablementCompat }; } -function setBundledCapabilityFixture(contractKey: string) { +function setBundledCapabilityFixture( + contractKey: string, + pluginId = "openai", + providerId = pluginId, +) { mocks.loadPluginManifestRegistry.mockReturnValue({ plugins: [ { - id: "openai", + id: pluginId, origin: "bundled", - contracts: { [contractKey]: ["openai"] }, + contracts: { [contractKey]: [providerId] }, }, { id: "custom-plugin", @@ -230,7 +234,7 @@ describe("resolvePluginCapabilityProviders", () => { expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith(); }); - it("uses active non-speech capability providers even when cfg is passed", () => { + it("uses active non-speech capability providers even when cfg has explicit plugin entries", () => { const active = createEmptyPluginRegistry(); active.mediaUnderstandingProviders.push({ pluginId: "deepgram", @@ -246,6 +250,7 @@ describe("resolvePluginCapabilityProviders", () => { const providers = resolvePluginCapabilityProviders({ key: "mediaUnderstandingProviders", cfg: { + plugins: { entries: { deepgram: { enabled: true } } }, tools: { media: { models: [{ provider: "deepgram" }], @@ -603,16 +608,7 @@ describe("resolvePluginCapabilityProviders", () => { nativeDocumentInputs: ["pdf"], }, } as never); - mocks.loadPluginManifestRegistry.mockReturnValue({ - plugins: [ - { - id: "google", - origin: "bundled", - contracts: { mediaUnderstandingProviders: ["google"] }, - }, - ] as never, - diagnostics: [], - }); + setBundledCapabilityFixture("mediaUnderstandingProviders", "google", "google"); mocks.withBundledPluginEnablementCompat.mockReturnValue(compatConfig); mocks.withBundledPluginVitestCompat.mockReturnValue(compatConfig); mocks.resolveRuntimePluginRegistry.mockImplementation((params?: unknown) => @@ -633,6 +629,100 @@ describe("resolvePluginCapabilityProviders", () => { }); }); + it.each([ + "imageGenerationProviders", + "videoGenerationProviders", + "musicGenerationProviders", + ] as const)("uses an explicit empty plugin scope for %s when no bundled owner exists", (key) => { + const providers = resolvePluginCapabilityProviders({ + key, + cfg: {} as OpenClawConfig, + }); + + expectNoResolvedCapabilityProviders(providers as Array<{ id: string }>); + expect(mocks.loadPluginManifestRegistry).toHaveBeenCalledWith({ + config: {}, + env: process.env, + }); + expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith(); + expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({ + config: expect.anything(), + onlyPluginIds: [], + activate: false, + }); + }); + + it("scopes media capability snapshot loads to manifest-derived bundled owners", () => { + const cfg = { plugins: { allow: ["openai", "minimax"] } } as OpenClawConfig; + mocks.loadPluginManifestRegistry.mockReturnValue({ + plugins: [ + { + id: "openai", + origin: "bundled", + contracts: { + imageGenerationProviders: ["openai"], + videoGenerationProviders: ["openai"], + }, + }, + { + id: "minimax", + origin: "bundled", + contracts: { + imageGenerationProviders: ["minimax"], + videoGenerationProviders: ["minimax"], + musicGenerationProviders: ["minimax"], + }, + }, + ] as never, + diagnostics: [], + }); + + resolvePluginCapabilityProviders({ key: "imageGenerationProviders", cfg }); + resolvePluginCapabilityProviders({ key: "videoGenerationProviders", cfg }); + resolvePluginCapabilityProviders({ key: "musicGenerationProviders", cfg }); + + const snapshotLoadOptions = mocks.resolveRuntimePluginRegistry.mock.calls + .map(([options]) => options) + .filter((options): options is { activate: boolean; onlyPluginIds?: string[] } => + Boolean(options && typeof options === "object" && "activate" in options), + ); + expect(snapshotLoadOptions.map((options) => options.onlyPluginIds)).toEqual([ + ["minimax", "openai"], + ["minimax", "openai"], + ["minimax"], + ]); + }); + + it("does not unscoped-load media generation capabilities without bundled owners", () => { + const cfg = { plugins: { allow: ["openai"] } } as OpenClawConfig; + mocks.loadPluginManifestRegistry.mockReturnValue({ + plugins: [ + { + id: "openai", + origin: "bundled", + contracts: { + imageGenerationProviders: ["openai"], + }, + }, + ] as never, + diagnostics: [], + }); + + expectNoResolvedCapabilityProviders( + resolvePluginCapabilityProviders({ key: "imageGenerationProviders", cfg }), + ); + expectNoResolvedCapabilityProviders( + resolvePluginCapabilityProviders({ key: "musicGenerationProviders", cfg }), + ); + + const snapshotLoadOptions = mocks.resolveRuntimePluginRegistry.mock.calls + .map(([options]) => options) + .filter((options): options is { activate: boolean; onlyPluginIds?: string[] } => + Boolean(options && typeof options === "object" && "activate" in options), + ); + expect(snapshotLoadOptions.map((options) => options.onlyPluginIds)).toEqual([["openai"], []]); + }); + it("loads only the bundled owner plugin for a targeted provider lookup", () => { const cfg = { plugins: { allow: ["custom-plugin"] } } as OpenClawConfig; const allowlistCompat = { diff --git a/src/plugins/capability-provider-runtime.ts b/src/plugins/capability-provider-runtime.ts index 67b412fcebb..8d30641b35b 100644 --- a/src/plugins/capability-provider-runtime.ts +++ b/src/plugins/capability-provider-runtime.ts @@ -4,7 +4,6 @@ import { withBundledPluginEnablementCompat, withBundledPluginVitestCompat, } from "./bundled-compat.js"; -import { hasExplicitPluginConfig } from "./config-policy.js"; import { resolveRuntimePluginRegistry } from "./loader.js"; import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js"; import type { PluginRegistry } from "./registry-types.js"; @@ -242,8 +241,7 @@ export function resolvePluginCapabilityProviders 0 && params.key !== "memoryEmbeddingProviders" && - params.key !== "speechProviders" && - !hasExplicitPluginConfig(params.cfg?.plugins) + params.key !== "speechProviders" ) { return activeProviders.map((entry) => entry.provider) as CapabilityProviderForKey[]; } diff --git a/src/plugins/provider-discovery.runtime.test.ts b/src/plugins/provider-discovery.runtime.test.ts index e1ec0ad6f51..3a1114801ed 100644 --- a/src/plugins/provider-discovery.runtime.test.ts +++ b/src/plugins/provider-discovery.runtime.test.ts @@ -226,4 +226,15 @@ describe("resolvePluginDiscoveryProvidersRuntime", () => { ]); expect(mocks.resolvePluginProviders).not.toHaveBeenCalled(); }); + + it("does not fall back to full plugin loading when discovery entries are requested only", () => { + mocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValue({ + plugins: [createManifestPluginWithoutDiscovery({ id: "deepseek" })], + diagnostics: [], + }); + + expect(resolvePluginDiscoveryProvidersRuntime({ discoveryEntriesOnly: true })).toEqual([]); + expect(resolvePluginDiscoveryProvidersRuntime({ discoveryEntriesOnly: true })).toEqual([]); + expect(mocks.resolvePluginProviders).not.toHaveBeenCalled(); + }); });