fix(web-fetch): scope fallback cache by provider

This commit is contained in:
Vincent Koc
2026-05-04 02:07:19 -07:00
parent 098b72910d
commit dade5f9133
3 changed files with 83 additions and 1 deletions

View File

@@ -63,6 +63,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- 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.
- 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.
- 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.
- 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.

View File

@@ -221,4 +221,77 @@ describe("web_fetch provider fallback normalization", () => {
}),
);
});
it("scopes provider fallback cache entries by the late-bound provider", async () => {
global.fetch = withFetchPreconnect(
vi.fn(async () => {
throw new Error("network failed");
}),
);
resolveWebFetchDefinitionMock.mockImplementation(
({ runtimeWebFetch }: { runtimeWebFetch?: { selectedProvider?: string } }) => {
const providerId = runtimeWebFetch?.selectedProvider ?? "unknown";
return {
provider: { id: providerId },
definition: {
description: providerId,
parameters: {},
execute: async () => ({
text: `${providerId} fallback body`,
}),
},
};
},
);
const executeWithProvider = async (providerId: string) => {
runtimeState.activeSecretsRuntimeSnapshot = {
config: {
tools: {
web: {
fetch: {
provider: providerId,
},
},
},
},
};
runtimeState.activeRuntimeWebToolsMetadata = {
fetch: {
providerConfigured: providerId,
providerSource: "configured",
selectedProvider: providerId,
selectedProviderKeySource: "config",
diagnostics: [],
},
diagnostics: [],
};
const tool = createWebFetchTool({
config: {} as OpenClawConfig,
sandboxed: false,
lateBindRuntimeConfig: true,
});
return tool?.execute?.("call-provider-fallback", {
url: "https://example.com/provider-cache-scope",
});
};
const first = await executeWithProvider("firecrawl");
const second = await executeWithProvider("perplexity-fetch");
const firstDetails = first?.details as {
externalContent?: { provider?: string };
text?: string;
};
const secondDetails = second?.details as {
cached?: boolean;
externalContent?: { provider?: string };
text?: string;
};
expect(firstDetails.externalContent?.provider).toBe("firecrawl");
expect(firstDetails.text).toContain("firecrawl fallback body");
expect(secondDetails.externalContent?.provider).toBe("perplexity-fetch");
expect(secondDetails.text).toContain("perplexity-fetch fallback body");
expect(secondDetails.cached).toBeUndefined();
});
});

View File

@@ -283,6 +283,7 @@ type WebFetchRuntimeParams = {
allowRfc2544BenchmarkRange?: boolean;
allowIpv6UniqueLocalRange?: boolean;
};
providerCacheKey?: string;
lookupFn?: LookupFn;
resolveProviderFallback: () => Promise<WebFetchProviderFallback>;
};
@@ -407,7 +408,7 @@ async function runWebFetch(params: WebFetchRuntimeParams): Promise<Record<string
}
: undefined;
const cacheKey = normalizeCacheKey(
`fetch:${params.url}:${params.extractMode}:${params.maxChars}${allowRfc2544BenchmarkRange ? ":allow-rfc2544" : ""}${allowIpv6UniqueLocalRange ? ":allow-ipv6-ula" : ""}${useTrustedEnvProxy ? ":trusted-env-proxy" : ""}`,
`fetch:${params.url}:${params.extractMode}:${params.maxChars}${params.providerCacheKey ? `:provider:${params.providerCacheKey}` : ""}${allowRfc2544BenchmarkRange ? ":allow-rfc2544" : ""}${allowIpv6UniqueLocalRange ? ":allow-ipv6-ula" : ""}${useTrustedEnvProxy ? ":trusted-env-proxy" : ""}`,
);
const cached = readCache(FETCH_CACHE, cacheKey);
if (cached) {
@@ -641,6 +642,12 @@ export function createWebFetchTool(options?: {
if (!resolveFetchEnabled({ fetch: executionFetch, sandboxed: options?.sandboxed })) {
throw new Error("web_fetch is disabled.");
}
const providerCacheKey =
normalizeOptionalLowercaseString(runtimeWebFetch?.selectedProvider) ??
normalizeOptionalLowercaseString(runtimeWebFetch?.providerConfigured) ??
(executionFetch && "provider" in executionFetch
? normalizeOptionalLowercaseString(executionFetch.provider)
: undefined);
const readabilityEnabled = resolveFetchReadabilityEnabled(executionFetch);
const userAgent =
(executionFetch &&
@@ -692,6 +699,7 @@ export function createWebFetchTool(options?: {
config,
useTrustedEnvProxy: resolveFetchUseTrustedEnvProxy(executionFetch),
ssrfPolicy: executionFetch?.ssrfPolicy,
...(providerCacheKey ? { providerCacheKey } : {}),
lookupFn: options?.lookupFn,
resolveProviderFallback,
});