refactor(web): use canonical provider contracts (#130334)

This commit is contained in:
Peter Steinberger
2026-08-26 13:36:15 -07:00
committed by GitHub
parent 1fd3c071d8
commit 57aa2232fa
13 changed files with 63 additions and 200 deletions
+1 -14
View File
@@ -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";
@@ -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";
@@ -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");
+1 -10
View File
@@ -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";
+1 -14
View File
@@ -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";
@@ -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";
+1 -36
View File
@@ -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";
+1 -29
View File
@@ -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";
@@ -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,
);
+2 -17
View File
@@ -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";
@@ -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";
+1 -32
View File
@@ -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";
@@ -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 = {