mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 18:24:35 +00:00
fix(web-search): handle xAI Responses API format in Grok provider
The xAI /v1/responses API returns content in a structured format with typed output blocks (type: 'message') containing typed content blocks (type: 'output_text') and url_citation annotations. The previous code only checked output[0].content[0].text without filtering by type, which could miss content in responses with multiple output entries. Changes: - Update GrokSearchResponse type to include annotations on content blocks - Filter output blocks by type='message' and content by type='output_text' - Extract url_citation annotations as fallback citations when top-level citations array is empty - Deduplicate annotation-derived citation URLs - Update tests for the new structured return type Closes #13520
This commit is contained in:
@@ -145,21 +145,66 @@ describe("web_search grok config resolution", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("web_search grok response parsing", () => {
|
describe("web_search grok response parsing", () => {
|
||||||
it("extracts content from Responses API output blocks", () => {
|
it("extracts content from Responses API message blocks", () => {
|
||||||
expect(
|
const result = extractGrokContent({
|
||||||
extractGrokContent({
|
output: [
|
||||||
output: [
|
{
|
||||||
{
|
type: "message",
|
||||||
content: [{ text: "hello from output" }],
|
content: [{ type: "output_text", text: "hello from output" }],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
});
|
||||||
).toBe("hello from output");
|
expect(result.text).toBe("hello from output");
|
||||||
|
expect(result.annotationCitations).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("extracts url_citation annotations from content blocks", () => {
|
||||||
|
const result = extractGrokContent({
|
||||||
|
output: [
|
||||||
|
{
|
||||||
|
type: "message",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "output_text",
|
||||||
|
text: "hello with citations",
|
||||||
|
annotations: [
|
||||||
|
{
|
||||||
|
type: "url_citation",
|
||||||
|
url: "https://example.com/a",
|
||||||
|
start_index: 0,
|
||||||
|
end_index: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "url_citation",
|
||||||
|
url: "https://example.com/b",
|
||||||
|
start_index: 6,
|
||||||
|
end_index: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "url_citation",
|
||||||
|
url: "https://example.com/a",
|
||||||
|
start_index: 11,
|
||||||
|
end_index: 15,
|
||||||
|
}, // duplicate
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(result.text).toBe("hello with citations");
|
||||||
|
expect(result.annotationCitations).toEqual(["https://example.com/a", "https://example.com/b"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("falls back to deprecated output_text", () => {
|
it("falls back to deprecated output_text", () => {
|
||||||
expect(extractGrokContent({ output_text: "hello from output_text" })).toBe(
|
const result = extractGrokContent({ output_text: "hello from output_text" });
|
||||||
"hello from output_text",
|
expect(result.text).toBe("hello from output_text");
|
||||||
);
|
expect(result.annotationCitations).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns undefined text when no content found", () => {
|
||||||
|
const result = extractGrokContent({});
|
||||||
|
expect(result.text).toBeUndefined();
|
||||||
|
expect(result.annotationCitations).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -109,6 +109,12 @@ type GrokSearchResponse = {
|
|||||||
content?: Array<{
|
content?: Array<{
|
||||||
type?: string;
|
type?: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
|
annotations?: Array<{
|
||||||
|
type?: string;
|
||||||
|
url?: string;
|
||||||
|
start_index?: number;
|
||||||
|
end_index?: number;
|
||||||
|
}>;
|
||||||
}>;
|
}>;
|
||||||
}>;
|
}>;
|
||||||
output_text?: string; // deprecated field - kept for backwards compatibility
|
output_text?: string; // deprecated field - kept for backwards compatibility
|
||||||
@@ -131,13 +137,28 @@ type PerplexitySearchResponse = {
|
|||||||
|
|
||||||
type PerplexityBaseUrlHint = "direct" | "openrouter";
|
type PerplexityBaseUrlHint = "direct" | "openrouter";
|
||||||
|
|
||||||
function extractGrokContent(data: GrokSearchResponse): string | undefined {
|
function extractGrokContent(data: GrokSearchResponse): {
|
||||||
// xAI Responses API format: output[0].content[0].text
|
text: string | undefined;
|
||||||
const fromResponses = data.output?.[0]?.content?.[0]?.text;
|
annotationCitations: string[];
|
||||||
if (typeof fromResponses === "string" && fromResponses) {
|
} {
|
||||||
return fromResponses;
|
// xAI Responses API format: find the message output with text content
|
||||||
|
for (const output of data.output ?? []) {
|
||||||
|
if (output.type !== "message") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const block of output.content ?? []) {
|
||||||
|
if (block.type === "output_text" && typeof block.text === "string" && block.text) {
|
||||||
|
// Extract url_citation annotations from this content block
|
||||||
|
const urls = (block.annotations ?? [])
|
||||||
|
.filter((a) => a.type === "url_citation" && typeof a.url === "string")
|
||||||
|
.map((a) => a.url as string);
|
||||||
|
return { text: block.text, annotationCitations: [...new Set(urls)] };
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return typeof data.output_text === "string" ? data.output_text : undefined;
|
// Fallback: deprecated output_text field
|
||||||
|
const text = typeof data.output_text === "string" ? data.output_text : undefined;
|
||||||
|
return { text, annotationCitations: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveSearchConfig(cfg?: OpenClawConfig): WebSearchConfig {
|
function resolveSearchConfig(cfg?: OpenClawConfig): WebSearchConfig {
|
||||||
@@ -494,8 +515,10 @@ async function runGrokSearch(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = (await res.json()) as GrokSearchResponse;
|
const data = (await res.json()) as GrokSearchResponse;
|
||||||
const content = extractGrokContent(data) ?? "No response";
|
const { text: extractedText, annotationCitations } = extractGrokContent(data);
|
||||||
const citations = data.citations ?? [];
|
const content = extractedText ?? "No response";
|
||||||
|
// Prefer top-level citations; fall back to annotation-derived ones
|
||||||
|
const citations = (data.citations ?? []).length > 0 ? data.citations! : annotationCitations;
|
||||||
const inlineCitations = data.inline_citations;
|
const inlineCitations = data.inline_citations;
|
||||||
|
|
||||||
return { content, citations, inlineCitations };
|
return { content, citations, inlineCitations };
|
||||||
|
|||||||
Reference in New Issue
Block a user