diff --git a/docs/providers/xai.md b/docs/providers/xai.md
index 628efd8a846d..9924fa7c669b 100644
--- a/docs/providers/xai.md
+++ b/docs/providers/xai.md
@@ -194,6 +194,15 @@ stale context metadata on active 4.20 rows. It does not pin active 4.20
## Features
+
+ `x_search` and `code_execution` run on xAI's servers. xAI bills $5 per 1,000
+ tool calls, plus the model's input and output tokens. With each tool's
+ `enabled` setting omitted, OpenClaw exposes it only for an active xAI model.
+ A known non-xAI model provider requires an explicit per-tool `enabled: true`;
+ a missing or unresolved provider fails closed. xAI auth is always required,
+ and `enabled: false` disables the tool for every provider.
+
+
The bundled `grok` web-search provider prefers xAI OAuth, then falls back
@@ -429,15 +438,15 @@ stale context metadata on active 4.20 rows. It does not pin active 4.20
Config path: `plugins.entries.xai.config.xSearch`
- | Key | Type | Default | Description |
- | ----------------- | ------- | ------------------------------ | ------------------------------------- |
- | `enabled` | boolean | `true` (if key available) | Enable or disable x_search |
- | `model` | string | `grok-4.3` | Model used for x_search requests |
- | `baseUrl` | string | - | xAI Responses base URL override |
- | `inlineCitations` | boolean | - | Include inline citations in results |
- | `maxTurns` | number | - | Maximum conversation turns |
- | `timeoutSeconds` | number | `30` | Request timeout in seconds |
- | `cacheTtlMinutes` | number | `15` | Cache time-to-live in minutes |
+ | Key | Type | Default | Description |
+ | ----------------- | ------- | ------------------------- | ------------------------------------------------ |
+ | `enabled` | boolean | Automatic for xAI models | Disable, or opt in for a known non-xAI provider |
+ | `model` | string | `grok-4.3` | Model used for x_search requests |
+ | `baseUrl` | string | - | xAI Responses base URL override |
+ | `inlineCitations` | boolean | - | Include inline citations in results |
+ | `maxTurns` | number | - | Maximum conversation turns |
+ | `timeoutSeconds` | number | `30` | Request timeout in seconds |
+ | `cacheTtlMinutes` | number | `15` | Cache time-to-live in minutes |
```json5
{
@@ -466,12 +475,12 @@ stale context metadata on active 4.20 rows. It does not pin active 4.20
Config path: `plugins.entries.xai.config.codeExecution`
- | Key | Type | Default | Description |
- | ---------------- | ------- | -------------------------- | ---------------------------------------- |
- | `enabled` | boolean | `true` (if key available) | Enable or disable code execution |
- | `model` | string | `grok-4.3` | Model used for code execution requests |
- | `maxTurns` | number | - | Maximum conversation turns |
- | `timeoutSeconds` | number | `30` | Request timeout in seconds |
+ | Key | Type | Default | Description |
+ | ---------------- | ------- | ------------------------ | ------------------------------------------------ |
+ | `enabled` | boolean | Automatic for xAI models | Disable, or opt in for a known non-xAI provider |
+ | `model` | string | `grok-4.3` | Model used for code execution requests |
+ | `maxTurns` | number | - | Maximum conversation turns |
+ | `timeoutSeconds` | number | `30` | Request timeout in seconds |
This is remote xAI sandbox execution, not local [`exec`](/tools/exec).
diff --git a/docs/tools/code-execution.md b/docs/tools/code-execution.md
index c52d993f49d8..5d099322bd07 100644
--- a/docs/tools/code-execution.md
+++ b/docs/tools/code-execution.md
@@ -11,6 +11,11 @@ title: "Code execution"
(`https://api.x.ai/v1/responses`, same endpoint `x_search` uses). It is
registered by the bundled `xai` plugin under the `tools` contract.
+
+ `code_execution` runs on xAI's servers. xAI bills $5 per 1,000 tool calls,
+ plus the model's input and output tokens.
+
+
| Property | Value |
| ------------------ | --------------------------------------------------------------------------------- |
| Tool name | `code_execution` |
@@ -77,9 +82,15 @@ For local execution, use [`exec`](/tools/exec) instead.
- `code_execution` is available whenever xAI credentials resolve. Set
- `plugins.entries.xai.config.codeExecution.enabled` to `false` to disable
- it, or use the same block to override the model, turn cap, or timeout:
+ With `enabled` omitted, `code_execution` is exposed only when the active
+ model's provider is `xai` and xAI credentials resolve. For an active model
+ with a known non-xAI provider, set
+ `plugins.entries.xai.config.codeExecution.enabled` to `true` to opt in to
+ cross-provider use. If the active model provider is missing or unresolved,
+ the tool stays hidden. Set `enabled` to `false` to disable it for every
+ provider. xAI credentials are always required.
+
+ Use the same block to override the model, turn cap, or timeout:
```json5
{
@@ -88,7 +99,7 @@ For local execution, use [`exec`](/tools/exec) instead.
xai: {
config: {
codeExecution: {
- enabled: true,
+ enabled: true, // required for a known non-xAI model provider
model: "grok-4.3", // override the default xAI code-execution model
maxTurns: 2, // optional cap on internal tool turns
timeoutSeconds: 30, // request timeout (default: 30)
@@ -108,7 +119,7 @@ For local execution, use [`exec`](/tools/exec) instead.
```
`code_execution` appears in the agent's tool list once the xAI plugin
- re-registers with `enabled: true`.
+ re-registers and the provider, enablement, and auth checks above pass.
diff --git a/docs/tools/web.md b/docs/tools/web.md
index 2ddba95b9c33..03494450d8ef 100644
--- a/docs/tools/web.md
+++ b/docs/tools/web.md
@@ -418,6 +418,11 @@ optional structured filters. OpenClaw constructs the built-in xAI `x_search`
tool per request rather than keeping it permanently registered, so it is only
active for the turn that actually calls it.
+
+ `x_search` runs on xAI's servers. xAI bills $5 per 1,000 tool calls, plus the
+ model's input and output tokens.
+
+
xAI documents `x_search` as supporting keyword search, semantic search, user
search, and thread fetch. For per-post engagement stats such as reposts,
@@ -429,6 +434,13 @@ active for the turn that actually calls it.
### x_search config
+With `enabled` omitted, `x_search` is exposed only when the active model's
+provider is `xai` and xAI credentials resolve. For an active model with a known
+non-xAI provider, set `plugins.entries.xai.config.xSearch.enabled` to `true` to
+opt in to cross-provider use. If the active model provider is missing or
+unresolved, the tool stays hidden. Set `enabled` to `false` to disable it for
+every provider. xAI credentials are always required.
+
```json5
{
plugins: {
@@ -436,7 +448,7 @@ active for the turn that actually calls it.
xai: {
config: {
xSearch: {
- enabled: true,
+ enabled: true, // required for a known non-xAI model provider
model: "grok-4.3",
baseUrl: "https://api.x.ai/v1", // optional, overrides webSearch.baseUrl
inlineCitations: false,
diff --git a/extensions/xai/index.test.ts b/extensions/xai/index.test.ts
index c78c582360bd..2853f8b3e0dc 100644
--- a/extensions/xai/index.test.ts
+++ b/extensions/xai/index.test.ts
@@ -73,14 +73,58 @@ function requireEntry(entries: T[], id: string): T {
return entry;
}
+type XaiBilledToolName = "code_execution" | "x_search";
+
+function registerXaiBilledToolFactories() {
+ const tools = new Map[0]>();
+ plugin.register(
+ createTestPluginApi({
+ registerTool(tool, opts) {
+ if (opts?.name) {
+ tools.set(opts.name, tool);
+ }
+ },
+ }),
+ );
+
+ function requireFactory(name: XaiBilledToolName) {
+ const factory = tools.get(name);
+ if (typeof factory !== "function") {
+ throw new Error(`Expected ${name} to register a tool factory`);
+ }
+ return factory;
+ }
+
+ return {
+ code_execution: requireFactory("code_execution"),
+ x_search: requireFactory("x_search"),
+ };
+}
+
+function createXaiBilledToolConfig(name: XaiBilledToolName, enabled?: boolean) {
+ const toolConfig = enabled === undefined ? {} : { enabled };
+ return {
+ plugins: {
+ entries: {
+ xai: {
+ config:
+ name === "code_execution" ? { codeExecution: toolConfig } : { xSearch: toolConfig },
+ },
+ },
+ },
+ };
+}
+
describe("xai provider plugin", () => {
beforeEach(() => {
clearLiveCatalogCacheForTests();
providerAuthRuntimeMocks.resolveApiKeyForProvider.mockReset();
+ vi.stubEnv("XAI_API_KEY", "");
});
afterEach(() => {
vi.unstubAllGlobals();
+ vi.unstubAllEnvs();
});
it("exposes xAI OAuth and preserves the explicit device-code alias", async () => {
@@ -450,6 +494,117 @@ describe("xai provider plugin", () => {
expect(realtimeProvider.aliases).toContain("xai-realtime");
});
+ describe.each(["code_execution", "x_search"] as const)("%s exposure", (toolName) => {
+ it.each([
+ {
+ label: "exposes by default for an xAI model with auth",
+ provider: "xai",
+ hasAuth: true,
+ expected: true,
+ },
+ {
+ label: "exposes when explicitly enabled for an xAI model with auth",
+ provider: "xai",
+ enabled: true,
+ hasAuth: true,
+ expected: true,
+ },
+ {
+ label: "hides when explicitly disabled for an xAI model",
+ provider: "xai",
+ enabled: false,
+ hasAuth: true,
+ expected: false,
+ },
+ {
+ label: "hides by default for a known non-xAI model",
+ provider: "openai",
+ hasAuth: true,
+ expected: false,
+ },
+ {
+ label: "hides when explicitly disabled for a known non-xAI model",
+ provider: "openai",
+ enabled: false,
+ hasAuth: true,
+ expected: false,
+ },
+ {
+ label: "exposes when explicitly enabled for a known non-xAI model with auth",
+ provider: "openai",
+ enabled: true,
+ hasAuth: true,
+ expected: true,
+ },
+ {
+ label: "hides when the active provider is missing",
+ enabled: true,
+ hasAuth: true,
+ expected: false,
+ },
+ {
+ label: "hides when the active provider is blank",
+ provider: " ",
+ enabled: true,
+ hasAuth: true,
+ expected: false,
+ },
+ {
+ label: "hides an xAI model without auth",
+ provider: "xai",
+ hasAuth: false,
+ expected: false,
+ },
+ {
+ label: "hides an explicit non-xAI opt-in without auth",
+ provider: "openai",
+ enabled: true,
+ hasAuth: false,
+ expected: false,
+ },
+ ])("$label", ({ provider, enabled, hasAuth, expected }) => {
+ const factory = registerXaiBilledToolFactories()[toolName];
+ const tool = factory({
+ config: createXaiBilledToolConfig(toolName, enabled),
+ activeModel: provider === undefined ? {} : { provider },
+ hasAuthForProvider: (providerId) => hasAuth && providerId === "xai",
+ resolveApiKeyForProvider: async (providerId) =>
+ hasAuth && providerId === "xai" ? "xai-test-key" : undefined,
+ });
+
+ expect(tool).toEqual(expected ? expect.objectContaining({ name: toolName }) : null);
+ });
+
+ it.each([
+ {
+ label: "runtime false overrides source true",
+ provider: "xai",
+ sourceEnabled: true,
+ runtimeEnabled: false,
+ expected: false,
+ },
+ {
+ label: "runtime true overrides source false for a known non-xAI provider",
+ provider: "openai",
+ sourceEnabled: false,
+ runtimeEnabled: true,
+ expected: true,
+ },
+ ])("$label", ({ provider, sourceEnabled, runtimeEnabled, expected }) => {
+ const factory = registerXaiBilledToolFactories()[toolName];
+ const tool = factory({
+ config: createXaiBilledToolConfig(toolName, sourceEnabled),
+ runtimeConfig: createXaiBilledToolConfig(toolName, runtimeEnabled),
+ activeModel: { provider },
+ hasAuthForProvider: (providerId) => providerId === "xai",
+ resolveApiKeyForProvider: async (providerId) =>
+ providerId === "xai" ? "xai-test-key" : undefined,
+ });
+
+ expect(tool).toEqual(expected ? expect.objectContaining({ name: toolName }) : null);
+ });
+ });
+
it("declares setup auto-enable reasons for plugin-owned tool config", () => {
const probe = registerXaiAutoEnableProbe();
diff --git a/extensions/xai/index.ts b/extensions/xai/index.ts
index e4849f7214b2..29919a40d692 100644
--- a/extensions/xai/index.ts
+++ b/extensions/xai/index.ts
@@ -1,5 +1,6 @@
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
// Xai plugin entrypoint registers its OpenClaw integration.
+import type { OpenClawPluginToolContext } from "openclaw/plugin-sdk/plugin-entry";
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
import { OPENAI_COMPATIBLE_REPLAY_HOOKS } from "openclaw/plugin-sdk/provider-model-shared";
import { defaultToolStreamExtraParams } from "openclaw/plugin-sdk/provider-stream-shared";
@@ -92,13 +93,30 @@ function isXSearchEnabled(config: unknown, auth?: XaiToolAuthContext): boolean {
return hasResolvableXaiApiKey(config, auth);
}
-function createLazyCodeExecutionTool(ctx: {
- config?: Record;
- runtimeConfig?: Record;
- hasAuthForProvider?: XaiToolAuthContext["hasAuthForProvider"];
- resolveApiKeyForProvider?: XaiToolAuthContext["resolveApiKeyForProvider"];
-}) {
+function shouldExposeXaiBilledTool(params: {
+ activeProvider?: string;
+ enabled?: unknown;
+}): boolean {
+ const activeProvider = params.activeProvider?.trim();
+ if (!activeProvider || params.enabled === false) {
+ return false;
+ }
+ // Cross-provider billing requires explicit consent; xAI models retain the
+ // credential-backed default. Unknown providers fail closed.
+ return activeProvider === PROVIDER_ID || params.enabled === true;
+}
+
+function createLazyCodeExecutionTool(ctx: OpenClawPluginToolContext) {
const effectiveConfig = ctx.runtimeConfig ?? ctx.config;
+ const codeExecutionConfig = readPluginCodeExecutionConfig(effectiveConfig);
+ if (
+ !shouldExposeXaiBilledTool({
+ activeProvider: ctx.activeModel?.provider,
+ enabled: codeExecutionConfig?.enabled,
+ })
+ ) {
+ return null;
+ }
if (!isCodeExecutionEnabled(effectiveConfig, ctx)) {
return null;
}
@@ -119,13 +137,17 @@ function createLazyCodeExecutionTool(ctx: {
);
}
-function createLazyXSearchTool(ctx: {
- config?: Record;
- runtimeConfig?: Record;
- hasAuthForProvider?: XaiToolAuthContext["hasAuthForProvider"];
- resolveApiKeyForProvider?: XaiToolAuthContext["resolveApiKeyForProvider"];
-}) {
+function createLazyXSearchTool(ctx: OpenClawPluginToolContext) {
const effectiveConfig = ctx.runtimeConfig ?? ctx.config;
+ const xSearchConfig = resolveEffectiveXSearchConfig(effectiveConfig);
+ if (
+ !shouldExposeXaiBilledTool({
+ activeProvider: ctx.activeModel?.provider,
+ enabled: xSearchConfig?.enabled,
+ })
+ ) {
+ return null;
+ }
if (!isXSearchEnabled(effectiveConfig, ctx)) {
return null;
}
diff --git a/extensions/xai/openclaw.plugin.json b/extensions/xai/openclaw.plugin.json
index a1d05edaf42a..164f1735dfaf 100644
--- a/extensions/xai/openclaw.plugin.json
+++ b/extensions/xai/openclaw.plugin.json
@@ -140,11 +140,11 @@
},
"codeExecution.enabled": {
"label": "Enable Code Execution",
- "help": "Enable the code_execution tool for remote xAI sandbox analysis."
+ "help": "Expose code_execution on active xAI models; true opts in when the active provider is known non-xAI, false disables, and missing provider fails closed. Requires xAI auth; xAI bills $5/1,000 calls plus model tokens."
},
"xSearch.enabled": {
"label": "Enable X Search",
- "help": "Enable the x_search tool for searching X posts with xAI."
+ "help": "Expose x_search on active xAI models; true opts in when the active provider is known non-xAI, false disables, and missing provider fails closed. Requires xAI auth; xAI bills $5/1,000 calls plus model tokens."
},
"xSearch.model": {
"label": "X Search Model",
diff --git a/extensions/xai/xai.live.test.ts b/extensions/xai/xai.live.test.ts
index 5a6944c9052e..62955f0cccf2 100644
--- a/extensions/xai/xai.live.test.ts
+++ b/extensions/xai/xai.live.test.ts
@@ -4,6 +4,8 @@ import os from "node:os";
import path from "node:path";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { encodePngRgba, fillPixel } from "openclaw/plugin-sdk/media-runtime";
+import type { OpenClawPluginToolFactory } from "openclaw/plugin-sdk/plugin-entry";
+import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api";
import {
registerProviderPlugin,
requireRegisteredProvider,
@@ -96,6 +98,20 @@ const registerXaiPlugin = () =>
name: "xAI Provider",
});
+function registerXaiToolFactories(): Map {
+ const factories = new Map();
+ plugin.register(
+ createTestPluginApi({
+ registerTool(tool, options) {
+ if (typeof tool === "function" && options?.name) {
+ factories.set(options.name, tool);
+ }
+ },
+ }),
+ );
+ return factories;
+}
+
async function runXaiLiveCase(label: string, run: () => Promise): Promise {
try {
await run();
@@ -114,6 +130,76 @@ function isRealtimeOpenBillingDrift(error: Error): boolean {
}
describeLive("xai plugin live", () => {
+ it("gates registered billed tools and honors explicit cross-provider consent", async () => {
+ await runXaiLiveCase("billed-tool-policy", async () => {
+ const codeExecutionFactory = registerXaiToolFactories().get("code_execution");
+ if (!codeExecutionFactory) {
+ throw new Error("expected code_execution factory to be registered");
+ }
+ const baseConfig = {
+ plugins: {
+ entries: {
+ xai: {
+ config: {
+ webSearch: { apiKey: XAI_API_KEY },
+ },
+ },
+ },
+ },
+ } as OpenClawConfig;
+ const explicitConfig = {
+ plugins: {
+ entries: {
+ xai: {
+ config: {
+ webSearch: { apiKey: XAI_API_KEY },
+ codeExecution: { enabled: true, maxTurns: 1, timeoutSeconds: 90 },
+ },
+ },
+ },
+ },
+ } as OpenClawConfig;
+
+ expect(
+ codeExecutionFactory({
+ config: baseConfig,
+ activeModel: { provider: "xai", modelId: "grok-4.3" },
+ }),
+ ).not.toBeNull();
+ expect(
+ codeExecutionFactory({
+ config: baseConfig,
+ activeModel: { provider: "openai", modelId: "gpt-5.4" },
+ }),
+ ).toBeNull();
+ expect(
+ codeExecutionFactory({
+ config: explicitConfig,
+ }),
+ ).toBeNull();
+
+ const explicitCrossProviderTool = codeExecutionFactory({
+ config: explicitConfig,
+ activeModel: { provider: "openai", modelId: "gpt-5.4" },
+ });
+ if (!explicitCrossProviderTool || Array.isArray(explicitCrossProviderTool)) {
+ throw new Error("expected explicit cross-provider code_execution tool");
+ }
+ const result = await explicitCrossProviderTool.execute("code-execution:cross-provider-live", {
+ task: "Use the code interpreter to calculate 6 multiplied by 7.",
+ });
+ const details = (result.details ?? {}) as {
+ content?: string;
+ model?: string;
+ usedCodeExecution?: boolean;
+ };
+
+ expect(details.model).toBe("grok-4.3");
+ expect(details.usedCodeExecution).toBe(true);
+ expect(details.content).toContain("42");
+ });
+ }, 120_000);
+
it("runs remote code execution with the current default model", async () => {
await runXaiLiveCase("code-execution", async () => {
const tool = createCodeExecutionTool({