feat: support freshness parameter for Perplexity web_search provider (#15343)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 01aba2bfba
Co-authored-by: echoVic <16428813+echoVic@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
This commit is contained in:
青雲
2026-02-14 11:18:16 +08:00
committed by GitHub
parent 7f227fc8cc
commit 89fa93ed75
6 changed files with 69 additions and 18 deletions

View File

@@ -31,6 +31,7 @@ const {
isDirectPerplexityBaseUrl,
resolvePerplexityRequestModel,
normalizeFreshness,
freshnessToPerplexityRecency,
resolveGrokApiKey,
resolveGrokModel,
resolveGrokInlineCitations,
@@ -128,6 +129,24 @@ describe("web_search freshness normalization", () => {
});
});
describe("freshnessToPerplexityRecency", () => {
it("maps Brave shortcuts to Perplexity recency values", () => {
expect(freshnessToPerplexityRecency("pd")).toBe("day");
expect(freshnessToPerplexityRecency("pw")).toBe("week");
expect(freshnessToPerplexityRecency("pm")).toBe("month");
expect(freshnessToPerplexityRecency("py")).toBe("year");
});
it("returns undefined for date ranges (not supported by Perplexity)", () => {
expect(freshnessToPerplexityRecency("2024-01-01to2024-01-31")).toBeUndefined();
});
it("returns undefined for undefined/empty input", () => {
expect(freshnessToPerplexityRecency(undefined)).toBeUndefined();
expect(freshnessToPerplexityRecency("")).toBeUndefined();
});
});
describe("web_search grok config resolution", () => {
it("uses config apiKey when provided", () => {
expect(resolveGrokApiKey({ apiKey: "xai-test-key" })).toBe("xai-test-key");

View File

@@ -64,7 +64,7 @@ const WebSearchSchema = Type.Object({
freshness: Type.Optional(
Type.String({
description:
"Filter results by discovery time (Brave only). Values: 'pd' (past 24h), 'pw' (past week), 'pm' (past month), 'py' (past year), or date range 'YYYY-MM-DDtoYYYY-MM-DD'.",
"Filter results by discovery time. Brave supports 'pd', 'pw', 'pm', 'py', and date range 'YYYY-MM-DDtoYYYY-MM-DD'. Perplexity supports 'pd', 'pw', 'pm', and 'py'.",
}),
),
});
@@ -403,6 +403,23 @@ function normalizeFreshness(value: string | undefined): string | undefined {
return `${start}to${end}`;
}
/**
* Map normalized freshness values (pd/pw/pm/py) to Perplexity's
* search_recency_filter values (day/week/month/year).
*/
function freshnessToPerplexityRecency(freshness: string | undefined): string | undefined {
if (!freshness) {
return undefined;
}
const map: Record<string, string> = {
pd: "day",
pw: "week",
pm: "month",
py: "year",
};
return map[freshness] ?? undefined;
}
function isValidIsoDate(value: string): boolean {
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
return false;
@@ -435,11 +452,27 @@ async function runPerplexitySearch(params: {
baseUrl: string;
model: string;
timeoutSeconds: number;
freshness?: string;
}): Promise<{ content: string; citations: string[] }> {
const baseUrl = params.baseUrl.trim().replace(/\/$/, "");
const endpoint = `${baseUrl}/chat/completions`;
const model = resolvePerplexityRequestModel(baseUrl, params.model);
const body: Record<string, unknown> = {
model,
messages: [
{
role: "user",
content: params.query,
},
],
};
const recencyFilter = freshnessToPerplexityRecency(params.freshness);
if (recencyFilter) {
body.search_recency_filter = recencyFilter;
}
const res = await fetch(endpoint, {
method: "POST",
headers: {
@@ -448,15 +481,7 @@ async function runPerplexitySearch(params: {
"HTTP-Referer": "https://openclaw.ai",
"X-Title": "OpenClaw Web Search",
},
body: JSON.stringify({
model,
messages: [
{
role: "user",
content: params.query,
},
],
}),
body: JSON.stringify(body),
signal: withTimeout(undefined, params.timeoutSeconds * 1000),
});
@@ -544,7 +569,7 @@ async function runWebSearch(params: {
params.provider === "brave"
? `${params.provider}:${params.query}:${params.count}:${params.country || "default"}:${params.search_lang || "default"}:${params.ui_lang || "default"}:${params.freshness || "default"}`
: params.provider === "perplexity"
? `${params.provider}:${params.query}:${params.perplexityBaseUrl ?? DEFAULT_PERPLEXITY_BASE_URL}:${params.perplexityModel ?? DEFAULT_PERPLEXITY_MODEL}`
? `${params.provider}:${params.query}:${params.perplexityBaseUrl ?? DEFAULT_PERPLEXITY_BASE_URL}:${params.perplexityModel ?? DEFAULT_PERPLEXITY_MODEL}:${params.freshness || "default"}`
: `${params.provider}:${params.query}:${params.grokModel ?? DEFAULT_GROK_MODEL}:${String(params.grokInlineCitations ?? false)}`,
);
const cached = readCache(SEARCH_CACHE, cacheKey);
@@ -561,6 +586,7 @@ async function runWebSearch(params: {
baseUrl: params.perplexityBaseUrl ?? DEFAULT_PERPLEXITY_BASE_URL,
model: params.perplexityModel ?? DEFAULT_PERPLEXITY_MODEL,
timeoutSeconds: params.timeoutSeconds,
freshness: params.freshness,
});
const payload = {
@@ -722,10 +748,10 @@ export function createWebSearchTool(options?: {
const search_lang = readStringParam(params, "search_lang");
const ui_lang = readStringParam(params, "ui_lang");
const rawFreshness = readStringParam(params, "freshness");
if (rawFreshness && provider !== "brave") {
if (rawFreshness && provider !== "brave" && provider !== "perplexity") {
return jsonResult({
error: "unsupported_freshness",
message: "freshness is only supported by the Brave web_search provider.",
message: "freshness is only supported by the Brave and Perplexity web_search providers.",
docs: "https://docs.openclaw.ai/tools/web",
});
}
@@ -769,6 +795,7 @@ export const __testing = {
isDirectPerplexityBaseUrl,
resolvePerplexityRequestModel,
normalizeFreshness,
freshnessToPerplexityRecency,
resolveGrokApiKey,
resolveGrokModel,
resolveGrokInlineCitations,

View File

@@ -159,7 +159,7 @@ describe("web_search perplexity baseUrl defaults", () => {
expect(body.model).toBe("sonar-pro");
});
it("rejects freshness for Perplexity provider", async () => {
it("passes freshness to Perplexity provider as search_recency_filter", async () => {
vi.stubEnv("PERPLEXITY_API_KEY", "pplx-test");
const mockFetch = vi.fn(() =>
Promise.resolve({
@@ -174,10 +174,11 @@ describe("web_search perplexity baseUrl defaults", () => {
config: { tools: { web: { search: { provider: "perplexity" } } } },
sandboxed: true,
});
const result = await tool?.execute?.(1, { query: "test", freshness: "pw" });
await tool?.execute?.(1, { query: "perplexity-freshness-test", freshness: "pw" });
expect(mockFetch).not.toHaveBeenCalled();
expect(result?.details).toMatchObject({ error: "unsupported_freshness" });
expect(mockFetch).toHaveBeenCalledOnce();
const body = JSON.parse(mockFetch.mock.calls[0][1].body as string);
expect(body.search_recency_filter).toBe("week");
});
it("defaults to OpenRouter when OPENROUTER_API_KEY is set", async () => {