fix: Grok web_search extracts output_text blocks at top level (openclaw#20508) thanks @echoVic

Verified:
- pnpm build
- pnpm check
- pnpm test:macmini

Co-authored-by: echoVic <16428813+echoVic@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
青雲
2026-02-20 10:37:15 +08:00
committed by GitHub
parent d9e46028f5
commit 21448508a1
3 changed files with 55 additions and 10 deletions

View File

@@ -22,6 +22,7 @@ Docs: https://docs.openclaw.ai
- CLI/Config: add canonical `--strict-json` parsing for `config set` and keep `--json` as a legacy alias to reduce help/behavior drift. (#21332) thanks @adhitShet.
- CLI: keep `openclaw -v` as a root-only version alias so subcommand `-v, --verbose` flags (for example ACP/hooks/skills) are no longer intercepted globally. (#21303) thanks @adhitShet.
- Config/Memory: restore schema help/label metadata for hybrid `mmr` and `temporalDecay` settings so configuration surfaces show correct names and guidance. (#18786) Thanks @rodrigouroz.
- Tools/web_search: handle xAI Responses API payloads that emit top-level `output_text` blocks (without a `message` wrapper) so Grok web_search no longer returns `No response` for those results. (#20508) Thanks @echoVic.
- Discord/Gateway: handle close code 4014 (missing privileged gateway intents) without crashing the gateway. Thanks @thewilloftheshadow.
- Security/Net: strip sensitive headers (`Authorization`, `Proxy-Authorization`, `Cookie`, `Cookie2`) on cross-origin redirects in `fetchWithSsrFGuard` to prevent credential forwarding across origin boundaries. (#20313) Thanks @afurm.

View File

@@ -219,4 +219,26 @@ describe("web_search grok response parsing", () => {
expect(result.text).toBeUndefined();
expect(result.annotationCitations).toEqual([]);
});
it("extracts output_text blocks directly in output array (no message wrapper)", () => {
const result = extractGrokContent({
output: [
{ type: "web_search_call" },
{
type: "output_text",
text: "direct output text",
annotations: [
{
type: "url_citation",
url: "https://example.com/direct",
start_index: 0,
end_index: 5,
},
],
},
],
} as Parameters<typeof extractGrokContent>[0]);
expect(result.text).toBe("direct output text");
expect(result.annotationCitations).toEqual(["https://example.com/direct"]);
});
});

View File

@@ -106,6 +106,7 @@ type GrokSearchResponse = {
output?: Array<{
type?: string;
role?: string;
text?: string; // present when type === "output_text" (top-level output_text block)
content?: Array<{
type?: string;
text?: string;
@@ -116,6 +117,12 @@ type GrokSearchResponse = {
end_index?: number;
}>;
}>;
annotations?: Array<{
type?: string;
url?: string;
start_index?: number;
end_index?: number;
}>;
}>;
output_text?: string; // deprecated field - kept for backwards compatibility
citations?: string[];
@@ -143,18 +150,33 @@ function extractGrokContent(data: GrokSearchResponse): {
} {
// 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)] };
if (output.type === "message") {
for (const block of output.content ?? []) {
if (block.type === "output_text" && typeof block.text === "string" && block.text) {
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)] };
}
}
}
// Some xAI responses place output_text blocks directly in the output array
// without a message wrapper.
if (
output.type === "output_text" &&
"text" in output &&
typeof output.text === "string" &&
output.text
) {
const rawAnnotations =
"annotations" in output && Array.isArray(output.annotations) ? output.annotations : [];
const urls = rawAnnotations
.filter(
(a: Record<string, unknown>) => a.type === "url_citation" && typeof a.url === "string",
)
.map((a: Record<string, unknown>) => a.url as string);
return { text: output.text, annotationCitations: [...new Set(urls)] };
}
}
// Fallback: deprecated output_text field
const text = typeof data.output_text === "string" ? data.output_text : undefined;