Skip to content

Commit 740459d

Browse files
committed
fix(web-search): load explicit provider from plugin discovery
1 parent dc0e88d commit 740459d

5 files changed

Lines changed: 82 additions & 28 deletions

File tree

CHANGELOG.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4703,7 +4703,6 @@ This audited record covers the complete v2026.5.28..v2026.5.31-beta.4 history: 4
47034703
- Doctor/plugins: remove stale managed install records for bundled plugins even when the bundled plugin is not explicitly configured, so doctor cleanup cannot leave orphaned install metadata behind. Thanks @vincentkoc.
47044704
- Web fetch: scope provider fallback cache entries by the selected fetch provider so config reloads cannot reuse another provider's cached fallback payload. Thanks @vincentkoc.
47054705
- Web search: honor late-bound `tools.web.search.enabled: false` during tool execution so config reloads cannot leave an already-created `web_search` tool runnable. Thanks @vincentkoc.
4706-
- Web search: honor explicit `tools.web.search.provider` and `web_search` provider routing even when stale runtime metadata points at another provider or the active runtime registry is missing the configured provider.
47074706
- Plugins/packages: reject inferred built runtime entries that exist but fail package-boundary checks instead of falling back to TypeScript source for installed packages. Thanks @vincentkoc.
47084707
- Plugins/loader: do not retry native-loaded JavaScript plugin modules through the source transformer after native evaluation has already reached a missing dependency, avoiding duplicate top-level side effects. Thanks @vincentkoc.
47094708
- Plugins/packages: reject blank `openclaw.runtimeExtensions` entries instead of silently ignoring them and falling back to inferred TypeScript runtime entries. Thanks @vincentkoc.

src/agents/tools/web-tool-runtime-context.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,23 @@ describe("web tool runtime context", () => {
127127
expect(mocks.resolveManifestContractOwnerPluginId).not.toHaveBeenCalled();
128128
});
129129

130+
it("keeps fetch provider ids aligned with runtime metadata precedence", () => {
131+
resolveWebFetchToolRuntimeContext({
132+
config: { tools: { web: { fetch: { provider: "firecrawl" } } } },
133+
runtimeWebFetch: {
134+
providerConfigured: "perplexity-fetch",
135+
providerSource: "configured",
136+
selectedProvider: "perplexity-fetch",
137+
selectedProviderKeySource: "config",
138+
diagnostics: [],
139+
},
140+
});
141+
142+
const ownerLookup = latestOwnerLookupParams();
143+
expect(ownerLookup.contract).toBe("webFetchProviders");
144+
expect(ownerLookup.value).toBe("perplexity-fetch");
145+
});
146+
130147
it("keeps runtime providers disabled for bundled fetch owners", async () => {
131148
mocks.resolveManifestContractOwnerPluginId.mockReturnValue("firecrawl");
132149

src/agents/tools/web-tool-runtime-context.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,18 @@ function resolveRuntimeWebProviderId(metadata: WebProviderRuntimeMetadata | unde
3434
return metadata?.selectedProvider ?? metadata?.providerConfigured ?? "";
3535
}
3636

37+
function resolveWebProviderSelectionId(params: {
38+
config: OpenClawConfig | undefined;
39+
kind: WebProviderKind;
40+
runtimeMetadata: WebProviderRuntimeMetadata | undefined;
41+
}): string {
42+
const configuredProviderId = resolveConfiguredWebProviderId(params.config, params.kind);
43+
const runtimeProviderId = resolveRuntimeWebProviderId(params.runtimeMetadata);
44+
return params.kind === "search"
45+
? configuredProviderId || runtimeProviderId
46+
: runtimeProviderId || configuredProviderId;
47+
}
48+
3749
function shouldPreferRuntimeProviders(params: {
3850
config?: OpenClawConfig;
3951
kind: WebProviderKind;
@@ -69,9 +81,11 @@ function resolveWebToolRuntimeContext<TMetadata extends WebProviderRuntimeMetada
6981
params.lateBindRuntimeConfig === true
7082
? (getActiveSecretsRuntimeConfigSnapshot()?.config ?? params.capturedConfig)
7183
: params.capturedConfig;
72-
const providerSelectionId =
73-
resolveConfiguredWebProviderId(config, params.kind) ||
74-
resolveRuntimeWebProviderId(runtimeMetadata);
84+
const providerSelectionId = resolveWebProviderSelectionId({
85+
config,
86+
kind: params.kind,
87+
runtimeMetadata,
88+
});
7589
return {
7690
config,
7791
preferRuntimeProviders: shouldPreferRuntimeProviders({

src/web-search/runtime.test.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -718,7 +718,7 @@ describe("web search runtime", () => {
718718
).rejects.toThrow("web_search is disabled or no provider is available.");
719719
});
720720

721-
it("ignores auto-detected runtime metadata after config names an unknown provider", async () => {
721+
it("fails closed instead of using auto-detected runtime metadata after config names an unknown provider", async () => {
722722
const createTool = vi.fn(() => createCustomSearchTool());
723723
resolveRuntimeWebSearchProvidersMock.mockReturnValue([
724724
createGoogleSearchProvider({
@@ -759,7 +759,7 @@ describe("web search runtime", () => {
759759
config: structuredClone(config),
760760
args: { query: "runtime-config-typo" },
761761
}),
762-
).rejects.toThrow("web_search is disabled or no provider is available.");
762+
).rejects.toThrow('Unknown web_search provider "missing-id".');
763763
expect(createTool).not.toHaveBeenCalled();
764764
});
765765

@@ -1083,7 +1083,6 @@ describe("web search runtime", () => {
10831083
},
10841084
},
10851085
},
1086-
providerId: "custom",
10871086
runtimeWebSearch: {
10881087
providerConfigured: "stale",
10891088
selectedProvider: "stale",
@@ -1157,7 +1156,7 @@ describe("web search runtime", () => {
11571156
},
11581157
args: { query: "config-typo" },
11591158
}),
1160-
).rejects.toThrow("web_search is disabled or no provider is available.");
1159+
).rejects.toThrow('Unknown web_search provider "missing-id".');
11611160
});
11621161

11631162
it("honors preferRuntimeProviders during execution", async () => {

src/web-search/runtime.ts

Lines changed: 45 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ import {
44
normalizeOptionalLowercaseString,
55
} from "@openclaw/normalization-core/string-coerce";
66
import { uniqueStrings } from "@openclaw/normalization-core/string-normalization";
7+
import {
8+
hasWebProviderEntryCredential,
9+
providerRequiresCredential,
10+
readWebProviderEnvValue,
11+
resolveWebProviderConfig,
12+
} from "../../packages/web-content-core/src/provider-runtime-shared.js";
713
import { resolveDefaultAgentDir } from "../agents/agent-scope-config.js";
814
import { hasAuthProfileForProvider } from "../agents/tools/model-config.helpers.js";
915
import {
@@ -22,12 +28,6 @@ import {
2228
import { sortWebSearchProvidersForAutoDetect } from "../plugins/web-search-providers.shared.js";
2329
import { getActiveRuntimeWebToolsMetadata } from "../secrets/runtime-web-tools-state.js";
2430
import type { RuntimeWebSearchMetadata } from "../secrets/runtime-web-tools.types.js";
25-
import {
26-
hasWebProviderEntryCredential,
27-
providerRequiresCredential,
28-
readWebProviderEnvValue,
29-
resolveWebProviderConfig,
30-
} from "../../packages/web-content-core/src/provider-runtime-shared.js";
3131
import type {
3232
ResolveWebSearchDefinitionParams,
3333
RunWebSearchParams,
@@ -324,24 +324,46 @@ function loadSortedWebSearchProviders(
324324
preferRuntimeProviders?: boolean;
325325
},
326326
): PluginWebSearchProviderEntry[] {
327+
const explicitProviderId = resolveExplicitWebSearchProviderId({
328+
search: params.search,
329+
providerId: params.providerId,
330+
});
327331
const loadScope = resolveWebSearchProviderLoadScope({
328332
config: params.config,
329333
search: params.search,
330334
runtimeWebSearch: params.runtimeWebSearch,
331335
providerId: params.providerId,
332336
includeRuntimeSelection: Boolean(params.preferRuntimeProviders),
333337
});
334-
return sortWebSearchProvidersForAutoDetect(
335-
params.preferRuntimeProviders
336-
? resolveRuntimeWebSearchProviders({
337-
config: params.config,
338-
...loadScope,
339-
})
340-
: resolvePluginWebSearchProviders({
341-
config: params.config,
342-
...loadScope,
343-
}),
344-
);
338+
if (!params.preferRuntimeProviders) {
339+
return sortWebSearchProvidersForAutoDetect(
340+
resolvePluginWebSearchProviders({
341+
config: params.config,
342+
...loadScope,
343+
}),
344+
);
345+
}
346+
const runtimeProviders = resolveRuntimeWebSearchProviders({
347+
config: params.config,
348+
...loadScope,
349+
});
350+
if (
351+
!explicitProviderId ||
352+
runtimeProviders.some((provider) => provider.id === explicitProviderId)
353+
) {
354+
return sortWebSearchProvidersForAutoDetect(runtimeProviders);
355+
}
356+
const explicitLoadScope = resolveWebSearchProviderLoadScope({
357+
config: params.config,
358+
search: params.search,
359+
providerId: params.providerId,
360+
includeRuntimeSelection: false,
361+
});
362+
const pluginProviders = resolvePluginWebSearchProviders({
363+
config: params.config,
364+
...explicitLoadScope,
365+
});
366+
return sortWebSearchProvidersForAutoDetect([...pluginProviders, ...runtimeProviders]);
345367
}
346368

347369
function resolveWebSearchCandidates(
@@ -359,14 +381,17 @@ function resolveWebSearchCandidates(
359381
providerId: options?.providerId,
360382
preferRuntimeProviders: options?.preferRuntimeProviders,
361383
}).filter(Boolean);
362-
const explicitProviderId = options?.providerId?.trim();
384+
const explicitProviderId = resolveExplicitWebSearchProviderId({
385+
search,
386+
providerId: options?.providerId,
387+
});
363388
if (providers.length === 0) {
364389
return [];
365390
}
366391

367392
const preferredIds = uniqueStrings(
368393
[
369-
options?.providerId,
394+
explicitProviderId,
370395
resolveRuntimePreferredWebSearchProviderId({
371396
config,
372397
search,
@@ -421,7 +446,7 @@ function hasExplicitWebSearchSelection(params: {
421446
params.search && "provider" in params.search && typeof params.search.provider === "string"
422447
? normalizeLowercaseStringOrEmpty(params.search.provider)
423448
: "";
424-
if (configuredProviderId && availableProviderIds.has(configuredProviderId)) {
449+
if (configuredProviderId) {
425450
return true;
426451
}
427452
const runtimeConfiguredId = normalizeOptionalLowercaseString(

0 commit comments

Comments
 (0)