fix(web_search): align brave language codes with API

This commit is contained in:
Vignesh Natarajan
2026-03-05 22:12:57 -08:00
parent a939a15607
commit dfe23b9cc4
3 changed files with 96 additions and 5 deletions

View File

@@ -31,6 +31,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Tools/web_search Brave language-code validation: align `search_lang` handling with Brave-supported codes (including `zh-hans`, `zh-hant`, `en-gb`, and `pt-br`), map common alias inputs (`zh`, `ja`) to valid Brave values, and reject unsupported codes before upstream requests to prevent 422 failures. (#37260) Thanks @heyanming.
- Models/openai-completions streaming compatibility: force `compat.supportsUsageInStreaming=false` for non-native OpenAI-compatible endpoints during model normalization, preventing usage-only stream chunks from triggering `choices[0]` parser crashes in provider streams. (#8714) Thanks @nonanon1.
- Tools/xAI native web-search collision guard: drop OpenClaw `web_search` from tool registration when routing to xAI/Grok model providers (including OpenRouter `x-ai/*`) to avoid duplicate tool-name request failures against provider-native `web_search`. (#14749) Thanks @realsamrat.
- TUI/token copy-safety rendering: treat long credential-like mixed alphanumeric tokens (including quoted forms) as copy-sensitive in render sanitization so formatter hard-wrap guards no longer inject visible spaces into auth-style values before display. (#26710) Thanks @jasonthane.

View File

@@ -40,7 +40,67 @@ 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_SEARCH_LANG_CODES = new Set([
"ar",
"eu",
"bn",
"bg",
"ca",
"zh-hans",
"zh-hant",
"hr",
"cs",
"da",
"nl",
"en",
"en-gb",
"et",
"fi",
"fr",
"gl",
"de",
"el",
"gu",
"he",
"hi",
"hu",
"is",
"it",
"jp",
"kn",
"ko",
"lv",
"lt",
"ms",
"ml",
"mr",
"nb",
"pl",
"pt-br",
"pt-pt",
"pa",
"ro",
"ru",
"sr",
"sk",
"sl",
"es",
"sv",
"ta",
"te",
"th",
"tr",
"uk",
"vi",
]);
const BRAVE_SEARCH_LANG_ALIASES: Record<string, string> = {
ja: "jp",
zh: "zh-hans",
"zh-cn": "zh-hans",
"zh-hk": "zh-hant",
"zh-sg": "zh-hans",
"zh-tw": "zh-hant",
};
const BRAVE_UI_LANG_LOCALE = /^([a-z]{2})-([a-z]{2})$/i;
const PERPLEXITY_RECENCY_VALUES = new Set(["day", "week", "month", "year"]);
@@ -127,7 +187,7 @@ function createWebSearchSchema(provider: (typeof SEARCH_PROVIDERS)[number]) {
search_lang: Type.Optional(
Type.String({
description:
"Short ISO language code for search results (e.g., 'de', 'en', 'fr', 'tr'). Must be a 2-letter code, NOT a locale.",
"Brave language code for search results (e.g., 'en', 'de', 'en-gb', 'zh-hans', 'zh-hant', 'pt-br').",
}),
),
ui_lang: Type.Optional(
@@ -731,10 +791,14 @@ function normalizeBraveSearchLang(value: string | undefined): string | undefined
return undefined;
}
const trimmed = value.trim();
if (!trimmed || !BRAVE_SEARCH_LANG_CODE.test(trimmed)) {
if (!trimmed) {
return undefined;
}
return trimmed.toLowerCase();
const canonical = BRAVE_SEARCH_LANG_ALIASES[trimmed.toLowerCase()] ?? trimmed.toLowerCase();
if (!BRAVE_SEARCH_LANG_CODES.has(canonical)) {
return undefined;
}
return canonical;
}
function normalizeBraveUiLang(value: string | undefined): string | undefined {
@@ -1473,7 +1537,7 @@ export function createWebSearchTool(options?: {
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').",
"search_lang must be a Brave-supported language code like 'en', 'en-gb', 'zh-hans', or 'zh-hant'.",
docs: "https://docs.openclaw.ai/tools/web",
});
}

View File

@@ -155,6 +155,8 @@ describe("web_search country and language parameters", () => {
async function runBraveSearchAndGetUrl(
params: Partial<{
country: string;
language: string;
search_lang: string;
ui_lang: string;
freshness: string;
}>,
@@ -185,6 +187,30 @@ describe("web_search country and language parameters", () => {
expect(url.searchParams.get("search_lang")).toBe("de");
});
it("maps legacy zh language code to Brave zh-hans search_lang", async () => {
const url = await runBraveSearchAndGetUrl({ language: "zh" });
expect(url.searchParams.get("search_lang")).toBe("zh-hans");
});
it("maps ja language code to Brave jp search_lang", async () => {
const url = await runBraveSearchAndGetUrl({ language: "ja" });
expect(url.searchParams.get("search_lang")).toBe("jp");
});
it("passes Brave extended language code variants unchanged", async () => {
const url = await runBraveSearchAndGetUrl({ search_lang: "zh-hant" });
expect(url.searchParams.get("search_lang")).toBe("zh-hant");
});
it("rejects unsupported Brave search_lang values before upstream request", async () => {
const mockFetch = installMockFetch({ web: { results: [] } });
const tool = createWebSearchTool({ config: undefined, sandboxed: true });
const result = await tool?.execute?.("call-1", { query: "test", search_lang: "xx" });
expect(mockFetch).not.toHaveBeenCalled();
expect(result?.details).toMatchObject({ error: "invalid_search_lang" });
});
it("rejects invalid freshness values", async () => {
const mockFetch = installMockFetch({ web: { results: [] } });
const tool = createWebSearchTool({ config: undefined, sandboxed: true });