diff --git a/config/assertion-safety-baseline.txt b/config/assertion-safety-baseline.txt index 0da1d9d579ba..5419ee10614e 100644 --- a/config/assertion-safety-baseline.txt +++ b/config/assertion-safety-baseline.txt @@ -1457,12 +1457,11 @@ extensions/xai/realtime-voice-config.ts 2 extensions/xai/runtime-model-compat.ts 1 extensions/xai/speech-provider-metadata.ts 3 extensions/xai/src/code-execution-config.ts 10 -extensions/xai/src/code-execution-shared.ts 1 extensions/xai/src/tool-auth-shared.ts 2 extensions/xai/src/web-search-provider.runtime.ts 2 -extensions/xai/src/web-search-shared.ts 2 +extensions/xai/src/web-search-shared.ts 1 extensions/xai/src/x-search-config.ts 4 -extensions/xai/src/x-search-shared.ts 2 +extensions/xai/src/x-search-shared.ts 1 extensions/xai/stream.ts 5 extensions/xai/tts.ts 1 extensions/xai/x-search.ts 7 diff --git a/extensions/xai/code-execution.test.ts b/extensions/xai/code-execution.test.ts index fd2004736abc..875c6ba502fd 100644 --- a/extensions/xai/code-execution.test.ts +++ b/extensions/xai/code-execution.test.ts @@ -219,7 +219,7 @@ describe("xai code_execution tool", () => { }); }); - it("reuses the xAI plugin web search key for code_execution requests", async () => { + it("reuses the xAI plugin web search key without overriding custom model reasoning", async () => { const mockFetch = installCodeExecutionFetch(); const tool = createCodeExecutionTool({ config: { @@ -230,6 +230,7 @@ describe("xai code_execution tool", () => { webSearch: { apiKey: "xai-plugin-key", // pragma: allowlist secret }, + codeExecution: { model: "grok-build-0.1" }, }, }, }, @@ -242,6 +243,14 @@ describe("xai code_execution tool", () => { }); expect(firstAuthorizationHeader(mockFetch)).toBe("Bearer xai-plugin-key"); + const body = parseFirstRequestBody(mockFetch); + expect(body.model).toBe("grok-build-0.1"); + expect(body.input).toEqual([ + { role: "user", content: "Compute the standard deviation of [1, 2, 3]" }, + ]); + expect(body.store).toBe(false); + expect(body).not.toHaveProperty("reasoning"); + expect(body).not.toHaveProperty("max_turns"); }); it("reports malformed code_execution JSON as a provider error", async () => { diff --git a/extensions/xai/src/code-execution-shared.ts b/extensions/xai/src/code-execution-shared.ts index f715b5a75afe..2948473ac678 100644 --- a/extensions/xai/src/code-execution-shared.ts +++ b/extensions/xai/src/code-execution-shared.ts @@ -1,9 +1,7 @@ // Xai plugin module implements code execution shared behavior. -import { readProviderJsonObjectResponse } from "openclaw/plugin-sdk/provider-http"; -import { postTrustedWebToolsJson } from "openclaw/plugin-sdk/provider-web-search"; import { XAI_DEFAULT_MODEL_ID } from "../model-definitions.js"; import { - buildXaiResponsesToolBody, + requestXaiResponsesTool, requireXaiResponseTextAndCitations, XAI_RESPONSES_ENDPOINT, } from "./responses-tool-shared.js"; @@ -11,17 +9,10 @@ import { resolveNormalizedXaiToolModel, resolvePositiveIntegerToolConfig, } from "./tool-config-shared.js"; -import type { XaiWebSearchResponse } from "./web-search-shared.js"; const XAI_CODE_EXECUTION_ENDPOINT = XAI_RESPONSES_ENDPOINT; const XAI_DEFAULT_CODE_EXECUTION_MODEL = XAI_DEFAULT_MODEL_ID; -type XaiCodeExecutionResponse = XaiWebSearchResponse & { - output?: Array<{ - type?: string; - }>; -}; - type XaiCodeExecutionResult = { content: string; citations: string[]; @@ -70,25 +61,16 @@ export async function requestXaiCodeExecution(params: { maxTurns?: number; task: string; }): Promise { - return await postTrustedWebToolsJson( + return await requestXaiResponsesTool( { - url: XAI_CODE_EXECUTION_ENDPOINT, - timeoutSeconds: params.timeoutSeconds, - apiKey: params.apiKey, - body: buildXaiResponsesToolBody({ - model: params.model, - inputText: params.task, - tools: [{ type: "code_interpreter" }], - maxTurns: params.maxTurns, - reasoningEffort: params.model === XAI_DEFAULT_CODE_EXECUTION_MODEL ? "low" : undefined, - }), - errorLabel: "xAI", + ...params, + endpoint: XAI_CODE_EXECUTION_ENDPOINT, + inputText: params.task, + tools: [{ type: "code_interpreter" }], + reasoningEffort: params.model === XAI_DEFAULT_CODE_EXECUTION_MODEL ? "low" : undefined, + errorLabel: "xAI code execution failed", }, - async (response) => { - const data = (await readProviderJsonObjectResponse( - response, - "xAI code execution failed", - )) as XaiCodeExecutionResponse; + (data) => { const { content, citations } = requireXaiResponseTextAndCitations( data, "xAI code execution failed", diff --git a/extensions/xai/src/responses-tool-shared.test.ts b/extensions/xai/src/responses-tool-shared.test.ts index 1c629c90f688..b12d0da264b0 100644 --- a/extensions/xai/src/responses-tool-shared.test.ts +++ b/extensions/xai/src/responses-tool-shared.test.ts @@ -1,47 +1,12 @@ // Xai tests cover responses tool shared plugin behavior. import { describe, expect, it } from "vitest"; import { - buildXaiResponsesToolBody, extractXaiWebSearchContent, requireXaiResponseTextAndCitations, requireXaiResponseTextCitationsAndInline, } from "./responses-tool-shared.js"; describe("xai responses tool helpers", () => { - it("builds the shared xAI Responses tool body", () => { - expect( - buildXaiResponsesToolBody({ - model: "grok-4.3", - inputText: "search for openclaw", - tools: [{ type: "x_search" }], - maxTurns: 2, - reasoningEffort: "none", - }), - ).toEqual({ - model: "grok-4.3", - input: [{ role: "user", content: "search for openclaw" }], - tools: [{ type: "x_search" }], - store: false, - reasoning: { effort: "none" }, - max_turns: 2, - }); - }); - - it("keeps custom model reasoning untouched while disabling response storage", () => { - expect( - buildXaiResponsesToolBody({ - model: "grok-build-0.1", - inputText: "run code", - tools: [{ type: "code_interpreter" }], - }), - ).toEqual({ - model: "grok-build-0.1", - input: [{ role: "user", content: "run code" }], - tools: [{ type: "code_interpreter" }], - store: false, - }); - }); - it("falls back to annotation citations when the API omits top-level citations", () => { expect( requireXaiResponseTextAndCitations( diff --git a/extensions/xai/src/responses-tool-shared.ts b/extensions/xai/src/responses-tool-shared.ts index cb6281e39d16..6eda8187d97f 100644 --- a/extensions/xai/src/responses-tool-shared.ts +++ b/extensions/xai/src/responses-tool-shared.ts @@ -1,4 +1,6 @@ // Xai plugin module implements responses tool shared behavior. +import { readProviderJsonObjectResponse } from "openclaw/plugin-sdk/provider-http"; +import { postTrustedWebToolsJson } from "openclaw/plugin-sdk/provider-web-search"; import { truncateSanitizedExternalContent } from "openclaw/plugin-sdk/security-runtime"; import { isRecord, @@ -55,7 +57,7 @@ export function resolveXaiResponsesEndpoint(baseUrl?: unknown): string { return `${(trimString(baseUrl) ?? XAI_RESPONSES_BASE_URL).replace(/\/+$/, "")}/responses`; } -export function buildXaiResponsesToolBody(params: { +function buildXaiResponsesToolBody(params: { model: string; inputText: string; tools: Array>; @@ -72,6 +74,30 @@ export function buildXaiResponsesToolBody(params: { }; } +export async function requestXaiResponsesTool( + params: Parameters[0] & { + apiKey: string; + endpoint: string; + timeoutSeconds: number; + errorLabel: string; + signal?: AbortSignal; + }, + parseResponse: (data: XaiWebSearchResponse) => T, +): Promise { + return await postTrustedWebToolsJson( + { + url: params.endpoint, + timeoutSeconds: params.timeoutSeconds, + apiKey: params.apiKey, + ...(params.signal ? { signal: params.signal } : {}), + body: buildXaiResponsesToolBody(params), + errorLabel: "xAI", + }, + async (response) => + parseResponse(await readProviderJsonObjectResponse(response, params.errorLabel)), + ); +} + export function extractXaiWebSearchContent( data: XaiWebSearchResponse, maxContentChars?: number, diff --git a/extensions/xai/src/web-search-shared.ts b/extensions/xai/src/web-search-shared.ts index 314ccbaf6e77..3b05a306fd60 100644 --- a/extensions/xai/src/web-search-shared.ts +++ b/extensions/xai/src/web-search-shared.ts @@ -1,11 +1,10 @@ // Xai plugin module implements web search shared behavior. -import { readProviderJsonObjectResponse } from "openclaw/plugin-sdk/provider-http"; -import { postTrustedWebToolsJson, wrapWebContent } from "openclaw/plugin-sdk/provider-web-search"; +import { wrapWebContent } from "openclaw/plugin-sdk/provider-web-search"; import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime"; import { XAI_DEFAULT_MODEL_ID } from "../model-definitions.js"; import { normalizeXaiModelId } from "../model-id.js"; import { - buildXaiResponsesToolBody, + requestXaiResponsesTool, requireXaiResponseTextCitationsAndInline, resolveXaiResponsesEndpoint, } from "./responses-tool-shared.js"; @@ -108,32 +107,21 @@ export async function requestXaiWebSearch(params: { signal?: AbortSignal; }): Promise { params.signal?.throwIfAborted(); - return await postTrustedWebToolsJson( + return await requestXaiResponsesTool( { - url: params.endpoint, - timeoutSeconds: params.timeoutSeconds, - apiKey: params.apiKey, - ...(params.signal ? { signal: params.signal } : {}), - body: buildXaiResponsesToolBody({ - model: params.model, - inputText: params.query, - tools: [{ type: "web_search" }], - reasoningEffort: params.model === XAI_DEFAULT_WEB_SEARCH_MODEL ? "low" : undefined, - }), - errorLabel: "xAI", + ...params, + inputText: params.query, + tools: [{ type: "web_search" }], + reasoningEffort: params.model === XAI_DEFAULT_WEB_SEARCH_MODEL ? "low" : undefined, + errorLabel: "xAI web search failed", }, - async (response) => { - const data = (await readProviderJsonObjectResponse( - response, - "xAI web search failed", - )) as XaiWebSearchResponse; - return requireXaiResponseTextCitationsAndInline( + (data) => + requireXaiResponseTextCitationsAndInline( data, "xAI web search failed", params.inlineCitations, XAI_WEB_SEARCH_MAX_CONTENT_CHARS, - ); - }, + ), ).catch((error: unknown) => { if (params.signal?.aborted && error === params.signal.reason) { throw error; diff --git a/extensions/xai/src/x-search-shared.ts b/extensions/xai/src/x-search-shared.ts index 4822ad8bd791..6dee3c731069 100644 --- a/extensions/xai/src/x-search-shared.ts +++ b/extensions/xai/src/x-search-shared.ts @@ -1,9 +1,8 @@ // Xai plugin module implements x search shared behavior. -import { readProviderJsonObjectResponse } from "openclaw/plugin-sdk/provider-http"; -import { postTrustedWebToolsJson, wrapWebContent } from "openclaw/plugin-sdk/provider-web-search"; +import { wrapWebContent } from "openclaw/plugin-sdk/provider-web-search"; import { XAI_DEFAULT_MODEL_ID } from "../model-definitions.js"; import { - buildXaiResponsesToolBody, + requestXaiResponsesTool, requireXaiResponseTextCitationsAndInline, resolveXaiResponsesEndpoint, } from "./responses-tool-shared.js"; @@ -126,32 +125,20 @@ export async function requestXaiXSearch(params: { signal?: AbortSignal; }): Promise { params.signal?.throwIfAborted(); - return await postTrustedWebToolsJson( + return await requestXaiResponsesTool( { - url: params.endpoint, - timeoutSeconds: params.timeoutSeconds, - apiKey: params.apiKey, - ...(params.signal ? { signal: params.signal } : {}), - body: buildXaiResponsesToolBody({ - model: params.model, - inputText: params.options.query, - tools: [buildXSearchTool(params.options)], - maxTurns: params.maxTurns, - reasoningEffort: params.model === XAI_DEFAULT_X_SEARCH_MODEL ? "none" : undefined, - }), - errorLabel: "xAI", + ...params, + inputText: params.options.query, + tools: [buildXSearchTool(params.options)], + reasoningEffort: params.model === XAI_DEFAULT_X_SEARCH_MODEL ? "none" : undefined, + errorLabel: "xAI X search failed", }, - async (response) => { - const data = (await readProviderJsonObjectResponse( - response, - "xAI X search failed", - )) as XaiWebSearchResponse; - return requireXaiResponseTextCitationsAndInline( + (data) => + requireXaiResponseTextCitationsAndInline( data, "xAI X search failed", params.inlineCitations, XAI_X_SEARCH_MAX_CONTENT_CHARS, - ); - }, + ), ); } diff --git a/extensions/xai/x-search.test.ts b/extensions/xai/x-search.test.ts index adea96d5a51f..4d443c313ebb 100644 --- a/extensions/xai/x-search.test.ts +++ b/extensions/xai/x-search.test.ts @@ -315,6 +315,7 @@ describe("xai x_search tool", () => { expect(firstFetchUrl(mockFetch)).toContain("api.x.ai/v1/responses"); const body = parseFirstRequestBody(mockFetch); expect(body.model).toBe("grok-4.3"); + expect(body.input).toEqual([{ role: "user", content: "dinner recipes" }]); expect(body.store).toBe(false); expect(body.reasoning).toEqual({ effort: "none" }); expect(body.max_turns).toBe(2);