refactor: cache provider tool runtimes

This commit is contained in:
Peter Steinberger
2026-04-18 18:57:29 +01:00
parent c39314c14a
commit a7e029fde9
12 changed files with 179 additions and 18 deletions

View File

@@ -6,6 +6,16 @@ import type {
import { createWebSearchProviderContractFields } from "openclaw/plugin-sdk/provider-web-search-config-contract";
const BRAVE_CREDENTIAL_PATH = "plugins.entries.brave.config.webSearch.apiKey";
type BraveWebSearchRuntime = typeof import("./brave-web-search-provider.runtime.js");
let braveWebSearchRuntimePromise: Promise<BraveWebSearchRuntime> | undefined;
function loadBraveWebSearchRuntime(): Promise<BraveWebSearchRuntime> {
braveWebSearchRuntimePromise ??= import("./brave-web-search-provider.runtime.js");
return braveWebSearchRuntimePromise;
}
const BraveSearchSchema = {
type: "object",
properties: {
@@ -111,7 +121,7 @@ function createBraveToolDefinition(
: "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.",
parameters: BraveSearchSchema,
execute: async (args) => {
const { executeBraveSearch } = await import("./brave-web-search-provider.runtime.js");
const { executeBraveSearch } = await loadBraveWebSearchRuntime();
return await executeBraveSearch(args, searchConfig);
},
};

View File

@@ -1,8 +1,18 @@
import { readNumberParam, readStringParam } from "openclaw/plugin-sdk/param-readers";
import {
createWebSearchProviderContractFields,
type WebSearchProviderPlugin,
} from "openclaw/plugin-sdk/provider-web-search-contract";
type DuckDuckGoClientModule = typeof import("./ddg-client.js");
let duckDuckGoClientModulePromise: Promise<DuckDuckGoClientModule> | undefined;
function loadDuckDuckGoClientModule(): Promise<DuckDuckGoClientModule> {
duckDuckGoClientModulePromise ??= import("./ddg-client.js");
return duckDuckGoClientModulePromise;
}
const DuckDuckGoSearchSchema = {
type: "object",
properties: {
@@ -47,10 +57,7 @@ export function createDuckDuckGoWebSearchProvider(): WebSearchProviderPlugin {
"Search the web using DuckDuckGo. Returns titles, URLs, and snippets with no API key required.",
parameters: DuckDuckGoSearchSchema,
execute: async (args) => {
const [{ runDuckDuckGoSearch }, { readNumberParam, readStringParam }] = await Promise.all([
import("./ddg-client.js"),
import("openclaw/plugin-sdk/provider-web-search"),
]);
const { runDuckDuckGoSearch } = await loadDuckDuckGoClientModule();
return await runDuckDuckGoSearch({
config: ctx.config,
query: readStringParam(args, "query", { required: true }),

View File

@@ -8,6 +8,15 @@ const EXA_SEARCH_TYPES = ["auto", "neural", "fast", "deep", "deep-reasoning", "i
const EXA_FRESHNESS_VALUES = ["day", "week", "month", "year"] as const;
const EXA_MAX_SEARCH_COUNT = 100;
type ExaWebSearchRuntime = typeof import("./exa-web-search-provider.runtime.js");
let exaWebSearchRuntimePromise: Promise<ExaWebSearchRuntime> | undefined;
function loadExaWebSearchRuntime(): Promise<ExaWebSearchRuntime> {
exaWebSearchRuntimePromise ??= import("./exa-web-search-provider.runtime.js");
return exaWebSearchRuntimePromise;
}
const ExaSearchSchema = {
type: "object",
properties: {
@@ -81,8 +90,7 @@ export function createExaWebSearchProvider(): WebSearchProviderPlugin {
"Search the web using Exa AI. Supports neural or keyword search, publication date filters, and optional highlights or text extraction.",
parameters: ExaSearchSchema,
execute: async (args) => {
const { executeExaWebSearchProviderTool } =
await import("./exa-web-search-provider.runtime.js");
const { executeExaWebSearchProviderTool } = await loadExaWebSearchRuntime();
return await executeExaWebSearchProviderTool(ctx, args);
},
}),

View File

@@ -4,6 +4,16 @@ import {
} from "openclaw/plugin-sdk/provider-web-search-contract";
const FIRECRAWL_CREDENTIAL_PATH = "plugins.entries.firecrawl.config.webSearch.apiKey";
type FirecrawlClientModule = typeof import("./firecrawl-client.js");
let firecrawlClientModulePromise: Promise<FirecrawlClientModule> | undefined;
function loadFirecrawlClientModule(): Promise<FirecrawlClientModule> {
firecrawlClientModulePromise ??= import("./firecrawl-client.js");
return firecrawlClientModulePromise;
}
const GenericFirecrawlSearchSchema = {
type: "object",
properties: {
@@ -42,7 +52,7 @@ export function createFirecrawlWebSearchProvider(): WebSearchProviderPlugin {
"Search the web using Firecrawl. Returns structured results with snippets from Firecrawl Search. Use firecrawl_search for Firecrawl-specific knobs like sources or categories.",
parameters: GenericFirecrawlSearchSchema,
execute: async (args) => {
const { runFirecrawlSearch } = await import("./firecrawl-client.js");
const { runFirecrawlSearch } = await loadFirecrawlClientModule();
return await runFirecrawlSearch({
cfg: ctx.config,
query: typeof args.query === "string" ? args.query : "",

View File

@@ -8,6 +8,16 @@ import {
import { resolveGeminiApiKey, resolveGeminiModel } from "./gemini-web-search-provider.shared.js";
const GEMINI_CREDENTIAL_PATH = "plugins.entries.google.config.webSearch.apiKey";
type GeminiWebSearchRuntime = typeof import("./gemini-web-search-provider.runtime.js");
let geminiWebSearchRuntimePromise: Promise<GeminiWebSearchRuntime> | undefined;
function loadGeminiWebSearchRuntime(): Promise<GeminiWebSearchRuntime> {
geminiWebSearchRuntimePromise ??= import("./gemini-web-search-provider.runtime.js");
return geminiWebSearchRuntimePromise;
}
const GEMINI_TOOL_PARAMETERS = {
type: "object",
properties: {
@@ -35,7 +45,7 @@ function createGeminiToolDefinition(
"Search the web using Gemini with Google Search grounding. Returns AI-synthesized answers with citations from Google Search.",
parameters: GEMINI_TOOL_PARAMETERS,
execute: async (args) => {
const { executeGeminiSearch } = await import("./gemini-web-search-provider.runtime.js");
const { executeGeminiSearch } = await loadGeminiWebSearchRuntime();
return await executeGeminiSearch(args, searchConfig);
},
};

View File

@@ -6,6 +6,15 @@ import {
const MINIMAX_CREDENTIAL_PATH = "plugins.entries.minimax.config.webSearch.apiKey";
const MINIMAX_CODING_PLAN_ENV_VARS = ["MINIMAX_CODE_PLAN_KEY", "MINIMAX_CODING_API_KEY"] as const;
type MiniMaxWebSearchRuntime = typeof import("./minimax-web-search-provider.runtime.js");
let miniMaxWebSearchRuntimePromise: Promise<MiniMaxWebSearchRuntime> | undefined;
function loadMiniMaxWebSearchRuntime(): Promise<MiniMaxWebSearchRuntime> {
miniMaxWebSearchRuntimePromise ??= import("./minimax-web-search-provider.runtime.js");
return miniMaxWebSearchRuntimePromise;
}
const MiniMaxSearchSchema = {
type: "object",
properties: {
@@ -41,8 +50,7 @@ export function createMiniMaxWebSearchProvider(): WebSearchProviderPlugin {
"Search the web using MiniMax Search API. Returns titles, URLs, snippets, and related search suggestions.",
parameters: MiniMaxSearchSchema,
execute: async (args) => {
const { executeMiniMaxWebSearchProviderTool } =
await import("./minimax-web-search-provider.runtime.js");
const { executeMiniMaxWebSearchProviderTool } = await loadMiniMaxWebSearchRuntime();
return await executeMiniMaxWebSearchProviderTool(ctx, args);
},
}),

View File

@@ -9,6 +9,15 @@ import { resolvePerplexityRuntimeTransport } from "./perplexity-web-search-provi
const PERPLEXITY_CREDENTIAL_PATH = "plugins.entries.perplexity.config.webSearch.apiKey";
type PerplexityWebSearchRuntime = typeof import("./perplexity-web-search-provider.runtime.js");
let perplexityWebSearchRuntimePromise: Promise<PerplexityWebSearchRuntime> | undefined;
function loadPerplexityWebSearchRuntime(): Promise<PerplexityWebSearchRuntime> {
perplexityWebSearchRuntimePromise ??= import("./perplexity-web-search-provider.runtime.js");
return perplexityWebSearchRuntimePromise;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
@@ -95,8 +104,7 @@ function createPerplexityToolDefinition(
: "Search the web using Perplexity. Runtime routing decides between native Search API and Sonar chat-completions compatibility. Structured filters are available on the native Search API path.",
parameters: createPerplexityParameters(schemaTransport),
execute: async (args) => {
const { executePerplexitySearch } =
await import("./perplexity-web-search-provider.runtime.js");
const { executePerplexitySearch } = await loadPerplexityWebSearchRuntime();
return await executePerplexitySearch(args, searchConfig);
},
};

View File

@@ -6,6 +6,15 @@ import {
const SEARXNG_CREDENTIAL_PATH = "plugins.entries.searxng.config.webSearch.baseUrl";
type SearxngClientModule = typeof import("./searxng-client.js");
let searxngClientModulePromise: Promise<SearxngClientModule> | undefined;
function loadSearxngClientModule(): Promise<SearxngClientModule> {
searxngClientModulePromise ??= import("./searxng-client.js");
return searxngClientModulePromise;
}
const SearxngSearchSchema = {
type: "object",
properties: {
@@ -52,7 +61,7 @@ export function createSearxngWebSearchProvider(): WebSearchProviderPlugin {
"Search the web using a self-hosted SearXNG instance. Returns titles, URLs, and snippets.",
parameters: SearxngSearchSchema,
execute: async (args) => {
const { runSearxngSearch } = await import("./searxng-client.js");
const { runSearxngSearch } = await loadSearxngClientModule();
return await runSearxngSearch({
config: ctx.config,
query: readStringParam(args, "query", { required: true }),

View File

@@ -4,6 +4,16 @@ import {
} from "openclaw/plugin-sdk/provider-web-search-contract";
const TAVILY_CREDENTIAL_PATH = "plugins.entries.tavily.config.webSearch.apiKey";
type TavilyClientModule = typeof import("./tavily-client.js");
let tavilyClientModulePromise: Promise<TavilyClientModule> | undefined;
function loadTavilyClientModule(): Promise<TavilyClientModule> {
tavilyClientModulePromise ??= import("./tavily-client.js");
return tavilyClientModulePromise;
}
const GenericTavilySearchSchema = {
type: "object",
properties: {
@@ -42,7 +52,7 @@ export function createTavilyWebSearchProvider(): WebSearchProviderPlugin {
"Search the web using Tavily. Returns structured results with snippets. Use tavily_search for Tavily-specific options like search depth, topic filtering, or AI answers.",
parameters: GenericTavilySearchSchema,
execute: async (args) => {
const { runTavilySearch } = await import("./tavily-client.js");
const { runTavilySearch } = await loadTavilyClientModule();
return await runTavilySearch({
cfg: ctx.config,
query: typeof args.query === "string" ? args.query : "",

View File

@@ -24,6 +24,15 @@ import {
} from "./x-search-tool-shared.js";
const PROVIDER_ID = "xai";
type CodeExecutionModule = typeof import("./code-execution.js");
let codeExecutionModulePromise: Promise<CodeExecutionModule> | undefined;
function loadCodeExecutionModule(): Promise<CodeExecutionModule> {
codeExecutionModulePromise ??= import("./code-execution.js");
return codeExecutionModulePromise;
}
function hasResolvableXaiApiKey(config: unknown): boolean {
return Boolean(
resolveFallbackXaiAuth(config as never)?.apiKey || readProviderEnvValue(["XAI_API_KEY"]),
@@ -89,7 +98,7 @@ function createLazyCodeExecutionTool(ctx: {
}),
}),
execute: async (toolCallId: string, args: Record<string, unknown>) => {
const { createCodeExecutionTool } = await import("./code-execution.js");
const { createCodeExecutionTool } = await loadCodeExecutionModule();
const tool = createCodeExecutionTool({
config: ctx.config as never,
runtimeConfig: (ctx.runtimeConfig as never) ?? null,

View File

@@ -40,6 +40,26 @@ function isTypeOnlyImportDeclaration(node) {
);
}
function readDeclarationName(node) {
if (
(ts.isFunctionDeclaration(node) ||
ts.isMethodDeclaration(node) ||
ts.isVariableDeclaration(node)) &&
node.name &&
ts.isIdentifier(node.name)
) {
return node.name.text;
}
if (ts.isPropertyAssignment(node)) {
if (ts.isIdentifier(node.name) || ts.isStringLiteral(node.name)) {
return node.name.text;
}
}
return null;
}
function isIgnoredTestHelperContent(content) {
return /\bfrom\s+["']vitest["']/.test(content) || /\bfrom\s+["']@vitest\//.test(content);
}
@@ -61,6 +81,8 @@ export function findDynamicImportAdvisories(content, fileName = "source.ts") {
const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true);
const staticRuntimeImports = new Map();
const dynamicImports = new Map();
const directExecuteImports = [];
const declarationStack = [];
const addLine = (map, specifier, line) => {
const lines = map.get(specifier) ?? [];
@@ -69,6 +91,11 @@ export function findDynamicImportAdvisories(content, fileName = "source.ts") {
};
const visit = (node) => {
const declarationName = readDeclarationName(node);
if (declarationName) {
declarationStack.push(declarationName);
}
if (
ts.isImportDeclaration(node) &&
ts.isStringLiteral(node.moduleSpecifier) &&
@@ -84,16 +111,26 @@ export function findDynamicImportAdvisories(content, fileName = "source.ts") {
) {
const specifier = readStringLiteral(node.arguments[0]);
if (specifier) {
addLine(dynamicImports, specifier, toLine(sourceFile, node));
const line = toLine(sourceFile, node);
addLine(dynamicImports, specifier, line);
if (declarationStack.includes("execute")) {
directExecuteImports.push({
line,
reason: `direct dynamic import of "${specifier}" inside execute path; move it behind a cached loader`,
});
}
}
}
ts.forEachChild(node, visit);
if (declarationName) {
declarationStack.pop();
}
};
visit(sourceFile);
const advisories = [];
const advisories = [...directExecuteImports];
for (const [specifier, dynamicLines] of dynamicImports) {
const staticLines = staticRuntimeImports.get(specifier);
if (staticLines?.length) {

View File

@@ -54,4 +54,39 @@ describe("check-dynamic-import-warts", () => {
`;
expect(findDynamicImportAdvisories(source)).toEqual([]);
});
it("flags direct dynamic imports inside execute paths", () => {
const source = `
export function createTool() {
return {
execute: async () => {
return await import("./runtime.js");
},
};
}
`;
expect(findDynamicImportAdvisories(source)).toEqual([
{
line: 5,
reason:
'direct dynamic import of "./runtime.js" inside execute path; move it behind a cached loader',
},
]);
});
it("allows execute paths that call cached loaders", () => {
const source = `
let runtimePromise: Promise<typeof import("./runtime.js")> | undefined;
function loadRuntime() {
runtimePromise ??= import("./runtime.js");
return runtimePromise;
}
export function createTool() {
return {
execute: async () => await loadRuntime(),
};
}
`;
expect(findDynamicImportAdvisories(source)).toEqual([]);
});
});