fix: land Brave llm-context gaps (#33383) (thanks @thirumaleshp)

This commit is contained in:
Peter Steinberger
2026-03-08 13:54:50 +00:00
parent 8a1015f1aa
commit acac7e3132
5 changed files with 265 additions and 5 deletions

View File

@@ -1682,6 +1682,14 @@ export function createWebSearchTool(options?: {
}
const resolvedSearchLang = normalizedBraveLanguageParams.search_lang;
const resolvedUiLang = normalizedBraveLanguageParams.ui_lang;
if (resolvedUiLang && provider === "brave" && braveMode === "llm-context") {
return jsonResult({
error: "unsupported_ui_lang",
message:
"ui_lang is not supported by Brave llm-context mode. Remove ui_lang or use Brave web mode for locale-based UI hints.",
docs: "https://docs.openclaw.ai/tools/web",
});
}
const rawFreshness = readStringParam(params, "freshness");
if (rawFreshness && provider !== "brave" && provider !== "perplexity") {
return jsonResult({
@@ -1690,6 +1698,14 @@ export function createWebSearchTool(options?: {
docs: "https://docs.openclaw.ai/tools/web",
});
}
if (rawFreshness && provider === "brave" && braveMode === "llm-context") {
return jsonResult({
error: "unsupported_freshness",
message:
"freshness filtering is not supported by Brave llm-context mode. Remove freshness or use Brave web mode.",
docs: "https://docs.openclaw.ai/tools/web",
});
}
const freshness = rawFreshness ? normalizeFreshness(rawFreshness, provider) : undefined;
if (rawFreshness && !freshness) {
return jsonResult({
@@ -1715,6 +1731,14 @@ export function createWebSearchTool(options?: {
docs: "https://docs.openclaw.ai/tools/web",
});
}
if ((rawDateAfter || rawDateBefore) && provider === "brave" && braveMode === "llm-context") {
return jsonResult({
error: "unsupported_date_filter",
message:
"date_after/date_before filtering is not supported by Brave llm-context mode. Use Brave web mode for date filters.",
docs: "https://docs.openclaw.ai/tools/web",
});
}
const dateAfter = rawDateAfter ? normalizeToIsoDate(rawDateAfter) : undefined;
if (rawDateAfter && !dateAfter) {
return jsonResult({

View File

@@ -31,6 +31,23 @@ function createPerplexitySearchTool(perplexityConfig?: { apiKey?: string }) {
});
}
function createBraveSearchTool(braveConfig?: { mode?: "web" | "llm-context" }) {
return createWebSearchTool({
config: {
tools: {
web: {
search: {
provider: "brave",
apiKey: "brave-config-test", // pragma: allowlist secret
...(braveConfig ? { brave: braveConfig } : {}),
},
},
},
},
sandboxed: true,
});
}
function createKimiSearchTool(kimiConfig?: { apiKey?: string; baseUrl?: string; model?: string }) {
return createWebSearchTool({
config: {
@@ -162,7 +179,7 @@ describe("web_search country and language parameters", () => {
}>,
) {
const mockFetch = installMockFetch({ web: { results: [] } });
const tool = createWebSearchTool({ config: undefined, sandboxed: true });
const tool = createBraveSearchTool();
expect(tool).not.toBeNull();
await tool?.execute?.("call-1", { query: "test", ...params });
expect(mockFetch).toHaveBeenCalled();
@@ -180,7 +197,7 @@ describe("web_search country and language parameters", () => {
it("should pass language parameter to Brave API as search_lang", async () => {
const mockFetch = installMockFetch({ web: { results: [] } });
const tool = createWebSearchTool({ config: undefined, sandboxed: true });
const tool = createBraveSearchTool();
await tool?.execute?.("call-1", { query: "test", language: "de" });
const url = new URL(mockFetch.mock.calls[0][0] as string);
@@ -204,7 +221,7 @@ describe("web_search country and language parameters", () => {
it("rejects unsupported Brave search_lang values before upstream request", async () => {
const mockFetch = installMockFetch({ web: { results: [] } });
const tool = createWebSearchTool({ config: undefined, sandboxed: true });
const tool = createBraveSearchTool();
const result = await tool?.execute?.("call-1", { query: "test", search_lang: "xx" });
expect(mockFetch).not.toHaveBeenCalled();
@@ -511,8 +528,27 @@ describe("web_search external content wrapping", () => {
return mock;
}
function installBraveLlmContextFetch(
result: Record<string, unknown>,
mock = vi.fn(async (_input: RequestInfo | URL, _init?: RequestInit) =>
Promise.resolve({
ok: true,
json: () =>
Promise.resolve({
grounding: {
generic: [result],
},
sources: [{ url: "https://example.com/ctx", hostname: "example.com" }],
}),
} as Response),
),
) {
global.fetch = withFetchPreconnect(mock);
return mock;
}
async function executeBraveSearch(query: string) {
const tool = createWebSearchTool({ config: undefined, sandboxed: true });
const tool = createBraveSearchTool();
return tool?.execute?.("call-1", { query });
}
@@ -545,6 +581,154 @@ describe("web_search external content wrapping", () => {
});
});
it("uses Brave llm-context endpoint when mode is configured", async () => {
vi.stubEnv("BRAVE_API_KEY", "test-key");
const mockFetch = installBraveLlmContextFetch({
title: "Context title",
url: "https://example.com/ctx",
snippets: [{ text: "Context chunk one" }, { text: "Context chunk two" }],
});
const tool = createWebSearchTool({
config: {
tools: {
web: {
search: {
provider: "brave",
brave: {
mode: "llm-context",
},
},
},
},
},
sandboxed: true,
});
const result = await tool?.execute?.("call-1", {
query: "llm-context test",
country: "DE",
search_lang: "de",
});
const requestUrl = new URL(mockFetch.mock.calls[0]?.[0] as string);
expect(requestUrl.pathname).toBe("/res/v1/llm/context");
expect(requestUrl.searchParams.get("q")).toBe("llm-context test");
expect(requestUrl.searchParams.get("country")).toBe("DE");
expect(requestUrl.searchParams.get("search_lang")).toBe("de");
const details = result?.details as {
mode?: string;
results?: Array<{
title?: string;
url?: string;
snippets?: string[];
siteName?: string;
}>;
sources?: Array<{ hostname?: string }>;
};
expect(details.mode).toBe("llm-context");
expect(details.results?.[0]?.url).toBe("https://example.com/ctx");
expect(details.results?.[0]?.title).toContain("<<<EXTERNAL_UNTRUSTED_CONTENT");
expect(details.results?.[0]?.snippets?.[0]).toContain("<<<EXTERNAL_UNTRUSTED_CONTENT");
expect(details.results?.[0]?.snippets?.[0]).toContain("Context chunk one");
expect(details.results?.[0]?.siteName).toBe("example.com");
expect(details.sources?.[0]?.hostname).toBe("example.com");
});
it("rejects freshness in Brave llm-context mode", async () => {
vi.stubEnv("BRAVE_API_KEY", "test-key");
const mockFetch = installBraveLlmContextFetch({
title: "unused",
url: "https://example.com",
snippets: ["unused"],
});
const tool = createWebSearchTool({
config: {
tools: {
web: {
search: {
provider: "brave",
brave: {
mode: "llm-context",
},
},
},
},
},
sandboxed: true,
});
const result = await tool?.execute?.("call-1", { query: "test", freshness: "week" });
expect(result?.details).toMatchObject({ error: "unsupported_freshness" });
expect(mockFetch).not.toHaveBeenCalled();
});
it("rejects date_after/date_before in Brave llm-context mode", async () => {
vi.stubEnv("BRAVE_API_KEY", "test-key");
const mockFetch = installBraveLlmContextFetch({
title: "unused",
url: "https://example.com",
snippets: ["unused"],
});
const tool = createWebSearchTool({
config: {
tools: {
web: {
search: {
provider: "brave",
brave: {
mode: "llm-context",
},
},
},
},
},
sandboxed: true,
});
const result = await tool?.execute?.("call-1", {
query: "test",
date_after: "2025-01-01",
date_before: "2025-01-31",
});
expect(result?.details).toMatchObject({ error: "unsupported_date_filter" });
expect(mockFetch).not.toHaveBeenCalled();
});
it("rejects ui_lang in Brave llm-context mode", async () => {
vi.stubEnv("BRAVE_API_KEY", "test-key");
const mockFetch = installBraveLlmContextFetch({
title: "unused",
url: "https://example.com",
snippets: ["unused"],
});
const tool = createWebSearchTool({
config: {
tools: {
web: {
search: {
provider: "brave",
brave: {
mode: "llm-context",
},
},
},
},
},
sandboxed: true,
});
const result = await tool?.execute?.("call-1", {
query: "test",
ui_lang: "de-DE",
});
expect(result?.details).toMatchObject({ error: "unsupported_ui_lang" });
expect(mockFetch).not.toHaveBeenCalled();
});
it("does not wrap Brave result urls (raw for tool chaining)", async () => {
vi.stubEnv("BRAVE_API_KEY", "test-key");
const url = "https://example.com/some-page";

View File

@@ -48,6 +48,32 @@ describe("web search provider config", () => {
expect(res.ok).toBe(true);
});
it("accepts brave llm-context mode config", () => {
const res = validateConfigObject(
buildWebSearchProviderConfig({
provider: "brave",
providerConfig: {
mode: "llm-context",
},
}),
);
expect(res.ok).toBe(true);
});
it("rejects invalid brave mode config values", () => {
const res = validateConfigObject(
buildWebSearchProviderConfig({
provider: "brave",
providerConfig: {
mode: "invalid-mode",
},
}),
);
expect(res.ok).toBe(false);
});
});
describe("web search provider auto-detection", () => {