From 57aa2232fa6067d61545e873cfffcba6785894cb Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 26 Aug 2026 13:36:15 -0700 Subject: [PATCH] refactor(web): use canonical provider contracts (#130334) --- extensions/brave/web-search-contract-api.ts | 15 +------ .../duckduckgo/web-search-contract-api.ts | 11 +---- .../exa/src/exa-web-search-provider.test.ts | 10 ++++- extensions/exa/web-search-contract-api.ts | 11 +---- .../firecrawl/web-fetch-contract-api.ts | 15 +------ .../firecrawl/web-search-contract-api.ts | 22 +--------- extensions/minimax/web-search-contract-api.ts | 37 +---------------- .../moonshot/web-search-contract-api.ts | 30 +------------- .../src/parallel-web-search-provider.test.ts | 5 ++- .../parallel/web-search-contract-api.ts | 19 +-------- .../perplexity/web-search-contract-api.ts | 15 +------ extensions/tavily/web-search-contract-api.ts | 33 +-------------- .../web-search-providers.runtime.test.ts | 40 +++++++++++++++++++ 13 files changed, 63 insertions(+), 200 deletions(-) diff --git a/extensions/brave/web-search-contract-api.ts b/extensions/brave/web-search-contract-api.ts index 85bc7ac45a8d..09fdf4c993d7 100644 --- a/extensions/brave/web-search-contract-api.ts +++ b/extensions/brave/web-search-contract-api.ts @@ -1,14 +1 @@ -/** - * Brave Search contract provider. It exposes provider metadata without creating - * the runtime search tool. - */ -import type { WebSearchProviderPlugin } from "openclaw/plugin-sdk/provider-web-search-config-contract"; -import { buildBraveWebSearchProviderBase } from "./web-search-shared.js"; - -/** Create the Brave provider descriptor for contract checks. */ -export function createBraveWebSearchProvider(): WebSearchProviderPlugin { - return { - ...buildBraveWebSearchProviderBase(), - createTool: () => null, - }; -} +export { createBraveWebSearchProvider } from "./web-search-provider.js"; diff --git a/extensions/duckduckgo/web-search-contract-api.ts b/extensions/duckduckgo/web-search-contract-api.ts index 987a9ca0127f..c12be47209b8 100644 --- a/extensions/duckduckgo/web-search-contract-api.ts +++ b/extensions/duckduckgo/web-search-contract-api.ts @@ -1,10 +1 @@ -// Duckduckgo API module exposes the plugin public contract. -import type { WebSearchProviderPlugin } from "openclaw/plugin-sdk/provider-web-search-contract"; -import { createDuckDuckGoWebSearchProviderBase } from "./src/ddg-search-provider.shared.js"; - -export function createDuckDuckGoWebSearchProvider(): WebSearchProviderPlugin { - return { - ...createDuckDuckGoWebSearchProviderBase(), - createTool: () => null, - }; -} +export { createDuckDuckGoWebSearchProvider } from "./web-search-provider.js"; diff --git a/extensions/exa/src/exa-web-search-provider.test.ts b/extensions/exa/src/exa-web-search-provider.test.ts index 4769f3e83cd3..7b2b10ae8be7 100644 --- a/extensions/exa/src/exa-web-search-provider.test.ts +++ b/extensions/exa/src/exa-web-search-provider.test.ts @@ -138,7 +138,7 @@ describe("exa web search provider", () => { expect(pluginEntry.enabled).toBe(true); }); - it("keeps the lightweight contract surface aligned with provider metadata", () => { + it("keeps the contract export aligned with provider metadata", () => { const provider = createExaWebSearchProvider(); const contractProvider = createContractExaWebSearchProvider(); if (!contractProvider.applySelectionConfig) { @@ -171,7 +171,13 @@ describe("exa web search provider", () => { autoDetectOrder: provider.autoDetectOrder, credentialPath: provider.credentialPath, }); - expect(contractProvider.createTool({ config: {}, searchConfig: {} })).toBeNull(); + const fetchMock = vi.spyOn(globalThis, "fetch"); + try { + expect(contractProvider.createTool({ config: {}, searchConfig: {} })).not.toBeNull(); + expect(fetchMock).not.toHaveBeenCalled(); + } finally { + fetchMock.mockRestore(); + } const pluginEntry = applied.plugins?.entries?.exa; if (!pluginEntry) { throw new Error("expected contract Exa plugin entry"); diff --git a/extensions/exa/web-search-contract-api.ts b/extensions/exa/web-search-contract-api.ts index a9bb7efdd7ba..ca4ed24f477b 100644 --- a/extensions/exa/web-search-contract-api.ts +++ b/extensions/exa/web-search-contract-api.ts @@ -1,10 +1 @@ -// Exa API module exposes the plugin public contract. -import type { WebSearchProviderPlugin } from "openclaw/plugin-sdk/provider-web-search-contract"; -import { createExaWebSearchProviderBase } from "./src/exa-web-search-provider.shared.js"; - -export function createExaWebSearchProvider(): WebSearchProviderPlugin { - return { - ...createExaWebSearchProviderBase(), - createTool: () => null, - }; -} +export { createExaWebSearchProvider } from "./web-search-provider.js"; diff --git a/extensions/firecrawl/web-fetch-contract-api.ts b/extensions/firecrawl/web-fetch-contract-api.ts index e63d1e0e182d..3fc16f5a1c97 100644 --- a/extensions/firecrawl/web-fetch-contract-api.ts +++ b/extensions/firecrawl/web-fetch-contract-api.ts @@ -1,14 +1 @@ -// Firecrawl API module exposes the plugin public contract. -import { - enablePluginInConfig, - type WebFetchProviderPlugin, -} from "openclaw/plugin-sdk/provider-web-fetch-contract"; -import { FIRECRAWL_WEB_FETCH_PROVIDER_SHARED } from "./src/firecrawl-fetch-provider-shared.js"; - -export function createFirecrawlWebFetchProvider(): WebFetchProviderPlugin { - return { - ...FIRECRAWL_WEB_FETCH_PROVIDER_SHARED, - applySelectionConfig: (config) => enablePluginInConfig(config, "firecrawl").config, - createTool: () => null, - }; -} +export { createFirecrawlWebFetchProvider } from "./web-fetch-provider.js"; diff --git a/extensions/firecrawl/web-search-contract-api.ts b/extensions/firecrawl/web-search-contract-api.ts index 2c78b27d4bfb..5f6099406610 100644 --- a/extensions/firecrawl/web-search-contract-api.ts +++ b/extensions/firecrawl/web-search-contract-api.ts @@ -1,20 +1,2 @@ -// Firecrawl API module exposes the plugin public contract. -import type { WebSearchProviderPlugin } from "openclaw/plugin-sdk/provider-web-search-contract"; -import { - buildFirecrawlFreeWebSearchProviderBase, - buildFirecrawlWebSearchProviderBase, -} from "./web-search-shared.js"; - -export function createFirecrawlWebSearchProvider(): WebSearchProviderPlugin { - return { - ...buildFirecrawlWebSearchProviderBase(), - createTool: () => null, - }; -} - -export function createFirecrawlFreeWebSearchProvider(): WebSearchProviderPlugin { - return { - ...buildFirecrawlFreeWebSearchProviderBase(), - createTool: () => null, - }; -} +export { createFirecrawlFreeWebSearchProvider } from "./web-search-provider.js"; +export { createFirecrawlWebSearchProvider } from "./web-search-provider.js"; diff --git a/extensions/minimax/web-search-contract-api.ts b/extensions/minimax/web-search-contract-api.ts index 49d089a2f968..bc9d95739690 100644 --- a/extensions/minimax/web-search-contract-api.ts +++ b/extensions/minimax/web-search-contract-api.ts @@ -1,36 +1 @@ -// Minimax API module exposes the plugin public contract. -import { - createWebSearchProviderContractFields, - type WebSearchProviderPlugin, -} from "openclaw/plugin-sdk/provider-web-search-config-contract"; - -const MINIMAX_TOKEN_PLAN_ENV_VARS = [ - "MINIMAX_CODE_PLAN_KEY", - "MINIMAX_CODING_API_KEY", - "MINIMAX_OAUTH_TOKEN", -] as const; -const MINIMAX_WEB_SEARCH_ENV_VARS = [...MINIMAX_TOKEN_PLAN_ENV_VARS, "MINIMAX_API_KEY"] as const; - -export function createMiniMaxWebSearchProvider(): WebSearchProviderPlugin { - const credentialPath = "plugins.entries.minimax.config.webSearch.apiKey"; - - return { - id: "minimax", - label: "MiniMax Search", - hint: "Structured results via MiniMax Token Plan search API", - onboardingScopes: ["text-inference"], - credentialLabel: "MiniMax Token Plan key or OAuth token", - envVars: [...MINIMAX_WEB_SEARCH_ENV_VARS], - placeholder: "sk-cp-...", - signupUrl: "https://platform.minimax.io/user-center/basic-information/interface-key", - docsUrl: "https://docs.openclaw.ai/tools/minimax-search", - autoDetectOrder: 15, - credentialPath, - ...createWebSearchProviderContractFields({ - credentialPath, - searchCredential: { type: "top-level" }, - configuredCredential: { pluginId: "minimax" }, - }), - createTool: () => null, - }; -} +export { createMiniMaxWebSearchProvider } from "./web-search-provider.js"; diff --git a/extensions/moonshot/web-search-contract-api.ts b/extensions/moonshot/web-search-contract-api.ts index 5a3761ac0407..080e381eab1b 100644 --- a/extensions/moonshot/web-search-contract-api.ts +++ b/extensions/moonshot/web-search-contract-api.ts @@ -1,29 +1 @@ -// Moonshot API module exposes the plugin public contract. -import { - createWebSearchProviderContractFields, - type WebSearchProviderPlugin, -} from "openclaw/plugin-sdk/provider-web-search-config-contract"; - -export function createKimiWebSearchProvider(): WebSearchProviderPlugin { - const credentialPath = "plugins.entries.moonshot.config.webSearch.apiKey"; - - return { - id: "kimi", - label: "Kimi (Moonshot)", - hint: "Requires Moonshot / Kimi API key ยท Moonshot web search", - onboardingScopes: ["text-inference"], - credentialLabel: "Moonshot / Kimi API key", - envVars: ["KIMI_API_KEY", "MOONSHOT_API_KEY"], - placeholder: "sk-...", - signupUrl: "https://platform.moonshot.cn/", - docsUrl: "https://docs.openclaw.ai/tools/web", - autoDetectOrder: 40, - credentialPath, - ...createWebSearchProviderContractFields({ - credentialPath, - searchCredential: { type: "scoped", scopeId: "kimi" }, - configuredCredential: { pluginId: "moonshot" }, - }), - createTool: () => null, - }; -} +export { createKimiWebSearchProvider } from "./web-search-provider.js"; diff --git a/extensions/parallel/src/parallel-web-search-provider.test.ts b/extensions/parallel/src/parallel-web-search-provider.test.ts index c96f082389d2..ac86ae9862a4 100644 --- a/extensions/parallel/src/parallel-web-search-provider.test.ts +++ b/extensions/parallel/src/parallel-web-search-provider.test.ts @@ -143,7 +143,7 @@ describe("parallel web search provider", () => { const countParam = (paidTool({}).parameters as ToolParameters).properties.count; expect(countParam).toMatchObject({ type: "integer", minimum: 1, maximum: 40 }); }); - it("keeps the lightweight contract surface aligned with provider metadata", () => { + it("keeps the contract export aligned with provider metadata", () => { const provider = createParallelWebSearchProvider(); const contractProvider = createContractParallelWebSearchProvider(); const applied = expectDefined( @@ -166,7 +166,8 @@ describe("parallel web search provider", () => { expect(Object.fromEntries(keys.map((key) => [key, contractProvider[key]]))).toEqual( Object.fromEntries(keys.map((key) => [key, provider[key]])), ); - expect(contractProvider.createTool({ config: {}, searchConfig: {} })).toBeNull(); + expect(contractProvider.createTool({ config: {}, searchConfig: {} })).not.toBeNull(); + expect(endpointMockState.calls).toHaveLength(0); expect(expectDefined(applied.plugins?.entries?.parallel, "contract plugin entry").enabled).toBe( true, ); diff --git a/extensions/parallel/web-search-contract-api.ts b/extensions/parallel/web-search-contract-api.ts index 788d4037456d..2102bc924613 100644 --- a/extensions/parallel/web-search-contract-api.ts +++ b/extensions/parallel/web-search-contract-api.ts @@ -1,17 +1,2 @@ -import type { WebSearchProviderPlugin } from "openclaw/plugin-sdk/provider-web-search-contract"; -import { createParallelFreeWebSearchProviderBase } from "./src/parallel-free-web-search-provider.shared.js"; -import { createParallelWebSearchProviderBase } from "./src/parallel-web-search-provider.shared.js"; - -export function createParallelWebSearchProvider(): WebSearchProviderPlugin { - return { - ...createParallelWebSearchProviderBase(), - createTool: () => null, - }; -} - -export function createParallelFreeWebSearchProvider(): WebSearchProviderPlugin { - return { - ...createParallelFreeWebSearchProviderBase(), - createTool: () => null, - }; -} +export { createParallelFreeWebSearchProvider } from "./web-search-provider.js"; +export { createParallelWebSearchProvider } from "./web-search-provider.js"; diff --git a/extensions/perplexity/web-search-contract-api.ts b/extensions/perplexity/web-search-contract-api.ts index 268a024dbcac..0027a472b3b0 100644 --- a/extensions/perplexity/web-search-contract-api.ts +++ b/extensions/perplexity/web-search-contract-api.ts @@ -1,14 +1 @@ -// Perplexity API module exposes the plugin public contract. -import type { WebSearchProviderPlugin } from "openclaw/plugin-sdk/provider-web-search-config-contract"; -import { - createPerplexityWebSearchProviderBase, - resolvePerplexityWebSearchRuntimeMetadata, -} from "./src/perplexity-web-search-provider.shared.js"; - -export function createPerplexityWebSearchProvider(): WebSearchProviderPlugin { - return { - ...createPerplexityWebSearchProviderBase(), - resolveRuntimeMetadata: resolvePerplexityWebSearchRuntimeMetadata, - createTool: () => null, - }; -} +export { createPerplexityWebSearchProvider } from "./web-search-provider.js"; diff --git a/extensions/tavily/web-search-contract-api.ts b/extensions/tavily/web-search-contract-api.ts index 321c62cc88d1..1d2b52a7ab55 100644 --- a/extensions/tavily/web-search-contract-api.ts +++ b/extensions/tavily/web-search-contract-api.ts @@ -1,32 +1 @@ -import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime"; -// Tavily API module exposes the plugin public contract. -import type { WebSearchProviderPlugin } from "openclaw/plugin-sdk/provider-web-search-config-contract"; -import { - buildTavilyWebSearchProviderBase, - TAVILY_GENERIC_SEARCH_DESCRIPTION, - TAVILY_GENERIC_SEARCH_SCHEMA, -} from "./web-search-shared.js"; - -const loadTavilySearchProviderModule = createLazyRuntimeModule( - () => import("./src/tavily-search-provider.js"), -); - -export function createTavilyWebSearchProvider(): WebSearchProviderPlugin { - return { - ...buildTavilyWebSearchProviderBase(), - createTool: (ctx) => ({ - description: TAVILY_GENERIC_SEARCH_DESCRIPTION, - parameters: TAVILY_GENERIC_SEARCH_SCHEMA, - execute: async (args, executionContext) => { - executionContext?.signal?.throwIfAborted(); - const { createTavilyWebSearchProvider: createRuntimeProvider } = - await loadTavilySearchProviderModule(); - const tool = createRuntimeProvider().createTool(ctx); - if (!tool) { - throw new Error("Tavily web_search provider did not create a runtime tool."); - } - return await tool.execute(args, executionContext); - }, - }), - }; -} +export { createTavilyWebSearchProvider } from "./web-search-provider.js"; diff --git a/src/plugins/web-search-providers.runtime.test.ts b/src/plugins/web-search-providers.runtime.test.ts index e9dda7bd8d92..ff7482f7cef4 100644 --- a/src/plugins/web-search-providers.runtime.test.ts +++ b/src/plugins/web-search-providers.runtime.test.ts @@ -376,6 +376,46 @@ describe("resolvePluginWebSearchProviders", () => { expect(loadOpenClawPluginsMock).not.toHaveBeenCalled(); }); + it("preserves Moonshot region and model setup without activating the plugin", async () => { + loadInstalledPluginManifestRegistryMock.mockReturnValueOnce({ + plugins: [createWebSearchManifestRecord({ id: "moonshot", providerId: "kimi" })], + diagnostics: [], + }); + const config = { plugins: { allow: ["moonshot"] } }; + const providers = resolvePluginWebSearchProviders({ config, mode: "setup", activate: false }); + const provider = providers[0]; + if (!provider?.runSetup) { + throw new Error("Expected Moonshot web-search setup from the public artifact"); + } + const select = vi.fn(async (params: { initialValue?: unknown }) => params.initialValue); + + const next = await provider.runSetup({ + config, + runtime: {} as never, + prompter: { select } as never, + }); + + expect(loadOpenClawPluginsMock).not.toHaveBeenCalled(); + expect(select.mock.calls.map(([params]) => requireRecord(params).message)).toEqual([ + "Kimi API region", + "Kimi web search model", + ]); + expect(next).toMatchObject({ + plugins: { + entries: { + moonshot: { + config: { + webSearch: { + baseUrl: "https://api.moonshot.ai/v1", + model: "kimi-k2.6", + }, + }, + }, + }, + }, + }); + }); + it("loads plugin web-search providers from the auto-enabled config snapshot", () => { const rawConfig = createBraveAllowConfig(); const autoEnabledConfig = {