mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 02:00:21 +00:00
!refactor(xai): move x_search config behind plugin boundary (#59674)
* refactor(xai): move x_search config behind plugin boundary * chore(changelog): note x_search config migration * fix(xai): include x_search migration helpers
This commit is contained in:
@@ -38,6 +38,30 @@
|
||||
"label": "Enable Code Execution",
|
||||
"help": "Enable the code_execution tool for remote xAI sandbox analysis."
|
||||
},
|
||||
"xSearch.enabled": {
|
||||
"label": "Enable X Search",
|
||||
"help": "Enable the x_search tool for searching X posts with xAI."
|
||||
},
|
||||
"xSearch.model": {
|
||||
"label": "X Search Model",
|
||||
"help": "xAI model override for x_search."
|
||||
},
|
||||
"xSearch.inlineCitations": {
|
||||
"label": "X Search Inline Citations",
|
||||
"help": "Keep inline markdown citations from xAI in x_search responses when available."
|
||||
},
|
||||
"xSearch.maxTurns": {
|
||||
"label": "X Search Max Turns",
|
||||
"help": "Optional max internal tool turns xAI may use per x_search request."
|
||||
},
|
||||
"xSearch.timeoutSeconds": {
|
||||
"label": "X Search Timeout",
|
||||
"help": "Timeout in seconds for x_search requests."
|
||||
},
|
||||
"xSearch.cacheTtlMinutes": {
|
||||
"label": "X Search Cache TTL",
|
||||
"help": "Cache TTL in minutes for x_search results."
|
||||
},
|
||||
"codeExecution.model": {
|
||||
"label": "Code Execution Model",
|
||||
"help": "xAI model override for code_execution."
|
||||
@@ -74,6 +98,30 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"xSearch": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"inlineCitations": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"maxTurns": {
|
||||
"type": "number"
|
||||
},
|
||||
"timeoutSeconds": {
|
||||
"type": "number"
|
||||
},
|
||||
"cacheTtlMinutes": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codeExecution": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
56
extensions/xai/src/x-search-config.ts
Normal file
56
extensions/xai/src/x-search-config.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
function isRecord(value: unknown): value is JsonRecord {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function cloneRecord<T extends JsonRecord | undefined>(value: T): T {
|
||||
if (!value) {
|
||||
return value;
|
||||
}
|
||||
return { ...value } as T;
|
||||
}
|
||||
|
||||
export function resolveLegacyXSearchConfig(config?: OpenClawConfig): JsonRecord | undefined {
|
||||
const web = config?.tools?.web as Record<string, unknown> | undefined;
|
||||
const xSearch = web?.x_search;
|
||||
return isRecord(xSearch) ? cloneRecord(xSearch) : undefined;
|
||||
}
|
||||
|
||||
export function resolvePluginXSearchConfig(config?: OpenClawConfig): JsonRecord | undefined {
|
||||
const pluginConfig = config?.plugins?.entries?.xai?.config;
|
||||
if (!isRecord(pluginConfig?.xSearch)) {
|
||||
return undefined;
|
||||
}
|
||||
return cloneRecord(pluginConfig.xSearch);
|
||||
}
|
||||
|
||||
export function resolveEffectiveXSearchConfig(config?: OpenClawConfig): JsonRecord | undefined {
|
||||
const legacy = resolveLegacyXSearchConfig(config);
|
||||
const pluginOwned = resolvePluginXSearchConfig(config);
|
||||
if (!legacy) {
|
||||
return pluginOwned;
|
||||
}
|
||||
if (!pluginOwned) {
|
||||
return legacy;
|
||||
}
|
||||
return {
|
||||
...legacy,
|
||||
...pluginOwned,
|
||||
};
|
||||
}
|
||||
|
||||
export function setPluginXSearchConfigValue(
|
||||
configTarget: OpenClawConfig,
|
||||
key: string,
|
||||
value: unknown,
|
||||
): void {
|
||||
const plugins = (configTarget.plugins ??= {}) as { entries?: Record<string, unknown> };
|
||||
const entries = (plugins.entries ??= {});
|
||||
const entry = (entries.xai ??= {}) as { config?: Record<string, unknown> };
|
||||
const config = (entry.config ??= {});
|
||||
const xSearch = (config.xSearch ??= {}) as Record<string, unknown>;
|
||||
xSearch[key] = value;
|
||||
}
|
||||
@@ -112,7 +112,7 @@ describe("xai web search config resolution", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("offers plugin-owned x_search setup after Grok is selected", async () => {
|
||||
it("offers plugin-owned xSearch setup after Grok is selected", async () => {
|
||||
const provider = createXaiWebSearchProvider();
|
||||
const select = vi.fn().mockResolvedValueOnce("yes").mockResolvedValueOnce("grok-4-1-fast");
|
||||
const prompter = createWizardPrompter({
|
||||
@@ -146,24 +146,32 @@ describe("xai web search config resolution", () => {
|
||||
prompter,
|
||||
});
|
||||
|
||||
expect(next?.tools?.web?.x_search).toMatchObject({
|
||||
expect(next?.plugins?.entries?.xai?.config?.xSearch).toMatchObject({
|
||||
enabled: true,
|
||||
model: "grok-4-1-fast",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps explicit x_search disablement untouched during provider-owned setup", async () => {
|
||||
it("keeps explicit xSearch disablement untouched during provider-owned setup", async () => {
|
||||
const provider = createXaiWebSearchProvider();
|
||||
const config = {
|
||||
plugins: {
|
||||
entries: {
|
||||
xai: {
|
||||
config: {
|
||||
xSearch: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
provider: "grok",
|
||||
enabled: true,
|
||||
},
|
||||
x_search: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -27,6 +27,10 @@ import {
|
||||
resolveXaiInlineCitations,
|
||||
resolveXaiWebSearchModel,
|
||||
} from "./src/web-search-shared.js";
|
||||
import {
|
||||
resolveEffectiveXSearchConfig,
|
||||
setPluginXSearchConfigValue,
|
||||
} from "./src/x-search-config.js";
|
||||
import { XAI_DEFAULT_X_SEARCH_MODEL } from "./src/x-search-shared.js";
|
||||
|
||||
const XAI_WEB_SEARCH_CACHE = new Map<
|
||||
@@ -50,28 +54,7 @@ const X_SEARCH_MODEL_OPTIONS = [
|
||||
function resolveXSearchConfigRecord(
|
||||
config?: WebSearchProviderSetupContext["config"],
|
||||
): Record<string, unknown> | undefined {
|
||||
const xSearch = config?.tools?.web?.x_search;
|
||||
return xSearch && typeof xSearch === "object" ? (xSearch as Record<string, unknown>) : undefined;
|
||||
}
|
||||
|
||||
function applyXSearchSetupConfig(
|
||||
config: WebSearchProviderSetupContext["config"],
|
||||
params: { enabled: boolean; model: string },
|
||||
): WebSearchProviderSetupContext["config"] {
|
||||
return {
|
||||
...config,
|
||||
tools: {
|
||||
...config.tools,
|
||||
web: {
|
||||
...config.tools?.web,
|
||||
x_search: {
|
||||
...config.tools?.web?.x_search,
|
||||
enabled: params.enabled,
|
||||
model: params.model,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return resolveEffectiveXSearchConfig(config);
|
||||
}
|
||||
|
||||
async function runXaiSearchProviderSetup(
|
||||
@@ -136,10 +119,10 @@ async function runXaiSearchProviderSetup(
|
||||
model = customModel.trim() || XAI_DEFAULT_X_SEARCH_MODEL;
|
||||
}
|
||||
|
||||
return applyXSearchSetupConfig(ctx.config, {
|
||||
enabled: true,
|
||||
model: model || XAI_DEFAULT_X_SEARCH_MODEL,
|
||||
});
|
||||
const next = structuredClone(ctx.config);
|
||||
setPluginXSearchConfigValue(next, "enabled", true);
|
||||
setPluginXSearchConfigValue(next, "model", model || XAI_DEFAULT_X_SEARCH_MODEL);
|
||||
return next;
|
||||
}
|
||||
|
||||
function runXaiWebSearch(params: {
|
||||
|
||||
@@ -10,11 +10,15 @@ describeLive("xai x_search live", () => {
|
||||
it("queries X through xAI Responses", async () => {
|
||||
const tool = createXSearchTool({
|
||||
config: {
|
||||
tools: {
|
||||
web: {
|
||||
x_search: {
|
||||
enabled: true,
|
||||
model: "grok-4-1-fast-non-reasoning",
|
||||
plugins: {
|
||||
entries: {
|
||||
xai: {
|
||||
config: {
|
||||
xSearch: {
|
||||
enabled: true,
|
||||
model: "grok-4-1-fast-non-reasoning",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -44,14 +44,18 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("xai x_search tool", () => {
|
||||
it("enables x_search when runtime metadata marks an xAI key active", () => {
|
||||
it("enables x_search when runtime config carries the shared xAI key", () => {
|
||||
const tool = createXSearchTool({
|
||||
config: {},
|
||||
runtimeConfig: {
|
||||
tools: {
|
||||
web: {
|
||||
x_search: {
|
||||
apiKey: "x-search-runtime-key", // pragma: allowlist secret
|
||||
plugins: {
|
||||
entries: {
|
||||
xai: {
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: "x-search-runtime-key", // pragma: allowlist secret
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -85,12 +89,18 @@ describe("xai x_search tool", () => {
|
||||
const mockFetch = installXSearchFetch();
|
||||
const tool = createXSearchTool({
|
||||
config: {
|
||||
tools: {
|
||||
web: {
|
||||
x_search: {
|
||||
apiKey: "xai-config-test", // pragma: allowlist secret
|
||||
model: "grok-4-1-fast-non-reasoning",
|
||||
maxTurns: 2,
|
||||
plugins: {
|
||||
entries: {
|
||||
xai: {
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: "xai-config-test", // pragma: allowlist secret
|
||||
},
|
||||
xSearch: {
|
||||
model: "grok-4-1-fast-non-reasoning",
|
||||
maxTurns: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -154,23 +164,31 @@ describe("xai x_search tool", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("prefers the active runtime config for SecretRef-backed x_search keys", async () => {
|
||||
it("prefers the active runtime config for shared xAI keys", async () => {
|
||||
const mockFetch = installXSearchFetch();
|
||||
const tool = createXSearchTool({
|
||||
config: {
|
||||
tools: {
|
||||
web: {
|
||||
x_search: {
|
||||
apiKey: { source: "env", provider: "default", id: "X_SEARCH_KEY_REF" },
|
||||
plugins: {
|
||||
entries: {
|
||||
xai: {
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: { source: "env", provider: "default", id: "X_SEARCH_KEY_REF" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
runtimeConfig: {
|
||||
tools: {
|
||||
web: {
|
||||
x_search: {
|
||||
apiKey: "x-search-runtime-key", // pragma: allowlist secret
|
||||
plugins: {
|
||||
entries: {
|
||||
xai: {
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: "x-search-runtime-key", // pragma: allowlist secret
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -217,10 +235,14 @@ describe("xai x_search tool", () => {
|
||||
const mockFetch = installXSearchFetch();
|
||||
const tool = createXSearchTool({
|
||||
config: {
|
||||
tools: {
|
||||
web: {
|
||||
x_search: {
|
||||
apiKey: "xai-config-test", // pragma: allowlist secret
|
||||
plugins: {
|
||||
entries: {
|
||||
xai: {
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: "xai-config-test", // pragma: allowlist secret
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -13,6 +13,10 @@ import {
|
||||
resolveTimeoutSeconds,
|
||||
writeCache,
|
||||
} from "openclaw/plugin-sdk/provider-web-search";
|
||||
import {
|
||||
resolveEffectiveXSearchConfig,
|
||||
resolveLegacyXSearchConfig,
|
||||
} from "./src/x-search-config.js";
|
||||
import {
|
||||
buildXaiXSearchPayload,
|
||||
requestXaiXSearch,
|
||||
@@ -22,12 +26,6 @@ import {
|
||||
type XaiXSearchOptions,
|
||||
} from "./src/x-search-shared.js";
|
||||
|
||||
type XSearchConfig = NonNullable<OpenClawConfig["tools"]>["web"] extends infer Web
|
||||
? Web extends { x_search?: infer XSearch }
|
||||
? XSearch
|
||||
: undefined
|
||||
: undefined;
|
||||
|
||||
class PluginToolInputError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
@@ -79,39 +77,32 @@ function resolveFallbackXaiApiKey(cfg?: OpenClawConfig): string | undefined {
|
||||
return readPluginXaiWebSearchApiKey(cfg) ?? readLegacyGrokApiKey(cfg);
|
||||
}
|
||||
|
||||
function resolveXSearchConfig(cfg?: OpenClawConfig): XSearchConfig {
|
||||
const xSearch = cfg?.tools?.web?.x_search;
|
||||
if (!xSearch || typeof xSearch !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
return xSearch as XSearchConfig;
|
||||
function readLegacyXSearchApiKey(cfg?: OpenClawConfig): string | undefined {
|
||||
const legacyConfig = resolveLegacyXSearchConfig(cfg);
|
||||
return readConfiguredSecretString(legacyConfig?.apiKey, "tools.web.x_search.apiKey");
|
||||
}
|
||||
|
||||
function resolveXSearchConfig(cfg?: OpenClawConfig): Record<string, unknown> | undefined {
|
||||
return resolveEffectiveXSearchConfig(cfg);
|
||||
}
|
||||
|
||||
function resolveXSearchEnabled(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
config?: XSearchConfig;
|
||||
config?: Record<string, unknown>;
|
||||
runtimeConfig?: OpenClawConfig;
|
||||
}): boolean {
|
||||
if (params.config?.enabled === false) {
|
||||
return false;
|
||||
}
|
||||
const runtimeXSearchConfig =
|
||||
params.runtimeConfig && params.runtimeConfig !== params.cfg
|
||||
? resolveXSearchConfig(params.runtimeConfig)
|
||||
: undefined;
|
||||
if (
|
||||
readConfiguredSecretString(runtimeXSearchConfig?.apiKey, "tools.web.x_search.apiKey") ||
|
||||
resolveFallbackXaiApiKey(params.runtimeConfig)
|
||||
resolveFallbackXaiApiKey(params.runtimeConfig) ||
|
||||
readLegacyXSearchApiKey(params.runtimeConfig)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const configuredApiKey = readConfiguredSecretString(
|
||||
params.config?.apiKey,
|
||||
"tools.web.x_search.apiKey",
|
||||
);
|
||||
return Boolean(
|
||||
configuredApiKey ||
|
||||
resolveFallbackXaiApiKey(params.cfg) ||
|
||||
readLegacyXSearchApiKey(params.cfg) ||
|
||||
readProviderEnvValue(["XAI_API_KEY"]),
|
||||
);
|
||||
}
|
||||
@@ -126,10 +117,10 @@ function resolveXSearchApiKey(params: {
|
||||
? resolveXSearchConfig(params.runtimeConfig)
|
||||
: undefined;
|
||||
return (
|
||||
readConfiguredSecretString(runtimeXSearchConfig?.apiKey, "tools.web.x_search.apiKey") ??
|
||||
readConfiguredSecretString(sourceXSearchConfig?.apiKey, "tools.web.x_search.apiKey") ??
|
||||
resolveFallbackXaiApiKey(params.runtimeConfig) ??
|
||||
resolveFallbackXaiApiKey(params.sourceConfig) ??
|
||||
readLegacyXSearchApiKey(params.runtimeConfig) ??
|
||||
readLegacyXSearchApiKey(params.sourceConfig) ??
|
||||
readProviderEnvValue(["XAI_API_KEY"])
|
||||
);
|
||||
}
|
||||
@@ -234,7 +225,7 @@ export function createXSearchTool(options?: {
|
||||
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 tools.web.x_search.apiKey or plugins.entries.xai.config.webSearch.apiKey.",
|
||||
"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",
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user