alphabetize web search providers (#40259)

Merged via squash.

Prepared head SHA: be6350e5ae
Co-authored-by: kesku <62210496+kesku@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
This commit is contained in:
Kesku 2026-03-08 20:24:54 -07:00 committed by GitHub
parent e3df94365b
commit adec8b28bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 84 additions and 80 deletions

View File

@ -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

View File

@ -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

View File

@ -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";

View File

@ -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(

View File

@ -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__";

View File

@ -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", () => {

View File

@ -649,11 +649,13 @@ export const FIELD_HELP: Record<string, string> = {
"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<string, string> = {
"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":

View File

@ -218,17 +218,17 @@ export const FIELD_LABELS: Record<string, string> = {
"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",

View File

@ -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?: {