From afefcdf8fbc46580d954465865390b40bdff0e91 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 18 Jan 2026 01:15:28 +0000 Subject: [PATCH] fix: improve Perplexity web_search defaults (#1131) (thanks @CMLKevin) --- CHANGELOG.md | 1 + docs/perplexity.md | 55 +++++++++ docs/tools/web.md | 40 ++++--- src/agents/system-prompt.ts | 2 +- .../tools/web-tools.enabled-defaults.test.ts | 99 ++++++++++++++++ src/agents/tools/web-tools.ts | 106 ++++++++++++++++-- src/commands/configure.wizard.ts | 100 +++++++++++++---- src/commands/onboard-non-interactive/local.ts | 2 +- .../onboard-non-interactive/remote.ts | 2 +- src/config/schema.ts | 10 +- src/config/types.tools.ts | 2 +- src/wizard/onboarding.finalize.ts | 41 +++++-- 12 files changed, 400 insertions(+), 60 deletions(-) create mode 100644 docs/perplexity.md diff --git a/CHANGELOG.md b/CHANGELOG.md index acc7dc28bc..ba15d616cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Docs: https://docs.clawd.bot ### Changes - Tools: allow `sessions_spawn` to override thinking level for sub-agent runs. +- Tools: add Perplexity Sonar provider for `web_search` with direct Perplexity/OpenRouter support. (#1131) — thanks @CMLKevin. ### Fixes - Memory: apply OpenAI batch defaults even without explicit remote config. diff --git a/docs/perplexity.md b/docs/perplexity.md new file mode 100644 index 0000000000..8ed4d9b93f --- /dev/null +++ b/docs/perplexity.md @@ -0,0 +1,55 @@ +--- +summary: "Perplexity Sonar setup for web_search" +read_when: + - You want to use Perplexity Sonar for web search + - You need PERPLEXITY_API_KEY or OpenRouter setup +--- + +# Perplexity Sonar + +Clawdbot can use Perplexity Sonar for the `web_search` tool. You can connect +through Perplexity’s direct API or via OpenRouter. + +## API options + +### Perplexity (direct) + +- Base URL: https://api.perplexity.ai +- Environment variable: `PERPLEXITY_API_KEY` + +### OpenRouter (alternative) + +- Base URL: https://openrouter.ai/api/v1 +- Environment variable: `OPENROUTER_API_KEY` +- Supports prepaid/crypto credits. + +## Config example + +```json5 +{ + tools: { + web: { + search: { + provider: "perplexity", + perplexity: { + apiKey: "pplx-...", + baseUrl: "https://api.perplexity.ai", + model: "perplexity/sonar-pro" + } + } + } + } +} +``` + +If both `PERPLEXITY_API_KEY` and `OPENROUTER_API_KEY` are set, set +`tools.web.search.perplexity.baseUrl` (or `tools.web.search.perplexity.apiKey`) +to disambiguate. + +## Models + +- `perplexity/sonar` — fast Q&A with web search +- `perplexity/sonar-pro` (default) — multi-step reasoning + web search +- `perplexity/sonar-reasoning-pro` — deep research + +See [Web tools](/tools/web) for the full web_search configuration. diff --git a/docs/tools/web.md b/docs/tools/web.md index f36f0f0b44..1961fb9402 100644 --- a/docs/tools/web.md +++ b/docs/tools/web.md @@ -1,5 +1,5 @@ --- -summary: "Web search + fetch tools (Brave Search API, Perplexity via OpenRouter)" +summary: "Web search + fetch tools (Brave Search API, Perplexity Sonar)" read_when: - You want to enable web_search or web_fetch - You need Brave Search API key setup @@ -10,7 +10,7 @@ read_when: Clawdbot ships two lightweight web tools: -- `web_search` — Search the web via Brave Search API (default) or Perplexity Sonar (via OpenRouter). +- `web_search` — Search the web via Brave Search API (default) or Perplexity Sonar (direct or via OpenRouter). - `web_fetch` — HTTP fetch + readable extraction (HTML → markdown/text). These are **not** browser automation. For JS-heavy sites or logins, use the @@ -28,10 +28,12 @@ These are **not** browser automation. For JS-heavy sites or logins, use the ## Choosing a search provider +Provider docs: [Brave Search API](https://brave.com/search/api/) and [Perplexity Sonar](/perplexity). + | Provider | Pros | Cons | API Key | |----------|------|------|---------| | **Brave** (default) | Fast, structured results, free tier | Traditional search results | `BRAVE_API_KEY` | -| **Perplexity** | AI-synthesized answers, citations, real-time | Requires OpenRouter credits | `OPENROUTER_API_KEY` or `PERPLEXITY_API_KEY` | +| **Perplexity** | AI-synthesized answers, citations, real-time | Requires Perplexity or OpenRouter credits | `PERPLEXITY_API_KEY` or `OPENROUTER_API_KEY` | Set the provider in config: @@ -65,13 +67,22 @@ current limits and pricing. environment. For a daemon install, put it in `~/.clawdbot/.env` (or your service environment). See [Env vars](/start/faq#how-does-clawdbot-load-environment-variables). -## Using Perplexity (via OpenRouter) +## Using Perplexity Sonar Perplexity Sonar models have built-in web search capabilities and return AI-synthesized -answers with citations. You can use them via OpenRouter (no credit card required - supports -crypto/prepaid). +answers with citations. You can use them either directly via Perplexity’s API or via OpenRouter +(supports crypto/prepaid). -### Getting an OpenRouter API key +See [Perplexity Sonar](/perplexity) for a dedicated setup guide. + +### Getting an API key + +**Perplexity (direct):** + +1) Create a Perplexity account and generate an API key. +2) Set `PERPLEXITY_API_KEY` in the Gateway environment or store the key in config. + +**OpenRouter (alternative):** 1) Create an account at https://openrouter.ai/ 2) Add credits (supports crypto, prepaid, or credit card) @@ -88,9 +99,11 @@ crypto/prepaid). provider: "perplexity", perplexity: { // API key (optional if OPENROUTER_API_KEY or PERPLEXITY_API_KEY is set) - apiKey: "sk-or-v1-...", - // Base URL (defaults to OpenRouter) - baseUrl: "https://openrouter.ai/api/v1", + apiKey: "pplx-... or sk-or-v1-...", + // Base URL: + // - https://api.perplexity.ai (default when PERPLEXITY_API_KEY is set) + // - https://openrouter.ai/api/v1 (default when only OPENROUTER_API_KEY is set) + baseUrl: "https://api.perplexity.ai", // Model (defaults to perplexity/sonar-pro) model: "perplexity/sonar-pro" } @@ -100,8 +113,9 @@ crypto/prepaid). } ``` -**Environment alternative:** set `OPENROUTER_API_KEY` or `PERPLEXITY_API_KEY` in the Gateway -environment. For a daemon install, put it in `~/.clawdbot/.env`. +**Environment alternative:** set `PERPLEXITY_API_KEY` or `OPENROUTER_API_KEY` in the Gateway +environment. For a daemon install, put it in `~/.clawdbot/.env`. If you set both keys, set +`tools.web.search.perplexity.baseUrl` (or `tools.web.search.perplexity.apiKey`) to disambiguate. ### Available Perplexity models @@ -120,7 +134,7 @@ Search the web using your configured provider. - `tools.web.search.enabled` must not be `false` (default: enabled) - API key for your chosen provider: - **Brave**: `BRAVE_API_KEY` or `tools.web.search.apiKey` - - **Perplexity**: `OPENROUTER_API_KEY`, `PERPLEXITY_API_KEY`, or `tools.web.search.perplexity.apiKey` + - **Perplexity**: `PERPLEXITY_API_KEY` (direct), `OPENROUTER_API_KEY` (OpenRouter), or `tools.web.search.perplexity.apiKey` ### Config diff --git a/src/agents/system-prompt.ts b/src/agents/system-prompt.ts index fea4ef28ea..18037aea0a 100644 --- a/src/agents/system-prompt.ts +++ b/src/agents/system-prompt.ts @@ -168,7 +168,7 @@ export function buildAgentSystemPrompt(params: { ls: "List directory contents", exec: "Run shell commands (pty available for TTY-required CLIs)", process: "Manage background exec sessions", - web_search: "Search the web (Brave API)", + web_search: "Search the web (Brave or Perplexity)", web_fetch: "Fetch and extract readable content from a URL", // Channel docking: add login tools here when a channel needs interactive linking. browser: "Control web browser", diff --git a/src/agents/tools/web-tools.enabled-defaults.test.ts b/src/agents/tools/web-tools.enabled-defaults.test.ts index 979a161d21..fd17e22602 100644 --- a/src/agents/tools/web-tools.enabled-defaults.test.ts +++ b/src/agents/tools/web-tools.enabled-defaults.test.ts @@ -89,3 +89,102 @@ describe("web_search country and language parameters", () => { expect(url.searchParams.get("ui_lang")).toBe("de"); }); }); + +describe("web_search Perplexity provider configuration", () => { + const priorFetch = global.fetch; + + beforeEach(() => { + vi.stubEnv("PERPLEXITY_API_KEY", "pplx-test"); + vi.stubEnv("OPENROUTER_API_KEY", "sk-or-test"); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + // @ts-expect-error global fetch cleanup + global.fetch = priorFetch; + }); + + it("defaults to Perplexity base URL when PERPLEXITY_API_KEY is set", async () => { + const mockFetch = vi.fn((_input: RequestInfo, _init?: RequestInit) => + Promise.resolve({ + ok: true, + json: () => + Promise.resolve({ + choices: [{ message: { content: "ok" } }], + citations: [], + }), + } as Response), + ); + // @ts-expect-error mock fetch + global.fetch = mockFetch; + + const tool = createWebSearchTool({ + config: { tools: { web: { search: { provider: "perplexity" } } } }, + sandboxed: true, + }); + + await tool?.execute?.(1, { query: "test" }); + + expect(mockFetch).toHaveBeenCalled(); + const [url, init] = mockFetch.mock.calls[0] as [string, RequestInit?]; + expect(url).toBe("https://api.perplexity.ai/chat/completions"); + const headers = (init?.headers ?? {}) as Record; + expect(headers.Authorization).toBe("Bearer pplx-test"); + }); + + it("uses OpenRouter base URL when configured, even with PERPLEXITY_API_KEY set", async () => { + const mockFetch = vi.fn((_input: RequestInfo, _init?: RequestInit) => + Promise.resolve({ + ok: true, + json: () => + Promise.resolve({ + choices: [{ message: { content: "ok" } }], + citations: [], + }), + } as Response), + ); + // @ts-expect-error mock fetch + global.fetch = mockFetch; + + const tool = createWebSearchTool({ + config: { + tools: { + web: { + search: { provider: "perplexity", perplexity: { baseUrl: "https://openrouter.ai/api/v1" } }, + }, + }, + }, + sandboxed: true, + }); + + await tool?.execute?.(1, { query: "test" }); + + expect(mockFetch).toHaveBeenCalled(); + const [url, init] = mockFetch.mock.calls[0] as [string, RequestInit?]; + expect(url).toBe("https://openrouter.ai/api/v1/chat/completions"); + const headers = (init?.headers ?? {}) as Record; + expect(headers.Authorization).toBe("Bearer sk-or-test"); + }); + + it("returns missing key when OpenRouter base URL is configured without OPENROUTER_API_KEY", async () => { + vi.stubEnv("OPENROUTER_API_KEY", ""); + const mockFetch = vi.fn(); + // @ts-expect-error mock fetch + global.fetch = mockFetch; + + const tool = createWebSearchTool({ + config: { + tools: { + web: { + search: { provider: "perplexity", perplexity: { baseUrl: "https://openrouter.ai/api/v1" } }, + }, + }, + }, + sandboxed: true, + }); + + const result = await tool?.execute?.(1, { query: "test" }); + expect(mockFetch).not.toHaveBeenCalled(); + expect(result?.details).toMatchObject({ error: "missing_openrouter_api_key" }); + }); +}); diff --git a/src/agents/tools/web-tools.ts b/src/agents/tools/web-tools.ts index 6efa53ee15..9e9c9f7cc1 100644 --- a/src/agents/tools/web-tools.ts +++ b/src/agents/tools/web-tools.ts @@ -20,7 +20,8 @@ const DEFAULT_FETCH_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_7_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"; const BRAVE_SEARCH_ENDPOINT = "https://api.search.brave.com/res/v1/web/search"; -const DEFAULT_PERPLEXITY_BASE_URL = "https://openrouter.ai/api/v1"; +const DEFAULT_OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"; +const DEFAULT_PERPLEXITY_BASE_URL = "https://api.perplexity.ai"; const DEFAULT_PERPLEXITY_MODEL = "perplexity/sonar-pro"; type WebSearchConfig = NonNullable["web"] extends infer Web @@ -198,8 +199,27 @@ function resolveFirecrawlMaxAgeMsOrDefault(firecrawl?: FirecrawlFetchConfig): nu return DEFAULT_FIRECRAWL_MAX_AGE_MS; } -function missingSearchKeyPayload(provider: (typeof SEARCH_PROVIDERS)[number]) { +function missingSearchKeyPayload( + provider: (typeof SEARCH_PROVIDERS)[number], + perplexityBaseUrl?: string, +) { if (provider === "perplexity") { + if (perplexityBaseUrl && isOpenRouterBaseUrl(perplexityBaseUrl)) { + return { + error: "missing_openrouter_api_key", + message: + "web_search (perplexity via OpenRouter) needs an API key. Set OPENROUTER_API_KEY in the Gateway environment, or configure tools.web.search.perplexity.apiKey.", + docs: "https://docs.clawd.bot/tools/web", + }; + } + if (perplexityBaseUrl && isPerplexityBaseUrl(perplexityBaseUrl)) { + return { + error: "missing_perplexity_api_key", + message: + "web_search (perplexity direct) needs an API key. Set PERPLEXITY_API_KEY in the Gateway environment, or configure tools.web.search.perplexity.apiKey.", + docs: "https://docs.clawd.bot/tools/web", + }; + } return { error: "missing_perplexity_api_key", message: @@ -231,6 +251,32 @@ type PerplexityConfig = { model?: string; }; +function normalizeBaseUrl(value: string): string { + return value.trim().replace(/\/+$/, ""); +} + +function isPerplexityBaseUrl(value: string): boolean { + try { + return new URL(value).hostname === "api.perplexity.ai"; + } catch { + return false; + } +} + +function isOpenRouterBaseUrl(value: string): boolean { + try { + return new URL(value).hostname === "openrouter.ai"; + } catch { + return false; + } +} + +function inferPerplexityBaseUrlFromKey(apiKey: string): string | undefined { + if (apiKey.startsWith("pplx-")) return DEFAULT_PERPLEXITY_BASE_URL; + if (apiKey.startsWith("sk-or-")) return DEFAULT_OPENROUTER_BASE_URL; + return undefined; +} + function resolvePerplexityConfig(search?: WebSearchConfig): PerplexityConfig { if (!search || typeof search !== "object") return {}; const perplexity = "perplexity" in search ? search.perplexity : undefined; @@ -238,14 +284,24 @@ function resolvePerplexityConfig(search?: WebSearchConfig): PerplexityConfig { return perplexity as PerplexityConfig; } -function resolvePerplexityApiKey(perplexity?: PerplexityConfig): string | undefined { +function resolvePerplexityApiKey( + perplexity: PerplexityConfig | undefined, + baseUrl: string, +): string | undefined { const fromConfig = perplexity && "apiKey" in perplexity && typeof perplexity.apiKey === "string" ? perplexity.apiKey.trim() : ""; + if (fromConfig) return fromConfig; const fromEnvPerplexity = (process.env.PERPLEXITY_API_KEY ?? "").trim(); const fromEnvOpenRouter = (process.env.OPENROUTER_API_KEY ?? "").trim(); - return fromConfig || fromEnvPerplexity || fromEnvOpenRouter || undefined; + if (isPerplexityBaseUrl(baseUrl)) { + return fromEnvPerplexity || undefined; + } + if (isOpenRouterBaseUrl(baseUrl)) { + return fromEnvOpenRouter || undefined; + } + return fromEnvPerplexity || fromEnvOpenRouter || undefined; } function resolvePerplexityBaseUrl(perplexity?: PerplexityConfig): string { @@ -253,7 +309,20 @@ function resolvePerplexityBaseUrl(perplexity?: PerplexityConfig): string { perplexity && "baseUrl" in perplexity && typeof perplexity.baseUrl === "string" ? perplexity.baseUrl.trim() : ""; - return fromConfig || DEFAULT_PERPLEXITY_BASE_URL; + if (fromConfig) return normalizeBaseUrl(fromConfig); + const fromConfigKey = + perplexity && "apiKey" in perplexity && typeof perplexity.apiKey === "string" + ? perplexity.apiKey.trim() + : ""; + const inferredFromConfigKey = fromConfigKey + ? inferPerplexityBaseUrlFromKey(fromConfigKey) + : undefined; + if (inferredFromConfigKey) return inferredFromConfigKey; + const fromEnvPerplexity = (process.env.PERPLEXITY_API_KEY ?? "").trim(); + const fromEnvOpenRouter = (process.env.OPENROUTER_API_KEY ?? "").trim(); + if (fromEnvPerplexity) return DEFAULT_PERPLEXITY_BASE_URL; + if (fromEnvOpenRouter) return DEFAULT_OPENROUTER_BASE_URL; + return DEFAULT_OPENROUTER_BASE_URL; } function resolvePerplexityModel(perplexity?: PerplexityConfig): string { @@ -545,6 +614,13 @@ type PerplexitySearchResponse = { citations?: string[]; }; +function resolvePerplexityEndpoint(baseUrl: string): string { + const trimmed = normalizeBaseUrl(baseUrl); + if (!trimmed) return `${DEFAULT_OPENROUTER_BASE_URL}/chat/completions`; + if (trimmed.endsWith("/chat/completions")) return trimmed; + return `${trimmed}/chat/completions`; +} + async function runPerplexitySearch(params: { query: string; apiKey: string; @@ -552,7 +628,7 @@ async function runPerplexitySearch(params: { model: string; timeoutSeconds: number; }): Promise<{ content: string; citations: string[] }> { - const endpoint = `${params.baseUrl.replace(/\/$/, "")}/chat/completions`; + const endpoint = resolvePerplexityEndpoint(params.baseUrl); const res = await fetch(endpoint, { method: "POST", @@ -599,8 +675,12 @@ async function runWebSearch(params: { perplexityBaseUrl?: string; perplexityModel?: string; }): Promise> { + const providerKey = + params.provider === "perplexity" + ? `${params.provider}:${params.perplexityModel ?? DEFAULT_PERPLEXITY_MODEL}:${params.perplexityBaseUrl ?? DEFAULT_OPENROUTER_BASE_URL}` + : params.provider; const cacheKey = normalizeCacheKey( - `${params.provider}:${params.query}:${params.count}:${params.country || "default"}:${params.search_lang || "default"}:${params.ui_lang || "default"}`, + `${providerKey}:${params.query}:${params.count}:${params.country || "default"}:${params.search_lang || "default"}:${params.ui_lang || "default"}`, ); const cached = readCache(SEARCH_CACHE, cacheKey); if (cached) return { ...cached.value, cached: true }; @@ -899,11 +979,13 @@ export function createWebSearchTool(options?: { const provider = resolveSearchProvider(search); const perplexityConfig = resolvePerplexityConfig(search); + const perplexityBaseUrl = resolvePerplexityBaseUrl(perplexityConfig); + const perplexityModel = resolvePerplexityModel(perplexityConfig); // Determine description based on provider const description = provider === "perplexity" - ? "Search the web using Perplexity Sonar (via OpenRouter). Returns AI-synthesized answers with citations from real-time web search." + ? "Search the web using Perplexity Sonar (direct API or via OpenRouter). Returns AI-synthesized answers with citations from real-time web search." : "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."; return { @@ -915,11 +997,11 @@ export function createWebSearchTool(options?: { // Resolve API key based on provider const apiKey = provider === "perplexity" - ? resolvePerplexityApiKey(perplexityConfig) + ? resolvePerplexityApiKey(perplexityConfig, perplexityBaseUrl) : resolveSearchApiKey(search); if (!apiKey) { - return jsonResult(missingSearchKeyPayload(provider)); + return jsonResult(missingSearchKeyPayload(provider, perplexityBaseUrl)); } const params = args as Record; const query = readStringParam(params, "query", { required: true }); @@ -938,8 +1020,8 @@ export function createWebSearchTool(options?: { country, search_lang, ui_lang, - perplexityBaseUrl: resolvePerplexityBaseUrl(perplexityConfig), - perplexityModel: resolvePerplexityModel(perplexityConfig), + perplexityBaseUrl, + perplexityModel, }); return jsonResult(result); }, diff --git a/src/commands/configure.wizard.ts b/src/commands/configure.wizard.ts index 4c5f5c8741..38332c40a0 100644 --- a/src/commands/configure.wizard.ts +++ b/src/commands/configure.wizard.ts @@ -97,12 +97,15 @@ async function promptWebToolsConfig( ): Promise { const existingSearch = nextConfig.tools?.web?.search; const existingFetch = nextConfig.tools?.web?.fetch; - const hasSearchKey = Boolean(existingSearch?.apiKey); + const existingProvider = existingSearch?.provider === "perplexity" ? "perplexity" : "brave"; + const hasBraveKey = Boolean(existingSearch?.apiKey); + const hasPerplexityKey = Boolean(existingSearch?.perplexity?.apiKey); + const hasSearchKey = existingProvider === "perplexity" ? hasPerplexityKey : hasBraveKey; note( [ "Web search lets your agent look things up online using the `web_search` tool.", - "It requires a Brave Search API key (you can store it in the config or set BRAVE_API_KEY in the Gateway environment).", + "Choose Brave (structured results) or Perplexity Sonar (answers + citations).", "Docs: https://docs.clawd.bot/tools/web", ].join("\n"), "Web search", @@ -110,7 +113,7 @@ async function promptWebToolsConfig( const enableSearch = guardCancel( await confirm({ - message: "Enable web_search (Brave Search)?", + message: "Enable web_search?", initialValue: existingSearch?.enabled ?? hasSearchKey, }), runtime, @@ -122,27 +125,84 @@ async function promptWebToolsConfig( }; if (enableSearch) { - const keyInput = guardCancel( - await text({ - message: hasSearchKey - ? "Brave Search API key (leave blank to keep current or use BRAVE_API_KEY)" - : "Brave Search API key (paste it here; leave blank to use BRAVE_API_KEY)", - placeholder: hasSearchKey ? "Leave blank to keep current" : "BSA...", + const provider = guardCancel( + await select<"brave" | "perplexity">({ + message: "Web search provider", + options: [ + { value: "brave", label: "Brave Search (structured results)" }, + { value: "perplexity", label: "Perplexity Sonar (answers + citations)" }, + ], + initialValue: existingProvider, }), runtime, ); - const key = String(keyInput ?? "").trim(); - if (key) { - nextSearch = { ...nextSearch, apiKey: key }; - } else if (!hasSearchKey) { - note( - [ - "No key stored yet, so web_search will stay unavailable.", - "Store a key here or set BRAVE_API_KEY in the Gateway environment.", - "Docs: https://docs.clawd.bot/tools/web", - ].join("\n"), - "Web search", + + nextSearch = { ...nextSearch, provider }; + + if (provider === "brave") { + const keyInput = guardCancel( + await text({ + message: hasBraveKey + ? "Brave Search API key (leave blank to keep current or use BRAVE_API_KEY)" + : "Brave Search API key (paste it here; leave blank to use BRAVE_API_KEY)", + placeholder: hasBraveKey ? "Leave blank to keep current" : "BSA...", + }), + runtime, ); + const key = String(keyInput ?? "").trim(); + if (key) { + nextSearch = { ...nextSearch, apiKey: key }; + } else if (!hasBraveKey) { + note( + [ + "No key stored yet, so web_search will stay unavailable.", + "Store a key here or set BRAVE_API_KEY in the Gateway environment.", + "Docs: https://docs.clawd.bot/tools/web", + ].join("\n"), + "Web search", + ); + } + } else { + const existingPerplexity = existingSearch?.perplexity ?? {}; + const keyInput = guardCancel( + await text({ + message: hasPerplexityKey + ? "Perplexity/OpenRouter API key (leave blank to keep current or use PERPLEXITY_API_KEY/OPENROUTER_API_KEY)" + : "Perplexity/OpenRouter API key (paste it here; leave blank to use PERPLEXITY_API_KEY/OPENROUTER_API_KEY)", + placeholder: hasPerplexityKey ? "Leave blank to keep current" : "pplx-... or sk-or-...", + }), + runtime, + ); + const key = String(keyInput ?? "").trim(); + let nextPerplexity = { ...existingPerplexity }; + if (key) { + nextPerplexity = { ...nextPerplexity, apiKey: key }; + } + + const baseUrlInput = guardCancel( + await text({ + message: "Perplexity base URL (optional; leave blank for default)", + placeholder: existingPerplexity.baseUrl ?? "https://api.perplexity.ai", + }), + runtime, + ); + const baseUrl = String(baseUrlInput ?? "").trim(); + if (baseUrl) { + nextPerplexity = { ...nextPerplexity, baseUrl }; + } + + nextSearch = { ...nextSearch, perplexity: nextPerplexity }; + + if (!key && !hasPerplexityKey) { + note( + [ + "No key stored yet, so web_search will stay unavailable.", + "Store a key here or set PERPLEXITY_API_KEY/OPENROUTER_API_KEY in the Gateway environment.", + "Docs: https://docs.clawd.bot/tools/web", + ].join("\n"), + "Web search", + ); + } } } diff --git a/src/commands/onboard-non-interactive/local.ts b/src/commands/onboard-non-interactive/local.ts index 1d42a5fafd..16bf0dd6ba 100644 --- a/src/commands/onboard-non-interactive/local.ts +++ b/src/commands/onboard-non-interactive/local.ts @@ -123,7 +123,7 @@ export async function runNonInteractiveOnboardingLocal(params: { if (!opts.json) { runtime.log( - "Tip: run `clawdbot configure --section web` to store your Brave API key for web_search. Docs: https://docs.clawd.bot/tools/web", + "Tip: run `clawdbot configure --section web` to choose Brave or Perplexity and store your API key for web_search. Docs: https://docs.clawd.bot/tools/web", ); } } diff --git a/src/commands/onboard-non-interactive/remote.ts b/src/commands/onboard-non-interactive/remote.ts index b75d78d871..a1cade4b58 100644 --- a/src/commands/onboard-non-interactive/remote.ts +++ b/src/commands/onboard-non-interactive/remote.ts @@ -45,7 +45,7 @@ export async function runNonInteractiveOnboardingRemote(params: { runtime.log(`Remote gateway: ${remoteUrl}`); runtime.log(`Auth: ${payload.auth}`); runtime.log( - "Tip: run `clawdbot configure --section web` to store your Brave API key for web_search. Docs: https://docs.clawd.bot/tools/web", + "Tip: run `clawdbot configure --section web` to choose Brave or Perplexity and store your API key for web_search. Docs: https://docs.clawd.bot/tools/web", ); } } diff --git a/src/config/schema.ts b/src/config/schema.ts index 3cee0b1771..52010ef4cf 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -315,9 +315,15 @@ const FIELD_HELP: Record = { "tools.message.crossContext.marker.suffix": 'Text suffix for cross-context markers (supports "{channel}").', "tools.message.broadcast.enabled": "Enable broadcast action (default: true).", - "tools.web.search.enabled": "Enable the web_search tool (requires Brave API key).", - "tools.web.search.provider": 'Search provider (only "brave" supported today).', + "tools.web.search.enabled": "Enable the web_search tool (requires provider API key).", + "tools.web.search.provider": 'Search provider ("brave" or "perplexity").', "tools.web.search.apiKey": "Brave Search API key (fallback: BRAVE_API_KEY env var).", + "tools.web.search.perplexity.apiKey": + "Perplexity/OpenRouter API key (fallback: PERPLEXITY_API_KEY or OPENROUTER_API_KEY env vars).", + "tools.web.search.perplexity.baseUrl": + "Perplexity/OpenRouter base URL for web_search (defaults to Perplexity when PERPLEXITY_API_KEY is set, otherwise OpenRouter).", + "tools.web.search.perplexity.model": + 'Perplexity Sonar model to use for web_search (default: "perplexity/sonar-pro").', "tools.web.search.maxResults": "Default 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.", diff --git a/src/config/types.tools.ts b/src/config/types.tools.ts index 4c115c81f1..4aaee54e61 100644 --- a/src/config/types.tools.ts +++ b/src/config/types.tools.ts @@ -236,7 +236,7 @@ export type ToolsConfig = { perplexity?: { /** API key for Perplexity or OpenRouter (defaults to PERPLEXITY_API_KEY or OPENROUTER_API_KEY env var). */ apiKey?: string; - /** Base URL for API requests (defaults to OpenRouter: https://openrouter.ai/api/v1). */ + /** Base URL for API requests (defaults to Perplexity if PERPLEXITY_API_KEY is set, otherwise OpenRouter). */ baseUrl?: string; /** Model to use (defaults to "perplexity/sonar-pro"). */ model?: string; diff --git a/src/wizard/onboarding.finalize.ts b/src/wizard/onboarding.finalize.ts index ce085a0102..32037c8764 100644 --- a/src/wizard/onboarding.finalize.ts +++ b/src/wizard/onboarding.finalize.ts @@ -375,29 +375,52 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption ); } - const webSearchKey = (nextConfig.tools?.web?.search?.apiKey ?? "").trim(); - const webSearchEnv = (process.env.BRAVE_API_KEY ?? "").trim(); - const hasWebSearchKey = Boolean(webSearchKey || webSearchEnv); + const webSearchProvider = + nextConfig.tools?.web?.search?.provider === "perplexity" ? "perplexity" : "brave"; + const braveKey = (nextConfig.tools?.web?.search?.apiKey ?? "").trim(); + const braveEnv = (process.env.BRAVE_API_KEY ?? "").trim(); + const perplexityKey = (nextConfig.tools?.web?.search?.perplexity?.apiKey ?? "").trim(); + const perplexityEnv = (process.env.PERPLEXITY_API_KEY ?? "").trim(); + const openRouterEnv = (process.env.OPENROUTER_API_KEY ?? "").trim(); + const perplexityBaseUrl = (nextConfig.tools?.web?.search?.perplexity?.baseUrl ?? "").trim(); + const hasWebSearchKey = + webSearchProvider === "perplexity" + ? Boolean(perplexityKey || perplexityEnv || openRouterEnv) + : Boolean(braveKey || braveEnv); await prompter.note( hasWebSearchKey ? [ "Web search is enabled, so your agent can look things up online when needed.", "", - webSearchKey - ? "API key: stored in config (tools.web.search.apiKey)." - : "API key: provided via BRAVE_API_KEY env var (Gateway environment).", + webSearchProvider === "perplexity" + ? perplexityKey + ? "API key: stored in config (tools.web.search.perplexity.apiKey)." + : perplexityBaseUrl.includes("openrouter.ai") + ? "API key: provided via OPENROUTER_API_KEY env var (Gateway environment)." + : perplexityBaseUrl.includes("api.perplexity.ai") + ? "API key: provided via PERPLEXITY_API_KEY env var (Gateway environment)." + : perplexityEnv + ? "API key: provided via PERPLEXITY_API_KEY env var (Gateway environment)." + : "API key: provided via OPENROUTER_API_KEY env var (Gateway environment)." + : braveKey + ? "API key: stored in config (tools.web.search.apiKey)." + : "API key: provided via BRAVE_API_KEY env var (Gateway environment).", "Docs: https://docs.clawd.bot/tools/web", ].join("\n") : [ "If you want your agent to be able to search the web, you’ll need an API key.", "", - "Clawdbot uses Brave Search for the `web_search` tool. Without a Brave Search API key, web search won’t work.", + webSearchProvider === "perplexity" + ? "Clawdbot can use Perplexity Sonar for the `web_search` tool. Without a Perplexity/OpenRouter API key, web search won’t work." + : "Clawdbot uses Brave Search for the `web_search` tool. Without a Brave Search API key, web search won’t work.", "", "Set it up interactively:", "- Run: clawdbot configure --section web", - "- Enable web_search and paste your Brave Search API key", + "- Enable web_search and paste your API key", "", - "Alternative: set BRAVE_API_KEY in the Gateway environment (no config changes).", + webSearchProvider === "perplexity" + ? "Alternative: set PERPLEXITY_API_KEY or OPENROUTER_API_KEY in the Gateway environment (no config changes)." + : "Alternative: set BRAVE_API_KEY in the Gateway environment (no config changes).", "Docs: https://docs.clawd.bot/tools/web", ].join("\n"), "Web search (optional)",