diff --git a/extensions/brave/src/brave-web-search-provider.runtime.ts b/extensions/brave/src/brave-web-search-provider.runtime.ts index bd8abada80b6..dc7805e03226 100644 --- a/extensions/brave/src/brave-web-search-provider.runtime.ts +++ b/extensions/brave/src/brave-web-search-provider.runtime.ts @@ -483,74 +483,46 @@ export async function executeBraveSearch( const start = Date.now(); const timeoutSeconds = resolveSearchTimeoutSeconds(searchConfig); const cacheTtlMs = resolveSearchCacheTtlMs(searchConfig); - - if (braveMode === "llm-context") { - const { results, sources } = await runBraveLlmContextSearch({ - baseUrl: braveBaseUrl, - endpointMode: braveEndpointMode, - query, - apiKey, - timeoutSeconds, - diagnostics, - signal: options?.signal, - country: country ?? undefined, - search_lang: normalizedLanguage.search_lang, - freshness, - dateAfter, - dateBefore, - }); - options?.signal?.throwIfAborted(); - const payload = { - query, - provider: "brave", - mode: "llm-context" as const, - count: results.length, - tookMs: Date.now() - start, - externalContent: { - untrusted: true, - source: "web_search", - provider: "brave", - wrapped: true, - }, - results: results.map((entry) => ({ - title: entry.title ? wrapWebContent(entry.title, "web_search") : "", - url: entry.url, - snippets: entry.snippets.map((snippet) => wrapWebContent(snippet, "web_search")), - siteName: entry.siteName, - })), - sources, - }; - writeCachedSearchPayload(cacheKey, payload, cacheTtlMs); - logBraveHttp(diagnostics, "cache write", { - mode: "llm-context", - query, - cacheKey, - ttlMs: cacheTtlMs, - count: results.length, - }); - return payload; - } - - const results = await runBraveWebSearch({ + const request = { baseUrl: braveBaseUrl, endpointMode: braveEndpointMode, query, - count: resolveSearchCount(count, DEFAULT_SEARCH_COUNT), apiKey, timeoutSeconds, diagnostics, signal: options?.signal, country: country ?? undefined, search_lang: normalizedLanguage.search_lang, - ui_lang: normalizedLanguage.ui_lang, freshness, dateAfter, dateBefore, - }); + }; + const response = + braveMode === "llm-context" + ? { ...(await runBraveLlmContextSearch(request)), mode: "llm-context" as const } + : { + results: await runBraveWebSearch({ + ...request, + count: resolveSearchCount(count, DEFAULT_SEARCH_COUNT), + ui_lang: normalizedLanguage.ui_lang, + }), + mode: "web" as const, + }; + // A completed upstream response must not write cache state after its caller aborts. options?.signal?.throwIfAborted(); + const results = + response.mode === "llm-context" + ? response.results.map((entry) => ({ + title: entry.title ? wrapWebContent(entry.title, "web_search") : "", + url: entry.url, + snippets: entry.snippets.map((snippet) => wrapWebContent(snippet, "web_search")), + siteName: entry.siteName, + })) + : response.results; const payload = { query, provider: "brave", + ...(response.mode === "llm-context" ? { mode: response.mode } : {}), count: results.length, tookMs: Date.now() - start, externalContent: { @@ -560,10 +532,11 @@ export async function executeBraveSearch( wrapped: true, }, results, + ...(response.mode === "llm-context" ? { sources: response.sources } : {}), }; writeCachedSearchPayload(cacheKey, payload, cacheTtlMs); logBraveHttp(diagnostics, "cache write", { - mode: "web", + mode: response.mode, query, cacheKey, ttlMs: cacheTtlMs, diff --git a/extensions/brave/src/brave-web-search-provider.shared.ts b/extensions/brave/src/brave-web-search-provider.shared.ts index 37dfdbb1e03a..45c207211523 100644 --- a/extensions/brave/src/brave-web-search-provider.shared.ts +++ b/extensions/brave/src/brave-web-search-provider.shared.ts @@ -2,6 +2,7 @@ * Brave Search request normalization and result mapping. It validates Brave * country/language params and converts LLM-context responses into web results. */ +import { resolveSiteName } from "openclaw/plugin-sdk/provider-web-search"; import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, @@ -213,17 +214,6 @@ export function normalizeBraveLanguageParams(params: { search_lang?: string; ui_ return { search_lang, ui_lang }; } -function resolveSiteName(url: string | undefined): string | undefined { - if (!url) { - return undefined; - } - try { - return new URL(url).hostname; - } catch { - return undefined; - } -} - /** Map Brave LLM Context API grounding results into web-search result rows. */ export function mapBraveLlmContextResults( data: BraveLlmContextResponse, diff --git a/extensions/brave/src/brave-web-search-provider.test.ts b/extensions/brave/src/brave-web-search-provider.test.ts index 2a80140157bc..1d10febe8c44 100644 --- a/extensions/brave/src/brave-web-search-provider.test.ts +++ b/extensions/brave/src/brave-web-search-provider.test.ts @@ -228,6 +228,46 @@ describe("brave web search provider", () => { }, ); + it.each(["web", "llm-context"] as const)( + "does not cache a %s response completed after caller cancellation", + async (mode) => { + const controller = new AbortController(); + const reason = new Error(`Brave ${mode} canceled after response`); + const payload = + mode === "web" ? { web: { results: [] } } : { grounding: { generic: [] }, sources: [] }; + let firstRequest = true; + const fetchMock = vi.fn(async () => { + if (!firstRequest) { + return jsonResponse(payload); + } + firstRequest = false; + let emitted = false; + return new Response( + new ReadableStream({ + pull(stream) { + if (!emitted) { + emitted = true; + stream.enqueue(new TextEncoder().encode(JSON.stringify(payload))); + return; + } + stream.close(); + controller.abort(reason); + }, + }), + { headers: { "content-type": "application/json" } }, + ); + }); + global.fetch = fetchMock as typeof global.fetch; + const tool = createBraveTool({ webSearch: { apiKey: "brave-test-key", mode } }); + const args = { query: `brave post-response cancellation ${mode}` }; + + await expect(tool.execute(args, { signal: controller.signal })).rejects.toBe(reason); + await tool.execute(args); + + expect(fetchMock).toHaveBeenCalledTimes(2); + }, + ); + it("normalizes brave language parameters and swaps reversed ui/search inputs", () => { expect( testing.normalizeBraveLanguageParams({