From 87ff446588dc92e7c45c86cff6ee1bd79da8819c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 25 Aug 2026 22:05:50 -0700 Subject: [PATCH] refactor(perplexity): unify search option validation (#129877) --- .../perplexity-web-search-provider.runtime.ts | 104 +++++-------- .../perplexity-web-search-provider.test.ts | 143 ++++++++++++++++++ 2 files changed, 177 insertions(+), 70 deletions(-) diff --git a/extensions/perplexity/src/perplexity-web-search-provider.runtime.ts b/extensions/perplexity/src/perplexity-web-search-provider.runtime.ts index d846dbf5252f..bd801e1e2269 100644 --- a/extensions/perplexity/src/perplexity-web-search-provider.runtime.ts +++ b/extensions/perplexity/src/perplexity-web-search-provider.runtime.ts @@ -6,7 +6,7 @@ import { isoToPerplexityDate, MAX_SEARCH_COUNT, normalizeFreshness, - normalizeToIsoDate, + parseWebSearchTimeFilters, readCachedSearchPayload, readConfiguredSecretString, readPositiveIntegerParam, @@ -361,45 +361,26 @@ export async function executePerplexitySearch( }); if (!structured) { - if (country) { - return { - error: "unsupported_country", - message: - "country filtering is only supported by the native Perplexity Search API path. Remove Perplexity baseUrl/model overrides or use a direct PERPLEXITY_API_KEY to enable it.", - docs: "https://docs.openclaw.ai/tools/web", - }; - } - if (language) { - return { - error: "unsupported_language", - message: - "language filtering is only supported by the native Perplexity Search API path. Remove Perplexity baseUrl/model overrides or use a direct PERPLEXITY_API_KEY to enable it.", - docs: "https://docs.openclaw.ai/tools/web", - }; - } - if (rawDateAfter || rawDateBefore) { - return { - error: "unsupported_date_filter", - message: - "date_after/date_before are only supported by the native Perplexity Search API path. Remove Perplexity baseUrl/model overrides or use a direct PERPLEXITY_API_KEY to enable them.", - docs: "https://docs.openclaw.ai/tools/web", - }; - } - if (domainFilter?.length) { - return { - error: "unsupported_domain_filter", - message: - "domain_filter is only supported by the native Perplexity Search API path. Remove Perplexity baseUrl/model overrides or use a direct PERPLEXITY_API_KEY to enable it.", - docs: "https://docs.openclaw.ai/tools/web", - }; - } - if (maxTokens !== undefined || maxTokensPerPage !== undefined) { - return { - error: "unsupported_content_budget", - message: - "max_tokens and max_tokens_per_page are only supported by the native Perplexity Search API path. Remove Perplexity baseUrl/model overrides or use a direct PERPLEXITY_API_KEY to enable them.", - docs: "https://docs.openclaw.ai/tools/web", - }; + const unsupportedOptions = [ + [country, "unsupported_country", "country filtering", "it"], + [language, "unsupported_language", "language filtering", "it"], + [rawDateAfter || rawDateBefore, "unsupported_date_filter", "date_after/date_before", "them"], + [domainFilter?.length, "unsupported_domain_filter", "domain_filter", "it"], + [ + maxTokens !== undefined || maxTokensPerPage !== undefined, + "unsupported_content_budget", + "max_tokens and max_tokens_per_page", + "them", + ], + ] as const; + for (const [value, error, option, pronoun] of unsupportedOptions) { + if (value) { + return { + error, + message: `${option} ${pronoun === "them" ? "are" : "is"} only supported by the native Perplexity Search API path. Remove Perplexity baseUrl/model overrides or use a direct PERPLEXITY_API_KEY to enable ${pronoun}.`, + docs: "https://docs.openclaw.ai/tools/web", + }; + } } } @@ -410,37 +391,20 @@ export async function executePerplexitySearch( docs: "https://docs.openclaw.ai/tools/web", }; } - if (rawFreshness && (rawDateAfter || rawDateBefore)) { - return { - error: "conflicting_time_filters", - message: - "freshness and date_after/date_before cannot be used together. Use either freshness (day/week/month/year) or a date range (date_after/date_before), not both.", - docs: "https://docs.openclaw.ai/tools/web", - }; - } - const dateAfter = rawDateAfter ? normalizeToIsoDate(rawDateAfter) : undefined; - const dateBefore = rawDateBefore ? normalizeToIsoDate(rawDateBefore) : undefined; - if (rawDateAfter && !dateAfter) { - return { - error: "invalid_date", - message: "date_after must be YYYY-MM-DD format.", - docs: "https://docs.openclaw.ai/tools/web", - }; - } - if (rawDateBefore && !dateBefore) { - return { - error: "invalid_date", - message: "date_before must be YYYY-MM-DD format.", - docs: "https://docs.openclaw.ai/tools/web", - }; - } - if (dateAfter && dateBefore && dateAfter > dateBefore) { - return { - error: "invalid_date_range", - message: "date_after must be before date_before.", - docs: "https://docs.openclaw.ai/tools/web", - }; + const parsedTimeFilters = parseWebSearchTimeFilters({ + rawFreshness, + rawDateAfter, + rawDateBefore, + freshnessProvider: "perplexity", + invalidFreshnessMessage: "freshness must be day, week, month, or year.", + invalidDateAfterMessage: "date_after must be YYYY-MM-DD format.", + invalidDateBeforeMessage: "date_before must be YYYY-MM-DD format.", + invalidDateRangeMessage: "date_after must be before date_before.", + }); + if ("error" in parsedTimeFilters) { + return parsedTimeFilters; } + const { dateAfter, dateBefore } = parsedTimeFilters; if (domainFilter?.length) { const hasDeny = domainFilter.some((entry) => entry.startsWith("-")); const hasAllow = domainFilter.some((entry) => !entry.startsWith("-")); diff --git a/extensions/perplexity/src/perplexity-web-search-provider.test.ts b/extensions/perplexity/src/perplexity-web-search-provider.test.ts index 5745e2486b0a..fb6f432068cc 100644 --- a/extensions/perplexity/src/perplexity-web-search-provider.test.ts +++ b/extensions/perplexity/src/perplexity-web-search-provider.test.ts @@ -69,6 +69,149 @@ describe("perplexity web search provider", () => { ); }); + it.each([ + { + name: "country before every other unsupported chat option", + structured: false, + args: { + country: "US", + language: "en", + date_after: "2024-01-01", + domain_filter: ["a.test"], + max_tokens: 1, + }, + error: "unsupported_country", + message: + "country filtering is only supported by the native Perplexity Search API path. Remove Perplexity baseUrl/model overrides or use a direct PERPLEXITY_API_KEY to enable it.", + }, + { + name: "language before unsupported chat dates, domains, and budget", + structured: false, + args: { language: "en", date_after: "2024-01-01", domain_filter: ["a.test"], max_tokens: 1 }, + error: "unsupported_language", + message: + "language filtering is only supported by the native Perplexity Search API path. Remove Perplexity baseUrl/model overrides or use a direct PERPLEXITY_API_KEY to enable it.", + }, + { + name: "date before unsupported chat domains and budget", + structured: false, + args: { date_after: "2024-01-01", domain_filter: ["a.test"], max_tokens: 1 }, + error: "unsupported_date_filter", + message: + "date_after/date_before are only supported by the native Perplexity Search API path. Remove Perplexity baseUrl/model overrides or use a direct PERPLEXITY_API_KEY to enable them.", + }, + { + name: "unsupported chat language before language validation and dates", + structured: false, + args: { language: "invalid", date_after: "2024-01-01" }, + error: "unsupported_language", + message: + "language filtering is only supported by the native Perplexity Search API path. Remove Perplexity baseUrl/model overrides or use a direct PERPLEXITY_API_KEY to enable it.", + }, + { + name: "unsupported chat date before a valid freshness conflict", + structured: false, + args: { freshness: "day", date_after: "2024-01-01" }, + error: "unsupported_date_filter", + message: + "date_after/date_before are only supported by the native Perplexity Search API path. Remove Perplexity baseUrl/model overrides or use a direct PERPLEXITY_API_KEY to enable them.", + }, + { + name: "domain before unsupported chat content budget", + structured: false, + args: { domain_filter: ["a.test"], max_tokens: 1 }, + error: "unsupported_domain_filter", + message: + "domain_filter is only supported by the native Perplexity Search API path. Remove Perplexity baseUrl/model overrides or use a direct PERPLEXITY_API_KEY to enable it.", + }, + { + name: "unsupported chat content budget", + structured: false, + args: { max_tokens_per_page: 1 }, + error: "unsupported_content_budget", + message: + "max_tokens and max_tokens_per_page are only supported by the native Perplexity Search API path. Remove Perplexity baseUrl/model overrides or use a direct PERPLEXITY_API_KEY to enable them.", + }, + { + name: "invalid freshness before reading an invalid native budget", + structured: true, + args: { freshness: "invalid", max_tokens: 0 }, + error: "invalid_freshness", + message: "freshness must be day, week, month, or year.", + }, + { + name: "invalid freshness before reading an invalid chat budget", + structured: false, + args: { freshness: "invalid", country: "US", max_tokens: 0 }, + error: "invalid_freshness", + message: "freshness must be day, week, month, or year.", + }, + { + name: "invalid native language before conflicting date filters", + structured: true, + args: { language: "invalid", freshness: "day", date_after: "invalid" }, + error: "invalid_language", + message: "language must be a 2-letter ISO 639-1 code like 'en', 'de', or 'fr'.", + }, + { + name: "conflicting freshness before invalid date format", + structured: true, + args: { freshness: "day", date_after: "invalid" }, + error: "conflicting_time_filters", + message: + "freshness and date_after/date_before cannot be used together. Use either freshness (day/week/month/year) or a date range (date_after/date_before), not both.", + }, + { + name: "invalid date_after before invalid date_before", + structured: true, + args: { date_after: "invalid", date_before: "also-invalid" }, + error: "invalid_date", + message: "date_after must be YYYY-MM-DD format.", + }, + { + name: "invalid date_before after valid date_after", + structured: true, + args: { date_after: "2024-01-01", date_before: "invalid" }, + error: "invalid_date", + message: "date_before must be YYYY-MM-DD format.", + }, + { + name: "invalid chronological date range", + structured: true, + args: { date_after: "2024-06-01", date_before: "2024-01-01" }, + error: "invalid_date_range", + message: "date_after must be before date_before.", + }, + { + name: "invalid date before mixed native domain filters", + structured: true, + args: { date_after: "invalid", domain_filter: ["allowed.test", "-denied.test"] }, + error: "invalid_date", + message: "date_after must be YYYY-MM-DD format.", + }, + ])( + "preserves provider validation precedence: $name", + async ({ structured, args, error, message }) => { + await expect( + createConfiguredPerplexityTool(structured).execute({ query: "validation", ...args }), + ).resolves.toEqual({ + error, + message, + docs: "https://docs.openclaw.ai/tools/web", + }); + }, + ); + + it("validates chat token budgets before unsupported country precedence", async () => { + await expect( + createConfiguredPerplexityTool(false).execute({ + query: "validation", + country: "US", + max_tokens: 0, + }), + ).rejects.toThrow("max_tokens must be a positive integer."); + }); + it.each([ { name: "native Search API", webSearch: { apiKey: "pplx-test" } }, {