fix(grok-cli): sanitize function_call_output before Grok Build dispatch (#7611) (#8030)

* fix(grok-cli): sanitize function_call_output before Grok Build dispatch (#7611)

Grok Build cli-chat-proxy rejects Responses bodies when tool-result
outputs contain incomplete \u escapes or other malformed JSON text.
Sanitize function_call_output.output values in GrokCliExecutor so
large agent tool transcripts no longer fail intermittently with 400
body-parse errors.

* fix(grok-cli): type test credentials instead of casting through any (#7611)

tests/ has no-explicit-any as an ESLint error; the 3 `{ accessToken: "tok" }
as any` casts passed to transformRequest() were the proven, non-drift cause
of this PR's own "No new ESLint warnings" CI failure. Replace them with a
single properly-typed ProviderCredentials literal (all fields on that type
are optional, so no cast is needed).

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
Ravi Tharuma
2026-07-22 12:06:15 +02:00
committed by GitHub
parent fb6ea295bf
commit 23aa75ddd5
3 changed files with 152 additions and 1 deletions

View File

@@ -0,0 +1 @@
- **grok-cli:** sanitize Responses `function_call_output.output` values before dispatch so incomplete `\u` escapes / malformed tool results no longer cause intermittent 400 body-parse failures on cli-chat-proxy (#7611).

View File

@@ -33,6 +33,67 @@ const GROK_BUILD_UNSUPPORTED_PARAMS = [
"reasoning_effort",
];
/**
* Grok Build's cli-chat-proxy is stricter about Responses `function_call_output.output`
* than OpenAI's Responses API. Agent tool results can contain truncated / incomplete
* JSON strings or invalid `\uXXXX` escapes, which fail upstream with:
* Failed to parse the request body as JSON: input[N].output: unexpected end of hex escape
*
* Normalize outputs to valid JSON text (or plain text) before dispatch (#7611).
*/
function sanitizeGrokBuildFunctionCallOutput(output: unknown): string {
if (output == null) return "";
if (typeof output === "string") {
const value = output;
try {
return JSON.stringify(JSON.parse(value));
} catch {
// fall through
}
// Drop incomplete \u escapes (0-3 hex digits) that break strict JSON parsers.
const repaired = value.replace(/\\u([0-9A-Fa-f]{0,3})(?![0-9A-Fa-f])/g, "");
try {
return JSON.stringify(JSON.parse(repaired));
} catch {
return repaired.replace(/[\uD800-\uDFFF]/g, "\uFFFD");
}
}
if (Array.isArray(output)) {
const textParts = output
.map((part) => {
if (part && typeof part === "object") {
const rec = part as Record<string, unknown>;
if (typeof rec.text === "string") return rec.text;
}
return typeof part === "string" ? part : JSON.stringify(part);
})
.join("\n");
return sanitizeGrokBuildFunctionCallOutput(textParts);
}
try {
return JSON.stringify(output);
} catch {
return String(output);
}
}
function sanitizeGrokBuildResponsesBody(body: Record<string, unknown>): Record<string, unknown> {
const input = body.input;
if (!Array.isArray(input)) return body;
let changed = false;
const nextInput = input.map((item) => {
if (!item || typeof item !== "object") return item;
const rec = item as Record<string, unknown>;
if (rec.type !== "function_call_output") return item;
const sanitized = sanitizeGrokBuildFunctionCallOutput(rec.output);
if (sanitized === rec.output) return item;
changed = true;
return { ...rec, output: sanitized };
});
return changed ? { ...body, input: nextInput } : body;
}
type GrokBuildRefreshResult = Partial<ProviderCredentials> | null | undefined;
function nonEmptyString(value: unknown): string | null {
@@ -247,6 +308,7 @@ export class GrokCliExecutor extends BaseExecutor {
transformed.tools = transformed.tools.slice(0, GROK_BUILD_MAX_TOOLS);
}
return transformed;
// Repair tool-result payloads that would fail Grok's strict JSON body parser (#7611).
return sanitizeGrokBuildResponsesBody(transformed);
}
}

View File

@@ -0,0 +1,88 @@
import test from "node:test";
import assert from "node:assert/strict";
import { GrokCliExecutor } from "../../open-sse/executors/grok-cli.ts";
import type { ProviderCredentials } from "../../open-sse/executors/base.ts";
const testCredentials: ProviderCredentials = { accessToken: "tok" };
test("#7611: GrokCliExecutor sanitizes incomplete hex escapes in function_call_output", () => {
const executor = new GrokCliExecutor();
const body = {
model: "grok-4.5",
input: [
{
type: "function_call",
call_id: "call_1",
name: "read_file",
arguments: "{}",
},
{
type: "function_call_output",
call_id: "call_1",
// incomplete \u escape — the production failure mode
output: '{"path":"foo","snippet":"bad \\u12"}',
},
],
};
const transformed = executor.transformRequest("grok-4.5", body, true, testCredentials) as Record<
string,
unknown
>;
const input = transformed.input as Array<Record<string, unknown>>;
const outputItem = input.find((item) => item.type === "function_call_output");
assert.ok(outputItem, "function_call_output item should remain");
assert.equal(typeof outputItem.output, "string");
// Must be re-parseable as a JSON string payload after sanitization.
assert.doesNotThrow(() => JSON.parse(JSON.stringify({ output: outputItem.output })));
assert.doesNotMatch(String(outputItem.output), /\\u12(?![0-9A-Fa-f])/);
});
test("#7611: valid JSON tool outputs are re-serialized cleanly", () => {
const executor = new GrokCliExecutor();
const payload = { ok: true, data: ["a", "b"], note: "café" };
const body = {
model: "grok-4.5",
input: [
{
type: "function_call_output",
call_id: "call_2",
output: JSON.stringify(payload),
},
],
};
const transformed = executor.transformRequest("grok-4.5", body, false, testCredentials) as Record<
string,
unknown
>;
const input = transformed.input as Array<Record<string, unknown>>;
const outputItem = input.find((item) => item.type === "function_call_output");
assert.deepEqual(JSON.parse(String(outputItem?.output)), payload);
});
test("#7611: array content parts are flattened to text", () => {
const executor = new GrokCliExecutor();
const body = {
model: "grok-4.5",
input: [
{
type: "function_call_output",
call_id: "call_3",
output: [
{ type: "input_text", text: "hello" },
{ type: "input_text", text: "world" },
],
},
],
};
const transformed = executor.transformRequest("grok-4.5", body, false, testCredentials) as Record<
string,
unknown
>;
const input = transformed.input as Array<Record<string, unknown>>;
const outputItem = input.find((item) => item.type === "function_call_output");
assert.equal(typeof outputItem?.output, "string");
assert.match(String(outputItem?.output), /hello/);
assert.match(String(outputItem?.output), /world/);
});