Skip to content

Commit 76cf58c

Browse files
committed
Web tools: canonicalize Tavily config
1 parent 0caed95 commit 76cf58c

15 files changed

Lines changed: 171 additions & 69 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ Docs: https://docs.openclaw.ai
4747
- Plugins/Xiaomi: switch the bundled Xiaomi provider to the `/v1` OpenAI-compatible endpoint and add MiMo V2 Pro plus MiMo V2 Omni to the built-in catalog. (#49214) thanks @DJjjjhao.
4848
- Plugins/Matrix: add `allowBots` room policy so configured Matrix bot accounts can talk to each other, with optional mention-only gating. Thanks @gumadeiras.
4949
- Plugins/Matrix: add per-account `allowPrivateNetwork` opt-in for private/internal homeservers, while keeping public cleartext homeservers blocked. Thanks @gumadeiras.
50+
- Web tools/Tavily: add Tavily as a bundled web-search provider with dedicated `tavily_search` and `tavily_extract` tools, using canonical plugin-owned config under `plugins.entries.tavily.config.webSearch.*`. (#49200) thanks @lakshyaag-tavily.
5051

5152
### Fixes
5253

docs/tools/tavily.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,19 @@ from URLs (including JavaScript-rendered pages).
3434
entries: {
3535
tavily: {
3636
enabled: true,
37+
config: {
38+
webSearch: {
39+
apiKey: "tvly-...", // optional if TAVILY_API_KEY is set
40+
baseUrl: "https://api.tavily.com",
41+
},
42+
},
3743
},
3844
},
3945
},
4046
tools: {
4147
web: {
4248
search: {
4349
provider: "tavily",
44-
tavily: {
45-
apiKey: "tvly-...", // optional if TAVILY_API_KEY is set
46-
},
4750
},
4851
},
4952
},
@@ -54,6 +57,7 @@ Notes:
5457

5558
- Choosing Tavily in onboarding or `openclaw configure --section web` enables
5659
the bundled Tavily plugin automatically.
60+
- Store Tavily config under `plugins.entries.tavily.config.webSearch.*`.
5761
- `web_search` with Tavily supports `query` and `count` (up to 20 results).
5862
- For Tavily-specific controls like `search_depth`, `topic`, `include_answer`,
5963
or domain filters, use `tavily_search`.

docs/tools/web.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ See [Perplexity Search API Docs](https://docs.perplexity.ai/guides/search-quicks
100100
- Grok: `plugins.entries.xai.config.webSearch.apiKey`
101101
- Kimi: `plugins.entries.moonshot.config.webSearch.apiKey`
102102
- Perplexity: `plugins.entries.perplexity.config.webSearch.apiKey`
103+
- Tavily: `plugins.entries.tavily.config.webSearch.apiKey`
103104

104105
All of these fields also support SecretRef objects.
105106

@@ -111,6 +112,7 @@ All of these fields also support SecretRef objects.
111112
- Grok: `XAI_API_KEY`
112113
- Kimi: `KIMI_API_KEY` or `MOONSHOT_API_KEY`
113114
- Perplexity: `PERPLEXITY_API_KEY` or `OPENROUTER_API_KEY`
115+
- Tavily: `TAVILY_API_KEY`
114116

115117
For a gateway install, put these in `~/.openclaw/.env` (or your service environment). See [Env vars](/help/faq#how-does-openclaw-load-environment-variables).
116118

@@ -187,6 +189,12 @@ When you choose Firecrawl in onboarding or `openclaw configure --section web`, O
187189
entries: {
188190
tavily: {
189191
enabled: true,
192+
config: {
193+
webSearch: {
194+
apiKey: "tvly-...", // optional if TAVILY_API_KEY is set
195+
baseUrl: "https://api.tavily.com",
196+
},
197+
},
190198
},
191199
},
192200
},
@@ -195,9 +203,6 @@ When you choose Firecrawl in onboarding or `openclaw configure --section web`, O
195203
search: {
196204
enabled: true,
197205
provider: "tavily",
198-
tavily: {
199-
apiKey: "tvly-...", // optional if TAVILY_API_KEY is set
200-
},
201206
},
202207
},
203208
},
Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,34 @@
11
{
22
"id": "tavily",
33
"skills": ["./skills"],
4+
"uiHints": {
5+
"webSearch.apiKey": {
6+
"label": "Tavily API Key",
7+
"help": "Tavily API key for web search and extraction (fallback: TAVILY_API_KEY env var).",
8+
"sensitive": true,
9+
"placeholder": "tvly-..."
10+
},
11+
"webSearch.baseUrl": {
12+
"label": "Tavily Base URL",
13+
"help": "Tavily API base URL override."
14+
}
15+
},
416
"configSchema": {
517
"type": "object",
618
"additionalProperties": false,
719
"properties": {
8-
"apiKey": { "type": "string" },
9-
"baseUrl": { "type": "string" }
20+
"webSearch": {
21+
"type": "object",
22+
"additionalProperties": false,
23+
"properties": {
24+
"apiKey": {
25+
"type": ["string", "object"]
26+
},
27+
"baseUrl": {
28+
"type": "string"
29+
}
30+
}
31+
}
1032
}
1133
}
1234
}

extensions/tavily/src/config.ts

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,13 @@ export const DEFAULT_TAVILY_BASE_URL = "https://api.tavily.com";
66
export const DEFAULT_TAVILY_SEARCH_TIMEOUT_SECONDS = 30;
77
export const DEFAULT_TAVILY_EXTRACT_TIMEOUT_SECONDS = 60;
88

9-
type WebSearchConfig = NonNullable<OpenClawConfig["tools"]>["web"] extends infer Web
10-
? Web extends { search?: infer Search }
11-
? Search
12-
: undefined
13-
: undefined;
14-
159
type TavilySearchConfig =
1610
| {
1711
apiKey?: unknown;
1812
baseUrl?: string;
1913
}
2014
| undefined;
2115

22-
function resolveSearchConfig(cfg?: OpenClawConfig): WebSearchConfig {
23-
const search = cfg?.tools?.web?.search;
24-
if (!search || typeof search !== "object") {
25-
return undefined;
26-
}
27-
return search as WebSearchConfig;
28-
}
29-
3016
type PluginEntryConfig = {
3117
webSearch?: {
3218
apiKey?: unknown;
@@ -35,22 +21,12 @@ type PluginEntryConfig = {
3521
};
3622

3723
export function resolveTavilySearchConfig(cfg?: OpenClawConfig): TavilySearchConfig {
38-
// Prefer the new plugin config path (plugins.entries.tavily.config.webSearch).
3924
const pluginConfig = cfg?.plugins?.entries?.tavily?.config as PluginEntryConfig;
4025
const pluginWebSearch = pluginConfig?.webSearch;
4126
if (pluginWebSearch && typeof pluginWebSearch === "object" && !Array.isArray(pluginWebSearch)) {
4227
return pluginWebSearch;
4328
}
44-
// Fall back to the legacy config path (tools.web.search.tavily).
45-
const search = resolveSearchConfig(cfg);
46-
if (!search || typeof search !== "object") {
47-
return undefined;
48-
}
49-
const tavily = "tavily" in search ? search.tavily : undefined;
50-
if (!tavily || typeof tavily !== "object") {
51-
return undefined;
52-
}
53-
return tavily as TavilySearchConfig;
29+
return undefined;
5430
}
5531

5632
function normalizeConfiguredSecret(value: unknown, path: string): string | undefined {
@@ -66,7 +42,6 @@ export function resolveTavilyApiKey(cfg?: OpenClawConfig): string | undefined {
6642
const search = resolveTavilySearchConfig(cfg);
6743
return (
6844
normalizeConfiguredSecret(search?.apiKey, "plugins.entries.tavily.config.webSearch.apiKey") ||
69-
normalizeConfiguredSecret(search?.apiKey, "tools.web.search.tavily.apiKey") ||
7045
normalizeSecretInput(process.env.TAVILY_API_KEY) ||
7146
undefined
7247
);

extensions/tavily/src/tavily-client.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ export async function runTavilySearch(
108108
const apiKey = resolveTavilyApiKey(params.cfg);
109109
if (!apiKey) {
110110
throw new Error(
111-
"web_search (tavily) needs a Tavily API key. Set TAVILY_API_KEY in the Gateway environment, or configure tools.web.search.tavily.apiKey.",
111+
"web_search (tavily) needs a Tavily API key. Set TAVILY_API_KEY in the Gateway environment, or configure plugins.entries.tavily.config.webSearch.apiKey.",
112112
);
113113
}
114114
const count =
@@ -199,7 +199,7 @@ export async function runTavilyExtract(
199199
const apiKey = resolveTavilyApiKey(params.cfg);
200200
if (!apiKey) {
201201
throw new Error(
202-
"tavily_extract needs a Tavily API key. Set TAVILY_API_KEY in the Gateway environment, or configure tools.web.search.tavily.apiKey.",
202+
"tavily_extract needs a Tavily API key. Set TAVILY_API_KEY in the Gateway environment, or configure plugins.entries.tavily.config.webSearch.apiKey.",
203203
);
204204
}
205205
const baseUrl = resolveTavilyBaseUrl(params.cfg);

pnpm-lock.yaml

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/commands/onboard-search.test.ts

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ function createPerplexityConfig(apiKey: string, enabled?: boolean): OpenClawConf
4848
};
4949
}
5050

51+
function pluginWebSearchApiKey(config: OpenClawConfig, pluginId: string): unknown {
52+
const entry = (
53+
config.plugins?.entries as
54+
| Record<string, { config?: { webSearch?: { apiKey?: unknown } } }>
55+
| undefined
56+
)?.[pluginId];
57+
return entry?.config?.webSearch?.apiKey;
58+
}
59+
5160
async function runBlankPerplexityKeyEntry(
5261
apiKey: string,
5362
enabled?: boolean,
@@ -88,8 +97,9 @@ describe("setupSearch", () => {
8897
});
8998
const result = await setupSearch(cfg, runtime, prompter);
9099
expect(result.tools?.web?.search?.provider).toBe("perplexity");
91-
expect(result.tools?.web?.search?.perplexity?.apiKey).toBe("pplx-test-key");
100+
expect(pluginWebSearchApiKey(result, "perplexity")).toBe("pplx-test-key");
92101
expect(result.tools?.web?.search?.enabled).toBe(true);
102+
expect(result.plugins?.entries?.perplexity?.enabled).toBe(true);
93103
});
94104

95105
it("sets provider and key for brave", async () => {
@@ -101,7 +111,8 @@ describe("setupSearch", () => {
101111
const result = await setupSearch(cfg, runtime, prompter);
102112
expect(result.tools?.web?.search?.provider).toBe("brave");
103113
expect(result.tools?.web?.search?.enabled).toBe(true);
104-
expect(result.tools?.web?.search?.apiKey).toBe("BSA-test-key");
114+
expect(pluginWebSearchApiKey(result, "brave")).toBe("BSA-test-key");
115+
expect(result.plugins?.entries?.brave?.enabled).toBe(true);
105116
});
106117

107118
it("sets provider and key for gemini", async () => {
@@ -113,7 +124,8 @@ describe("setupSearch", () => {
113124
const result = await setupSearch(cfg, runtime, prompter);
114125
expect(result.tools?.web?.search?.provider).toBe("gemini");
115126
expect(result.tools?.web?.search?.enabled).toBe(true);
116-
expect(result.tools?.web?.search?.gemini?.apiKey).toBe("AIza-test");
127+
expect(pluginWebSearchApiKey(result, "google")).toBe("AIza-test");
128+
expect(result.plugins?.entries?.google?.enabled).toBe(true);
117129
});
118130

119131
it("sets provider and key for firecrawl and enables the plugin", async () => {
@@ -125,7 +137,7 @@ describe("setupSearch", () => {
125137
const result = await setupSearch(cfg, runtime, prompter);
126138
expect(result.tools?.web?.search?.provider).toBe("firecrawl");
127139
expect(result.tools?.web?.search?.enabled).toBe(true);
128-
expect(result.tools?.web?.search?.firecrawl?.apiKey).toBe("fc-test-key");
140+
expect(pluginWebSearchApiKey(result, "firecrawl")).toBe("fc-test-key");
129141
expect(result.plugins?.entries?.firecrawl?.enabled).toBe(true);
130142
});
131143

@@ -150,7 +162,21 @@ describe("setupSearch", () => {
150162
const result = await setupSearch(cfg, runtime, prompter);
151163
expect(result.tools?.web?.search?.provider).toBe("kimi");
152164
expect(result.tools?.web?.search?.enabled).toBe(true);
153-
expect(result.tools?.web?.search?.kimi?.apiKey).toBe("sk-moonshot");
165+
expect(pluginWebSearchApiKey(result, "moonshot")).toBe("sk-moonshot");
166+
expect(result.plugins?.entries?.moonshot?.enabled).toBe(true);
167+
});
168+
169+
it("sets provider and key for tavily and enables the plugin", async () => {
170+
const cfg: OpenClawConfig = {};
171+
const { prompter } = createPrompter({
172+
selectValue: "tavily",
173+
textValue: "tvly-test-key",
174+
});
175+
const result = await setupSearch(cfg, runtime, prompter);
176+
expect(result.tools?.web?.search?.provider).toBe("tavily");
177+
expect(result.tools?.web?.search?.enabled).toBe(true);
178+
expect(pluginWebSearchApiKey(result, "tavily")).toBe("tvly-test-key");
179+
expect(result.plugins?.entries?.tavily?.enabled).toBe(true);
154180
});
155181

156182
it("shows missing-key note when no key is provided and no env var", async () => {
@@ -198,7 +224,7 @@ describe("setupSearch", () => {
198224
"stored-pplx-key", // pragma: allowlist secret
199225
);
200226
expect(result.tools?.web?.search?.provider).toBe("perplexity");
201-
expect(result.tools?.web?.search?.perplexity?.apiKey).toBe("stored-pplx-key");
227+
expect(pluginWebSearchApiKey(result, "perplexity")).toBe("stored-pplx-key");
202228
expect(result.tools?.web?.search?.enabled).toBe(true);
203229
expect(prompter.text).not.toHaveBeenCalled();
204230
});
@@ -209,7 +235,7 @@ describe("setupSearch", () => {
209235
false,
210236
);
211237
expect(result.tools?.web?.search?.provider).toBe("perplexity");
212-
expect(result.tools?.web?.search?.perplexity?.apiKey).toBe("stored-pplx-key");
238+
expect(pluginWebSearchApiKey(result, "perplexity")).toBe("stored-pplx-key");
213239
expect(result.tools?.web?.search?.enabled).toBe(false);
214240
expect(prompter.text).not.toHaveBeenCalled();
215241
});
@@ -268,7 +294,7 @@ describe("setupSearch", () => {
268294
secretInputMode: "ref", // pragma: allowlist secret
269295
});
270296
expect(result.tools?.web?.search?.provider).toBe("perplexity");
271-
expect(result.tools?.web?.search?.perplexity?.apiKey).toEqual({
297+
expect(pluginWebSearchApiKey(result, "perplexity")).toEqual({
272298
source: "env",
273299
provider: "default",
274300
id: "PERPLEXITY_API_KEY", // pragma: allowlist secret
@@ -299,7 +325,7 @@ describe("setupSearch", () => {
299325
const result = await setupSearch(cfg, runtime, prompter, {
300326
secretInputMode: "ref", // pragma: allowlist secret
301327
});
302-
expect(result.tools?.web?.search?.perplexity?.apiKey).toEqual({
328+
expect(pluginWebSearchApiKey(result, "perplexity")).toEqual({
303329
source: "env",
304330
provider: "default",
305331
id: "OPENROUTER_API_KEY", // pragma: allowlist secret
@@ -326,22 +352,49 @@ describe("setupSearch", () => {
326352
secretInputMode: "ref", // pragma: allowlist secret
327353
});
328354
expect(result.tools?.web?.search?.provider).toBe("brave");
329-
expect(result.tools?.web?.search?.apiKey).toEqual({
355+
expect(pluginWebSearchApiKey(result, "brave")).toEqual({
330356
source: "env",
331357
provider: "default",
332358
id: "BRAVE_API_KEY",
333359
});
360+
expect(result.plugins?.entries?.brave?.enabled).toBe(true);
334361
expect(prompter.text).not.toHaveBeenCalled();
335362
});
336363

364+
it("stores env-backed SecretRef when secretInputMode=ref for tavily", async () => {
365+
const original = process.env.TAVILY_API_KEY;
366+
delete process.env.TAVILY_API_KEY;
367+
const cfg: OpenClawConfig = {};
368+
try {
369+
const { prompter } = createPrompter({ selectValue: "tavily" });
370+
const result = await setupSearch(cfg, runtime, prompter, {
371+
secretInputMode: "ref", // pragma: allowlist secret
372+
});
373+
expect(result.tools?.web?.search?.provider).toBe("tavily");
374+
expect(pluginWebSearchApiKey(result, "tavily")).toEqual({
375+
source: "env",
376+
provider: "default",
377+
id: "TAVILY_API_KEY",
378+
});
379+
expect(result.plugins?.entries?.tavily?.enabled).toBe(true);
380+
expect(prompter.text).not.toHaveBeenCalled();
381+
} finally {
382+
if (original === undefined) {
383+
delete process.env.TAVILY_API_KEY;
384+
} else {
385+
process.env.TAVILY_API_KEY = original;
386+
}
387+
}
388+
});
389+
337390
it("stores plaintext key when secretInputMode is unset", async () => {
338391
const cfg: OpenClawConfig = {};
339392
const { prompter } = createPrompter({
340393
selectValue: "brave",
341394
textValue: "BSA-plain",
342395
});
343396
const result = await setupSearch(cfg, runtime, prompter);
344-
expect(result.tools?.web?.search?.apiKey).toBe("BSA-plain");
397+
expect(pluginWebSearchApiKey(result, "brave")).toBe("BSA-plain");
345398
});
346399

347400
it("exports all 7 providers in SEARCH_PROVIDER_OPTIONS", () => {

src/commands/onboard-search.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ export function applySearchKey(
104104
bundledAllowlistCompat: true,
105105
}).find((candidate) => candidate.id === provider);
106106
const search: MutableSearchConfig = { ...config.tools?.web?.search, provider, enabled: true };
107-
if (providerEntry) {
107+
if (providerEntry && !providerEntry.setConfiguredCredentialValue) {
108108
providerEntry.setCredentialValue(search, key);
109109
}
110110
const nextBase: OpenClawConfig = {
@@ -114,7 +114,9 @@ export function applySearchKey(
114114
web: { ...config.tools?.web, search },
115115
},
116116
};
117-
return providerEntry?.applySelectionConfig?.(nextBase) ?? nextBase;
117+
const next = providerEntry?.applySelectionConfig?.(nextBase) ?? nextBase;
118+
providerEntry?.setConfiguredCredentialValue?.(next, key);
119+
return next;
118120
}
119121

120122
function applyProviderOnly(config: OpenClawConfig, provider: SearchProvider): OpenClawConfig {

0 commit comments

Comments
 (0)