fix(web-fetch): resolve external providers

This commit is contained in:
Peter Steinberger
2026-05-02 04:08:59 +01:00
parent 2f2bb7dac6
commit e4c127e678
5 changed files with 90 additions and 12 deletions

View File

@@ -35,6 +35,7 @@ Docs: https://docs.openclaw.ai
- Web search: point missing-key errors to `web_fetch` for known URLs and the browser tool for interactive pages. Thanks @zhaoyang97.
- Web search: late-bind managed agent `web_search` calls to the current runtime config snapshot, so existing sessions do not keep stale unresolved SecretRefs after secrets reload. Fixes #75420. Thanks @richardmqq.
- Web search: honor `baseUrl` overrides for Gemini, Grok, and x_search provider-owned config, so proxy-backed search tools no longer dial hardcoded public endpoints. Supersedes #61972. Thanks @Lanfei.
- Web fetch: resolve external plugin `webFetchProviders` for non-sandboxed `web_fetch`, while keeping sandboxed fetches limited to bundled providers. Fixes #74915. Thanks @ultrahighsuper and @mingmingtsao.
- Heartbeat: strip legacy `[TOOL_CALL]...[/TOOL_CALL]` and `[TOOL_RESULT]...[/TOOL_RESULT]` pseudo-call blocks from heartbeat replies before channel delivery. Fixes #54138. Thanks @Deniable9570.
- macOS/Voice Wake: send wake-word and Push-to-Talk transcripts through the selected macOS session target instead of always falling back to main WebChat. Fixes #51040. Thanks @carl-jeffrolc.
- Providers/xAI: give Grok `web_search` a 60s default timeout, harden malformed xAI Responses parsing, and return structured timeout errors instead of aborting the tool call. Fixes #58063 and #58733. Thanks @dnishimura, @marvcasasola-svg, and @Nanako0129.

View File

@@ -134,7 +134,10 @@ Current runtime behavior:
- `tools.web.fetch.provider` selects the fetch fallback provider explicitly.
- If `provider` is omitted, OpenClaw auto-detects the first ready web-fetch
provider from available credentials. Today the bundled provider is Firecrawl.
provider from available credentials. Non-sandboxed `web_fetch` can use
installed plugins that declare `contracts.webFetchProviders` and register a
matching provider at runtime. Today the bundled provider is Firecrawl.
- Sandboxed `web_fetch` calls stay limited to bundled providers.
- If Readability is disabled, `web_fetch` skips straight to the selected
provider fallback. If no provider is available, it fails closed.

View File

@@ -221,6 +221,8 @@ examples.
- choose it with `tools.web.fetch.provider`
- or omit that field and let OpenClaw auto-detect the first ready web-fetch
provider from available credentials
- non-sandboxed `web_fetch` can use installed plugin providers that declare
`contracts.webFetchProviders`; sandboxed fetches stay bundled-only
- today the bundled web-fetch provider is Firecrawl, configured under
`plugins.entries.firecrawl.config.webFetch.*`

View File

@@ -201,7 +201,7 @@ describe("web fetch runtime", () => {
expect(resolved?.provider.id).toBe("firecrawl");
});
it("keeps non-sandboxed web fetch on bundled providers even when runtime providers are preferred", () => {
it("uses runtime providers for non-sandboxed web fetch when runtime providers are preferred", () => {
const bundled = createFirecrawlProvider({
getConfiguredCredentialValue: () => "bundled-key",
});
@@ -215,6 +215,48 @@ describe("web fetch runtime", () => {
preferRuntimeProviders: true,
});
expect(resolved?.provider.id).toBe("firecrawl");
expect(resolved?.provider.id).toBe("thirdparty");
});
it("resolves an explicitly configured non-bundled provider from plugin providers", () => {
const bundled = createFirecrawlProvider({
getConfiguredCredentialValue: () => "bundled-key",
});
const external = createThirdPartyFetchProvider();
resolvePluginWebFetchProvidersMock.mockReturnValue([bundled, external]);
const resolved = resolveWebFetchDefinition({
config: {
tools: { web: { fetch: { provider: "thirdparty" } } },
} as OpenClawConfig,
sandboxed: false,
preferRuntimeProviders: false,
});
expect(resolved?.provider.id).toBe("thirdparty");
});
it("prefers an explicitly configured non-bundled provider over runtime metadata", () => {
const bundled = createFirecrawlProvider({
getConfiguredCredentialValue: () => "bundled-key",
});
const external = createThirdPartyFetchProvider();
resolveRuntimeWebFetchProvidersMock.mockReturnValue([bundled, external]);
const resolved = resolveWebFetchDefinition({
config: {
tools: { web: { fetch: { provider: "thirdparty" } } },
} as OpenClawConfig,
runtimeWebFetch: {
providerSource: "auto-detect",
selectedProvider: "firecrawl",
selectedProviderKeySource: "env",
diagnostics: [],
},
sandboxed: false,
preferRuntimeProviders: true,
});
expect(resolved?.provider.id).toBe("thirdparty");
});
});

View File

@@ -4,7 +4,10 @@ import type {
PluginWebFetchProviderEntry,
WebFetchProviderToolDefinition,
} from "../plugins/types.js";
import { resolvePluginWebFetchProviders } from "../plugins/web-fetch-providers.runtime.js";
import {
resolvePluginWebFetchProviders,
resolveRuntimeWebFetchProviders,
} from "../plugins/web-fetch-providers.runtime.js";
import { sortWebFetchProvidersForAutoDetect } from "../plugins/web-fetch-providers.shared.js";
import { getActiveRuntimeWebToolsMetadata } from "../secrets/runtime-web-tools-state.js";
import type { RuntimeWebFetchMetadata } from "../secrets/runtime-web-tools.types.js";
@@ -81,7 +84,6 @@ export function listWebFetchProviders(params?: {
return resolvePluginWebFetchProviders({
config: params?.config,
bundledAllowlistCompat: true,
origin: "bundled",
});
}
@@ -104,7 +106,6 @@ export function resolveWebFetchProviderId(params: {
resolvePluginWebFetchProviders({
config: params.config,
bundledAllowlistCompat: true,
origin: "bundled",
}),
);
const raw =
@@ -138,6 +139,20 @@ export function resolveWebFetchProviderId(params: {
return "";
}
function resolveConfiguredWebFetchProviderId(params: {
fetch?: WebFetchConfig;
providers: PluginWebFetchProviderEntry[];
}): string | undefined {
const raw =
params.fetch && "provider" in params.fetch
? normalizeLowercaseStringOrEmpty(params.fetch.provider)
: "";
if (!raw) {
return undefined;
}
return params.providers.find((provider) => provider.id === raw)?.id;
}
export function resolveWebFetchDefinition(
options?: ResolveWebFetchDefinitionParams,
): { provider: PluginWebFetchProviderEntry; definition: WebFetchProviderToolDefinition } | null {
@@ -146,18 +161,33 @@ export function resolveWebFetchDefinition(
| undefined;
const runtimeWebFetch = options?.runtimeWebFetch ?? getActiveRuntimeWebToolsMetadata()?.fetch;
const providers = sortWebFetchProvidersForAutoDetect(
resolvePluginWebFetchProviders({
config: options?.config,
bundledAllowlistCompat: true,
origin: "bundled",
}),
options?.sandboxed
? resolvePluginWebFetchProviders({
config: options?.config,
bundledAllowlistCompat: true,
origin: "bundled",
})
: options?.preferRuntimeProviders
? resolveRuntimeWebFetchProviders({
config: options?.config,
bundledAllowlistCompat: true,
})
: resolvePluginWebFetchProviders({
config: options?.config,
bundledAllowlistCompat: true,
}),
);
return resolveWebProviderDefinition({
config: options?.config,
toolConfig: fetch as Record<string, unknown> | undefined,
runtimeMetadata: runtimeWebFetch,
sandboxed: options?.sandboxed,
providerId: options?.providerId,
providerId:
options?.providerId ??
resolveConfiguredWebFetchProviderId({
fetch,
providers,
}),
providers,
resolveEnabled: ({ toolConfig, sandboxed }) =>
resolveWebFetchEnabled({