From adec8b28bbb475ee882190818df590d6a5e49542 Mon Sep 17 00:00:00 2001 From: Kesku <62210496+kesku@users.noreply.github.com> Date: Sun, 8 Mar 2026 20:24:54 -0700 Subject: [PATCH] alphabetize web search providers (#40259) Merged via squash. Prepared head SHA: be6350e5ae1c0610f813483dca0a5c98813a7f8e Co-authored-by: kesku <62210496+kesku@users.noreply.github.com> Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com> Reviewed-by: @obviyus --- CHANGELOG.md | 1 + docs/tools/web.md | 8 +-- src/agents/tools/web-search.ts | 70 +++++++++---------- src/commands/configure.wizard.ts | 5 +- src/commands/onboard-search.ts | 14 ++-- src/config/config.web-search-provider.test.ts | 12 ++-- src/config/schema.help.ts | 8 +-- src/config/schema.labels.ts | 8 +-- src/config/types.tools.ts | 38 +++++----- 9 files changed, 84 insertions(+), 80 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 437757f61a6..87586e9f767 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai - CLI/backup: add `openclaw backup create` and `openclaw backup verify` for local state archives, including `--only-config`, `--no-include-workspace`, manifest/payload validation, and backup guidance in destructive flows. (#40163) thanks @shichangs. - CLI/backup: improve archive naming for date sorting, add config-only backup mode, and harden backup planning, publication, and verification edge cases. (#40163) Thanks @gumadeiras. - ACP/Provenance: add optional ACP ingress provenance metadata and visible receipt injection (`openclaw acp --provenance off|meta|meta+receipt`) so OpenClaw agents can retain and report ACP-origin context with session trace IDs. (#40473) thanks @mbelinky. +- Tools/web search: alphabetize provider ordering across runtime selection, onboarding/configure pickers, and config metadata, so provider lists stay neutral and multi-key auto-detect now prefers Grok before Kimi. (#40259) thanks @kesku. ### Breaking diff --git a/docs/tools/web.md b/docs/tools/web.md index 25cb5d7f4f0..1eeb4eba7db 100644 --- a/docs/tools/web.md +++ b/docs/tools/web.md @@ -43,9 +43,9 @@ The table above is alphabetical. If no `provider` is explicitly set, runtime aut 1. **Brave** — `BRAVE_API_KEY` env var or `tools.web.search.apiKey` config 2. **Gemini** — `GEMINI_API_KEY` env var or `tools.web.search.gemini.apiKey` config -3. **Kimi** — `KIMI_API_KEY` / `MOONSHOT_API_KEY` env var or `tools.web.search.kimi.apiKey` config -4. **Perplexity** — `PERPLEXITY_API_KEY`, `OPENROUTER_API_KEY`, or `tools.web.search.perplexity.apiKey` config -5. **Grok** — `XAI_API_KEY` env var or `tools.web.search.grok.apiKey` config +3. **Grok** — `XAI_API_KEY` env var or `tools.web.search.grok.apiKey` config +4. **Kimi** — `KIMI_API_KEY` / `MOONSHOT_API_KEY` env var or `tools.web.search.kimi.apiKey` config +5. **Perplexity** — `PERPLEXITY_API_KEY`, `OPENROUTER_API_KEY`, or `tools.web.search.perplexity.apiKey` config If no keys are found, it falls back to Brave (you'll get a missing-key error prompting you to configure one). @@ -212,10 +212,10 @@ Search the web using your configured provider. - `tools.web.search.enabled` must not be `false` (default: enabled) - API key for your chosen provider: - **Brave**: `BRAVE_API_KEY` or `tools.web.search.apiKey` - - **Perplexity**: `PERPLEXITY_API_KEY`, `OPENROUTER_API_KEY`, or `tools.web.search.perplexity.apiKey` - **Gemini**: `GEMINI_API_KEY` or `tools.web.search.gemini.apiKey` - **Grok**: `XAI_API_KEY` or `tools.web.search.grok.apiKey` - **Kimi**: `KIMI_API_KEY`, `MOONSHOT_API_KEY`, or `tools.web.search.kimi.apiKey` + - **Perplexity**: `PERPLEXITY_API_KEY`, `OPENROUTER_API_KEY`, or `tools.web.search.perplexity.apiKey` ### Config diff --git a/src/agents/tools/web-search.ts b/src/agents/tools/web-search.ts index 1501063a9cf..47c5a5abc94 100644 --- a/src/agents/tools/web-search.ts +++ b/src/agents/tools/web-search.ts @@ -21,7 +21,7 @@ import { writeCache, } from "./web-shared.js"; -const SEARCH_PROVIDERS = ["brave", "perplexity", "grok", "gemini", "kimi"] as const; +const SEARCH_PROVIDERS = ["brave", "gemini", "grok", "kimi", "perplexity"] as const; const DEFAULT_SEARCH_COUNT = 5; const MAX_SEARCH_COUNT = 10; @@ -492,19 +492,10 @@ function resolveSearchApiKey(search?: WebSearchConfig): string | undefined { } function missingSearchKeyPayload(provider: (typeof SEARCH_PROVIDERS)[number]) { - if (provider === "perplexity") { + if (provider === "brave") { return { - error: "missing_perplexity_api_key", - message: - "web_search (perplexity) needs an API key. Set PERPLEXITY_API_KEY or OPENROUTER_API_KEY in the Gateway environment, or configure tools.web.search.perplexity.apiKey.", - docs: "https://docs.openclaw.ai/tools/web", - }; - } - if (provider === "grok") { - return { - error: "missing_xai_api_key", - message: - "web_search (grok) needs an xAI API key. Set XAI_API_KEY in the Gateway environment, or configure tools.web.search.grok.apiKey.", + error: "missing_brave_api_key", + message: `web_search (brave) needs a Brave Search API key. Run \`${formatCliCommand("openclaw configure --section web")}\` to store it, or set BRAVE_API_KEY in the Gateway environment.`, docs: "https://docs.openclaw.ai/tools/web", }; } @@ -516,6 +507,14 @@ function missingSearchKeyPayload(provider: (typeof SEARCH_PROVIDERS)[number]) { docs: "https://docs.openclaw.ai/tools/web", }; } + if (provider === "grok") { + return { + error: "missing_xai_api_key", + message: + "web_search (grok) needs an xAI API key. Set XAI_API_KEY in the Gateway environment, or configure tools.web.search.grok.apiKey.", + docs: "https://docs.openclaw.ai/tools/web", + }; + } if (provider === "kimi") { return { error: "missing_kimi_api_key", @@ -525,8 +524,9 @@ function missingSearchKeyPayload(provider: (typeof SEARCH_PROVIDERS)[number]) { }; } return { - error: "missing_brave_api_key", - message: `web_search needs a Brave Search API key. Run \`${formatCliCommand("openclaw configure --section web")}\` to store it, or set BRAVE_API_KEY in the Gateway environment.`, + error: "missing_perplexity_api_key", + message: + "web_search (perplexity) needs an API key. Set PERPLEXITY_API_KEY or OPENROUTER_API_KEY in the Gateway environment, or configure tools.web.search.perplexity.apiKey.", docs: "https://docs.openclaw.ai/tools/web", }; } @@ -536,32 +536,32 @@ function resolveSearchProvider(search?: WebSearchConfig): (typeof SEARCH_PROVIDE search && "provider" in search && typeof search.provider === "string" ? search.provider.trim().toLowerCase() : ""; - if (raw === "perplexity") { - return "perplexity"; - } - if (raw === "grok") { - return "grok"; + if (raw === "brave") { + return "brave"; } if (raw === "gemini") { return "gemini"; } + if (raw === "grok") { + return "grok"; + } if (raw === "kimi") { return "kimi"; } - if (raw === "brave") { - return "brave"; + if (raw === "perplexity") { + return "perplexity"; } - // Auto-detect provider from available API keys (priority order) + // Auto-detect provider from available API keys (alphabetical order) if (raw === "") { - // 1. Brave + // Brave if (resolveSearchApiKey(search)) { logVerbose( 'web_search: no provider configured, auto-detected "brave" from available API keys', ); return "brave"; } - // 2. Gemini + // Gemini const geminiConfig = resolveGeminiConfig(search); if (resolveGeminiApiKey(geminiConfig)) { logVerbose( @@ -569,7 +569,15 @@ function resolveSearchProvider(search?: WebSearchConfig): (typeof SEARCH_PROVIDE ); return "gemini"; } - // 3. Kimi + // Grok + const grokConfig = resolveGrokConfig(search); + if (resolveGrokApiKey(grokConfig)) { + logVerbose( + 'web_search: no provider configured, auto-detected "grok" from available API keys', + ); + return "grok"; + } + // Kimi const kimiConfig = resolveKimiConfig(search); if (resolveKimiApiKey(kimiConfig)) { logVerbose( @@ -577,7 +585,7 @@ function resolveSearchProvider(search?: WebSearchConfig): (typeof SEARCH_PROVIDE ); return "kimi"; } - // 4. Perplexity + // Perplexity const perplexityConfig = resolvePerplexityConfig(search); const { apiKey: perplexityKey } = resolvePerplexityApiKey(perplexityConfig); if (perplexityKey) { @@ -586,14 +594,6 @@ function resolveSearchProvider(search?: WebSearchConfig): (typeof SEARCH_PROVIDE ); return "perplexity"; } - // 5. Grok - const grokConfig = resolveGrokConfig(search); - if (resolveGrokApiKey(grokConfig)) { - logVerbose( - 'web_search: no provider configured, auto-detected "grok" from available API keys', - ); - return "grok"; - } } return "brave"; diff --git a/src/commands/configure.wizard.ts b/src/commands/configure.wizard.ts index 7a00fffbda1..80af67043ab 100644 --- a/src/commands/configure.wizard.ts +++ b/src/commands/configure.wizard.ts @@ -188,7 +188,10 @@ async function promptWebToolsConfig( if (stored && SEARCH_PROVIDER_OPTIONS.some((e) => e.value === stored)) { return stored; } - return SEARCH_PROVIDER_OPTIONS.find((e) => hasKeyForProvider(e.value))?.value ?? "brave"; + return ( + SEARCH_PROVIDER_OPTIONS.find((e) => hasKeyForProvider(e.value))?.value ?? + SEARCH_PROVIDER_OPTIONS[0].value + ); })(); note( diff --git a/src/commands/onboard-search.ts b/src/commands/onboard-search.ts index f71a37b55da..df2f4643b60 100644 --- a/src/commands/onboard-search.ts +++ b/src/commands/onboard-search.ts @@ -10,7 +10,7 @@ import type { RuntimeEnv } from "../runtime.js"; import type { WizardPrompter } from "../wizard/prompts.js"; import type { SecretInputMode } from "./onboard-types.js"; -export type SearchProvider = "perplexity" | "brave" | "gemini" | "grok" | "kimi"; +export type SearchProvider = "brave" | "gemini" | "grok" | "kimi" | "perplexity"; type SearchProviderEntry = { value: SearchProvider; @@ -73,14 +73,14 @@ function rawKeyValue(config: OpenClawConfig, provider: SearchProvider): unknown switch (provider) { case "brave": return search?.apiKey; - case "perplexity": - return search?.perplexity?.apiKey; case "gemini": return search?.gemini?.apiKey; case "grok": return search?.grok?.apiKey; case "kimi": return search?.kimi?.apiKey; + case "perplexity": + return search?.perplexity?.apiKey; } } @@ -132,9 +132,6 @@ export function applySearchKey( case "brave": search.apiKey = key; break; - case "perplexity": - search.perplexity = { ...search.perplexity, apiKey: key }; - break; case "gemini": search.gemini = { ...search.gemini, apiKey: key }; break; @@ -144,6 +141,9 @@ export function applySearchKey( case "kimi": search.kimi = { ...search.kimi, apiKey: key }; break; + case "perplexity": + search.perplexity = { ...search.perplexity, apiKey: key }; + break; } return { ...config, @@ -222,7 +222,7 @@ export async function setupSearch( if (detected) { return detected.value; } - return "brave"; + return SEARCH_PROVIDER_OPTIONS[0].value; })(); type PickerValue = SearchProvider | "__skip__"; diff --git a/src/config/config.web-search-provider.test.ts b/src/config/config.web-search-provider.test.ts index 6aca3cf0d17..7ddb4ca3ab4 100644 --- a/src/config/config.web-search-provider.test.ts +++ b/src/config/config.web-search-provider.test.ts @@ -142,7 +142,7 @@ describe("web search provider auto-detection", () => { expect(resolveSearchProvider({})).toBe("kimi"); }); - it("follows priority order — brave wins when multiple keys available", () => { + it("follows alphabetical order — brave wins when multiple keys available", () => { process.env.BRAVE_API_KEY = "test-brave-key"; // pragma: allowlist secret process.env.GEMINI_API_KEY = "test-gemini-key"; // pragma: allowlist secret process.env.PERPLEXITY_API_KEY = "test-perplexity-key"; // pragma: allowlist secret @@ -150,18 +150,18 @@ describe("web search provider auto-detection", () => { expect(resolveSearchProvider({})).toBe("brave"); }); - it("gemini wins over perplexity and grok when brave unavailable", () => { + it("gemini wins over grok, kimi, and perplexity when brave unavailable", () => { process.env.GEMINI_API_KEY = "test-gemini-key"; // pragma: allowlist secret process.env.PERPLEXITY_API_KEY = "test-perplexity-key"; // pragma: allowlist secret process.env.XAI_API_KEY = "test-xai-key"; // pragma: allowlist secret expect(resolveSearchProvider({})).toBe("gemini"); }); - it("brave wins over gemini and grok when perplexity unavailable", () => { - process.env.BRAVE_API_KEY = "test-brave-key"; // pragma: allowlist secret - process.env.GEMINI_API_KEY = "test-gemini-key"; // pragma: allowlist secret + it("grok wins over kimi and perplexity when brave and gemini unavailable", () => { process.env.XAI_API_KEY = "test-xai-key"; // pragma: allowlist secret - expect(resolveSearchProvider({})).toBe("brave"); + process.env.KIMI_API_KEY = "test-kimi-key"; // pragma: allowlist secret + process.env.PERPLEXITY_API_KEY = "test-perplexity-key"; // pragma: allowlist secret + expect(resolveSearchProvider({})).toBe("grok"); }); it("explicit provider always wins regardless of keys", () => { diff --git a/src/config/schema.help.ts b/src/config/schema.help.ts index ec02d1d106f..08c579f89e3 100644 --- a/src/config/schema.help.ts +++ b/src/config/schema.help.ts @@ -649,11 +649,13 @@ export const FIELD_HELP: Record = { "tools.message.broadcast.enabled": "Enable broadcast action (default: true).", "tools.web.search.enabled": "Enable the web_search tool (requires a provider API key).", "tools.web.search.provider": - 'Search provider ("brave", "perplexity", "grok", "gemini", or "kimi"). Auto-detected from available API keys if omitted.', + 'Search provider ("brave", "gemini", "grok", "kimi", or "perplexity"). Auto-detected from available API keys if omitted.', "tools.web.search.apiKey": "Brave Search API key (fallback: BRAVE_API_KEY env var).", - "tools.web.search.maxResults": "Default number of results to return (1-10).", + "tools.web.search.maxResults": "Number of results to return (1-10).", "tools.web.search.timeoutSeconds": "Timeout in seconds for web_search requests.", "tools.web.search.cacheTtlMinutes": "Cache TTL in minutes for web_search results.", + "tools.web.search.brave.mode": + 'Brave Search mode: "web" (URL results) or "llm-context" (pre-extracted page content for LLM grounding).', "tools.web.search.gemini.apiKey": "Gemini API key for Google Search grounding (fallback: GEMINI_API_KEY env var).", "tools.web.search.gemini.model": 'Gemini model override (default: "gemini-2.5-flash").', @@ -670,8 +672,6 @@ export const FIELD_HELP: Record = { "Optional Perplexity/OpenRouter chat-completions base URL override. Setting this opts Perplexity into the legacy Sonar/OpenRouter compatibility path.", "tools.web.search.perplexity.model": 'Optional Sonar/OpenRouter model override (default: "perplexity/sonar-pro"). Setting this opts Perplexity into the legacy chat-completions compatibility path.', - "tools.web.search.brave.mode": - 'Brave Search mode: "web" (URL results) or "llm-context" (pre-extracted page content for LLM grounding).', "tools.web.fetch.enabled": "Enable the web_fetch tool (lightweight HTTP fetch).", "tools.web.fetch.maxChars": "Max characters returned by web_fetch (truncated).", "tools.web.fetch.maxCharsCap": diff --git a/src/config/schema.labels.ts b/src/config/schema.labels.ts index ec9e8eb0c52..16bf21e8daf 100644 --- a/src/config/schema.labels.ts +++ b/src/config/schema.labels.ts @@ -218,17 +218,17 @@ export const FIELD_LABELS: Record = { "tools.web.search.maxResults": "Web Search Max Results", "tools.web.search.timeoutSeconds": "Web Search Timeout (sec)", "tools.web.search.cacheTtlMinutes": "Web Search Cache TTL (min)", - "tools.web.search.perplexity.apiKey": "Perplexity API Key", // pragma: allowlist secret - "tools.web.search.perplexity.baseUrl": "Perplexity Base URL", - "tools.web.search.perplexity.model": "Perplexity Model", + "tools.web.search.brave.mode": "Brave Search Mode", "tools.web.search.gemini.apiKey": "Gemini Search API Key", // pragma: allowlist secret "tools.web.search.gemini.model": "Gemini Search Model", "tools.web.search.grok.apiKey": "Grok Search API Key", // pragma: allowlist secret "tools.web.search.grok.model": "Grok Search Model", - "tools.web.search.brave.mode": "Brave Search Mode", "tools.web.search.kimi.apiKey": "Kimi Search API Key", // pragma: allowlist secret "tools.web.search.kimi.baseUrl": "Kimi Search Base URL", "tools.web.search.kimi.model": "Kimi Search Model", + "tools.web.search.perplexity.apiKey": "Perplexity API Key", // pragma: allowlist secret + "tools.web.search.perplexity.baseUrl": "Perplexity Base URL", + "tools.web.search.perplexity.model": "Perplexity Model", "tools.web.fetch.enabled": "Enable Web Fetch Tool", "tools.web.fetch.maxChars": "Web Fetch Max Chars", "tools.web.fetch.maxCharsCap": "Web Fetch Hard Max Chars", diff --git a/src/config/types.tools.ts b/src/config/types.tools.ts index e895e3bcf4f..89775758411 100644 --- a/src/config/types.tools.ts +++ b/src/config/types.tools.ts @@ -441,8 +441,8 @@ export type ToolsConfig = { search?: { /** Enable web search tool (default: true when API key is present). */ enabled?: boolean; - /** Search provider ("brave", "perplexity", "grok", "gemini", or "kimi"). */ - provider?: "brave" | "perplexity" | "grok" | "gemini" | "kimi"; + /** Search provider ("brave", "gemini", "grok", "kimi", or "perplexity"). */ + provider?: "brave" | "gemini" | "grok" | "kimi" | "perplexity"; /** Brave Search API key (optional; defaults to BRAVE_API_KEY env var). */ apiKey?: SecretInput; /** Default search results count (1-10). */ @@ -451,13 +451,16 @@ export type ToolsConfig = { timeoutSeconds?: number; /** Cache TTL in minutes for search results. */ cacheTtlMinutes?: number; - /** Perplexity-specific configuration (used when provider="perplexity"). */ - perplexity?: { - /** API key for Perplexity (defaults to PERPLEXITY_API_KEY env var). */ + /** Brave-specific configuration (used when provider="brave"). */ + brave?: { + /** Brave Search mode: "web" (standard results) or "llm-context" (pre-extracted page content). Default: "web". */ + mode?: "web" | "llm-context"; + }; + /** Gemini-specific configuration (used when provider="gemini"). */ + gemini?: { + /** Gemini API key (defaults to GEMINI_API_KEY env var). */ apiKey?: SecretInput; - /** @deprecated Legacy Sonar/OpenRouter field. Ignored by Search API. */ - baseUrl?: string; - /** @deprecated Legacy Sonar/OpenRouter field. Ignored by Search API. */ + /** Model to use for grounded search (defaults to "gemini-2.5-flash"). */ model?: string; }; /** Grok-specific configuration (used when provider="grok"). */ @@ -469,13 +472,6 @@ export type ToolsConfig = { /** Include inline citations in response text as markdown links (default: false). */ inlineCitations?: boolean; }; - /** Gemini-specific configuration (used when provider="gemini"). */ - gemini?: { - /** Gemini API key (defaults to GEMINI_API_KEY env var). */ - apiKey?: SecretInput; - /** Model to use for grounded search (defaults to "gemini-2.5-flash"). */ - model?: string; - }; /** Kimi-specific configuration (used when provider="kimi"). */ kimi?: { /** Moonshot/Kimi API key (defaults to KIMI_API_KEY or MOONSHOT_API_KEY env var). */ @@ -485,10 +481,14 @@ export type ToolsConfig = { /** Model to use (defaults to "moonshot-v1-128k"). */ model?: string; }; - /** Brave-specific configuration (used when provider="brave"). */ - brave?: { - /** Brave Search mode: "web" (standard results) or "llm-context" (pre-extracted page content). Default: "web". */ - mode?: "web" | "llm-context"; + /** Perplexity-specific configuration (used when provider="perplexity"). */ + perplexity?: { + /** API key for Perplexity (defaults to PERPLEXITY_API_KEY env var). */ + apiKey?: SecretInput; + /** @deprecated Legacy Sonar/OpenRouter field. Ignored by Search API. */ + baseUrl?: string; + /** @deprecated Legacy Sonar/OpenRouter field. Ignored by Search API. */ + model?: string; }; }; fetch?: {