refactor web search config ownership into extensions
This commit is contained in:
parent
84cf8c32aa
commit
a03f43d5bd
@ -1,8 +1,34 @@
|
||||
{
|
||||
"id": "brave",
|
||||
"uiHints": {
|
||||
"webSearch.apiKey": {
|
||||
"label": "Brave Search API Key",
|
||||
"help": "Brave Search API key (fallback: BRAVE_API_KEY env var).",
|
||||
"sensitive": true,
|
||||
"placeholder": "BSA..."
|
||||
},
|
||||
"webSearch.mode": {
|
||||
"label": "Brave Search Mode",
|
||||
"help": "Brave Search mode: web or llm-context."
|
||||
}
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {}
|
||||
"properties": {
|
||||
"webSearch": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"apiKey": {
|
||||
"type": ["string", "object"]
|
||||
},
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["web", "llm-context"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,7 +17,12 @@ import {
|
||||
withTrustedWebSearchEndpoint,
|
||||
writeCachedSearchPayload,
|
||||
} from "../../../src/agents/tools/web-search-provider-common.js";
|
||||
import {
|
||||
resolveProviderWebSearchPluginConfig,
|
||||
setProviderWebSearchPluginConfigValue,
|
||||
} from "../../../src/agents/tools/web-search-provider-config.js";
|
||||
import { formatCliCommand } from "../../../src/cli/command-format.js";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import type {
|
||||
WebSearchProviderPlugin,
|
||||
WebSearchProviderToolDefinition,
|
||||
@ -90,6 +95,7 @@ const BRAVE_SEARCH_LANG_ALIASES: Record<string, string> = {
|
||||
const BRAVE_UI_LANG_LOCALE = /^([a-z]{2})-([a-z]{2})$/i;
|
||||
|
||||
type BraveConfig = {
|
||||
apiKey?: unknown;
|
||||
mode?: string;
|
||||
};
|
||||
|
||||
@ -112,18 +118,41 @@ type BraveLlmContextResponse = {
|
||||
sources?: { url?: string; hostname?: string; date?: string }[];
|
||||
};
|
||||
|
||||
function resolveBraveConfig(searchConfig?: SearchConfigRecord): BraveConfig {
|
||||
const brave = searchConfig?.brave;
|
||||
return brave && typeof brave === "object" && !Array.isArray(brave) ? (brave as BraveConfig) : {};
|
||||
function resolveBraveConfig(
|
||||
config?: OpenClawConfig,
|
||||
searchConfig?: SearchConfigRecord,
|
||||
): BraveConfig {
|
||||
const pluginConfig = resolveProviderWebSearchPluginConfig(config, "brave");
|
||||
if (pluginConfig) {
|
||||
return pluginConfig as BraveConfig;
|
||||
}
|
||||
const scoped = (searchConfig as Record<string, unknown> | undefined)?.brave;
|
||||
return scoped && typeof scoped === "object" && !Array.isArray(scoped)
|
||||
? ({
|
||||
...(scoped as BraveConfig),
|
||||
apiKey: (searchConfig as Record<string, unknown> | undefined)?.apiKey,
|
||||
} as BraveConfig)
|
||||
: ({ apiKey: (searchConfig as Record<string, unknown> | undefined)?.apiKey } as BraveConfig);
|
||||
}
|
||||
|
||||
function resolveBraveMode(brave: BraveConfig): "web" | "llm-context" {
|
||||
return brave.mode === "llm-context" ? "llm-context" : "web";
|
||||
}
|
||||
|
||||
function resolveBraveApiKey(searchConfig?: SearchConfigRecord): string | undefined {
|
||||
function resolveBraveApiKey(
|
||||
config?: OpenClawConfig,
|
||||
searchConfig?: SearchConfigRecord,
|
||||
): string | undefined {
|
||||
const braveConfig = resolveBraveConfig(config, searchConfig);
|
||||
return (
|
||||
readConfiguredSecretString(searchConfig?.apiKey, "tools.web.search.apiKey") ??
|
||||
readConfiguredSecretString(
|
||||
braveConfig.apiKey,
|
||||
"plugins.entries.brave.config.webSearch.apiKey",
|
||||
) ??
|
||||
readConfiguredSecretString(
|
||||
(searchConfig as Record<string, unknown> | undefined)?.apiKey,
|
||||
"tools.web.search.apiKey",
|
||||
) ??
|
||||
readProviderEnvValue(["BRAVE_API_KEY"])
|
||||
);
|
||||
}
|
||||
@ -384,9 +413,10 @@ function missingBraveKeyPayload() {
|
||||
}
|
||||
|
||||
function createBraveToolDefinition(
|
||||
config?: OpenClawConfig,
|
||||
searchConfig?: SearchConfigRecord,
|
||||
): WebSearchProviderToolDefinition {
|
||||
const braveConfig = resolveBraveConfig(searchConfig);
|
||||
const braveConfig = resolveBraveConfig(config, searchConfig);
|
||||
const braveMode = resolveBraveMode(braveConfig);
|
||||
|
||||
return {
|
||||
@ -396,7 +426,7 @@ function createBraveToolDefinition(
|
||||
: "Search the web using Brave Search API. Supports region-specific and localized search via country and language parameters. Returns titles, URLs, and snippets for fast research.",
|
||||
parameters: createBraveSchema(),
|
||||
execute: async (args) => {
|
||||
const apiKey = resolveBraveApiKey(searchConfig);
|
||||
const apiKey = resolveBraveApiKey(config, searchConfig);
|
||||
if (!apiKey) {
|
||||
return missingBraveKeyPayload();
|
||||
}
|
||||
@ -594,14 +624,19 @@ export function createBraveWebSearchProvider(): WebSearchProviderPlugin {
|
||||
signupUrl: "https://brave.com/search/api/",
|
||||
docsUrl: "https://docs.openclaw.ai/brave-search",
|
||||
autoDetectOrder: 10,
|
||||
credentialPath: "tools.web.search.apiKey",
|
||||
inactiveSecretPaths: ["tools.web.search.apiKey"],
|
||||
credentialPath: "plugins.entries.brave.config.webSearch.apiKey",
|
||||
inactiveSecretPaths: ["plugins.entries.brave.config.webSearch.apiKey"],
|
||||
getCredentialValue: (searchConfig) => searchConfig?.apiKey,
|
||||
setCredentialValue: (searchConfigTarget, value) => {
|
||||
searchConfigTarget.apiKey = value;
|
||||
},
|
||||
getConfiguredCredentialValue: (config) =>
|
||||
resolveProviderWebSearchPluginConfig(config, "brave")?.apiKey,
|
||||
setConfiguredCredentialValue: (configTarget, value) => {
|
||||
setProviderWebSearchPluginConfigValue(configTarget, "brave", "apiKey", value);
|
||||
},
|
||||
createTool: (ctx) =>
|
||||
createBraveToolDefinition(ctx.searchConfig as SearchConfigRecord | undefined),
|
||||
createBraveToolDefinition(ctx.config, ctx.searchConfig as SearchConfigRecord | undefined),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +1,33 @@
|
||||
{
|
||||
"id": "firecrawl",
|
||||
"uiHints": {
|
||||
"webSearch.apiKey": {
|
||||
"label": "Firecrawl Search API Key",
|
||||
"help": "Firecrawl API key for web search (fallback: FIRECRAWL_API_KEY env var).",
|
||||
"sensitive": true,
|
||||
"placeholder": "fc-..."
|
||||
},
|
||||
"webSearch.baseUrl": {
|
||||
"label": "Firecrawl Search Base URL",
|
||||
"help": "Firecrawl Search base URL override."
|
||||
}
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {}
|
||||
"properties": {
|
||||
"webSearch": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"apiKey": {
|
||||
"type": ["string", "object"]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,6 +26,15 @@ type FirecrawlSearchConfig =
|
||||
}
|
||||
| undefined;
|
||||
|
||||
type PluginEntryConfig =
|
||||
| {
|
||||
webSearch?: {
|
||||
apiKey?: unknown;
|
||||
baseUrl?: string;
|
||||
};
|
||||
}
|
||||
| undefined;
|
||||
|
||||
type FirecrawlFetchConfig =
|
||||
| {
|
||||
apiKey?: unknown;
|
||||
@ -53,6 +62,11 @@ function resolveFetchConfig(cfg?: OpenClawConfig): WebFetchConfig {
|
||||
}
|
||||
|
||||
export function resolveFirecrawlSearchConfig(cfg?: OpenClawConfig): FirecrawlSearchConfig {
|
||||
const pluginConfig = cfg?.plugins?.entries?.firecrawl?.config as PluginEntryConfig;
|
||||
const pluginWebSearch = pluginConfig?.webSearch;
|
||||
if (pluginWebSearch && typeof pluginWebSearch === "object" && !Array.isArray(pluginWebSearch)) {
|
||||
return pluginWebSearch;
|
||||
}
|
||||
const search = resolveSearchConfig(cfg);
|
||||
if (!search || typeof search !== "object") {
|
||||
return undefined;
|
||||
@ -89,6 +103,10 @@ export function resolveFirecrawlApiKey(cfg?: OpenClawConfig): string | undefined
|
||||
const search = resolveFirecrawlSearchConfig(cfg);
|
||||
const fetch = resolveFirecrawlFetchConfig(cfg);
|
||||
return (
|
||||
normalizeConfiguredSecret(
|
||||
search?.apiKey,
|
||||
"plugins.entries.firecrawl.config.webSearch.apiKey",
|
||||
) ||
|
||||
normalizeConfiguredSecret(search?.apiKey, "tools.web.search.firecrawl.apiKey") ||
|
||||
normalizeConfiguredSecret(fetch?.apiKey, "tools.web.fetch.firecrawl.apiKey") ||
|
||||
normalizeSecretInput(process.env.FIRECRAWL_API_KEY) ||
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import {
|
||||
resolveProviderWebSearchPluginConfig,
|
||||
setProviderWebSearchPluginConfigValue,
|
||||
} from "../../../src/agents/tools/web-search-provider-config.js";
|
||||
import { enablePluginInConfig } from "../../../src/plugins/enable.js";
|
||||
import type { WebSearchProviderPlugin } from "../../../src/plugins/types.js";
|
||||
import { runFirecrawlSearch } from "./firecrawl-client.js";
|
||||
@ -47,10 +51,15 @@ export function createFirecrawlWebSearchProvider(): WebSearchProviderPlugin {
|
||||
signupUrl: "https://www.firecrawl.dev/",
|
||||
docsUrl: "https://docs.openclaw.ai/tools/firecrawl",
|
||||
autoDetectOrder: 60,
|
||||
credentialPath: "tools.web.search.firecrawl.apiKey",
|
||||
inactiveSecretPaths: ["tools.web.search.firecrawl.apiKey"],
|
||||
credentialPath: "plugins.entries.firecrawl.config.webSearch.apiKey",
|
||||
inactiveSecretPaths: ["plugins.entries.firecrawl.config.webSearch.apiKey"],
|
||||
getCredentialValue: getScopedCredentialValue,
|
||||
setCredentialValue: setScopedCredentialValue,
|
||||
getConfiguredCredentialValue: (config) =>
|
||||
resolveProviderWebSearchPluginConfig(config, "firecrawl")?.apiKey,
|
||||
setConfiguredCredentialValue: (configTarget, value) => {
|
||||
setProviderWebSearchPluginConfigValue(configTarget, "firecrawl", "apiKey", value);
|
||||
},
|
||||
applySelectionConfig: (config) => enablePluginInConfig(config, "firecrawl").config,
|
||||
createTool: (ctx) => ({
|
||||
description:
|
||||
|
||||
@ -29,9 +29,34 @@
|
||||
"groupHint": "Gemini API key + OAuth"
|
||||
}
|
||||
],
|
||||
"uiHints": {
|
||||
"webSearch.apiKey": {
|
||||
"label": "Gemini Search API Key",
|
||||
"help": "Gemini API key for Google Search grounding (fallback: GEMINI_API_KEY env var).",
|
||||
"sensitive": true,
|
||||
"placeholder": "AIza..."
|
||||
},
|
||||
"webSearch.model": {
|
||||
"label": "Gemini Search Model",
|
||||
"help": "Gemini model override for web search grounding."
|
||||
}
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {}
|
||||
"properties": {
|
||||
"webSearch": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"apiKey": {
|
||||
"type": ["string", "object"]
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,6 +15,11 @@ import {
|
||||
withTrustedWebSearchEndpoint,
|
||||
writeCachedSearchPayload,
|
||||
} from "../../../src/agents/tools/web-search-provider-common.js";
|
||||
import {
|
||||
resolveProviderWebSearchPluginConfig,
|
||||
setProviderWebSearchPluginConfigValue,
|
||||
} from "../../../src/agents/tools/web-search-provider-config.js";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import type {
|
||||
WebSearchProviderPlugin,
|
||||
WebSearchProviderToolDefinition,
|
||||
@ -52,8 +57,15 @@ type GeminiGroundingResponse = {
|
||||
};
|
||||
};
|
||||
|
||||
function resolveGeminiConfig(searchConfig?: SearchConfigRecord): GeminiConfig {
|
||||
const gemini = searchConfig?.gemini;
|
||||
function resolveGeminiConfig(
|
||||
config?: OpenClawConfig,
|
||||
searchConfig?: SearchConfigRecord,
|
||||
): GeminiConfig {
|
||||
const pluginConfig = resolveProviderWebSearchPluginConfig(config, "google");
|
||||
if (pluginConfig) {
|
||||
return pluginConfig as GeminiConfig;
|
||||
}
|
||||
const gemini = (searchConfig as Record<string, unknown> | undefined)?.gemini;
|
||||
return gemini && typeof gemini === "object" && !Array.isArray(gemini)
|
||||
? (gemini as GeminiConfig)
|
||||
: {};
|
||||
@ -61,7 +73,7 @@ function resolveGeminiConfig(searchConfig?: SearchConfigRecord): GeminiConfig {
|
||||
|
||||
function resolveGeminiApiKey(gemini?: GeminiConfig): string | undefined {
|
||||
return (
|
||||
readConfiguredSecretString(gemini?.apiKey, "tools.web.search.gemini.apiKey") ??
|
||||
readConfiguredSecretString(gemini?.apiKey, "plugins.entries.google.config.webSearch.apiKey") ??
|
||||
readProviderEnvValue(["GEMINI_API_KEY"])
|
||||
);
|
||||
}
|
||||
@ -168,6 +180,7 @@ function createGeminiSchema() {
|
||||
}
|
||||
|
||||
function createGeminiToolDefinition(
|
||||
config?: OpenClawConfig,
|
||||
searchConfig?: SearchConfigRecord,
|
||||
): WebSearchProviderToolDefinition {
|
||||
return {
|
||||
@ -194,13 +207,13 @@ function createGeminiToolDefinition(
|
||||
}
|
||||
}
|
||||
|
||||
const geminiConfig = resolveGeminiConfig(searchConfig);
|
||||
const geminiConfig = resolveGeminiConfig(config, searchConfig);
|
||||
const apiKey = resolveGeminiApiKey(geminiConfig);
|
||||
if (!apiKey) {
|
||||
return {
|
||||
error: "missing_gemini_api_key",
|
||||
message:
|
||||
"web_search (gemini) needs an API key. Set GEMINI_API_KEY in the Gateway environment, or configure tools.web.search.gemini.apiKey.",
|
||||
"web_search (gemini) needs an API key. Set GEMINI_API_KEY in the Gateway environment, or configure plugins.entries.google.config.webSearch.apiKey.",
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
};
|
||||
}
|
||||
@ -259,8 +272,8 @@ export function createGeminiWebSearchProvider(): WebSearchProviderPlugin {
|
||||
signupUrl: "https://aistudio.google.com/apikey",
|
||||
docsUrl: "https://docs.openclaw.ai/tools/web",
|
||||
autoDetectOrder: 20,
|
||||
credentialPath: "tools.web.search.gemini.apiKey",
|
||||
inactiveSecretPaths: ["tools.web.search.gemini.apiKey"],
|
||||
credentialPath: "plugins.entries.google.config.webSearch.apiKey",
|
||||
inactiveSecretPaths: ["plugins.entries.google.config.webSearch.apiKey"],
|
||||
getCredentialValue: (searchConfig) => {
|
||||
const gemini = searchConfig?.gemini;
|
||||
return gemini && typeof gemini === "object" && !Array.isArray(gemini)
|
||||
@ -275,8 +288,13 @@ export function createGeminiWebSearchProvider(): WebSearchProviderPlugin {
|
||||
}
|
||||
(scoped as Record<string, unknown>).apiKey = value;
|
||||
},
|
||||
getConfiguredCredentialValue: (config) =>
|
||||
resolveProviderWebSearchPluginConfig(config, "google")?.apiKey,
|
||||
setConfiguredCredentialValue: (configTarget, value) => {
|
||||
setProviderWebSearchPluginConfigValue(configTarget, "google", "apiKey", value);
|
||||
},
|
||||
createTool: (ctx) =>
|
||||
createGeminiToolDefinition(ctx.searchConfig as SearchConfigRecord | undefined),
|
||||
createGeminiToolDefinition(ctx.config, ctx.searchConfig as SearchConfigRecord | undefined),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -32,9 +32,40 @@
|
||||
"cliDescription": "Moonshot API key"
|
||||
}
|
||||
],
|
||||
"uiHints": {
|
||||
"webSearch.apiKey": {
|
||||
"label": "Kimi Search API Key",
|
||||
"help": "Moonshot/Kimi API key (fallback: KIMI_API_KEY or MOONSHOT_API_KEY env var).",
|
||||
"sensitive": true
|
||||
},
|
||||
"webSearch.baseUrl": {
|
||||
"label": "Kimi Search Base URL",
|
||||
"help": "Kimi base URL override."
|
||||
},
|
||||
"webSearch.model": {
|
||||
"label": "Kimi Search Model",
|
||||
"help": "Kimi model override."
|
||||
}
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {}
|
||||
"properties": {
|
||||
"webSearch": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"apiKey": {
|
||||
"type": ["string", "object"]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,6 +14,11 @@ import {
|
||||
withTrustedWebSearchEndpoint,
|
||||
writeCachedSearchPayload,
|
||||
} from "../../../src/agents/tools/web-search-provider-common.js";
|
||||
import {
|
||||
resolveProviderWebSearchPluginConfig,
|
||||
setProviderWebSearchPluginConfigValue,
|
||||
} from "../../../src/agents/tools/web-search-provider-config.js";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import type {
|
||||
WebSearchProviderPlugin,
|
||||
WebSearchProviderToolDefinition,
|
||||
@ -61,14 +66,18 @@ type KimiSearchResponse = {
|
||||
}>;
|
||||
};
|
||||
|
||||
function resolveKimiConfig(searchConfig?: SearchConfigRecord): KimiConfig {
|
||||
const kimi = searchConfig?.kimi;
|
||||
function resolveKimiConfig(config?: OpenClawConfig, searchConfig?: SearchConfigRecord): KimiConfig {
|
||||
const pluginConfig = resolveProviderWebSearchPluginConfig(config, "moonshot");
|
||||
if (pluginConfig) {
|
||||
return pluginConfig as KimiConfig;
|
||||
}
|
||||
const kimi = (searchConfig as Record<string, unknown> | undefined)?.kimi;
|
||||
return kimi && typeof kimi === "object" && !Array.isArray(kimi) ? (kimi as KimiConfig) : {};
|
||||
}
|
||||
|
||||
function resolveKimiApiKey(kimi?: KimiConfig): string | undefined {
|
||||
return (
|
||||
readConfiguredSecretString(kimi?.apiKey, "tools.web.search.kimi.apiKey") ??
|
||||
readConfiguredSecretString(kimi?.apiKey, "plugins.entries.moonshot.config.webSearch.apiKey") ??
|
||||
readProviderEnvValue(["KIMI_API_KEY", "MOONSHOT_API_KEY"])
|
||||
);
|
||||
}
|
||||
@ -237,6 +246,7 @@ function createKimiSchema() {
|
||||
}
|
||||
|
||||
function createKimiToolDefinition(
|
||||
config?: OpenClawConfig,
|
||||
searchConfig?: SearchConfigRecord,
|
||||
): WebSearchProviderToolDefinition {
|
||||
return {
|
||||
@ -263,13 +273,13 @@ function createKimiToolDefinition(
|
||||
}
|
||||
}
|
||||
|
||||
const kimiConfig = resolveKimiConfig(searchConfig);
|
||||
const kimiConfig = resolveKimiConfig(config, searchConfig);
|
||||
const apiKey = resolveKimiApiKey(kimiConfig);
|
||||
if (!apiKey) {
|
||||
return {
|
||||
error: "missing_kimi_api_key",
|
||||
message:
|
||||
"web_search (kimi) needs a Moonshot API key. Set KIMI_API_KEY or MOONSHOT_API_KEY in the Gateway environment, or configure tools.web.search.kimi.apiKey.",
|
||||
"web_search (kimi) needs a Moonshot API key. Set KIMI_API_KEY or MOONSHOT_API_KEY in the Gateway environment, or configure plugins.entries.moonshot.config.webSearch.apiKey.",
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
};
|
||||
}
|
||||
@ -331,8 +341,8 @@ export function createKimiWebSearchProvider(): WebSearchProviderPlugin {
|
||||
signupUrl: "https://platform.moonshot.cn/",
|
||||
docsUrl: "https://docs.openclaw.ai/tools/web",
|
||||
autoDetectOrder: 40,
|
||||
credentialPath: "tools.web.search.kimi.apiKey",
|
||||
inactiveSecretPaths: ["tools.web.search.kimi.apiKey"],
|
||||
credentialPath: "plugins.entries.moonshot.config.webSearch.apiKey",
|
||||
inactiveSecretPaths: ["plugins.entries.moonshot.config.webSearch.apiKey"],
|
||||
getCredentialValue: (searchConfig) => {
|
||||
const kimi = searchConfig?.kimi;
|
||||
return kimi && typeof kimi === "object" && !Array.isArray(kimi)
|
||||
@ -347,8 +357,13 @@ export function createKimiWebSearchProvider(): WebSearchProviderPlugin {
|
||||
}
|
||||
(scoped as Record<string, unknown>).apiKey = value;
|
||||
},
|
||||
getConfiguredCredentialValue: (config) =>
|
||||
resolveProviderWebSearchPluginConfig(config, "moonshot")?.apiKey,
|
||||
setConfiguredCredentialValue: (configTarget, value) => {
|
||||
setProviderWebSearchPluginConfigValue(configTarget, "moonshot", "apiKey", value);
|
||||
},
|
||||
createTool: (ctx) =>
|
||||
createKimiToolDefinition(ctx.searchConfig as SearchConfigRecord | undefined),
|
||||
createKimiToolDefinition(ctx.config, ctx.searchConfig as SearchConfigRecord | undefined),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +1,40 @@
|
||||
{
|
||||
"id": "perplexity",
|
||||
"uiHints": {
|
||||
"webSearch.apiKey": {
|
||||
"label": "Perplexity API Key",
|
||||
"help": "Perplexity or OpenRouter API key for web search.",
|
||||
"sensitive": true,
|
||||
"placeholder": "pplx-..."
|
||||
},
|
||||
"webSearch.baseUrl": {
|
||||
"label": "Perplexity Base URL",
|
||||
"help": "Optional Perplexity/OpenRouter chat-completions base URL override."
|
||||
},
|
||||
"webSearch.model": {
|
||||
"label": "Perplexity Model",
|
||||
"help": "Optional Sonar/OpenRouter model override."
|
||||
}
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {}
|
||||
"properties": {
|
||||
"webSearch": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"apiKey": {
|
||||
"type": ["string", "object"]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,6 +23,11 @@ import {
|
||||
withTrustedWebSearchEndpoint,
|
||||
writeCachedSearchPayload,
|
||||
} from "../../../src/agents/tools/web-search-provider-common.js";
|
||||
import {
|
||||
resolveProviderWebSearchPluginConfig,
|
||||
setProviderWebSearchPluginConfigValue,
|
||||
} from "../../../src/agents/tools/web-search-provider-config.js";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import type {
|
||||
WebSearchCredentialResolutionSource,
|
||||
WebSearchProviderPlugin,
|
||||
@ -71,8 +76,15 @@ type PerplexitySearchApiResponse = {
|
||||
}>;
|
||||
};
|
||||
|
||||
function resolvePerplexityConfig(searchConfig?: SearchConfigRecord): PerplexityConfig {
|
||||
const perplexity = searchConfig?.perplexity;
|
||||
function resolvePerplexityConfig(
|
||||
config?: OpenClawConfig,
|
||||
searchConfig?: SearchConfigRecord,
|
||||
): PerplexityConfig {
|
||||
const pluginConfig = resolveProviderWebSearchPluginConfig(config, "perplexity");
|
||||
if (pluginConfig) {
|
||||
return pluginConfig as PerplexityConfig;
|
||||
}
|
||||
const perplexity = (searchConfig as Record<string, unknown> | undefined)?.perplexity;
|
||||
return perplexity && typeof perplexity === "object" && !Array.isArray(perplexity)
|
||||
? (perplexity as PerplexityConfig)
|
||||
: {};
|
||||
@ -98,7 +110,7 @@ function resolvePerplexityApiKey(perplexity?: PerplexityConfig): {
|
||||
} {
|
||||
const fromConfig = readConfiguredSecretString(
|
||||
perplexity?.apiKey,
|
||||
"tools.web.search.perplexity.apiKey",
|
||||
"plugins.entries.perplexity.config.webSearch.apiKey",
|
||||
);
|
||||
if (fromConfig) {
|
||||
return { apiKey: fromConfig, source: "config" };
|
||||
@ -313,16 +325,16 @@ async function runPerplexitySearch(params: {
|
||||
}
|
||||
|
||||
function resolveRuntimeTransport(params: {
|
||||
config?: OpenClawConfig;
|
||||
searchConfig?: Record<string, unknown>;
|
||||
resolvedKey?: string;
|
||||
keySource: WebSearchCredentialResolutionSource;
|
||||
fallbackEnvVar?: string;
|
||||
}): PerplexityTransport | undefined {
|
||||
const perplexity = params.searchConfig?.perplexity;
|
||||
const scoped =
|
||||
perplexity && typeof perplexity === "object" && !Array.isArray(perplexity)
|
||||
? (perplexity as { baseUrl?: string; model?: string })
|
||||
: undefined;
|
||||
const scoped = resolvePerplexityConfig(
|
||||
params.config,
|
||||
params.searchConfig as SearchConfigRecord | undefined,
|
||||
);
|
||||
const configuredBaseUrl = typeof scoped?.baseUrl === "string" ? scoped.baseUrl.trim() : "";
|
||||
const configuredModel = typeof scoped?.model === "string" ? scoped.model.trim() : "";
|
||||
const baseUrl = (() => {
|
||||
@ -404,10 +416,11 @@ function createPerplexitySchema(transport?: PerplexityTransport) {
|
||||
}
|
||||
|
||||
function createPerplexityToolDefinition(
|
||||
config?: OpenClawConfig,
|
||||
searchConfig?: SearchConfigRecord,
|
||||
runtimeTransport?: PerplexityTransport,
|
||||
): WebSearchProviderToolDefinition {
|
||||
const perplexityConfig = resolvePerplexityConfig(searchConfig);
|
||||
const perplexityConfig = resolvePerplexityConfig(config, searchConfig);
|
||||
const schemaTransport =
|
||||
runtimeTransport ??
|
||||
(perplexityConfig.baseUrl || perplexityConfig.model ? "chat_completions" : undefined);
|
||||
@ -424,7 +437,7 @@ function createPerplexityToolDefinition(
|
||||
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.",
|
||||
"web_search (perplexity) needs an API key. Set PERPLEXITY_API_KEY or OPENROUTER_API_KEY in the Gateway environment, or configure plugins.entries.perplexity.config.webSearch.apiKey.",
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
};
|
||||
}
|
||||
@ -656,8 +669,8 @@ export function createPerplexityWebSearchProvider(): WebSearchProviderPlugin {
|
||||
signupUrl: "https://www.perplexity.ai/settings/api",
|
||||
docsUrl: "https://docs.openclaw.ai/perplexity",
|
||||
autoDetectOrder: 50,
|
||||
credentialPath: "tools.web.search.perplexity.apiKey",
|
||||
inactiveSecretPaths: ["tools.web.search.perplexity.apiKey"],
|
||||
credentialPath: "plugins.entries.perplexity.config.webSearch.apiKey",
|
||||
inactiveSecretPaths: ["plugins.entries.perplexity.config.webSearch.apiKey"],
|
||||
getCredentialValue: (searchConfig) => {
|
||||
const perplexity = searchConfig?.perplexity;
|
||||
return perplexity && typeof perplexity === "object" && !Array.isArray(perplexity)
|
||||
@ -672,8 +685,14 @@ export function createPerplexityWebSearchProvider(): WebSearchProviderPlugin {
|
||||
}
|
||||
(scoped as Record<string, unknown>).apiKey = value;
|
||||
},
|
||||
getConfiguredCredentialValue: (config) =>
|
||||
resolveProviderWebSearchPluginConfig(config, "perplexity")?.apiKey,
|
||||
setConfiguredCredentialValue: (configTarget, value) => {
|
||||
setProviderWebSearchPluginConfigValue(configTarget, "perplexity", "apiKey", value);
|
||||
},
|
||||
resolveRuntimeMetadata: (ctx) => ({
|
||||
perplexityTransport: resolveRuntimeTransport({
|
||||
config: ctx.config,
|
||||
searchConfig: ctx.searchConfig,
|
||||
resolvedKey: ctx.resolvedCredential?.value,
|
||||
keySource: ctx.resolvedCredential?.source ?? "missing",
|
||||
@ -682,6 +701,7 @@ export function createPerplexityWebSearchProvider(): WebSearchProviderPlugin {
|
||||
}),
|
||||
createTool: (ctx) =>
|
||||
createPerplexityToolDefinition(
|
||||
ctx.config,
|
||||
ctx.searchConfig as SearchConfigRecord | undefined,
|
||||
ctx.runtimeMetadata?.perplexityTransport as PerplexityTransport | undefined,
|
||||
),
|
||||
|
||||
@ -19,9 +19,40 @@
|
||||
"cliDescription": "xAI API key"
|
||||
}
|
||||
],
|
||||
"uiHints": {
|
||||
"webSearch.apiKey": {
|
||||
"label": "Grok Search API Key",
|
||||
"help": "xAI API key for Grok web search (fallback: XAI_API_KEY env var).",
|
||||
"sensitive": true
|
||||
},
|
||||
"webSearch.model": {
|
||||
"label": "Grok Search Model",
|
||||
"help": "Grok model override for web search."
|
||||
},
|
||||
"webSearch.inlineCitations": {
|
||||
"label": "Inline Citations",
|
||||
"help": "Include inline markdown citations in Grok responses."
|
||||
}
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {}
|
||||
"properties": {
|
||||
"webSearch": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"apiKey": {
|
||||
"type": ["string", "object"]
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"inlineCitations": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,6 +14,11 @@ import {
|
||||
withTrustedWebSearchEndpoint,
|
||||
writeCachedSearchPayload,
|
||||
} from "../../../src/agents/tools/web-search-provider-common.js";
|
||||
import {
|
||||
resolveProviderWebSearchPluginConfig,
|
||||
setProviderWebSearchPluginConfigValue,
|
||||
} from "../../../src/agents/tools/web-search-provider-config.js";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import type {
|
||||
WebSearchProviderPlugin,
|
||||
WebSearchProviderToolDefinition,
|
||||
@ -60,14 +65,18 @@ type GrokSearchResponse = {
|
||||
}>;
|
||||
};
|
||||
|
||||
function resolveGrokConfig(searchConfig?: SearchConfigRecord): GrokConfig {
|
||||
const grok = searchConfig?.grok;
|
||||
function resolveGrokConfig(config?: OpenClawConfig, searchConfig?: SearchConfigRecord): GrokConfig {
|
||||
const pluginConfig = resolveProviderWebSearchPluginConfig(config, "xai");
|
||||
if (pluginConfig) {
|
||||
return pluginConfig as GrokConfig;
|
||||
}
|
||||
const grok = (searchConfig as Record<string, unknown> | undefined)?.grok;
|
||||
return grok && typeof grok === "object" && !Array.isArray(grok) ? (grok as GrokConfig) : {};
|
||||
}
|
||||
|
||||
function resolveGrokApiKey(grok?: GrokConfig): string | undefined {
|
||||
return (
|
||||
readConfiguredSecretString(grok?.apiKey, "tools.web.search.grok.apiKey") ??
|
||||
readConfiguredSecretString(grok?.apiKey, "plugins.entries.xai.config.webSearch.apiKey") ??
|
||||
readProviderEnvValue(["XAI_API_KEY"])
|
||||
);
|
||||
}
|
||||
@ -179,6 +188,7 @@ function createGrokSchema() {
|
||||
}
|
||||
|
||||
function createGrokToolDefinition(
|
||||
config?: OpenClawConfig,
|
||||
searchConfig?: SearchConfigRecord,
|
||||
): WebSearchProviderToolDefinition {
|
||||
return {
|
||||
@ -205,13 +215,13 @@ function createGrokToolDefinition(
|
||||
}
|
||||
}
|
||||
|
||||
const grokConfig = resolveGrokConfig(searchConfig);
|
||||
const grokConfig = resolveGrokConfig(config, searchConfig);
|
||||
const apiKey = resolveGrokApiKey(grokConfig);
|
||||
if (!apiKey) {
|
||||
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.",
|
||||
"web_search (grok) needs an xAI API key. Set XAI_API_KEY in the Gateway environment, or configure plugins.entries.xai.config.webSearch.apiKey.",
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
};
|
||||
}
|
||||
@ -274,8 +284,8 @@ export function createGrokWebSearchProvider(): WebSearchProviderPlugin {
|
||||
signupUrl: "https://console.x.ai/",
|
||||
docsUrl: "https://docs.openclaw.ai/tools/web",
|
||||
autoDetectOrder: 30,
|
||||
credentialPath: "tools.web.search.grok.apiKey",
|
||||
inactiveSecretPaths: ["tools.web.search.grok.apiKey"],
|
||||
credentialPath: "plugins.entries.xai.config.webSearch.apiKey",
|
||||
inactiveSecretPaths: ["plugins.entries.xai.config.webSearch.apiKey"],
|
||||
getCredentialValue: (searchConfig) => {
|
||||
const grok = searchConfig?.grok;
|
||||
return grok && typeof grok === "object" && !Array.isArray(grok)
|
||||
@ -290,8 +300,13 @@ export function createGrokWebSearchProvider(): WebSearchProviderPlugin {
|
||||
}
|
||||
(scoped as Record<string, unknown>).apiKey = value;
|
||||
},
|
||||
getConfiguredCredentialValue: (config) =>
|
||||
resolveProviderWebSearchPluginConfig(config, "xai")?.apiKey,
|
||||
setConfiguredCredentialValue: (configTarget, value) => {
|
||||
setProviderWebSearchPluginConfigValue(configTarget, "xai", "apiKey", value);
|
||||
},
|
||||
createTool: (ctx) =>
|
||||
createGrokToolDefinition(ctx.searchConfig as SearchConfigRecord | undefined),
|
||||
createGrokToolDefinition(ctx.config, ctx.searchConfig as SearchConfigRecord | undefined),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { resolvePluginWebSearchConfig } from "../../config/legacy-web-search.js";
|
||||
|
||||
type ConfiguredWebSearchProvider = NonNullable<
|
||||
NonNullable<NonNullable<OpenClawConfig["tools"]>["web"]>["search"]
|
||||
@ -78,6 +79,40 @@ export function resolveSearchConfig(cfg?: OpenClawConfig): WebSearchConfig {
|
||||
return search as WebSearchConfig;
|
||||
}
|
||||
|
||||
export function resolveProviderWebSearchPluginConfig(
|
||||
config: OpenClawConfig | undefined,
|
||||
pluginId: string,
|
||||
): Record<string, unknown> | undefined {
|
||||
return resolvePluginWebSearchConfig(config, pluginId);
|
||||
}
|
||||
|
||||
function ensureObject(target: Record<string, unknown>, key: string): Record<string, unknown> {
|
||||
const current = target[key];
|
||||
if (current && typeof current === "object" && !Array.isArray(current)) {
|
||||
return current as Record<string, unknown>;
|
||||
}
|
||||
const next: Record<string, unknown> = {};
|
||||
target[key] = next;
|
||||
return next;
|
||||
}
|
||||
|
||||
export function setProviderWebSearchPluginConfigValue(
|
||||
configTarget: OpenClawConfig,
|
||||
pluginId: string,
|
||||
key: string,
|
||||
value: unknown,
|
||||
): void {
|
||||
const plugins = ensureObject(configTarget as Record<string, unknown>, "plugins");
|
||||
const entries = ensureObject(plugins, "entries");
|
||||
const entry = ensureObject(entries, pluginId);
|
||||
if (entry.enabled === undefined) {
|
||||
entry.enabled = true;
|
||||
}
|
||||
const config = ensureObject(entry, "config");
|
||||
const webSearch = ensureObject(config, "webSearch");
|
||||
webSearch[key] = value;
|
||||
}
|
||||
|
||||
export function resolveSearchEnabled(params: {
|
||||
search?: WebSearchConfig;
|
||||
sandboxed?: boolean;
|
||||
|
||||
@ -44,12 +44,14 @@ export function hasKeyInEnv(entry: SearchProviderEntry): boolean {
|
||||
}
|
||||
|
||||
function rawKeyValue(config: OpenClawConfig, provider: SearchProvider): unknown {
|
||||
const search = config.tools?.web?.search;
|
||||
const entry = resolvePluginWebSearchProviders({
|
||||
config,
|
||||
bundledAllowlistCompat: true,
|
||||
}).find((candidate) => candidate.id === provider);
|
||||
return entry?.getCredentialValue(search as Record<string, unknown> | undefined);
|
||||
return (
|
||||
entry?.getConfiguredCredentialValue?.(config) ??
|
||||
entry?.getCredentialValue(config.tools?.web?.search as Record<string, unknown> | undefined)
|
||||
);
|
||||
}
|
||||
|
||||
/** Returns the plaintext key string, or undefined for SecretRefs/missing. */
|
||||
@ -99,17 +101,24 @@ export function applySearchKey(
|
||||
config,
|
||||
bundledAllowlistCompat: true,
|
||||
}).find((candidate) => candidate.id === provider);
|
||||
const search = { ...config.tools?.web?.search, provider, enabled: true };
|
||||
if (providerEntry) {
|
||||
providerEntry.setCredentialValue(search as Record<string, unknown>, key);
|
||||
}
|
||||
const nextBase = {
|
||||
...config,
|
||||
tools: {
|
||||
...config.tools,
|
||||
web: { ...config.tools?.web, search },
|
||||
web: {
|
||||
...config.tools?.web,
|
||||
search: { ...config.tools?.web?.search, provider, enabled: true },
|
||||
},
|
||||
},
|
||||
};
|
||||
if (providerEntry?.setConfiguredCredentialValue) {
|
||||
providerEntry.setConfiguredCredentialValue(nextBase, key);
|
||||
} else {
|
||||
const search = nextBase.tools?.web?.search as Record<string, unknown> | undefined;
|
||||
if (providerEntry && search) {
|
||||
providerEntry.setCredentialValue(search, key);
|
||||
}
|
||||
}
|
||||
return providerEntry?.applySelectionConfig?.(nextBase) ?? nextBase;
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { validateConfigObject } from "./config.js";
|
||||
import { validateConfigObjectWithPlugins } from "./config.js";
|
||||
import { buildWebSearchProviderConfig } from "./test-helpers.js";
|
||||
|
||||
vi.mock("../runtime.js", () => ({
|
||||
@ -9,43 +9,55 @@ vi.mock("../runtime.js", () => ({
|
||||
vi.mock("../plugins/web-search-providers.js", () => {
|
||||
const getScoped = (key: string) => (search?: Record<string, unknown>) =>
|
||||
(search?.[key] as { apiKey?: unknown } | undefined)?.apiKey;
|
||||
const getConfigured = (pluginId: string) => (config?: Record<string, unknown>) =>
|
||||
(
|
||||
config?.plugins as
|
||||
| { entries?: Record<string, { config?: { webSearch?: { apiKey?: unknown } } }> }
|
||||
| undefined
|
||||
)?.entries?.[pluginId]?.config?.webSearch?.apiKey;
|
||||
return {
|
||||
resolvePluginWebSearchProviders: () => [
|
||||
{
|
||||
id: "brave",
|
||||
envVars: ["BRAVE_API_KEY"],
|
||||
credentialPath: "tools.web.search.apiKey",
|
||||
credentialPath: "plugins.entries.brave.config.webSearch.apiKey",
|
||||
getCredentialValue: (search?: Record<string, unknown>) => search?.apiKey,
|
||||
getConfiguredCredentialValue: getConfigured("brave"),
|
||||
},
|
||||
{
|
||||
id: "firecrawl",
|
||||
envVars: ["FIRECRAWL_API_KEY"],
|
||||
credentialPath: "tools.web.search.firecrawl.apiKey",
|
||||
credentialPath: "plugins.entries.firecrawl.config.webSearch.apiKey",
|
||||
getCredentialValue: getScoped("firecrawl"),
|
||||
getConfiguredCredentialValue: getConfigured("firecrawl"),
|
||||
},
|
||||
{
|
||||
id: "gemini",
|
||||
envVars: ["GEMINI_API_KEY"],
|
||||
credentialPath: "tools.web.search.gemini.apiKey",
|
||||
credentialPath: "plugins.entries.google.config.webSearch.apiKey",
|
||||
getCredentialValue: getScoped("gemini"),
|
||||
getConfiguredCredentialValue: getConfigured("google"),
|
||||
},
|
||||
{
|
||||
id: "grok",
|
||||
envVars: ["XAI_API_KEY"],
|
||||
credentialPath: "tools.web.search.grok.apiKey",
|
||||
credentialPath: "plugins.entries.xai.config.webSearch.apiKey",
|
||||
getCredentialValue: getScoped("grok"),
|
||||
getConfiguredCredentialValue: getConfigured("xai"),
|
||||
},
|
||||
{
|
||||
id: "kimi",
|
||||
envVars: ["KIMI_API_KEY", "MOONSHOT_API_KEY"],
|
||||
credentialPath: "tools.web.search.kimi.apiKey",
|
||||
credentialPath: "plugins.entries.moonshot.config.webSearch.apiKey",
|
||||
getCredentialValue: getScoped("kimi"),
|
||||
getConfiguredCredentialValue: getConfigured("moonshot"),
|
||||
},
|
||||
{
|
||||
id: "perplexity",
|
||||
envVars: ["PERPLEXITY_API_KEY", "OPENROUTER_API_KEY"],
|
||||
credentialPath: "tools.web.search.perplexity.apiKey",
|
||||
credentialPath: "plugins.entries.perplexity.config.webSearch.apiKey",
|
||||
getCredentialValue: getScoped("perplexity"),
|
||||
getConfiguredCredentialValue: getConfigured("perplexity"),
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -56,7 +68,7 @@ const { resolveSearchProvider } = __testing;
|
||||
|
||||
describe("web search provider config", () => {
|
||||
it("accepts perplexity provider and config", () => {
|
||||
const res = validateConfigObject(
|
||||
const res = validateConfigObjectWithPlugins(
|
||||
buildWebSearchProviderConfig({
|
||||
enabled: true,
|
||||
provider: "perplexity",
|
||||
@ -72,7 +84,7 @@ describe("web search provider config", () => {
|
||||
});
|
||||
|
||||
it("accepts gemini provider and config", () => {
|
||||
const res = validateConfigObject(
|
||||
const res = validateConfigObjectWithPlugins(
|
||||
buildWebSearchProviderConfig({
|
||||
enabled: true,
|
||||
provider: "gemini",
|
||||
@ -87,7 +99,7 @@ describe("web search provider config", () => {
|
||||
});
|
||||
|
||||
it("accepts firecrawl provider and config", () => {
|
||||
const res = validateConfigObject(
|
||||
const res = validateConfigObjectWithPlugins(
|
||||
buildWebSearchProviderConfig({
|
||||
enabled: true,
|
||||
provider: "firecrawl",
|
||||
@ -102,7 +114,7 @@ describe("web search provider config", () => {
|
||||
});
|
||||
|
||||
it("accepts gemini provider with no extra config", () => {
|
||||
const res = validateConfigObject(
|
||||
const res = validateConfigObjectWithPlugins(
|
||||
buildWebSearchProviderConfig({
|
||||
provider: "gemini",
|
||||
}),
|
||||
@ -112,7 +124,7 @@ describe("web search provider config", () => {
|
||||
});
|
||||
|
||||
it("accepts brave llm-context mode config", () => {
|
||||
const res = validateConfigObject(
|
||||
const res = validateConfigObjectWithPlugins(
|
||||
buildWebSearchProviderConfig({
|
||||
provider: "brave",
|
||||
providerConfig: {
|
||||
@ -125,7 +137,7 @@ describe("web search provider config", () => {
|
||||
});
|
||||
|
||||
it("rejects invalid brave mode config values", () => {
|
||||
const res = validateConfigObject(
|
||||
const res = validateConfigObjectWithPlugins(
|
||||
buildWebSearchProviderConfig({
|
||||
provider: "brave",
|
||||
providerConfig: {
|
||||
|
||||
146
src/config/legacy-web-search.ts
Normal file
146
src/config/legacy-web-search.ts
Normal file
@ -0,0 +1,146 @@
|
||||
import type { OpenClawConfig } from "./config.js";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
const GENERIC_WEB_SEARCH_KEYS = new Set([
|
||||
"enabled",
|
||||
"provider",
|
||||
"maxResults",
|
||||
"timeoutSeconds",
|
||||
"cacheTtlMinutes",
|
||||
]);
|
||||
|
||||
const LEGACY_PROVIDER_MAP = {
|
||||
brave: "brave",
|
||||
firecrawl: "firecrawl",
|
||||
gemini: "google",
|
||||
grok: "xai",
|
||||
kimi: "moonshot",
|
||||
perplexity: "perplexity",
|
||||
} as const;
|
||||
|
||||
type LegacyProviderId = keyof typeof LEGACY_PROVIDER_MAP;
|
||||
|
||||
function isRecord(value: unknown): value is JsonRecord {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function cloneRecord<T extends JsonRecord>(value: T | undefined): T {
|
||||
return { ...value } as T;
|
||||
}
|
||||
|
||||
function ensureRecord(target: JsonRecord, key: string): JsonRecord {
|
||||
const current = target[key];
|
||||
if (isRecord(current)) {
|
||||
return current;
|
||||
}
|
||||
const next: JsonRecord = {};
|
||||
target[key] = next;
|
||||
return next;
|
||||
}
|
||||
|
||||
function resolveLegacySearchConfig(raw: unknown): JsonRecord | undefined {
|
||||
if (!isRecord(raw)) {
|
||||
return undefined;
|
||||
}
|
||||
const tools = isRecord(raw.tools) ? raw.tools : undefined;
|
||||
const web = isRecord(tools?.web) ? tools.web : undefined;
|
||||
return isRecord(web?.search) ? web.search : undefined;
|
||||
}
|
||||
|
||||
function copyLegacyProviderConfig(
|
||||
search: JsonRecord,
|
||||
providerKey: LegacyProviderId,
|
||||
): JsonRecord | undefined {
|
||||
const current = search[providerKey];
|
||||
return isRecord(current) ? cloneRecord(current) : undefined;
|
||||
}
|
||||
|
||||
function setPluginWebSearchConfig(
|
||||
target: JsonRecord,
|
||||
pluginId: string,
|
||||
webSearchConfig: JsonRecord,
|
||||
): void {
|
||||
const plugins = ensureRecord(target, "plugins");
|
||||
const entries = ensureRecord(plugins, "entries");
|
||||
const entry = ensureRecord(entries, pluginId);
|
||||
if (entry.enabled === undefined) {
|
||||
entry.enabled = true;
|
||||
}
|
||||
const config = ensureRecord(entry, "config");
|
||||
config.webSearch = webSearchConfig;
|
||||
}
|
||||
|
||||
export function listLegacyWebSearchConfigPaths(raw: unknown): string[] {
|
||||
const search = resolveLegacySearchConfig(raw);
|
||||
if (!search) {
|
||||
return [];
|
||||
}
|
||||
const paths: string[] = [];
|
||||
|
||||
if ("apiKey" in search) {
|
||||
paths.push("tools.web.search.apiKey");
|
||||
}
|
||||
for (const providerId of Object.keys(LEGACY_PROVIDER_MAP) as LegacyProviderId[]) {
|
||||
const scoped = search[providerId];
|
||||
if (isRecord(scoped)) {
|
||||
for (const key of Object.keys(scoped)) {
|
||||
paths.push(`tools.web.search.${providerId}.${key}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
export function normalizeLegacyWebSearchConfig<T>(raw: T): T {
|
||||
if (!isRecord(raw)) {
|
||||
return raw;
|
||||
}
|
||||
|
||||
const search = resolveLegacySearchConfig(raw);
|
||||
if (!search) {
|
||||
return raw;
|
||||
}
|
||||
|
||||
const nextRoot = cloneRecord(raw);
|
||||
const tools = ensureRecord(nextRoot, "tools");
|
||||
const web = ensureRecord(tools, "web");
|
||||
const nextSearch: JsonRecord = {};
|
||||
|
||||
for (const [key, value] of Object.entries(search)) {
|
||||
if (GENERIC_WEB_SEARCH_KEYS.has(key)) {
|
||||
nextSearch[key] = value;
|
||||
}
|
||||
}
|
||||
web.search = nextSearch;
|
||||
|
||||
const braveConfig = copyLegacyProviderConfig(search, "brave") ?? {};
|
||||
if ("apiKey" in search) {
|
||||
braveConfig.apiKey = search.apiKey;
|
||||
}
|
||||
if (Object.keys(braveConfig).length > 0) {
|
||||
setPluginWebSearchConfig(nextRoot, LEGACY_PROVIDER_MAP.brave, braveConfig);
|
||||
}
|
||||
|
||||
for (const providerId of ["firecrawl", "gemini", "grok", "kimi", "perplexity"] as const) {
|
||||
const scoped = copyLegacyProviderConfig(search, providerId);
|
||||
if (!scoped || Object.keys(scoped).length === 0) {
|
||||
continue;
|
||||
}
|
||||
setPluginWebSearchConfig(nextRoot, LEGACY_PROVIDER_MAP[providerId], scoped);
|
||||
}
|
||||
|
||||
return nextRoot as T;
|
||||
}
|
||||
|
||||
export function resolvePluginWebSearchConfig(
|
||||
config: OpenClawConfig | undefined,
|
||||
pluginId: string,
|
||||
): Record<string, unknown> | undefined {
|
||||
const pluginConfig = config?.plugins?.entries?.[pluginId]?.config;
|
||||
if (!isRecord(pluginConfig)) {
|
||||
return undefined;
|
||||
}
|
||||
const webSearch = pluginConfig.webSearch;
|
||||
return isRecord(webSearch) ? webSearch : undefined;
|
||||
}
|
||||
@ -667,33 +667,10 @@ 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", "firecrawl", "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).",
|
||||
"Search provider id. Auto-detected from available API keys if omitted.",
|
||||
"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.firecrawl.apiKey":
|
||||
"Firecrawl API key for web search (fallback: FIRECRAWL_API_KEY env var).",
|
||||
"tools.web.search.firecrawl.baseUrl":
|
||||
'Firecrawl Search base URL override (default: "https://api.firecrawl.dev").',
|
||||
"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").',
|
||||
"tools.web.search.grok.apiKey": "Grok (xAI) API key (fallback: XAI_API_KEY env var).", // pragma: allowlist secret
|
||||
"tools.web.search.grok.model": 'Grok model override (default: "grok-4-1-fast").',
|
||||
"tools.web.search.kimi.apiKey":
|
||||
"Moonshot/Kimi API key (fallback: KIMI_API_KEY or MOONSHOT_API_KEY env var).",
|
||||
"tools.web.search.kimi.baseUrl":
|
||||
'Kimi base URL override (default: "https://api.moonshot.ai/v1").',
|
||||
"tools.web.search.kimi.model": 'Kimi model override (default: "moonshot-v1-128k").',
|
||||
"tools.web.search.perplexity.apiKey":
|
||||
"Perplexity or OpenRouter API key (fallback: PERPLEXITY_API_KEY or OPENROUTER_API_KEY env var). Direct Perplexity keys default to the Search API; OpenRouter keys use Sonar chat completions.",
|
||||
"tools.web.search.perplexity.baseUrl":
|
||||
"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.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":
|
||||
|
||||
@ -216,23 +216,9 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"tools.message.broadcast.enabled": "Enable Message Broadcast",
|
||||
"tools.web.search.enabled": "Enable Web Search Tool",
|
||||
"tools.web.search.provider": "Web Search Provider",
|
||||
"tools.web.search.apiKey": "Brave Search API Key",
|
||||
"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.brave.mode": "Brave Search Mode",
|
||||
"tools.web.search.firecrawl.apiKey": "Firecrawl Search API Key", // pragma: allowlist secret
|
||||
"tools.web.search.firecrawl.baseUrl": "Firecrawl Search Base URL",
|
||||
"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.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",
|
||||
|
||||
@ -64,14 +64,32 @@ export function buildWebSearchProviderConfig(params: {
|
||||
if (params.enabled !== undefined) {
|
||||
search.enabled = params.enabled;
|
||||
}
|
||||
if (params.providerConfig) {
|
||||
search[params.provider] = params.providerConfig;
|
||||
}
|
||||
const pluginId =
|
||||
params.provider === "gemini"
|
||||
? "google"
|
||||
: params.provider === "grok"
|
||||
? "xai"
|
||||
: params.provider === "kimi"
|
||||
? "moonshot"
|
||||
: params.provider;
|
||||
return {
|
||||
tools: {
|
||||
web: {
|
||||
search,
|
||||
},
|
||||
},
|
||||
...(params.providerConfig
|
||||
? {
|
||||
plugins: {
|
||||
entries: {
|
||||
[pluginId]: {
|
||||
config: {
|
||||
webSearch: params.providerConfig,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
@ -457,62 +457,14 @@ export type ToolsConfig = {
|
||||
search?: {
|
||||
/** Enable web search tool (default: true when API key is present). */
|
||||
enabled?: boolean;
|
||||
/** Search provider ("brave", "firecrawl", "gemini", "grok", "kimi", or "perplexity"). */
|
||||
provider?: "brave" | "firecrawl" | "gemini" | "grok" | "kimi" | "perplexity";
|
||||
/** Brave Search API key (optional; defaults to BRAVE_API_KEY env var). */
|
||||
apiKey?: SecretInput;
|
||||
/** Search provider id. */
|
||||
provider?: string;
|
||||
/** Default search results count (1-10). */
|
||||
maxResults?: number;
|
||||
/** Timeout in seconds for search requests. */
|
||||
timeoutSeconds?: number;
|
||||
/** Cache TTL in minutes for search results. */
|
||||
cacheTtlMinutes?: number;
|
||||
/** 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;
|
||||
/** Model to use for grounded search (defaults to "gemini-2.5-flash"). */
|
||||
model?: string;
|
||||
};
|
||||
/** Firecrawl-specific configuration (used when provider="firecrawl"). */
|
||||
firecrawl?: {
|
||||
/** Firecrawl API key (defaults to FIRECRAWL_API_KEY env var). */
|
||||
apiKey?: SecretInput;
|
||||
/** Base URL for API requests (defaults to "https://api.firecrawl.dev"). */
|
||||
baseUrl?: string;
|
||||
};
|
||||
/** Grok-specific configuration (used when provider="grok"). */
|
||||
grok?: {
|
||||
/** API key for xAI (defaults to XAI_API_KEY env var). */
|
||||
apiKey?: SecretInput;
|
||||
/** Model to use (defaults to "grok-4-1-fast"). */
|
||||
model?: string;
|
||||
/** Include inline citations in response text as markdown links (default: false). */
|
||||
inlineCitations?: boolean;
|
||||
};
|
||||
/** Kimi-specific configuration (used when provider="kimi"). */
|
||||
kimi?: {
|
||||
/** Moonshot/Kimi API key (defaults to KIMI_API_KEY or MOONSHOT_API_KEY env var). */
|
||||
apiKey?: SecretInput;
|
||||
/** Base URL for API requests (defaults to "https://api.moonshot.ai/v1"). */
|
||||
baseUrl?: string;
|
||||
/** Model to use (defaults to "moonshot-v1-128k"). */
|
||||
model?: string;
|
||||
};
|
||||
/** 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?: {
|
||||
/** Enable web fetch tool (default: true). */
|
||||
|
||||
@ -20,6 +20,10 @@ import { isRecord } from "../utils.js";
|
||||
import { findDuplicateAgentDirs, formatDuplicateAgentDirError } from "./agent-dirs.js";
|
||||
import { appendAllowedValuesHint, summarizeAllowedValues } from "./allowed-values.js";
|
||||
import { applyAgentDefaults, applyModelDefaults, applySessionDefaults } from "./defaults.js";
|
||||
import {
|
||||
listLegacyWebSearchConfigPaths,
|
||||
normalizeLegacyWebSearchConfig,
|
||||
} from "./legacy-web-search.js";
|
||||
import { findLegacyConfigIssues } from "./legacy.js";
|
||||
import type { OpenClawConfig, ConfigValidationIssue } from "./types.js";
|
||||
import { OpenClawSchema } from "./zod-schema.js";
|
||||
@ -229,7 +233,8 @@ function validateGatewayTailscaleBind(config: OpenClawConfig): ConfigValidationI
|
||||
export function validateConfigObjectRaw(
|
||||
raw: unknown,
|
||||
): { ok: true; config: OpenClawConfig } | { ok: false; issues: ConfigValidationIssue[] } {
|
||||
const legacyIssues = findLegacyConfigIssues(raw);
|
||||
const normalizedRaw = normalizeLegacyWebSearchConfig(raw);
|
||||
const legacyIssues = findLegacyConfigIssues(normalizedRaw);
|
||||
if (legacyIssues.length > 0) {
|
||||
return {
|
||||
ok: false,
|
||||
@ -239,7 +244,7 @@ export function validateConfigObjectRaw(
|
||||
})),
|
||||
};
|
||||
}
|
||||
const validated = OpenClawSchema.safeParse(raw);
|
||||
const validated = OpenClawSchema.safeParse(normalizedRaw);
|
||||
if (!validated.success) {
|
||||
return {
|
||||
ok: false,
|
||||
@ -322,7 +327,12 @@ function validateConfigObjectWithPluginsBase(
|
||||
|
||||
const config = base.config;
|
||||
const issues: ConfigValidationIssue[] = [];
|
||||
const warnings: ConfigValidationIssue[] = [];
|
||||
const warnings: ConfigValidationIssue[] = listLegacyWebSearchConfigPaths(raw).map((path) => ({
|
||||
path,
|
||||
message:
|
||||
`${path} is deprecated for web search provider config. ` +
|
||||
"Move it under plugins.entries.<plugin>.config.webSearch.*; OpenClaw mapped it automatically for compatibility.",
|
||||
}));
|
||||
const hasExplicitPluginsConfig =
|
||||
isRecord(raw) && Object.prototype.hasOwnProperty.call(raw, "plugins");
|
||||
|
||||
|
||||
@ -263,66 +263,10 @@ export const ToolPolicySchema = ToolPolicyBaseSchema.superRefine((value, ctx) =>
|
||||
export const ToolsWebSearchSchema = z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
provider: z
|
||||
.union([
|
||||
z.literal("brave"),
|
||||
z.literal("firecrawl"),
|
||||
z.literal("perplexity"),
|
||||
z.literal("grok"),
|
||||
z.literal("gemini"),
|
||||
z.literal("kimi"),
|
||||
])
|
||||
.optional(),
|
||||
apiKey: SecretInputSchema.optional().register(sensitive),
|
||||
provider: z.string().optional(),
|
||||
maxResults: z.number().int().positive().optional(),
|
||||
timeoutSeconds: z.number().int().positive().optional(),
|
||||
cacheTtlMinutes: z.number().nonnegative().optional(),
|
||||
perplexity: z
|
||||
.object({
|
||||
apiKey: SecretInputSchema.optional().register(sensitive),
|
||||
// Legacy Sonar/OpenRouter compatibility fields.
|
||||
// Setting either opts Perplexity back into the chat-completions path.
|
||||
baseUrl: z.string().optional(),
|
||||
model: z.string().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
grok: z
|
||||
.object({
|
||||
apiKey: SecretInputSchema.optional().register(sensitive),
|
||||
model: z.string().optional(),
|
||||
inlineCitations: z.boolean().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
gemini: z
|
||||
.object({
|
||||
apiKey: SecretInputSchema.optional().register(sensitive),
|
||||
model: z.string().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
firecrawl: z
|
||||
.object({
|
||||
apiKey: SecretInputSchema.optional().register(sensitive),
|
||||
baseUrl: z.string().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
kimi: z
|
||||
.object({
|
||||
apiKey: SecretInputSchema.optional().register(sensitive),
|
||||
baseUrl: z.string().optional(),
|
||||
model: z.string().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
brave: z
|
||||
.object({
|
||||
mode: z.union([z.literal("web"), z.literal("llm-context")]).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
@ -892,6 +892,8 @@ export type WebSearchProviderPlugin = {
|
||||
inactiveSecretPaths?: string[];
|
||||
getCredentialValue: (searchConfig?: Record<string, unknown>) => unknown;
|
||||
setCredentialValue: (searchConfigTarget: Record<string, unknown>, value: unknown) => void;
|
||||
getConfiguredCredentialValue?: (config?: OpenClawConfig) => unknown;
|
||||
setConfiguredCredentialValue?: (configTarget: OpenClawConfig, value: unknown) => void;
|
||||
applySelectionConfig?: (config: OpenClawConfig) => OpenClawConfig;
|
||||
resolveRuntimeMetadata?: (
|
||||
ctx: WebSearchRuntimeMetadataContext,
|
||||
|
||||
@ -23,12 +23,12 @@ describe("resolvePluginWebSearchProviders", () => {
|
||||
"firecrawl:firecrawl",
|
||||
]);
|
||||
expect(providers.map((provider) => provider.credentialPath)).toEqual([
|
||||
"tools.web.search.apiKey",
|
||||
"tools.web.search.gemini.apiKey",
|
||||
"tools.web.search.grok.apiKey",
|
||||
"tools.web.search.kimi.apiKey",
|
||||
"tools.web.search.perplexity.apiKey",
|
||||
"tools.web.search.firecrawl.apiKey",
|
||||
"plugins.entries.brave.config.webSearch.apiKey",
|
||||
"plugins.entries.google.config.webSearch.apiKey",
|
||||
"plugins.entries.xai.config.webSearch.apiKey",
|
||||
"plugins.entries.moonshot.config.webSearch.apiKey",
|
||||
"plugins.entries.perplexity.config.webSearch.apiKey",
|
||||
"plugins.entries.firecrawl.config.webSearch.apiKey",
|
||||
]);
|
||||
expect(providers.find((provider) => provider.id === "firecrawl")?.applySelectionConfig).toEqual(
|
||||
expect.any(Function),
|
||||
|
||||
@ -11,6 +11,19 @@ function asConfig(value: unknown): OpenClawConfig {
|
||||
return value as OpenClawConfig;
|
||||
}
|
||||
|
||||
function providerPluginId(provider: ProviderUnderTest): string {
|
||||
switch (provider) {
|
||||
case "gemini":
|
||||
return "google";
|
||||
case "grok":
|
||||
return "xai";
|
||||
case "kimi":
|
||||
return "moonshot";
|
||||
default:
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
|
||||
async function runRuntimeWebTools(params: { config: OpenClawConfig; env?: NodeJS.ProcessEnv }) {
|
||||
const sourceConfig = structuredClone(params.config);
|
||||
const resolvedConfig = structuredClone(params.config);
|
||||
@ -30,40 +43,32 @@ function createProviderSecretRefConfig(
|
||||
provider: ProviderUnderTest,
|
||||
envRefId: string,
|
||||
): OpenClawConfig {
|
||||
const search: Record<string, unknown> = {
|
||||
enabled: true,
|
||||
provider,
|
||||
};
|
||||
if (provider === "brave") {
|
||||
search.apiKey = { source: "env", provider: "default", id: envRefId };
|
||||
} else {
|
||||
search[provider] = {
|
||||
apiKey: { source: "env", provider: "default", id: envRefId },
|
||||
};
|
||||
}
|
||||
return asConfig({
|
||||
tools: {
|
||||
web: {
|
||||
search,
|
||||
search: {
|
||||
enabled: true,
|
||||
provider,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
[providerPluginId(provider)]: {
|
||||
enabled: true,
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: { source: "env", provider: "default", id: envRefId },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function readProviderKey(config: OpenClawConfig, provider: ProviderUnderTest): unknown {
|
||||
if (provider === "brave") {
|
||||
return config.tools?.web?.search?.apiKey;
|
||||
}
|
||||
if (provider === "gemini") {
|
||||
return config.tools?.web?.search?.gemini?.apiKey;
|
||||
}
|
||||
if (provider === "grok") {
|
||||
return config.tools?.web?.search?.grok?.apiKey;
|
||||
}
|
||||
if (provider === "kimi") {
|
||||
return config.tools?.web?.search?.kimi?.apiKey;
|
||||
}
|
||||
return config.tools?.web?.search?.perplexity?.apiKey;
|
||||
return config.plugins?.entries?.[providerPluginId(provider)]?.config?.webSearch?.apiKey;
|
||||
}
|
||||
|
||||
function expectInactiveFirecrawlSecretRef(params: {
|
||||
@ -171,18 +176,40 @@ describe("runtime web tools resolution", () => {
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
apiKey: { source: "env", provider: "default", id: "BRAVE_REF" },
|
||||
gemini: {
|
||||
apiKey: { source: "env", provider: "default", id: "GEMINI_REF" },
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
brave: {
|
||||
enabled: true,
|
||||
config: {
|
||||
webSearch: { apiKey: { source: "env", provider: "default", id: "BRAVE_REF" } },
|
||||
},
|
||||
grok: {
|
||||
apiKey: { source: "env", provider: "default", id: "GROK_REF" },
|
||||
},
|
||||
google: {
|
||||
enabled: true,
|
||||
config: {
|
||||
webSearch: { apiKey: { source: "env", provider: "default", id: "GEMINI_REF" } },
|
||||
},
|
||||
kimi: {
|
||||
apiKey: { source: "env", provider: "default", id: "KIMI_REF" },
|
||||
},
|
||||
xai: {
|
||||
enabled: true,
|
||||
config: {
|
||||
webSearch: { apiKey: { source: "env", provider: "default", id: "GROK_REF" } },
|
||||
},
|
||||
perplexity: {
|
||||
apiKey: { source: "env", provider: "default", id: "PERPLEXITY_REF" },
|
||||
},
|
||||
moonshot: {
|
||||
enabled: true,
|
||||
config: {
|
||||
webSearch: { apiKey: { source: "env", provider: "default", id: "KIMI_REF" } },
|
||||
},
|
||||
},
|
||||
perplexity: {
|
||||
enabled: true,
|
||||
config: {
|
||||
webSearch: { apiKey: { source: "env", provider: "default", id: "PERPLEXITY_REF" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -199,13 +226,13 @@ describe("runtime web tools resolution", () => {
|
||||
|
||||
expect(metadata.search.providerSource).toBe("auto-detect");
|
||||
expect(metadata.search.selectedProvider).toBe("brave");
|
||||
expect(resolvedConfig.tools?.web?.search?.apiKey).toBe("brave-precedence-key");
|
||||
expect(readProviderKey(resolvedConfig, "brave")).toBe("brave-precedence-key");
|
||||
expect(context.warnings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ path: "tools.web.search.gemini.apiKey" }),
|
||||
expect.objectContaining({ path: "tools.web.search.grok.apiKey" }),
|
||||
expect.objectContaining({ path: "tools.web.search.kimi.apiKey" }),
|
||||
expect.objectContaining({ path: "tools.web.search.perplexity.apiKey" }),
|
||||
expect.objectContaining({ path: "plugins.entries.google.config.webSearch.apiKey" }),
|
||||
expect.objectContaining({ path: "plugins.entries.xai.config.webSearch.apiKey" }),
|
||||
expect.objectContaining({ path: "plugins.entries.moonshot.config.webSearch.apiKey" }),
|
||||
expect.objectContaining({ path: "plugins.entries.perplexity.config.webSearch.apiKey" }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
@ -216,12 +243,25 @@ describe("runtime web tools resolution", () => {
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
apiKey: { source: "env", provider: "default", id: "BRAVE_API_KEY_REF" },
|
||||
gemini: {
|
||||
apiKey: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_GEMINI_API_KEY_REF",
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
brave: {
|
||||
enabled: true,
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: { source: "env", provider: "default", id: "BRAVE_API_KEY_REF" },
|
||||
},
|
||||
},
|
||||
},
|
||||
google: {
|
||||
enabled: true,
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: { source: "env", provider: "default", id: "MISSING_GEMINI_API_KEY_REF" },
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -236,8 +276,8 @@ describe("runtime web tools resolution", () => {
|
||||
expect(metadata.search.providerSource).toBe("auto-detect");
|
||||
expect(metadata.search.selectedProvider).toBe("brave");
|
||||
expect(metadata.search.selectedProviderKeySource).toBe("secretRef");
|
||||
expect(resolvedConfig.tools?.web?.search?.apiKey).toBe("brave-runtime-key");
|
||||
expect(resolvedConfig.tools?.web?.search?.gemini?.apiKey).toEqual({
|
||||
expect(readProviderKey(resolvedConfig, "brave")).toBe("brave-runtime-key");
|
||||
expect(readProviderKey(resolvedConfig, "gemini")).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_GEMINI_API_KEY_REF",
|
||||
@ -246,7 +286,7 @@ describe("runtime web tools resolution", () => {
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
code: "SECRETS_REF_IGNORED_INACTIVE_SURFACE",
|
||||
path: "tools.web.search.gemini.apiKey",
|
||||
path: "plugins.entries.google.config.webSearch.apiKey",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
@ -261,9 +301,26 @@ describe("runtime web tools resolution", () => {
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
apiKey: { source: "env", provider: "default", id: "MISSING_BRAVE_API_KEY_REF" },
|
||||
gemini: {
|
||||
apiKey: { source: "env", provider: "default", id: "GEMINI_API_KEY_REF" },
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
brave: {
|
||||
enabled: true,
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: { source: "env", provider: "default", id: "MISSING_BRAVE_API_KEY_REF" },
|
||||
},
|
||||
},
|
||||
},
|
||||
google: {
|
||||
enabled: true,
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: { source: "env", provider: "default", id: "GEMINI_API_KEY_REF" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -276,12 +333,12 @@ describe("runtime web tools resolution", () => {
|
||||
|
||||
expect(metadata.search.providerSource).toBe("auto-detect");
|
||||
expect(metadata.search.selectedProvider).toBe("gemini");
|
||||
expect(resolvedConfig.tools?.web?.search?.gemini?.apiKey).toBe("gemini-runtime-key");
|
||||
expect(readProviderKey(resolvedConfig, "gemini")).toBe("gemini-runtime-key");
|
||||
expect(context.warnings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
code: "SECRETS_REF_IGNORED_INACTIVE_SURFACE",
|
||||
path: "tools.web.search.apiKey",
|
||||
path: "plugins.entries.brave.config.webSearch.apiKey",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
@ -297,8 +354,17 @@ describe("runtime web tools resolution", () => {
|
||||
web: {
|
||||
search: {
|
||||
provider: "invalid-provider",
|
||||
gemini: {
|
||||
apiKey: { source: "env", provider: "default", id: "GEMINI_API_KEY_REF" },
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
google: {
|
||||
enabled: true,
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: { source: "env", provider: "default", id: "GEMINI_API_KEY_REF" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -312,7 +378,7 @@ describe("runtime web tools resolution", () => {
|
||||
expect(metadata.search.providerConfigured).toBeUndefined();
|
||||
expect(metadata.search.providerSource).toBe("auto-detect");
|
||||
expect(metadata.search.selectedProvider).toBe("gemini");
|
||||
expect(resolvedConfig.tools?.web?.search?.gemini?.apiKey).toBe("gemini-runtime-key");
|
||||
expect(readProviderKey(resolvedConfig, "gemini")).toBe("gemini-runtime-key");
|
||||
expect(metadata.search.diagnostics).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
@ -337,8 +403,17 @@ describe("runtime web tools resolution", () => {
|
||||
web: {
|
||||
search: {
|
||||
provider: "gemini",
|
||||
gemini: {
|
||||
apiKey: { source: "env", provider: "default", id: "MISSING_GEMINI_API_KEY_REF" },
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
google: {
|
||||
enabled: true,
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: { source: "env", provider: "default", id: "MISSING_GEMINI_API_KEY_REF" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -361,7 +436,7 @@ describe("runtime web tools resolution", () => {
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
code: "WEB_SEARCH_KEY_UNRESOLVED_NO_FALLBACK",
|
||||
path: "tools.web.search.gemini.apiKey",
|
||||
path: "plugins.entries.google.config.webSearch.apiKey",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
@ -221,6 +221,9 @@ function setResolvedWebSearchApiKey(params: {
|
||||
env: params.env,
|
||||
bundledAllowlistCompat: true,
|
||||
}).find((entry) => entry.id === params.provider);
|
||||
if (provider?.setConfiguredCredentialValue) {
|
||||
provider.setConfiguredCredentialValue(params.resolvedConfig, params.value);
|
||||
}
|
||||
provider?.setCredentialValue(search, params.value);
|
||||
}
|
||||
|
||||
@ -318,7 +321,9 @@ export async function resolveRuntimeWebTools(params: {
|
||||
|
||||
for (const provider of candidates) {
|
||||
const path = keyPathForProvider(provider);
|
||||
const value = provider.getCredentialValue(search);
|
||||
const value =
|
||||
provider.getConfiguredCredentialValue?.(params.sourceConfig) ??
|
||||
provider.getCredentialValue(search);
|
||||
const resolution = await resolveSecretInputWithEnvFallback({
|
||||
sourceConfig: params.sourceConfig,
|
||||
context: params.context,
|
||||
@ -451,7 +456,9 @@ export async function resolveRuntimeWebTools(params: {
|
||||
if (provider.id === searchMetadata.selectedProvider) {
|
||||
continue;
|
||||
}
|
||||
const value = provider.getCredentialValue(search);
|
||||
const value =
|
||||
provider.getConfiguredCredentialValue?.(params.sourceConfig) ??
|
||||
provider.getCredentialValue(search);
|
||||
if (!hasConfiguredSecretRef(value, defaults)) {
|
||||
continue;
|
||||
}
|
||||
@ -465,7 +472,9 @@ export async function resolveRuntimeWebTools(params: {
|
||||
}
|
||||
} else if (search && !searchEnabled) {
|
||||
for (const provider of providers) {
|
||||
const value = provider.getCredentialValue(search);
|
||||
const value =
|
||||
provider.getConfiguredCredentialValue?.(params.sourceConfig) ??
|
||||
provider.getCredentialValue(search);
|
||||
if (!hasConfiguredSecretRef(value, defaults)) {
|
||||
continue;
|
||||
}
|
||||
@ -484,7 +493,9 @@ export async function resolveRuntimeWebTools(params: {
|
||||
if (provider.id === configuredProvider) {
|
||||
continue;
|
||||
}
|
||||
const value = provider.getCredentialValue(search);
|
||||
const value =
|
||||
provider.getConfiguredCredentialValue?.(params.sourceConfig) ??
|
||||
provider.getCredentialValue(search);
|
||||
if (!hasConfiguredSecretRef(value, defaults)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -733,6 +733,17 @@ const SECRET_TARGET_REGISTRY: SecretTargetRegistryEntry[] = [
|
||||
includeInConfigure: true,
|
||||
includeInAudit: true,
|
||||
},
|
||||
{
|
||||
id: "plugins.entries.brave.config.webSearch.apiKey",
|
||||
targetType: "plugins.entries.brave.config.webSearch.apiKey",
|
||||
configFile: "openclaw.json",
|
||||
pathPattern: "plugins.entries.brave.config.webSearch.apiKey",
|
||||
secretShape: SECRET_INPUT_SHAPE,
|
||||
expectedResolvedValue: "string",
|
||||
includeInPlan: true,
|
||||
includeInConfigure: true,
|
||||
includeInAudit: true,
|
||||
},
|
||||
{
|
||||
id: "tools.web.search.gemini.apiKey",
|
||||
targetType: "tools.web.search.gemini.apiKey",
|
||||
@ -744,6 +755,17 @@ const SECRET_TARGET_REGISTRY: SecretTargetRegistryEntry[] = [
|
||||
includeInConfigure: true,
|
||||
includeInAudit: true,
|
||||
},
|
||||
{
|
||||
id: "plugins.entries.google.config.webSearch.apiKey",
|
||||
targetType: "plugins.entries.google.config.webSearch.apiKey",
|
||||
configFile: "openclaw.json",
|
||||
pathPattern: "plugins.entries.google.config.webSearch.apiKey",
|
||||
secretShape: SECRET_INPUT_SHAPE,
|
||||
expectedResolvedValue: "string",
|
||||
includeInPlan: true,
|
||||
includeInConfigure: true,
|
||||
includeInAudit: true,
|
||||
},
|
||||
{
|
||||
id: "tools.web.search.grok.apiKey",
|
||||
targetType: "tools.web.search.grok.apiKey",
|
||||
@ -755,6 +777,17 @@ const SECRET_TARGET_REGISTRY: SecretTargetRegistryEntry[] = [
|
||||
includeInConfigure: true,
|
||||
includeInAudit: true,
|
||||
},
|
||||
{
|
||||
id: "plugins.entries.xai.config.webSearch.apiKey",
|
||||
targetType: "plugins.entries.xai.config.webSearch.apiKey",
|
||||
configFile: "openclaw.json",
|
||||
pathPattern: "plugins.entries.xai.config.webSearch.apiKey",
|
||||
secretShape: SECRET_INPUT_SHAPE,
|
||||
expectedResolvedValue: "string",
|
||||
includeInPlan: true,
|
||||
includeInConfigure: true,
|
||||
includeInAudit: true,
|
||||
},
|
||||
{
|
||||
id: "tools.web.search.kimi.apiKey",
|
||||
targetType: "tools.web.search.kimi.apiKey",
|
||||
@ -766,6 +799,17 @@ const SECRET_TARGET_REGISTRY: SecretTargetRegistryEntry[] = [
|
||||
includeInConfigure: true,
|
||||
includeInAudit: true,
|
||||
},
|
||||
{
|
||||
id: "plugins.entries.moonshot.config.webSearch.apiKey",
|
||||
targetType: "plugins.entries.moonshot.config.webSearch.apiKey",
|
||||
configFile: "openclaw.json",
|
||||
pathPattern: "plugins.entries.moonshot.config.webSearch.apiKey",
|
||||
secretShape: SECRET_INPUT_SHAPE,
|
||||
expectedResolvedValue: "string",
|
||||
includeInPlan: true,
|
||||
includeInConfigure: true,
|
||||
includeInAudit: true,
|
||||
},
|
||||
{
|
||||
id: "tools.web.search.perplexity.apiKey",
|
||||
targetType: "tools.web.search.perplexity.apiKey",
|
||||
@ -777,6 +821,28 @@ const SECRET_TARGET_REGISTRY: SecretTargetRegistryEntry[] = [
|
||||
includeInConfigure: true,
|
||||
includeInAudit: true,
|
||||
},
|
||||
{
|
||||
id: "plugins.entries.perplexity.config.webSearch.apiKey",
|
||||
targetType: "plugins.entries.perplexity.config.webSearch.apiKey",
|
||||
configFile: "openclaw.json",
|
||||
pathPattern: "plugins.entries.perplexity.config.webSearch.apiKey",
|
||||
secretShape: SECRET_INPUT_SHAPE,
|
||||
expectedResolvedValue: "string",
|
||||
includeInPlan: true,
|
||||
includeInConfigure: true,
|
||||
includeInAudit: true,
|
||||
},
|
||||
{
|
||||
id: "plugins.entries.firecrawl.config.webSearch.apiKey",
|
||||
targetType: "plugins.entries.firecrawl.config.webSearch.apiKey",
|
||||
configFile: "openclaw.json",
|
||||
pathPattern: "plugins.entries.firecrawl.config.webSearch.apiKey",
|
||||
secretShape: SECRET_INPUT_SHAPE,
|
||||
expectedResolvedValue: "string",
|
||||
includeInPlan: true,
|
||||
includeInConfigure: true,
|
||||
includeInAudit: true,
|
||||
},
|
||||
];
|
||||
|
||||
export { SECRET_TARGET_REGISTRY };
|
||||
|
||||
@ -61,22 +61,26 @@ function readProviderEnvValue(envVars: string[]): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function hasProviderCredential(providerId: string, search: WebSearchConfig | undefined): boolean {
|
||||
function hasProviderCredential(
|
||||
providerId: string,
|
||||
config: OpenClawConfig | undefined,
|
||||
search: WebSearchConfig | undefined,
|
||||
): boolean {
|
||||
const providers = resolvePluginWebSearchProviders({
|
||||
config,
|
||||
bundledAllowlistCompat: true,
|
||||
});
|
||||
const provider = providers.find((entry) => entry.id === providerId);
|
||||
if (!provider) {
|
||||
return false;
|
||||
}
|
||||
const rawValue = provider.getCredentialValue(search as Record<string, unknown> | undefined);
|
||||
const rawValue =
|
||||
provider.getConfiguredCredentialValue?.(config) ??
|
||||
provider.getCredentialValue(search as Record<string, unknown> | undefined);
|
||||
const fromConfig = normalizeSecretInput(
|
||||
normalizeResolvedSecretInputString({
|
||||
value: rawValue,
|
||||
path:
|
||||
providerId === "brave"
|
||||
? "tools.web.search.apiKey"
|
||||
: `tools.web.search.${providerId}.apiKey`,
|
||||
path: provider.credentialPath,
|
||||
}),
|
||||
);
|
||||
return Boolean(fromConfig || readProviderEnvValue(provider.envVars));
|
||||
@ -93,11 +97,13 @@ export function listWebSearchProviders(params?: {
|
||||
|
||||
export function resolveWebSearchProviderId(params: {
|
||||
search?: WebSearchConfig;
|
||||
config?: OpenClawConfig;
|
||||
providers?: PluginWebSearchProviderEntry[];
|
||||
}): string {
|
||||
const providers =
|
||||
params.providers ??
|
||||
resolvePluginWebSearchProviders({
|
||||
config: params.config,
|
||||
bundledAllowlistCompat: true,
|
||||
});
|
||||
const raw =
|
||||
@ -114,7 +120,7 @@ export function resolveWebSearchProviderId(params: {
|
||||
|
||||
if (!raw) {
|
||||
for (const provider of providers) {
|
||||
if (!hasProviderCredential(provider.id, params.search)) {
|
||||
if (!hasProviderCredential(provider.id, params.config, params.search)) {
|
||||
continue;
|
||||
}
|
||||
logVerbose(
|
||||
@ -124,7 +130,7 @@ export function resolveWebSearchProviderId(params: {
|
||||
}
|
||||
}
|
||||
|
||||
return providers[0]?.id ?? "brave";
|
||||
return providers[0]?.id ?? "";
|
||||
}
|
||||
|
||||
export function resolveWebSearchDefinition(
|
||||
@ -154,10 +160,13 @@ export function resolveWebSearchDefinition(
|
||||
options?.providerId ??
|
||||
options?.runtimeWebSearch?.selectedProvider ??
|
||||
options?.runtimeWebSearch?.providerConfigured ??
|
||||
resolveWebSearchProviderId({ search, providers });
|
||||
resolveWebSearchProviderId({ config: options?.config, search, providers });
|
||||
const provider =
|
||||
providers.find((entry) => entry.id === providerId) ??
|
||||
providers.find((entry) => entry.id === resolveWebSearchProviderId({ search, providers })) ??
|
||||
providers.find(
|
||||
(entry) =>
|
||||
entry.id === resolveWebSearchProviderId({ config: options?.config, search, providers }),
|
||||
) ??
|
||||
providers[0];
|
||||
if (!provider) {
|
||||
return null;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user