feat: add Brave Search LLM Context API mode for web_search

Add support for Brave's LLM Context API endpoint (/res/v1/llm/context)
as an optional mode for the web_search tool. When configured with
tools.web.search.brave.mode set to llm-context, the tool returns
pre-extracted page content optimized for LLM grounding instead of
standard URL/snippet results.

The llm-context cache key excludes count and ui_lang parameters that
the LLM Context API does not accept, preventing unnecessary cache
misses.

Closes #14992

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Thirumalesh
2026-03-04 11:21:54 +05:30
committed by Peter Steinberger
parent 38f4ac5e3c
commit 8a1015f1aa
6 changed files with 173 additions and 2 deletions

View File

@@ -15,6 +15,7 @@ const {
resolveKimiModel,
resolveKimiBaseUrl,
extractKimiCitations,
resolveBraveMode,
} = __testing;
const kimiApiKeyEnv = ["KIMI_API", "KEY"].join("_");
@@ -276,3 +277,25 @@ describe("extractKimiCitations", () => {
).toEqual(["https://example.com/a", "https://example.com/b", "https://example.com/c"]);
});
});
describe("resolveBraveMode", () => {
it("defaults to 'web' when no config is provided", () => {
expect(resolveBraveMode({})).toBe("web");
});
it("defaults to 'web' when mode is undefined", () => {
expect(resolveBraveMode({ mode: undefined })).toBe("web");
});
it("returns 'llm-context' when configured", () => {
expect(resolveBraveMode({ mode: "llm-context" })).toBe("llm-context");
});
it("returns 'web' when mode is explicitly 'web'", () => {
expect(resolveBraveMode({ mode: "web" })).toBe("web");
});
it("falls back to 'web' for unrecognized mode values", () => {
expect(resolveBraveMode({ mode: "invalid" })).toBe("web");
});
});

View File

@@ -26,6 +26,7 @@ const DEFAULT_SEARCH_COUNT = 5;
const MAX_SEARCH_COUNT = 10;
const BRAVE_SEARCH_ENDPOINT = "https://api.search.brave.com/res/v1/web/search";
const BRAVE_LLM_CONTEXT_ENDPOINT = "https://api.search.brave.com/res/v1/llm/context";
const PERPLEXITY_SEARCH_ENDPOINT = "https://api.perplexity.ai/search";
const XAI_API_ENDPOINT = "https://api.x.ai/v1/responses";
@@ -247,6 +248,17 @@ type BraveSearchResponse = {
};
};
type BraveLlmContextSnippet = { text: string };
type BraveLlmContextResult = { url: string; title: string; snippets: BraveLlmContextSnippet[] };
type BraveLlmContextResponse = {
grounding: { generic?: BraveLlmContextResult[] };
sources?: { url?: string; hostname?: string; date?: string }[];
};
type BraveConfig = {
mode?: string;
};
type PerplexityConfig = {
apiKey?: string;
};
@@ -550,6 +562,21 @@ function resolveSearchProvider(search?: WebSearchConfig): (typeof SEARCH_PROVIDE
return "perplexity";
}
function resolveBraveConfig(search?: WebSearchConfig): BraveConfig {
if (!search || typeof search !== "object") {
return {};
}
const brave = "brave" in search ? search.brave : undefined;
if (!brave || typeof brave !== "object") {
return {};
}
return brave as BraveConfig;
}
function resolveBraveMode(brave: BraveConfig): "web" | "llm-context" {
return brave.mode === "llm-context" ? "llm-context" : "web";
}
function resolvePerplexityConfig(search?: WebSearchConfig): PerplexityConfig {
if (!search || typeof search !== "object") {
return {};
@@ -1213,6 +1240,67 @@ async function runKimiSearch(params: {
};
}
async function runBraveLlmContextSearch(params: {
query: string;
apiKey: string;
timeoutSeconds: number;
country?: string;
search_lang?: string;
freshness?: string;
}): Promise<{
results: Array<{
url: string;
title: string;
snippets: string[];
siteName?: string;
}>;
sources?: BraveLlmContextResponse["sources"];
}> {
const url = new URL(BRAVE_LLM_CONTEXT_ENDPOINT);
url.searchParams.set("q", params.query);
if (params.country) {
url.searchParams.set("country", params.country);
}
if (params.search_lang) {
url.searchParams.set("search_lang", params.search_lang);
}
if (params.freshness) {
url.searchParams.set("freshness", params.freshness);
}
return withTrustedWebSearchEndpoint(
{
url: url.toString(),
timeoutSeconds: params.timeoutSeconds,
init: {
method: "GET",
headers: {
Accept: "application/json",
"X-Subscription-Token": params.apiKey,
},
},
},
async (res) => {
if (!res.ok) {
const detailResult = await readResponseText(res, { maxBytes: 64_000 });
const detail = detailResult.text;
throw new Error(`Brave LLM Context API error (${res.status}): ${detail || res.statusText}`);
}
const data = (await res.json()) as BraveLlmContextResponse;
const genericResults = Array.isArray(data.grounding?.generic) ? data.grounding.generic : [];
const mapped = genericResults.map((entry) => ({
url: entry.url ?? "",
title: entry.title ?? "",
snippets: (entry.snippets ?? []).map((s) => s.text ?? "").filter(Boolean),
siteName: resolveSiteName(entry.url) || undefined,
}));
return { results: mapped, sources: data.sources };
},
);
}
async function runWebSearch(params: {
query: string;
count: number;
@@ -1235,7 +1323,9 @@ async function runWebSearch(params: {
geminiModel?: string;
kimiBaseUrl?: string;
kimiModel?: string;
braveMode?: "web" | "llm-context";
}): Promise<Record<string, unknown>> {
const effectiveBraveMode = params.braveMode ?? "web";
const providerSpecificKey =
params.provider === "grok"
? `${params.grokModel ?? DEFAULT_GROK_MODEL}:${String(params.grokInlineCitations ?? false)}`
@@ -1245,7 +1335,9 @@ async function runWebSearch(params: {
? `${params.kimiBaseUrl ?? DEFAULT_KIMI_BASE_URL}:${params.kimiModel ?? DEFAULT_KIMI_MODEL}`
: "";
const cacheKey = normalizeCacheKey(
`${params.provider}:${params.query}:${params.count}:${params.country || "default"}:${params.search_lang || params.language || "default"}:${params.ui_lang || "default"}:${params.freshness || "default"}:${params.dateAfter || "default"}:${params.dateBefore || "default"}:${params.searchDomainFilter?.join(",") || "default"}:${params.maxTokens || "default"}:${params.maxTokensPerPage || "default"}:${providerSpecificKey}`,
params.provider === "brave" && effectiveBraveMode === "llm-context"
? `${params.provider}:llm-context:${params.query}:${params.country || "default"}:${params.search_lang || params.language || "default"}:${params.freshness || "default"}`
: `${params.provider}:${effectiveBraveMode}:${params.query}:${params.count}:${params.country || "default"}:${params.search_lang || params.language || "default"}:${params.ui_lang || "default"}:${params.freshness || "default"}:${params.dateAfter || "default"}:${params.dateBefore || "default"}:${params.searchDomainFilter?.join(",") || "default"}:${params.maxTokens || "default"}:${params.maxTokensPerPage || "default"}:${providerSpecificKey}`,
);
const cached = readCache(SEARCH_CACHE, cacheKey);
if (cached) {
@@ -1372,6 +1464,42 @@ async function runWebSearch(params: {
throw new Error("Unsupported web search provider.");
}
if (effectiveBraveMode === "llm-context") {
const { results: llmResults, sources } = await runBraveLlmContextSearch({
query: params.query,
apiKey: params.apiKey,
timeoutSeconds: params.timeoutSeconds,
country: params.country,
search_lang: params.search_lang,
freshness: params.freshness,
});
const mapped = llmResults.map((entry) => ({
title: entry.title ? wrapWebContent(entry.title, "web_search") : "",
url: entry.url,
snippets: entry.snippets.map((s) => wrapWebContent(s, "web_search")),
siteName: entry.siteName,
}));
const payload = {
query: params.query,
provider: params.provider,
mode: "llm-context" as const,
count: mapped.length,
tookMs: Date.now() - start,
externalContent: {
untrusted: true,
source: "web_search",
provider: params.provider,
wrapped: true,
},
results: mapped,
sources,
};
writeCache(SEARCH_CACHE, cacheKey, payload, params.cacheTtlMs);
return payload;
}
const url = new URL(BRAVE_SEARCH_ENDPOINT);
url.searchParams.set("q", params.query);
url.searchParams.set("count", String(params.count));
@@ -1465,6 +1593,8 @@ export function createWebSearchTool(options?: {
const grokConfig = resolveGrokConfig(search);
const geminiConfig = resolveGeminiConfig(search);
const kimiConfig = resolveKimiConfig(search);
const braveConfig = resolveBraveConfig(search);
const braveMode = resolveBraveMode(braveConfig);
const description =
provider === "perplexity"
@@ -1475,7 +1605,9 @@ export function createWebSearchTool(options?: {
? "Search the web using Kimi by Moonshot. Returns AI-synthesized answers with citations from native $web_search."
: provider === "gemini"
? "Search the web using Gemini with Google Search grounding. Returns AI-synthesized answers with citations from Google Search."
: "Search the web using Brave Search API. Supports region-specific and localized search via country and language parameters. Returns titles, URLs, and snippets for fast research.";
: braveMode === "llm-context"
? "Search the web using Brave Search LLM Context API. Returns pre-extracted page content (text chunks, tables, code blocks) optimized for LLM grounding."
: "Search the web using Brave Search API. Supports region-specific and localized search via country and language parameters. Returns titles, URLs, and snippets for fast research.";
return {
label: "Web Search",
@@ -1660,6 +1792,7 @@ export function createWebSearchTool(options?: {
geminiModel: resolveGeminiModel(geminiConfig),
kimiBaseUrl: resolveKimiBaseUrl(kimiConfig),
kimiModel: resolveKimiModel(kimiConfig),
braveMode,
});
return jsonResult(result);
},
@@ -1684,4 +1817,5 @@ export const __testing = {
resolveKimiBaseUrl,
extractKimiCitations,
resolveRedirectUrl: resolveCitationRedirectUrl,
resolveBraveMode,
} as const;

View File

@@ -666,6 +666,8 @@ export const FIELD_HELP: Record<string, string> = {
"Perplexity base URL override (default: https://openrouter.ai/api/v1 or https://api.perplexity.ai).",
"tools.web.search.perplexity.model":
'Perplexity model override (default: "perplexity/sonar-pro").',
"tools.web.search.brave.mode":
'Brave Search mode: "web" (URL results) or "llm-context" (pre-extracted page content for LLM grounding).',
"tools.web.fetch.enabled": "Enable the web_fetch tool (lightweight HTTP fetch).",
"tools.web.fetch.maxChars": "Max characters returned by web_fetch (truncated).",
"tools.web.fetch.maxCharsCap":

View File

@@ -224,6 +224,7 @@ export const FIELD_LABELS: Record<string, string> = {
"tools.web.search.gemini.model": "Gemini Search Model",
"tools.web.search.grok.apiKey": "Grok Search API Key", // pragma: allowlist secret
"tools.web.search.grok.model": "Grok Search Model",
"tools.web.search.brave.mode": "Brave Search Mode",
"tools.web.search.kimi.apiKey": "Kimi Search API Key", // pragma: allowlist secret
"tools.web.search.kimi.baseUrl": "Kimi Search Base URL",
"tools.web.search.kimi.model": "Kimi Search Model",

View File

@@ -485,6 +485,11 @@ export type ToolsConfig = {
/** Model to use (defaults to "moonshot-v1-128k"). */
model?: string;
};
/** Brave-specific configuration (used when provider="brave"). */
brave?: {
/** Brave Search mode: "web" (standard results) or "llm-context" (pre-extracted page content). Default: "web". */
mode?: "web" | "llm-context";
};
};
fetch?: {
/** Enable web fetch tool (default: true). */

View File

@@ -308,6 +308,12 @@ export const ToolsWebSearchSchema = z
})
.strict()
.optional(),
brave: z
.object({
mode: z.union([z.literal("web"), z.literal("llm-context")]).optional(),
})
.strict()
.optional(),
})
.strict()
.optional();