diff --git a/extensions/google/src/gemini-web-search-provider.ts b/extensions/google/src/gemini-web-search-provider.ts index 3c7be2e7dfd..b5878da55e6 100644 --- a/extensions/google/src/gemini-web-search-provider.ts +++ b/extensions/google/src/gemini-web-search-provider.ts @@ -1,6 +1,7 @@ import { Type } from "@sinclair/typebox"; import { buildSearchCacheKey, + buildUnsupportedSearchFilterResponse, DEFAULT_SEARCH_COUNT, MAX_SEARCH_COUNT, readCachedSearchPayload, @@ -177,22 +178,9 @@ function createGeminiToolDefinition( parameters: createGeminiSchema(), execute: async (args) => { const params = args as Record; - for (const name of ["country", "language", "freshness", "date_after", "date_before"]) { - if (readStringParam(params, name)) { - const label = - name === "country" - ? "country filtering" - : name === "language" - ? "language filtering" - : name === "freshness" - ? "freshness filtering" - : "date_after/date_before filtering"; - return { - error: name.startsWith("date_") ? "unsupported_date_filter" : `unsupported_${name}`, - message: `${label} is not supported by the gemini provider. Only Brave and Perplexity support ${name === "country" ? "country filtering" : name === "language" ? "language filtering" : name === "freshness" ? "freshness" : "date filtering"}.`, - docs: "https://docs.openclaw.ai/tools/web", - }; - } + const unsupportedResponse = buildUnsupportedSearchFilterResponse(params, "gemini"); + if (unsupportedResponse) { + return unsupportedResponse; } const geminiConfig = resolveGeminiConfig(searchConfig); diff --git a/extensions/moonshot/src/kimi-web-search-provider.ts b/extensions/moonshot/src/kimi-web-search-provider.ts index db35822fbba..33f0f7e11cd 100644 --- a/extensions/moonshot/src/kimi-web-search-provider.ts +++ b/extensions/moonshot/src/kimi-web-search-provider.ts @@ -1,6 +1,7 @@ import { Type } from "@sinclair/typebox"; import { buildSearchCacheKey, + buildUnsupportedSearchFilterResponse, DEFAULT_SEARCH_COUNT, MAX_SEARCH_COUNT, readCachedSearchPayload, @@ -246,22 +247,9 @@ function createKimiToolDefinition( parameters: createKimiSchema(), execute: async (args) => { const params = args as Record; - for (const name of ["country", "language", "freshness", "date_after", "date_before"]) { - if (readStringParam(params, name)) { - const label = - name === "country" - ? "country filtering" - : name === "language" - ? "language filtering" - : name === "freshness" - ? "freshness filtering" - : "date_after/date_before filtering"; - return { - error: name.startsWith("date_") ? "unsupported_date_filter" : `unsupported_${name}`, - message: `${label} is not supported by the kimi provider. Only Brave and Perplexity support ${name === "country" ? "country filtering" : name === "language" ? "language filtering" : name === "freshness" ? "freshness" : "date filtering"}.`, - docs: "https://docs.openclaw.ai/tools/web", - }; - } + const unsupportedResponse = buildUnsupportedSearchFilterResponse(params, "kimi"); + if (unsupportedResponse) { + return unsupportedResponse; } const kimiConfig = resolveKimiConfig(searchConfig); diff --git a/extensions/xai/src/grok-web-search-provider.ts b/extensions/xai/src/grok-web-search-provider.ts index 11c1439f2d0..bc38abb6444 100644 --- a/extensions/xai/src/grok-web-search-provider.ts +++ b/extensions/xai/src/grok-web-search-provider.ts @@ -1,6 +1,7 @@ import { Type } from "@sinclair/typebox"; import { buildSearchCacheKey, + buildUnsupportedSearchFilterResponse, DEFAULT_SEARCH_COUNT, MAX_SEARCH_COUNT, readCachedSearchPayload, @@ -188,22 +189,9 @@ function createGrokToolDefinition( parameters: createGrokSchema(), execute: async (args) => { const params = args as Record; - for (const name of ["country", "language", "freshness", "date_after", "date_before"]) { - if (readStringParam(params, name)) { - const label = - name === "country" - ? "country filtering" - : name === "language" - ? "language filtering" - : name === "freshness" - ? "freshness filtering" - : "date_after/date_before filtering"; - return { - error: name.startsWith("date_") ? "unsupported_date_filter" : `unsupported_${name}`, - message: `${label} is not supported by the grok provider. Only Brave and Perplexity support ${name === "country" ? "country filtering" : name === "language" ? "language filtering" : name === "freshness" ? "freshness" : "date filtering"}.`, - docs: "https://docs.openclaw.ai/tools/web", - }; - } + const unsupportedResponse = buildUnsupportedSearchFilterResponse(params, "grok"); + if (unsupportedResponse) { + return unsupportedResponse; } const grokConfig = resolveGrokConfig(searchConfig); diff --git a/src/agents/tools/web-search-provider-common.ts b/src/agents/tools/web-search-provider-common.ts index 022054c5416..f69876ed04a 100644 --- a/src/agents/tools/web-search-provider-common.ts +++ b/src/agents/tools/web-search-provider-common.ts @@ -21,6 +21,13 @@ export type SearchConfigRecord = (NonNullable["web"] ex : never) & Record; +type UnsupportedWebSearchFilterName = + | "country" + | "language" + | "freshness" + | "date_after" + | "date_before"; + export const DEFAULT_SEARCH_COUNT = 5; export const MAX_SEARCH_COUNT = 10; @@ -210,3 +217,59 @@ export function writeCachedSearchPayload( ): void { writeCache(SEARCH_CACHE, cacheKey, payload, ttlMs); } + +function readUnsupportedSearchFilter( + params: Record, +): UnsupportedWebSearchFilterName | undefined { + for (const name of ["country", "language", "freshness", "date_after", "date_before"] as const) { + const value = params[name]; + if (typeof value === "string" && value.trim()) { + return name; + } + } + + return undefined; +} + +function describeUnsupportedSearchFilter(name: UnsupportedWebSearchFilterName): string { + switch (name) { + case "country": + return "country filtering"; + case "language": + return "language filtering"; + case "freshness": + return "freshness filtering"; + case "date_after": + case "date_before": + return "date_after/date_before filtering"; + } +} + +export function buildUnsupportedSearchFilterResponse( + params: Record, + provider: string, + docs = "https://docs.openclaw.ai/tools/web", +): + | { + error: string; + message: string; + docs: string; + } + | undefined { + const unsupported = readUnsupportedSearchFilter(params); + if (!unsupported) { + return undefined; + } + + const label = describeUnsupportedSearchFilter(unsupported); + const supportedLabel = + unsupported === "date_after" || unsupported === "date_before" ? "date filtering" : label; + + return { + error: unsupported.startsWith("date_") + ? "unsupported_date_filter" + : `unsupported_${unsupported}`, + message: `${label} is not supported by the ${provider} provider. Only Brave and Perplexity support ${supportedLabel}.`, + docs, + }; +} diff --git a/src/agents/tools/web-search.test.ts b/src/agents/tools/web-search.test.ts index 54242f362f0..ae7b517c788 100644 --- a/src/agents/tools/web-search.test.ts +++ b/src/agents/tools/web-search.test.ts @@ -3,6 +3,7 @@ import { __testing as braveTesting } from "../../../extensions/brave/src/brave-w import { __testing as moonshotTesting } from "../../../extensions/moonshot/src/kimi-web-search-provider.js"; import { __testing as perplexityTesting } from "../../../extensions/perplexity/web-search-provider.js"; import { __testing as xaiTesting } from "../../../extensions/xai/src/grok-web-search-provider.js"; +import { buildUnsupportedSearchFilterResponse } from "../../plugin-sdk/provider-web-search.js"; import { withEnv } from "../../test-utils/env.js"; const { inferPerplexityBaseUrlFromApiKey, @@ -198,6 +199,30 @@ describe("web_search date normalization", () => { }); }); +describe("web_search unsupported filter response", () => { + it("returns undefined when no unsupported filter is set", () => { + expect(buildUnsupportedSearchFilterResponse({ query: "openclaw" }, "gemini")).toBeUndefined(); + }); + + it("maps non-date filters to provider-specific unsupported errors", () => { + expect(buildUnsupportedSearchFilterResponse({ country: "us" }, "grok")).toEqual({ + error: "unsupported_country", + message: + "country filtering is not supported by the grok provider. Only Brave and Perplexity support country filtering.", + docs: "https://docs.openclaw.ai/tools/web", + }); + }); + + it("collapses date filters to unsupported_date_filter", () => { + expect(buildUnsupportedSearchFilterResponse({ date_before: "2026-03-19" }, "kimi")).toEqual({ + error: "unsupported_date_filter", + message: + "date_after/date_before filtering is not supported by the kimi provider. Only Brave and Perplexity support date filtering.", + docs: "https://docs.openclaw.ai/tools/web", + }); + }); +}); + describe("web_search kimi config resolution", () => { it("uses config apiKey when provided", () => { expect(resolveKimiApiKey({ apiKey: "kimi-test-key" })).toBe("kimi-test-key"); diff --git a/src/plugin-sdk/provider-web-search.ts b/src/plugin-sdk/provider-web-search.ts index 36de7dbc775..78c2fff4ce3 100644 --- a/src/plugin-sdk/provider-web-search.ts +++ b/src/plugin-sdk/provider-web-search.ts @@ -9,6 +9,7 @@ export { readNumberParam, readStringArrayParam, readStringParam } from "../agent export { resolveCitationRedirectUrl } from "../agents/tools/web-search-citation-redirect.js"; export { buildSearchCacheKey, + buildUnsupportedSearchFilterResponse, DEFAULT_SEARCH_COUNT, FRESHNESS_TO_RECENCY, isoToPerplexityDate,