diff --git a/extensions/xai/index.ts b/extensions/xai/index.ts index 3629a1fb310..3cc1ba30e40 100644 --- a/extensions/xai/index.ts +++ b/extensions/xai/index.ts @@ -1,10 +1,15 @@ +import { Type } from "@sinclair/typebox"; import { coerceSecretRef, resolveNonEnvSecretRefApiKeyMarker, } from "openclaw/plugin-sdk/provider-auth"; import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry"; import { createToolStreamWrapper } from "openclaw/plugin-sdk/provider-stream"; -import { resolveProviderWebSearchPluginConfig } from "openclaw/plugin-sdk/provider-web-search"; +import { + jsonResult, + readProviderEnvValue, + resolveProviderWebSearchPluginConfig, +} from "openclaw/plugin-sdk/provider-web-search"; import { normalizeSecretInputString } from "openclaw/plugin-sdk/secret-input"; import { applyXaiModelCompat, @@ -13,17 +18,16 @@ import { resolveXaiModelCompatPatch, shouldContributeXaiCompat, } from "./api.js"; -import { createCodeExecutionTool } from "./code-execution.js"; import { applyXaiConfig, XAI_DEFAULT_MODEL_REF } from "./onboard.js"; import { buildXaiProvider } from "./provider-catalog.js"; import { isModernXaiModel, resolveXaiForwardCompatModel } from "./provider-models.js"; +import { resolveEffectiveXSearchConfig } from "./src/x-search-config.js"; import { createXaiFastModeWrapper, createXaiToolCallArgumentDecodingWrapper, createXaiToolPayloadCompatibilityWrapper, } from "./stream.js"; import { createXaiWebSearchProvider } from "./web-search.js"; -import { createXSearchTool } from "./x-search.js"; const PROVIDER_ID = "xai"; @@ -78,6 +82,147 @@ function resolveXaiProviderFallbackAuth( return readLegacyGrokFallback(record); } +function hasResolvableXaiApiKey(config: unknown): boolean { + return Boolean( + resolveXaiProviderFallbackAuth(config)?.apiKey || readProviderEnvValue(["XAI_API_KEY"]), + ); +} + +function isCodeExecutionEnabled(config: unknown): boolean { + if (!config || typeof config !== "object") { + return hasResolvableXaiApiKey(config); + } + const entries = (config as Record).plugins; + const pluginEntries = + entries && typeof entries === "object" + ? ((entries as Record).entries as Record | undefined) + : undefined; + const xaiEntry = + pluginEntries && typeof pluginEntries.xai === "object" + ? (pluginEntries.xai as Record) + : undefined; + const pluginConfig = + xaiEntry && typeof xaiEntry.config === "object" + ? (xaiEntry.config as Record) + : undefined; + const codeExecution = + pluginConfig && typeof pluginConfig.codeExecution === "object" + ? (pluginConfig.codeExecution as Record) + : undefined; + if (codeExecution?.enabled === false) { + return false; + } + return hasResolvableXaiApiKey(config); +} + +function isXSearchEnabled(config: unknown): boolean { + const resolved = + config && typeof config === "object" + ? resolveEffectiveXSearchConfig(config as never) + : undefined; + if (resolved?.enabled === false) { + return false; + } + return hasResolvableXaiApiKey(config); +} + +function createLazyCodeExecutionTool(ctx: { + config?: Record; + runtimeConfig?: Record; +}) { + const effectiveConfig = ctx.runtimeConfig ?? ctx.config; + if (!isCodeExecutionEnabled(effectiveConfig)) { + return null; + } + + return { + label: "Code Execution", + name: "code_execution", + description: + "Run sandboxed Python analysis with xAI. Use for calculations, tabulation, summaries, and chart-style analysis without local machine access.", + parameters: Type.Object({ + task: Type.String({ + description: + "The full analysis task for xAI's remote Python sandbox. Include any data to analyze directly in the task.", + }), + }), + execute: async (toolCallId: string, args: Record) => { + const { createCodeExecutionTool } = await import("./code-execution.js"); + const tool = createCodeExecutionTool({ + config: ctx.config as never, + runtimeConfig: (ctx.runtimeConfig as never) ?? null, + }); + if (!tool) { + return jsonResult({ + error: "missing_xai_api_key", + message: + "code_execution 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/code-execution", + }); + } + return await tool.execute(toolCallId, args); + }, + }; +} + +function createLazyXSearchTool(ctx: { + config?: Record; + runtimeConfig?: Record; +}) { + const effectiveConfig = ctx.runtimeConfig ?? ctx.config; + if (!isXSearchEnabled(effectiveConfig)) { + 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); + }, + }; +} + export default defineSingleProviderPluginEntry({ id: "xai", name: "xAI Plugin", @@ -148,21 +293,7 @@ export default defineSingleProviderPluginEntry({ }, register(api) { api.registerWebSearchProvider(createXaiWebSearchProvider()); - api.registerTool( - (ctx) => - createCodeExecutionTool({ - config: ctx.config, - runtimeConfig: ctx.runtimeConfig, - }), - { name: "code_execution" }, - ); - api.registerTool( - (ctx) => - createXSearchTool({ - config: ctx.config, - runtimeConfig: ctx.runtimeConfig, - }), - { name: "x_search" }, - ); + api.registerTool((ctx) => createLazyCodeExecutionTool(ctx), { name: "code_execution" }); + api.registerTool((ctx) => createLazyXSearchTool(ctx), { name: "x_search" }); }, });