diff --git a/docs/tools/web.md b/docs/tools/web.md index 3e626400a..2e8dfe869 100644 --- a/docs/tools/web.md +++ b/docs/tools/web.md @@ -107,6 +107,7 @@ crypto/prepaid). search: { enabled: true, provider: "perplexity", + proxy: "http://127.0.0.1:7890", // optional perplexity: { // API key (optional if OPENROUTER_API_KEY or PERPLEXITY_API_KEY is set) apiKey: "sk-or-v1-...", @@ -160,7 +161,8 @@ Search the web using your configured provider. apiKey: "BRAVE_API_KEY_HERE", // optional if BRAVE_API_KEY is set maxResults: 5, timeoutSeconds: 30, - cacheTtlMinutes: 15 + cacheTtlMinutes: 15, + proxy: "http://127.0.0.1:7890" // optional } } } diff --git a/src/agents/tools/web-search.ts b/src/agents/tools/web-search.ts index bf5741490..b765a0f25 100644 --- a/src/agents/tools/web-search.ts +++ b/src/agents/tools/web-search.ts @@ -1,4 +1,5 @@ import { Type } from "@sinclair/typebox"; +import { ProxyAgent } from "undici"; import type { OpenClawConfig } from "../../config/config.js"; import { formatCliCommand } from "../../cli/command-format.js"; @@ -122,6 +123,12 @@ function resolveSearchApiKey(search?: WebSearchConfig): string | undefined { return fromConfig || fromEnv || undefined; } +function resolveProxy(search?: WebSearchConfig): string | undefined { + const fromConfig = + search && "proxy" in search && typeof search.proxy === "string" ? search.proxy.trim() : ""; + return fromConfig || undefined; +} + function missingSearchKeyPayload(provider: (typeof SEARCH_PROVIDERS)[number]) { if (provider === "perplexity") { return { @@ -271,9 +278,12 @@ async function runPerplexitySearch(params: { baseUrl: string; model: string; timeoutSeconds: number; + proxy?: string; }): Promise<{ content: string; citations: string[] }> { const endpoint = `${params.baseUrl.replace(/\/$/, "")}/chat/completions`; + const dispatcher = params.proxy ? new ProxyAgent(params.proxy) : undefined; + const res = await fetch(endpoint, { method: "POST", headers: { @@ -292,7 +302,8 @@ async function runPerplexitySearch(params: { ], }), signal: withTimeout(undefined, params.timeoutSeconds * 1000), - }); + dispatcher, + } as RequestInit); if (!res.ok) { const detail = await readResponseText(res); @@ -319,6 +330,7 @@ async function runWebSearch(params: { freshness?: string; perplexityBaseUrl?: string; perplexityModel?: string; + proxy?: string; }): Promise> { const cacheKey = normalizeCacheKey( params.provider === "brave" @@ -337,6 +349,7 @@ async function runWebSearch(params: { baseUrl: params.perplexityBaseUrl ?? DEFAULT_PERPLEXITY_BASE_URL, model: params.perplexityModel ?? DEFAULT_PERPLEXITY_MODEL, timeoutSeconds: params.timeoutSeconds, + proxy: params.proxy, }); const payload = { @@ -371,6 +384,8 @@ async function runWebSearch(params: { url.searchParams.set("freshness", params.freshness); } + const dispatcher = params.proxy ? new ProxyAgent(params.proxy) : undefined; + const res = await fetch(url.toString(), { method: "GET", headers: { @@ -378,7 +393,8 @@ async function runWebSearch(params: { "X-Subscription-Token": params.apiKey, }, signal: withTimeout(undefined, params.timeoutSeconds * 1000), - }); + dispatcher, + } as RequestInit); if (!res.ok) { const detail = await readResponseText(res); @@ -476,6 +492,7 @@ export function createWebSearchTool(options?: { perplexityAuth?.apiKey, ), perplexityModel: resolvePerplexityModel(perplexityConfig), + proxy: resolveProxy(search), }); return jsonResult(result); }, diff --git a/src/config/zod-schema.agent-runtime.ts b/src/config/zod-schema.agent-runtime.ts index 7e95c3538..6864d3785 100644 --- a/src/config/zod-schema.agent-runtime.ts +++ b/src/config/zod-schema.agent-runtime.ts @@ -170,6 +170,7 @@ export const ToolsWebSearchSchema = z maxResults: z.number().int().positive().optional(), timeoutSeconds: z.number().int().positive().optional(), cacheTtlMinutes: z.number().nonnegative().optional(), + proxy: z.string().optional(), perplexity: z .object({ apiKey: z.string().optional(),