From 78a948ee32bfeb5d2cc8c2b3822de9bce18e058c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 6 Apr 2026 20:29:47 +0100 Subject: [PATCH] refactor: dedupe xai x search tool helpers --- extensions/xai/index.ts | 62 +++------ extensions/xai/x-search-tool-shared.ts | 47 +++++++ extensions/xai/x-search.ts | 179 ++++++++++--------------- 3 files changed, 135 insertions(+), 153 deletions(-) create mode 100644 extensions/xai/x-search-tool-shared.ts diff --git a/extensions/xai/index.ts b/extensions/xai/index.ts index 1af0185ec67..18e86ee07fc 100644 --- a/extensions/xai/index.ts +++ b/extensions/xai/index.ts @@ -18,6 +18,10 @@ import { resolveEffectiveXSearchConfig } from "./src/x-search-config.js"; import { wrapXaiProviderStream } from "./stream.js"; import { buildXaiVideoGenerationProvider } from "./video-generation-provider.js"; import { createXaiWebSearchProvider } from "./web-search.js"; +import { + buildMissingXSearchApiKeyPayload, + createXSearchToolDefinition, +} from "./x-search-tool-shared.js"; const PROVIDER_ID = "xai"; const OPENAI_COMPATIBLE_REPLAY_HOOKS = buildProviderReplayFamilyHooks({ @@ -117,53 +121,17 @@ function createLazyXSearchTool(ctx: { return null; } - return { - label: "X Search", - name: "x_search", - description: - "Search X (formerly Twitter) using xAI, including targeted post or thread lookups. For per-post stats like reposts, replies, bookmarks, or views, prefer the exact post URL or status ID.", - parameters: Type.Object({ - query: Type.String({ description: "X search query string." }), - allowed_x_handles: Type.Optional( - Type.Array(Type.String({ minLength: 1 }), { - description: "Only include posts from these X handles.", - }), - ), - excluded_x_handles: Type.Optional( - Type.Array(Type.String({ minLength: 1 }), { - description: "Exclude posts from these X handles.", - }), - ), - from_date: Type.Optional( - Type.String({ description: "Only include posts on or after this date (YYYY-MM-DD)." }), - ), - to_date: Type.Optional( - Type.String({ description: "Only include posts on or before this date (YYYY-MM-DD)." }), - ), - enable_image_understanding: Type.Optional( - Type.Boolean({ description: "Allow xAI to inspect images attached to matching posts." }), - ), - enable_video_understanding: Type.Optional( - Type.Boolean({ description: "Allow xAI to inspect videos attached to matching posts." }), - ), - }), - execute: async (toolCallId: string, args: Record) => { - const { createXSearchTool } = await import("./x-search.js"); - const tool = createXSearchTool({ - config: ctx.config as never, - runtimeConfig: (ctx.runtimeConfig as never) ?? null, - }); - if (!tool) { - return jsonResult({ - error: "missing_xai_api_key", - message: - "x_search needs an xAI API key. Set XAI_API_KEY in the Gateway environment, or configure plugins.entries.xai.config.webSearch.apiKey.", - docs: "https://docs.openclaw.ai/tools/web", - }); - } - return await tool.execute(toolCallId, args); - }, - }; + return createXSearchToolDefinition(async (toolCallId: string, args: Record) => { + const { createXSearchTool } = await import("./x-search.js"); + const tool = createXSearchTool({ + config: ctx.config as never, + runtimeConfig: (ctx.runtimeConfig as never) ?? null, + }); + if (!tool) { + return jsonResult(buildMissingXSearchApiKeyPayload()); + } + return await tool.execute(toolCallId, args); + }); } export default defineSingleProviderPluginEntry({ diff --git a/extensions/xai/x-search-tool-shared.ts b/extensions/xai/x-search-tool-shared.ts new file mode 100644 index 00000000000..a4361541b03 --- /dev/null +++ b/extensions/xai/x-search-tool-shared.ts @@ -0,0 +1,47 @@ +import { Type } from "@sinclair/typebox"; + +export function buildMissingXSearchApiKeyPayload() { + return { + error: "missing_xai_api_key", + message: + "x_search needs an xAI API key. Set XAI_API_KEY in the Gateway environment, or configure plugins.entries.xai.config.webSearch.apiKey.", + docs: "https://docs.openclaw.ai/tools/web", + }; +} + +export function createXSearchToolDefinition( + execute: (toolCallId: string, args: Record) => Promise, +) { + return { + label: "X Search", + name: "x_search", + description: + "Search X (formerly Twitter) using xAI, including targeted post or thread lookups. For per-post stats like reposts, replies, bookmarks, or views, prefer the exact post URL or status ID.", + parameters: Type.Object({ + query: Type.String({ description: "X search query string." }), + allowed_x_handles: Type.Optional( + Type.Array(Type.String({ minLength: 1 }), { + description: "Only include posts from these X handles.", + }), + ), + excluded_x_handles: Type.Optional( + Type.Array(Type.String({ minLength: 1 }), { + description: "Exclude posts from these X handles.", + }), + ), + from_date: Type.Optional( + Type.String({ description: "Only include posts on or after this date (YYYY-MM-DD)." }), + ), + to_date: Type.Optional( + Type.String({ description: "Only include posts on or before this date (YYYY-MM-DD)." }), + ), + enable_image_understanding: Type.Optional( + Type.Boolean({ description: "Allow xAI to inspect images attached to matching posts." }), + ), + enable_video_understanding: Type.Optional( + Type.Boolean({ description: "Allow xAI to inspect videos attached to matching posts." }), + ), + }), + execute, + }; +} diff --git a/extensions/xai/x-search.ts b/extensions/xai/x-search.ts index 8aaa08b1302..b3736fe120c 100644 --- a/extensions/xai/x-search.ts +++ b/extensions/xai/x-search.ts @@ -1,4 +1,3 @@ -import { Type } from "@sinclair/typebox"; import { getRuntimeConfigSnapshot, type OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import { jsonResult, @@ -19,6 +18,10 @@ import { resolveXaiXSearchModel, type XaiXSearchOptions, } from "./src/x-search-shared.js"; +import { + buildMissingXSearchApiKeyPayload, + createXSearchToolDefinition, +} from "./x-search-tool-shared.js"; class PluginToolInputError extends Error { constructor(message: string) { @@ -132,116 +135,80 @@ export function createXSearchTool(options?: { return null; } - return { - label: "X Search", - name: "x_search", - description: - "Search X (formerly Twitter) using xAI, including targeted post or thread lookups. For per-post stats like reposts, replies, bookmarks, or views, prefer the exact post URL or status ID.", - parameters: Type.Object({ - query: Type.String({ description: "X search query string." }), - allowed_x_handles: Type.Optional( - Type.Array(Type.String({ minLength: 1 }), { - description: "Only include posts from these X handles.", - }), - ), - excluded_x_handles: Type.Optional( - Type.Array(Type.String({ minLength: 1 }), { - description: "Exclude posts from these X handles.", - }), - ), - from_date: Type.Optional( - Type.String({ description: "Only include posts on or after this date (YYYY-MM-DD)." }), - ), - to_date: Type.Optional( - Type.String({ description: "Only include posts on or before this date (YYYY-MM-DD)." }), - ), - enable_image_understanding: Type.Optional( - Type.Boolean({ description: "Allow xAI to inspect images attached to matching posts." }), - ), - enable_video_understanding: Type.Optional( - Type.Boolean({ description: "Allow xAI to inspect videos attached to matching posts." }), - ), - }), - execute: async (_toolCallId: string, args: Record) => { - const apiKey = resolveXSearchApiKey({ - sourceConfig: options?.config, - runtimeConfig: runtimeConfig ?? undefined, - }); - if (!apiKey) { - return jsonResult({ - error: "missing_xai_api_key", - message: - "x_search needs an xAI API key. Set XAI_API_KEY in the Gateway environment, or configure plugins.entries.xai.config.webSearch.apiKey.", - docs: "https://docs.openclaw.ai/tools/web", - }); - } + return createXSearchToolDefinition(async (_toolCallId: string, args: Record) => { + const apiKey = resolveXSearchApiKey({ + sourceConfig: options?.config, + runtimeConfig: runtimeConfig ?? undefined, + }); + if (!apiKey) { + return jsonResult(buildMissingXSearchApiKeyPayload()); + } - const query = readStringParam(args, "query", { required: true }); - const allowedXHandles = readStringArrayParam(args, "allowed_x_handles"); - const excludedXHandles = readStringArrayParam(args, "excluded_x_handles"); - const fromDate = normalizeOptionalIsoDate(readStringParam(args, "from_date"), "from_date"); - const toDate = normalizeOptionalIsoDate(readStringParam(args, "to_date"), "to_date"); - if (fromDate && toDate && fromDate > toDate) { - throw new PluginToolInputError("from_date must be on or before to_date"); - } + const query = readStringParam(args, "query", { required: true }); + const allowedXHandles = readStringArrayParam(args, "allowed_x_handles"); + const excludedXHandles = readStringArrayParam(args, "excluded_x_handles"); + const fromDate = normalizeOptionalIsoDate(readStringParam(args, "from_date"), "from_date"); + const toDate = normalizeOptionalIsoDate(readStringParam(args, "to_date"), "to_date"); + if (fromDate && toDate && fromDate > toDate) { + throw new PluginToolInputError("from_date must be on or before to_date"); + } - const xSearchOptions: XaiXSearchOptions = { - query, + const xSearchOptions: XaiXSearchOptions = { + query, + allowedXHandles, + excludedXHandles, + fromDate, + toDate, + enableImageUnderstanding: args.enable_image_understanding === true, + enableVideoUnderstanding: args.enable_video_understanding === true, + }; + const xSearchConfigRecord = xSearchConfig; + const model = resolveXaiXSearchModel(xSearchConfigRecord); + const inlineCitations = resolveXaiXSearchInlineCitations(xSearchConfigRecord); + const maxTurns = resolveXaiXSearchMaxTurns(xSearchConfigRecord); + const cacheKey = buildXSearchCacheKey({ + query, + model, + inlineCitations, + maxTurns, + options: { allowedXHandles, excludedXHandles, fromDate, toDate, - enableImageUnderstanding: args.enable_image_understanding === true, - enableVideoUnderstanding: args.enable_video_understanding === true, - }; - const xSearchConfigRecord = xSearchConfig; - const model = resolveXaiXSearchModel(xSearchConfigRecord); - const inlineCitations = resolveXaiXSearchInlineCitations(xSearchConfigRecord); - const maxTurns = resolveXaiXSearchMaxTurns(xSearchConfigRecord); - const cacheKey = buildXSearchCacheKey({ - query, - model, - inlineCitations, - maxTurns, - options: { - allowedXHandles, - excludedXHandles, - fromDate, - toDate, - enableImageUnderstanding: xSearchOptions.enableImageUnderstanding, - enableVideoUnderstanding: xSearchOptions.enableVideoUnderstanding, - }, - }); - const cached = readCache(X_SEARCH_CACHE, cacheKey); - if (cached) { - return jsonResult({ ...cached.value, cached: true }); - } + enableImageUnderstanding: xSearchOptions.enableImageUnderstanding, + enableVideoUnderstanding: xSearchOptions.enableVideoUnderstanding, + }, + }); + const cached = readCache(X_SEARCH_CACHE, cacheKey); + if (cached) { + return jsonResult({ ...cached.value, cached: true }); + } - const startedAt = Date.now(); - const result = await requestXaiXSearch({ - apiKey, - model, - timeoutSeconds: resolveTimeoutSeconds(xSearchConfig?.timeoutSeconds, 30), - inlineCitations, - maxTurns, - options: xSearchOptions, - }); - const payload = buildXaiXSearchPayload({ - query, - model, - tookMs: Date.now() - startedAt, - content: result.content, - citations: result.citations, - inlineCitations: result.inlineCitations, - options: xSearchOptions, - }); - writeCache( - X_SEARCH_CACHE, - cacheKey, - payload, - resolveCacheTtlMs(xSearchConfig?.cacheTtlMinutes, 15), - ); - return jsonResult(payload); - }, - }; + const startedAt = Date.now(); + const result = await requestXaiXSearch({ + apiKey, + model, + timeoutSeconds: resolveTimeoutSeconds(xSearchConfig?.timeoutSeconds, 30), + inlineCitations, + maxTurns, + options: xSearchOptions, + }); + const payload = buildXaiXSearchPayload({ + query, + model, + tookMs: Date.now() - startedAt, + content: result.content, + citations: result.citations, + inlineCitations: result.inlineCitations, + options: xSearchOptions, + }); + writeCache( + X_SEARCH_CACHE, + cacheKey, + payload, + resolveCacheTtlMs(xSearchConfig?.cacheTtlMinutes, 15), + ); + return jsonResult(payload); + }); }