diff --git a/docs/tools/web.md b/docs/tools/web.md index be2a57f9e..e6e395b68 100644 --- a/docs/tools/web.md +++ b/docs/tools/web.md @@ -175,6 +175,7 @@ Search the web using your configured provider. - `search_lang` (optional): ISO language code for search results (e.g., "de", "en", "fr") - `ui_lang` (optional): ISO language code for UI elements - `freshness` (optional, Brave only): filter by discovery time (`pd`, `pw`, `pm`, `py`, or `YYYY-MM-DDtoYYYY-MM-DD`) +- `model` (optional, Perplexity only): override the configured model per-query (e.g., "sonar", "sonar-pro") **Examples:** @@ -200,6 +201,18 @@ await web_search({ query: "TMBG interview", freshness: "pw" }); + +// Perplexity: use faster model for simple lookups +await web_search({ + query: "Nick Saban birthday", + model: "sonar" +}); + +// Perplexity: use deeper reasoning for complex analysis +await web_search({ + query: "compare React vs Vue performance benchmarks 2026", + model: "sonar-pro" +}); ``` ## web_fetch diff --git a/src/agents/tools/web-search.ts b/src/agents/tools/web-search.ts index 1d87676e8..37f195600 100644 --- a/src/agents/tools/web-search.ts +++ b/src/agents/tools/web-search.ts @@ -63,6 +63,12 @@ const WebSearchSchema = Type.Object({ "Filter results by discovery time (Brave only). Values: 'pd' (past 24h), 'pw' (past week), 'pm' (past month), 'py' (past year), or date range 'YYYY-MM-DDtoYYYY-MM-DD'.", }), ), + model: Type.Optional( + Type.String({ + description: + "Perplexity model to use (e.g., 'sonar', 'sonar-pro'). Overrides configured default. Perplexity provider only.", + }), + ), }); type WebSearchConfig = NonNullable["web"] extends infer Web @@ -459,6 +465,14 @@ export function createWebSearchTool(options?: { docs: "https://docs.molt.bot/tools/web", }); } + const modelOverride = readStringParam(params, "model"); + if (modelOverride && provider !== "perplexity") { + return jsonResult({ + error: "unsupported_model", + message: "model parameter is only supported by the Perplexity web_search provider.", + docs: "https://docs.molt.bot/tools/web", + }); + } const result = await runWebSearch({ query, count: resolveSearchCount(count, DEFAULT_SEARCH_COUNT), @@ -475,7 +489,7 @@ export function createWebSearchTool(options?: { perplexityAuth?.source, perplexityAuth?.apiKey, ), - perplexityModel: resolvePerplexityModel(perplexityConfig), + perplexityModel: modelOverride || resolvePerplexityModel(perplexityConfig), }); return jsonResult(result); }, diff --git a/src/agents/tools/web-tools.enabled-defaults.test.ts b/src/agents/tools/web-tools.enabled-defaults.test.ts index 41d44b12d..90d86a701 100644 --- a/src/agents/tools/web-tools.enabled-defaults.test.ts +++ b/src/agents/tools/web-tools.enabled-defaults.test.ts @@ -307,3 +307,115 @@ describe("web_search perplexity baseUrl defaults", () => { expect(mockFetch.mock.calls[0]?.[0]).toBe("https://openrouter.ai/api/v1/chat/completions"); }); }); + +describe("web_search model parameter", () => { + const priorFetch = global.fetch; + + afterEach(() => { + vi.unstubAllEnvs(); + // @ts-expect-error global fetch cleanup + global.fetch = priorFetch; + }); + + it("passes model parameter to Perplexity API", async () => { + vi.stubEnv("PERPLEXITY_API_KEY", "pplx-test"); + const mockFetch = vi.fn(() => + 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-model-param", model: "sonar" }); + + expect(mockFetch).toHaveBeenCalled(); + const body = JSON.parse(mockFetch.mock.calls[0]?.[1]?.body as string); + expect(body.model).toBe("sonar"); + }); + + it("uses configured model when no model parameter provided", async () => { + vi.stubEnv("PERPLEXITY_API_KEY", "pplx-test"); + const mockFetch = vi.fn(() => + 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: { model: "sonar-reasoning-pro" }, + }, + }, + }, + }, + sandboxed: true, + }); + await tool?.execute?.(1, { query: "test-configured-model" }); + + expect(mockFetch).toHaveBeenCalled(); + const body = JSON.parse(mockFetch.mock.calls[0]?.[1]?.body as string); + expect(body.model).toBe("sonar-reasoning-pro"); + }); + + it("model parameter overrides configured model", async () => { + vi.stubEnv("PERPLEXITY_API_KEY", "pplx-test"); + const mockFetch = vi.fn(() => + 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: { model: "sonar-pro" }, + }, + }, + }, + }, + sandboxed: true, + }); + await tool?.execute?.(1, { query: "test-model-override", model: "sonar" }); + + expect(mockFetch).toHaveBeenCalled(); + const body = JSON.parse(mockFetch.mock.calls[0]?.[1]?.body as string); + expect(body.model).toBe("sonar"); + }); + + it("rejects model parameter for Brave provider", async () => { + vi.stubEnv("BRAVE_API_KEY", "test-key"); + const mockFetch = vi.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve({ web: { results: [] } }), + } as Response), + ); + // @ts-expect-error mock fetch + global.fetch = mockFetch; + + const tool = createWebSearchTool({ config: undefined, sandboxed: true }); + const result = await tool?.execute?.(1, { query: "test-brave-model", model: "sonar" }); + + expect(mockFetch).not.toHaveBeenCalled(); + expect(result?.details).toMatchObject({ error: "unsupported_model" }); + }); +});