mirror of https://github.com/openclaw/openclaw.git
fix(brave-search): clarify ui_lang and search_lang format requirements (#25130)
* fix(brave-search): swap ui_lang and search_lang formats (#23826) * fix(web-search): normalize Brave ui_lang/search_lang params --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
parent
b564b72dc9
commit
6e97470515
|
|
@ -7,6 +7,7 @@ const {
|
|||
resolvePerplexityBaseUrl,
|
||||
isDirectPerplexityBaseUrl,
|
||||
resolvePerplexityRequestModel,
|
||||
normalizeBraveLanguageParams,
|
||||
normalizeFreshness,
|
||||
freshnessToPerplexityRecency,
|
||||
resolveGrokApiKey,
|
||||
|
|
@ -93,6 +94,28 @@ describe("web_search perplexity model normalization", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("web_search brave language param normalization", () => {
|
||||
it("normalizes and auto-corrects swapped Brave language params", () => {
|
||||
expect(normalizeBraveLanguageParams({ search_lang: "tr-TR", ui_lang: "tr" })).toEqual({
|
||||
search_lang: "tr",
|
||||
ui_lang: "tr-TR",
|
||||
});
|
||||
expect(normalizeBraveLanguageParams({ search_lang: "EN", ui_lang: "en-us" })).toEqual({
|
||||
search_lang: "en",
|
||||
ui_lang: "en-US",
|
||||
});
|
||||
});
|
||||
|
||||
it("flags invalid Brave language formats", () => {
|
||||
expect(normalizeBraveLanguageParams({ search_lang: "en-US" })).toEqual({
|
||||
invalidField: "search_lang",
|
||||
});
|
||||
expect(normalizeBraveLanguageParams({ ui_lang: "en" })).toEqual({
|
||||
invalidField: "ui_lang",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("web_search freshness normalization", () => {
|
||||
it("accepts Brave shortcut values", () => {
|
||||
expect(normalizeFreshness("pd")).toBe("pd");
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ const KIMI_WEB_SEARCH_TOOL = {
|
|||
const SEARCH_CACHE = new Map<string, CacheEntry<Record<string, unknown>>>();
|
||||
const BRAVE_FRESHNESS_SHORTCUTS = new Set(["pd", "pw", "pm", "py"]);
|
||||
const BRAVE_FRESHNESS_RANGE = /^(\d{4}-\d{2}-\d{2})to(\d{4}-\d{2}-\d{2})$/;
|
||||
const BRAVE_SEARCH_LANG_CODE = /^[a-z]{2}$/i;
|
||||
const BRAVE_UI_LANG_LOCALE = /^([a-z]{2})-([a-z]{2})$/i;
|
||||
const TRUSTED_NETWORK_SSRF_POLICY = { dangerouslyAllowPrivateNetwork: true } as const;
|
||||
|
||||
const WebSearchSchema = Type.Object({
|
||||
|
|
@ -62,12 +64,14 @@ const WebSearchSchema = Type.Object({
|
|||
),
|
||||
search_lang: Type.Optional(
|
||||
Type.String({
|
||||
description: "ISO language code for search results (e.g., 'de', 'en', 'fr').",
|
||||
description:
|
||||
"Short ISO language code for search results (e.g., 'de', 'en', 'fr', 'tr'). Must be a 2-letter code, NOT a locale.",
|
||||
}),
|
||||
),
|
||||
ui_lang: Type.Optional(
|
||||
Type.String({
|
||||
description: "ISO language code for UI elements.",
|
||||
description:
|
||||
"Locale code for UI elements in language-region format (e.g., 'en-US', 'de-DE', 'fr-FR', 'tr-TR'). Must include region subtag.",
|
||||
}),
|
||||
),
|
||||
freshness: Type.Optional(
|
||||
|
|
@ -705,6 +709,62 @@ function resolveSearchCount(value: unknown, fallback: number): number {
|
|||
return clamped;
|
||||
}
|
||||
|
||||
function normalizeBraveSearchLang(value: string | undefined): string | undefined {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed || !BRAVE_SEARCH_LANG_CODE.test(trimmed)) {
|
||||
return undefined;
|
||||
}
|
||||
return trimmed.toLowerCase();
|
||||
}
|
||||
|
||||
function normalizeBraveUiLang(value: string | undefined): string | undefined {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
const match = trimmed.match(BRAVE_UI_LANG_LOCALE);
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
const [, language, region] = match;
|
||||
return `${language.toLowerCase()}-${region.toUpperCase()}`;
|
||||
}
|
||||
|
||||
function normalizeBraveLanguageParams(params: { search_lang?: string; ui_lang?: string }): {
|
||||
search_lang?: string;
|
||||
ui_lang?: string;
|
||||
invalidField?: "search_lang" | "ui_lang";
|
||||
} {
|
||||
const rawSearchLang = params.search_lang?.trim() || undefined;
|
||||
const rawUiLang = params.ui_lang?.trim() || undefined;
|
||||
let searchLangCandidate = rawSearchLang;
|
||||
let uiLangCandidate = rawUiLang;
|
||||
|
||||
// Recover common LLM mix-up: locale in search_lang + short code in ui_lang.
|
||||
if (normalizeBraveUiLang(rawSearchLang) && normalizeBraveSearchLang(rawUiLang)) {
|
||||
searchLangCandidate = rawUiLang;
|
||||
uiLangCandidate = rawSearchLang;
|
||||
}
|
||||
|
||||
const search_lang = normalizeBraveSearchLang(searchLangCandidate);
|
||||
if (searchLangCandidate && !search_lang) {
|
||||
return { invalidField: "search_lang" };
|
||||
}
|
||||
|
||||
const ui_lang = normalizeBraveUiLang(uiLangCandidate);
|
||||
if (uiLangCandidate && !ui_lang) {
|
||||
return { invalidField: "ui_lang" };
|
||||
}
|
||||
|
||||
return { search_lang, ui_lang };
|
||||
}
|
||||
|
||||
function normalizeFreshness(value: string | undefined): string | undefined {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
|
|
@ -1289,8 +1349,29 @@ export function createWebSearchTool(options?: {
|
|||
const count =
|
||||
readNumberParam(params, "count", { integer: true }) ?? search?.maxResults ?? undefined;
|
||||
const country = readStringParam(params, "country");
|
||||
const search_lang = readStringParam(params, "search_lang");
|
||||
const ui_lang = readStringParam(params, "ui_lang");
|
||||
const rawSearchLang = readStringParam(params, "search_lang");
|
||||
const rawUiLang = readStringParam(params, "ui_lang");
|
||||
const normalizedBraveLanguageParams =
|
||||
provider === "brave"
|
||||
? normalizeBraveLanguageParams({ search_lang: rawSearchLang, ui_lang: rawUiLang })
|
||||
: { search_lang: rawSearchLang, ui_lang: rawUiLang };
|
||||
if (normalizedBraveLanguageParams.invalidField === "search_lang") {
|
||||
return jsonResult({
|
||||
error: "invalid_search_lang",
|
||||
message:
|
||||
"search_lang must be a 2-letter ISO language code like 'en' (not a locale like 'en-US').",
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
});
|
||||
}
|
||||
if (normalizedBraveLanguageParams.invalidField === "ui_lang") {
|
||||
return jsonResult({
|
||||
error: "invalid_ui_lang",
|
||||
message: "ui_lang must be a language-region locale like 'en-US'.",
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
});
|
||||
}
|
||||
const search_lang = normalizedBraveLanguageParams.search_lang;
|
||||
const ui_lang = normalizedBraveLanguageParams.ui_lang;
|
||||
const rawFreshness = readStringParam(params, "freshness");
|
||||
if (rawFreshness && provider !== "brave" && provider !== "perplexity") {
|
||||
return jsonResult({
|
||||
|
|
@ -1342,6 +1423,7 @@ export const __testing = {
|
|||
resolvePerplexityBaseUrl,
|
||||
isDirectPerplexityBaseUrl,
|
||||
resolvePerplexityRequestModel,
|
||||
normalizeBraveLanguageParams,
|
||||
normalizeFreshness,
|
||||
freshnessToPerplexityRecency,
|
||||
resolveGrokApiKey,
|
||||
|
|
|
|||
Loading…
Reference in New Issue