diff --git a/packages/builtin-tools/src/tools/WebFetchTool/utils.ts b/packages/builtin-tools/src/tools/WebFetchTool/utils.ts index 7c419a0ff..1ec2466a3 100644 --- a/packages/builtin-tools/src/tools/WebFetchTool/utils.ts +++ b/packages/builtin-tools/src/tools/WebFetchTool/utils.ts @@ -117,10 +117,19 @@ const MAX_HTTP_CONTENT_LENGTH = 10 * 1024 * 1024 // Timeout for the main HTTP fetch request (60 seconds). // Prevents hanging indefinitely on slow/unresponsive servers. -const FETCH_TIMEOUT_MS = 60_000 +// Overridable via settings.webFetchHttpTimeoutMs (set in /web-tools panel). +const DEFAULT_FETCH_TIMEOUT_MS = 60_000 + +function getFetchTimeoutMs(): number { + const settings = getSettings_DEPRECATED() as Record & { + webFetchHttpTimeoutMs?: number + } + return settings.webFetchHttpTimeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS +} // Cap same-host redirect hops. Without this a malicious server can return -// a redirect loop (/a → /b → /a …) and the per-request FETCH_TIMEOUT_MS +// a redirect loop (/a → /b → /a …) and the per-request timeout +// (controlled by settings.webFetchHttpTimeoutMs) // resets on every hop, hanging the tool until user interrupt. 10 matches // common client defaults (axios=5, follow-redirects=21, Chrome=20). const MAX_REDIRECTS = 10 @@ -238,7 +247,7 @@ export async function getWithPermittedRedirects( try { return await axios.get(url, { signal, - timeout: FETCH_TIMEOUT_MS, + timeout: getFetchTimeoutMs(), maxRedirects: 0, responseType: 'arraybuffer', maxContentLength: MAX_HTTP_CONTENT_LENGTH, @@ -488,7 +497,7 @@ export async function fetchContentWithTavily( }, { signal: abortSignal, - timeout: FETCH_TIMEOUT_MS, + timeout: getFetchTimeoutMs(), headers: { 'Content-Type': 'application/json' }, }, ) diff --git a/packages/builtin-tools/src/tools/WebSearchTool/adapters/braveAdapter.ts b/packages/builtin-tools/src/tools/WebSearchTool/adapters/braveAdapter.ts index 54b34904d..ea8f251df 100644 --- a/packages/builtin-tools/src/tools/WebSearchTool/adapters/braveAdapter.ts +++ b/packages/builtin-tools/src/tools/WebSearchTool/adapters/braveAdapter.ts @@ -5,6 +5,7 @@ import axios from 'axios' import { AbortError } from 'src/utils/errors.js' +import { getSettings_DEPRECATED } from 'src/utils/settings/settings.js' import type { SearchResult, SearchOptions, WebSearchAdapter } from './types.js' const FETCH_TIMEOUT_MS = 30_000 @@ -156,6 +157,14 @@ function normalizeSnippet(snippets: string[] | undefined): string | undefined { } function getBraveApiKey(): string { + // Priority: settings.braveApiKey (from /web-tools panel) > environment variable + const settings = getSettings_DEPRECATED() as Record & { + braveApiKey?: string + } + if (settings.braveApiKey?.trim()) { + return settings.braveApiKey.trim() + } + for (const envVar of BRAVE_API_KEY_ENV_VARS) { const value = process.env[envVar]?.trim() if (value) { diff --git a/packages/builtin-tools/src/tools/WebSearchTool/adapters/tavilyAdapter.ts b/packages/builtin-tools/src/tools/WebSearchTool/adapters/tavilyAdapter.ts index 4972ab027..7644ff6fb 100644 --- a/packages/builtin-tools/src/tools/WebSearchTool/adapters/tavilyAdapter.ts +++ b/packages/builtin-tools/src/tools/WebSearchTool/adapters/tavilyAdapter.ts @@ -43,7 +43,11 @@ export class TavilySearchAdapter implements WebSearchAdapter { const settings = getSettings_DEPRECATED() as Record & { tavilyEndpointUrl?: string } - const searchUrl = settings.tavilyEndpointUrl || DEFAULT_TAVILY_SEARCH_URL + const baseUrl = settings.tavilyEndpointUrl || DEFAULT_TAVILY_SEARCH_URL + // Ensure the URL ends with /search (same pattern as fetchContentWithTavily for /extract) + const searchUrl = baseUrl.endsWith('/search') + ? baseUrl + : `${baseUrl.replace(/\/$/, '')}/search` try { const response = await axios.post<{