fix(sse): normalize assistant input_text to output_text in Codex Responses input (#6932)

codex-cli replays assistant history with content parts typed as
`input_text`, but the Responses API only accepts `output_text`
(or `refusal`) on assistant turns — `input_text` is user-only.
`normalizeCodexMessageContentPart` previously only rewrote parts
literally typed `text`, leaving explicit `input_text` on assistant
turns untouched, which the Codex/OpenAI backend rejects with a 400.

Rewrite explicit `input_text` (and `text`) to `output_text` on
assistant-role parts, dropping the assistant-only `annotations`,
`logprobs`, and `obfuscation` fields. Mode-agnostic, applies to
all Codex models.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
Saren
2026-07-12 06:47:51 -07:00
committed by GitHub
parent a61a4dc673
commit d2d4cf0e17
3 changed files with 64 additions and 0 deletions

View File

@@ -0,0 +1 @@
- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932)

View File

@@ -10,6 +10,15 @@ function normalizeCodexMessageContentPart(part: unknown, role: string): unknown
const record = { ...(part as JsonRecord) };
if (record.type === "text") record.type = textPartTypeForRole(role);
// Assistant history in the Responses API must use `output_text` (or `refusal`),
// never `input_text` (which is user-only). codex-cli sends assistant turns as
// `input_text`; normalize them so the Codex/OpenAI backend accepts the replay.
if (role === "assistant" && (record.type === "input_text" || record.type === "text")) {
record.type = "output_text";
delete record.annotations;
delete record.logprobs;
delete record.obfuscation;
}
return record;
}

View File

@@ -126,6 +126,60 @@ test("Codex Responses input: null input normalizes to an empty list (not [null])
assert.deepEqual(body.input, []);
});
test("Codex Responses input: assistant history normalized to output_text (OpenAI/Codex rejects input_text on assistant turns)", () => {
const body: Record<string, unknown> = {
input: [
{
type: "message",
role: "assistant",
content: [
{
type: "input_text",
text: "Previous assistant answer",
annotations: [{ type: "url_citation", url: "https://example.com" }],
logprobs: [{ token: "Previous" }],
obfuscation: "opaque",
},
{ type: "scoped_content", scope: "internal", content: "Preserve me" },
],
},
{
type: "message",
role: "user",
content: [
{ type: "input_image", image_url: "https://example.com/image.png", detail: "high" },
{ type: "input_file", file_id: "file_123" },
],
},
{ type: "function_call", call_id: "call_123", name: "lookup", arguments: "{}" },
{ type: "function_call_output", call_id: "call_123", output: "done" },
],
};
normalizeCodexResponsesInput(body);
assert.deepEqual(body.input, [
{
type: "message",
role: "assistant",
content: [
{ type: "output_text", text: "Previous assistant answer" },
{ type: "scoped_content", scope: "internal", content: "Preserve me" },
],
},
{
type: "message",
role: "user",
content: [
{ type: "input_image", image_url: "https://example.com/image.png", detail: "high" },
{ type: "input_file", file_id: "file_123" },
],
},
{ type: "function_call", call_id: "call_123", name: "lookup", arguments: "{}" },
{ type: "function_call_output", call_id: "call_123", output: "done" },
]);
});
test("Responses→Chat: null input normalizes to an empty list (not [null])", () => {
assert.deepEqual(normalizeResponsesInputForChat(null), []);
});