fix(xai): wrap malformed tool json

This commit is contained in:
Vincent Koc
2026-05-15 08:47:02 +08:00
parent acbe461c16
commit ffae8f32d8
7 changed files with 111 additions and 3 deletions
+1
View File
@@ -83,6 +83,7 @@ Docs: https://docs.openclaw.ai
- ComfyUI: report malformed workflow API JSON responses with owned errors instead of leaking raw parser failures.
- DeepInfra video: report malformed successful API JSON responses with provider-owned errors instead of leaking raw parser failures.
- Brave Search: report malformed web and LLM-context API JSON with provider-owned errors instead of leaking raw parser failures.
- xAI tools: report malformed web search, X search, and code execution JSON with provider-owned errors instead of leaking raw parser failures.
- Twilio voice-call: report malformed successful API JSON responses with provider-owned errors instead of leaking raw parser failures.
- Voice-call provider APIs: report malformed successful guarded JSON responses with provider-prefixed errors instead of leaking raw parser failures.
- Realtime transcription: report malformed provider websocket JSON frames with owned parser errors instead of leaking raw `SyntaxError` objects.
+31
View File
@@ -175,6 +175,37 @@ describe("xai code_execution tool", () => {
expect(firstAuthorizationHeader(mockFetch)).toBe("Bearer xai-plugin-key");
});
it("reports malformed code_execution JSON as a provider error", async () => {
const mockFetch = vi.fn((_input?: unknown, _init?: unknown) =>
Promise.resolve({
ok: true,
json: () => Promise.reject(new SyntaxError("Unexpected token")),
} as Response),
);
global.fetch = withFetchPreconnect(mockFetch);
const tool = createCodeExecutionTool({
config: {
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: "xai-plugin-key", // pragma: allowlist secret
},
},
},
},
},
},
});
await expect(
tool?.execute?.("code-execution:malformed-json", {
task: "Calculate the mean of [40, 42, 44]",
}),
).rejects.toThrow("xAI code execution failed: malformed JSON response");
});
it("reuses the legacy grok web search key for code_execution requests", async () => {
const mockFetch = installCodeExecutionFetch();
const tool = createCodeExecutionTool({
+5 -1
View File
@@ -1,3 +1,4 @@
import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
import { postTrustedWebToolsJson } from "openclaw/plugin-sdk/provider-web-search";
import {
buildXaiResponsesToolBody,
@@ -81,7 +82,10 @@ export async function requestXaiCodeExecution(params: {
errorLabel: "xAI",
},
async (response) => {
const data = (await response.json()) as XaiCodeExecutionResponse;
const data = await readProviderJsonResponse<XaiCodeExecutionResponse>(
response,
"xAI code execution failed",
);
const { content, citations } = resolveXaiResponseTextAndCitations(data);
const outputTypes = Array.isArray(data.output)
? [
+5 -1
View File
@@ -1,3 +1,4 @@
import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
import { postTrustedWebToolsJson, wrapWebContent } from "openclaw/plugin-sdk/provider-web-search";
import { normalizeXaiModelId } from "../model-id.js";
import {
@@ -109,7 +110,10 @@ export async function requestXaiWebSearch(params: {
errorLabel: "xAI",
},
async (response) => {
const data = (await response.json()) as XaiWebSearchResponse;
const data = await readProviderJsonResponse<XaiWebSearchResponse>(
response,
"xAI web search failed",
);
return resolveXaiResponseTextCitationsAndInline(data, params.inlineCitations);
},
).catch((error: unknown) => wrapXaiWebSearchError(error, params.timeoutSeconds));
+5 -1
View File
@@ -1,3 +1,4 @@
import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
import { postTrustedWebToolsJson, wrapWebContent } from "openclaw/plugin-sdk/provider-web-search";
import {
buildXaiResponsesToolBody,
@@ -131,7 +132,10 @@ export async function requestXaiXSearch(params: {
errorLabel: "xAI",
},
async (response) => {
const data = (await response.json()) as XaiWebSearchResponse;
const data = await readProviderJsonResponse<XaiWebSearchResponse>(
response,
"xAI X search failed",
);
return resolveXaiResponseTextCitationsAndInline(data, params.inlineCitations);
},
);
+30
View File
@@ -355,6 +355,36 @@ describe("xai web search config resolution", () => {
expect(firstFetchUrl(mockFetch)).toBe("https://api.x.ai/proxy/v1/responses");
});
it("reports malformed xAI web search JSON as a provider error", async () => {
const mockFetch = vi.fn((_input?: unknown, _init?: unknown) =>
Promise.resolve({
ok: true,
json: () => Promise.reject(new SyntaxError("Unexpected token")),
} as Response),
);
global.fetch = withFetchPreconnect(mockFetch);
const provider = createXaiWebSearchProvider();
const tool = provider.createTool({
config: {
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: "xai-test-key", // pragma: allowlist secret
},
},
},
},
},
},
});
await expect(tool.execute({ query: "OpenClaw" })).rejects.toThrow(
"xAI web search failed: malformed JSON response",
);
});
it("normalizes deprecated grok 4.20 beta model ids to GA ids", () => {
expect(
resolveXaiWebSearchModel({
+34
View File
@@ -290,6 +290,40 @@ describe("xai x_search tool", () => {
expect(firstAuthorizationHeader(mockFetch)).toBe("Bearer xai-plugin-key");
});
it("reports malformed x_search JSON as a provider error", async () => {
const mockFetch = vi.fn((_input?: unknown, _init?: unknown) =>
Promise.resolve({
ok: true,
json: () => Promise.reject(new SyntaxError("Unexpected token")),
} as Response),
);
global.fetch = withFetchPreconnect(mockFetch);
const tool = createXSearchTool({
config: {
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: "xai-plugin-key", // pragma: allowlist secret
},
xSearch: {
enabled: true,
},
},
},
},
},
},
});
await expect(
tool?.execute?.("x-search:malformed-json", {
query: "latest post from huntharo",
}),
).rejects.toThrow("xAI X search failed: malformed JSON response");
});
it("prefers the active runtime config for shared xAI keys", async () => {
const mockFetch = installXSearchFetch();
const tool = createXSearchTool({