mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:00:42 +00:00
fix(web-search): support Brave llm-context date filters
This commit is contained in:
@@ -42,6 +42,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Web search: point missing-key errors to `web_fetch` for known URLs and the browser tool for interactive pages. Thanks @zhaoyang97.
|
||||
- Web search: late-bind managed agent `web_search` calls to the current runtime config snapshot, so existing sessions do not keep stale unresolved SecretRefs after secrets reload. Fixes #75420. Thanks @richardmqq.
|
||||
- Web search: honor `baseUrl` overrides for Gemini, Grok, and x_search provider-owned config, so proxy-backed search tools no longer dial hardcoded public endpoints. Supersedes #61972. Thanks @Lanfei.
|
||||
- Web search/Brave: allow `freshness` and bounded date ranges in `llm-context` mode, matching Brave's documented LLM Context API support. Supersedes #51005. Thanks @remusao.
|
||||
- Web fetch: resolve external plugin `webFetchProviders` for non-sandboxed `web_fetch`, while keeping sandboxed fetches limited to bundled providers. Fixes #74915. Thanks @ultrahighsuper and @mingmingtsao.
|
||||
- Heartbeat: strip legacy `[TOOL_CALL]...[/TOOL_CALL]` and `[TOOL_RESULT]...[/TOOL_RESULT]` pseudo-call blocks from heartbeat replies before channel delivery. Fixes #54138. Thanks @Deniable9570.
|
||||
- macOS/Voice Wake: send wake-word and Push-to-Talk transcripts through the selected macOS session target instead of always falling back to main WebChat. Fixes #51040. Thanks @carl-jeffrolc.
|
||||
|
||||
@@ -120,7 +120,7 @@ await web_search({
|
||||
- Each Brave plan includes **\$5/month in free credit** (renewing). The Search plan costs \$5 per 1,000 requests, so the credit covers 1,000 queries/month. Set your usage limit in the Brave dashboard to avoid unexpected charges. See the [Brave API portal](https://brave.com/search/api/) for current plans.
|
||||
- The Search plan includes the LLM Context endpoint and AI inference rights. Storing results to train or tune models requires a plan with explicit storage rights. See the Brave [Terms of Service](https://api-dashboard.search.brave.com/terms-of-service).
|
||||
- `llm-context` mode returns grounded source entries instead of the normal web-search snippet shape.
|
||||
- `llm-context` mode does not support `ui_lang`, `freshness`, `date_after`, or `date_before`.
|
||||
- `llm-context` mode supports `freshness` and bounded `date_after` + `date_before` ranges. It does not support `ui_lang`; `date_before` without `date_after` is rejected because Brave requires custom freshness ranges to include both start and end dates.
|
||||
- `ui_lang` must include a region subtag like `en-US`.
|
||||
- Results are cached for 15 minutes by default (configurable via `cacheTtlMinutes`).
|
||||
|
||||
|
||||
@@ -296,7 +296,8 @@ show the `x_search` prompt.
|
||||
|
||||
<Warning>
|
||||
Not all parameters work with all providers. Brave `llm-context` mode
|
||||
rejects `ui_lang`, `freshness`, `date_after`, and `date_before`.
|
||||
rejects `ui_lang`; `date_before` also needs `date_after` because Brave custom
|
||||
freshness ranges require both start and end dates.
|
||||
Gemini, Grok, and Kimi return one synthesized answer with citations. They
|
||||
accept `count` for shared-tool compatibility, but it does not change the
|
||||
grounded answer shape.
|
||||
|
||||
@@ -65,6 +65,8 @@ async function runBraveLlmContextSearch(params: {
|
||||
country?: string;
|
||||
search_lang?: string;
|
||||
freshness?: string;
|
||||
dateAfter?: string;
|
||||
dateBefore?: string;
|
||||
}): Promise<{
|
||||
results: Array<{
|
||||
url: string;
|
||||
@@ -84,6 +86,13 @@ async function runBraveLlmContextSearch(params: {
|
||||
}
|
||||
if (params.freshness) {
|
||||
url.searchParams.set("freshness", params.freshness);
|
||||
} else if (params.dateAfter && params.dateBefore) {
|
||||
url.searchParams.set("freshness", `${params.dateAfter}to${params.dateBefore}`);
|
||||
} else if (params.dateAfter) {
|
||||
url.searchParams.set(
|
||||
"freshness",
|
||||
`${params.dateAfter}to${new Date().toISOString().slice(0, 10)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return withTrustedWebSearchEndpoint(
|
||||
@@ -235,14 +244,6 @@ export async function executeBraveSearch(
|
||||
}
|
||||
|
||||
const rawFreshness = readStringParam(args, "freshness");
|
||||
if (rawFreshness && braveMode === "llm-context") {
|
||||
return {
|
||||
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, "brave") : undefined;
|
||||
if (rawFreshness && !freshness) {
|
||||
return {
|
||||
@@ -262,15 +263,6 @@ export async function executeBraveSearch(
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
};
|
||||
}
|
||||
if ((rawDateAfter || rawDateBefore) && braveMode === "llm-context") {
|
||||
return {
|
||||
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 parsedDateRange = parseIsoDateRange({
|
||||
rawDateAfter,
|
||||
rawDateBefore,
|
||||
@@ -283,18 +275,53 @@ export async function executeBraveSearch(
|
||||
}
|
||||
|
||||
const { dateAfter, dateBefore } = parsedDateRange;
|
||||
const cacheKey = buildSearchCacheKey([
|
||||
"brave",
|
||||
braveMode,
|
||||
query,
|
||||
resolveSearchCount(count, DEFAULT_SEARCH_COUNT),
|
||||
country,
|
||||
normalizedLanguage.search_lang,
|
||||
normalizedLanguage.ui_lang,
|
||||
freshness,
|
||||
dateAfter,
|
||||
dateBefore,
|
||||
]);
|
||||
if (braveMode === "llm-context") {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
if (dateAfter && !dateBefore && dateAfter > today) {
|
||||
return {
|
||||
error: "invalid_date_range",
|
||||
message: "date_after cannot be in the future for Brave llm-context mode.",
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
};
|
||||
}
|
||||
if (dateBefore && !dateAfter) {
|
||||
return {
|
||||
error: "unsupported_date_filter",
|
||||
message:
|
||||
"Brave llm-context mode requires date_after when date_before is set. Use a bounded date range or freshness.",
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
};
|
||||
}
|
||||
}
|
||||
const llmContextDateEnd =
|
||||
braveMode === "llm-context" && dateAfter
|
||||
? (dateBefore ?? new Date().toISOString().slice(0, 10))
|
||||
: dateBefore;
|
||||
const cacheKey = buildSearchCacheKey(
|
||||
braveMode === "llm-context"
|
||||
? [
|
||||
"brave",
|
||||
braveMode,
|
||||
query,
|
||||
country,
|
||||
normalizedLanguage.search_lang,
|
||||
freshness,
|
||||
dateAfter,
|
||||
llmContextDateEnd,
|
||||
]
|
||||
: [
|
||||
"brave",
|
||||
braveMode,
|
||||
query,
|
||||
resolveSearchCount(count, DEFAULT_SEARCH_COUNT),
|
||||
country,
|
||||
normalizedLanguage.search_lang,
|
||||
normalizedLanguage.ui_lang,
|
||||
freshness,
|
||||
dateAfter,
|
||||
dateBefore,
|
||||
],
|
||||
);
|
||||
const cached = readCachedSearchPayload(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
@@ -312,6 +339,8 @@ export async function executeBraveSearch(
|
||||
country: country ?? undefined,
|
||||
search_lang: normalizedLanguage.search_lang,
|
||||
freshness,
|
||||
dateAfter,
|
||||
dateBefore,
|
||||
});
|
||||
const payload = {
|
||||
query,
|
||||
|
||||
@@ -10,6 +10,28 @@ const braveManifest = JSON.parse(
|
||||
configSchema?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
function installBraveLlmContextFetch() {
|
||||
const mockFetch = vi.fn(async (_input?: unknown, _init?: unknown) => {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
grounding: {
|
||||
generic: [
|
||||
{
|
||||
url: "https://example.com/context",
|
||||
title: "Context",
|
||||
snippets: ["snippet"],
|
||||
},
|
||||
],
|
||||
},
|
||||
sources: [],
|
||||
}),
|
||||
} as Response;
|
||||
});
|
||||
global.fetch = mockFetch as typeof global.fetch;
|
||||
return mockFetch;
|
||||
}
|
||||
|
||||
describe("brave web search provider", () => {
|
||||
const priorFetch = global.fetch;
|
||||
|
||||
@@ -176,6 +198,129 @@ describe("brave web search provider", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("passes freshness to Brave llm-context endpoint", async () => {
|
||||
vi.stubEnv("BRAVE_API_KEY", "test-key");
|
||||
const mockFetch = installBraveLlmContextFetch();
|
||||
const provider = createBraveWebSearchProvider();
|
||||
const tool = provider.createTool({
|
||||
config: {},
|
||||
searchConfig: {
|
||||
apiKey: "BSA...",
|
||||
brave: { mode: "llm-context" },
|
||||
},
|
||||
});
|
||||
if (!tool) {
|
||||
throw new Error("Expected tool definition");
|
||||
}
|
||||
|
||||
await tool.execute({ query: "latest ai news", freshness: "week" });
|
||||
|
||||
const requestUrl = new URL(String(mockFetch.mock.calls[0]?.[0]));
|
||||
expect(requestUrl.pathname).toBe("/res/v1/llm/context");
|
||||
expect(requestUrl.searchParams.get("freshness")).toBe("pw");
|
||||
});
|
||||
|
||||
it("passes bounded date ranges to Brave llm-context endpoint", async () => {
|
||||
vi.stubEnv("BRAVE_API_KEY", "test-key");
|
||||
const mockFetch = installBraveLlmContextFetch();
|
||||
const provider = createBraveWebSearchProvider();
|
||||
const tool = provider.createTool({
|
||||
config: {},
|
||||
searchConfig: {
|
||||
apiKey: "BSA...",
|
||||
brave: { mode: "llm-context" },
|
||||
},
|
||||
});
|
||||
if (!tool) {
|
||||
throw new Error("Expected tool definition");
|
||||
}
|
||||
|
||||
await tool.execute({
|
||||
query: "latest ai news",
|
||||
date_after: "2025-01-01",
|
||||
date_before: "2025-01-31",
|
||||
});
|
||||
|
||||
const requestUrl = new URL(String(mockFetch.mock.calls[0]?.[0]));
|
||||
expect(requestUrl.pathname).toBe("/res/v1/llm/context");
|
||||
expect(requestUrl.searchParams.get("freshness")).toBe("2025-01-01to2025-01-31");
|
||||
});
|
||||
|
||||
it("uses today as the end date for Brave llm-context date_after-only ranges", async () => {
|
||||
vi.stubEnv("BRAVE_API_KEY", "test-key");
|
||||
const mockFetch = installBraveLlmContextFetch();
|
||||
const provider = createBraveWebSearchProvider();
|
||||
const tool = provider.createTool({
|
||||
config: {},
|
||||
searchConfig: {
|
||||
apiKey: "BSA...",
|
||||
brave: { mode: "llm-context" },
|
||||
},
|
||||
});
|
||||
if (!tool) {
|
||||
throw new Error("Expected tool definition");
|
||||
}
|
||||
|
||||
await tool.execute({ query: "latest ai news", date_after: "2025-01-01" });
|
||||
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const requestUrl = new URL(String(mockFetch.mock.calls[0]?.[0]));
|
||||
expect(requestUrl.pathname).toBe("/res/v1/llm/context");
|
||||
expect(requestUrl.searchParams.get("freshness")).toBe(`2025-01-01to${today}`);
|
||||
});
|
||||
|
||||
it("rejects future Brave llm-context date_after-only ranges before fetch", async () => {
|
||||
vi.stubEnv("BRAVE_API_KEY", "test-key");
|
||||
const mockFetch = installBraveLlmContextFetch();
|
||||
const provider = createBraveWebSearchProvider();
|
||||
const tool = provider.createTool({
|
||||
config: {},
|
||||
searchConfig: {
|
||||
apiKey: "BSA...",
|
||||
brave: { mode: "llm-context" },
|
||||
},
|
||||
});
|
||||
if (!tool) {
|
||||
throw new Error("Expected tool definition");
|
||||
}
|
||||
|
||||
const result = await tool.execute({
|
||||
query: "latest ai news",
|
||||
date_after: "2999-01-01",
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
error: "invalid_date_range",
|
||||
});
|
||||
expect(mockFetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects Brave llm-context date_before-only ranges before fetch", async () => {
|
||||
vi.stubEnv("BRAVE_API_KEY", "test-key");
|
||||
const mockFetch = installBraveLlmContextFetch();
|
||||
const provider = createBraveWebSearchProvider();
|
||||
const tool = provider.createTool({
|
||||
config: {},
|
||||
searchConfig: {
|
||||
apiKey: "BSA...",
|
||||
brave: { mode: "llm-context" },
|
||||
},
|
||||
});
|
||||
if (!tool) {
|
||||
throw new Error("Expected tool definition");
|
||||
}
|
||||
|
||||
const result = await tool.execute({
|
||||
query: "latest ai news",
|
||||
date_before: "2025-01-31",
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
error: "unsupported_date_filter",
|
||||
});
|
||||
expect(mockFetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back unsupported country values before calling Brave", async () => {
|
||||
vi.stubEnv("BRAVE_API_KEY", "test-key");
|
||||
const mockFetch = vi.fn(async (_input?: unknown, _init?: unknown) => {
|
||||
|
||||
Reference in New Issue
Block a user