fix(xai): preserve all response text blocks and citations (#118477)

This commit is contained in:
Peter Steinberger
2026-08-02 22:34:34 -07:00
committed by GitHub
parent 59cd041c37
commit 8a5cfa4cac
3 changed files with 117 additions and 21 deletions
+54
View File
@@ -165,6 +165,60 @@ describe("xai code_execution tool", () => {
).toBe(true);
});
it("returns every response answer and citation from the code execution HTTP boundary", async () => {
const mockFetch = installCodeExecutionFetch({
output: [
{ type: "code_interpreter_call" },
{
type: "message",
content: [
{
type: "output_text",
text: "Mean: ",
annotations: [{ type: "url_citation", url: "https://example.com/input.csv" }],
},
{
type: "output_text",
text: "42",
annotations: [
{ type: "url_citation", url: "https://example.com/result.csv" },
{ type: "url_citation", url: "https://example.com/input.csv" },
],
},
],
},
{
type: "message",
content: [{ type: "output_text", text: ". Verified." }],
},
],
});
const tool = createCodeExecutionTool({
config: {
plugins: {
entries: {
xai: {
config: {
webSearch: { apiKey: "xai-plugin-key" }, // pragma: allowlist secret
},
},
},
},
},
});
const result = await tool?.execute?.("code-execution:multi-block", {
task: "Calculate and verify the mean.",
});
expect(firstFetchUrl(mockFetch)).toContain("api.x.ai/v1/responses");
expect(result?.details).toMatchObject({
content: "Mean: 42. Verified.",
citations: ["https://example.com/input.csv", "https://example.com/result.csv"],
usedCodeExecution: true,
});
});
it("reuses the xAI plugin web search key for code_execution requests", async () => {
const mockFetch = installCodeExecutionFetch();
const tool = createCodeExecutionTool({
@@ -67,6 +67,49 @@ describe("xai responses tool helpers", () => {
});
});
it("collects every response text block and deduplicates citations across output items", () => {
expect(
requireXaiResponseTextAndCitations(
{
output: [
{ type: "web_search_call" },
{
type: "message",
content: [
{
type: "output_text",
text: "First ",
annotations: [{ type: "url_citation", url: "https://example.com/a" }],
},
{
type: "output_text",
text: "second",
annotations: [
{ type: "url_citation", url: "https://example.com/b" },
{ type: "url_citation", url: "https://example.com/a" },
],
},
],
},
{
type: "output_text",
text: " and ",
annotations: [{ type: "url_citation", url: "https://example.com/c" }],
},
{
type: "message",
content: [{ type: "output_text", text: "third" }],
},
],
},
"xAI tool failed",
),
).toEqual({
content: "First second and third",
citations: ["https://example.com/a", "https://example.com/b", "https://example.com/c"],
});
});
it("ignores malformed output, content, and annotation entries", () => {
expect(
extractXaiWebSearchContent({
+20 -21
View File
@@ -1,14 +1,11 @@
// Xai plugin module implements responses tool shared behavior.
import {
isRecord,
normalizeOptionalString as trimString,
uniqueStrings,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import type { XaiWebSearchResponse } from "./web-search-response.types.js";
function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object";
}
function extractUrlCitations(annotations: unknown): string[] {
if (!Array.isArray(annotations)) {
return [];
@@ -51,32 +48,34 @@ export function extractXaiWebSearchContent(data: XaiWebSearchResponse): {
text: string | undefined;
annotationCitations: string[];
} {
const textParts: string[] = [];
const annotationCitations: string[] = [];
for (const output of data.output ?? []) {
if (!isRecord(output)) {
continue;
}
if (output.type === "message") {
const content = Array.isArray(output.content) ? output.content : [];
for (const block of content) {
if (!isRecord(block)) {
continue;
}
if (block.type === "output_text" && typeof block.text === "string" && block.text) {
const urls = extractUrlCitations(block.annotations);
return { text: block.text, annotationCitations: uniqueStrings(urls) };
}
const blocks =
output.type === "message" && Array.isArray(output.content)
? output.content
: output.type === "output_text"
? [output]
: [];
for (const block of blocks) {
if (!isRecord(block) || block.type !== "output_text" || typeof block.text !== "string") {
continue;
}
if (block.text) {
textParts.push(block.text);
annotationCitations.push(...extractUrlCitations(block.annotations));
}
}
if (output.type === "output_text" && typeof output.text === "string" && output.text) {
const urls = extractUrlCitations(output.annotations);
return { text: output.text, annotationCitations: uniqueStrings(urls) };
}
}
// Match the Responses SDK: adjacent output text blocks have no separator.
const text = textParts.join("");
return {
text: typeof data.output_text === "string" ? data.output_text : undefined,
annotationCitations: [],
text: text || (typeof data.output_text === "string" ? data.output_text : undefined),
annotationCitations: uniqueStrings(annotationCitations),
};
}