fix(sse): stop Codex/Responses sanitizer turning system image_url into output_text (#8089) (#8147)

This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-22 11:28:16 -03:00
committed by GitHub
parent e6087c62b2
commit 9d0bdb871d
3 changed files with 95 additions and 1 deletions

View File

@@ -0,0 +1 @@
- fix(sse): stop Codex/Responses sanitizer from turning system/developer image_url parts into invalid output_text (#8089)

View File

@@ -67,7 +67,11 @@ function sanitizeContentPart(part: unknown, role: string): unknown {
if (record.type === "image_url") {
const url = imageUrlToText(record.image_url);
if (role === "user") {
// `output_text` is only a legal content-part type on assistant-role OUTPUT
// items. Every other role (user, system, developer, ...) is input-side and
// must use `input_image` -- otherwise the Codex/Responses backend rejects
// the replayed history with "Invalid value: 'output_text'" (#8089).
if (role !== "assistant") {
const next: JsonRecord = { type: "input_image", image_url: url };
const image = toRecord(record.image_url);
if (image?.detail !== undefined) next.detail = image.detail;

View File

@@ -0,0 +1,89 @@
import test from "node:test";
import assert from "node:assert/strict";
import { sanitizeResponsesInputItems } from "../../open-sse/services/responsesInputSanitizer.ts";
import { CodexExecutor } from "../../open-sse/executors/codex.ts";
test("[repro #8089] mid-conversation system-role message with image_url must NOT become output_text", () => {
const input = [
{
type: "message",
role: "system",
content: [
{ type: "text", text: "Reminder: verify the diff before committing." },
{ type: "image_url", image_url: { url: "https://example.com/diff-screenshot.png" } },
],
},
];
const sanitized = sanitizeResponsesInputItems(input, true, {}) as Array<{
content: Array<{ type: string }>;
}>;
const imagePart = sanitized[0].content.find(
(p) => p.type === "output_text" || p.type === "input_image"
);
assert.ok(imagePart);
assert.equal(imagePart?.type, "input_image");
});
test("[repro #8089] developer-role message with image_url must NOT become output_text", () => {
const input = [
{
type: "message",
role: "developer",
content: [{ type: "image_url", image_url: { url: "https://example.com/diff-screenshot.png" } }],
},
];
const sanitized = sanitizeResponsesInputItems(input, true, {}) as Array<{
content: Array<{ type: string }>;
}>;
assert.equal(sanitized[0].content[0].type, "input_image");
});
test("assistant-role message with image_url still becomes output_text (unchanged behavior)", () => {
const input = [
{
type: "message",
role: "assistant",
content: [{ type: "image_url", image_url: { url: "https://example.com/diff-screenshot.png" } }],
},
];
const sanitized = sanitizeResponsesInputItems(input, true, {}) as Array<{
content: Array<{ type: string }>;
}>;
assert.equal(sanitized[0].content[0].type, "output_text");
});
test("Codex native passthrough: mid-conversation system message with image content never serializes output_text (#8089)", () => {
const executor = new CodexExecutor();
const body = {
_nativeCodexPassthrough: true,
input: [
{
type: "message",
role: "system",
content: [
{ type: "text", text: "Reminder: verify the diff before committing." },
{ type: "image_url", image_url: { url: "https://example.com/diff-screenshot.png" } },
],
},
{
type: "message",
role: "user",
content: [{ type: "input_text", text: "What do you see?" }],
},
],
stream: false,
};
const result = executor.transformRequest("gpt-5.6", body, false, {
requestEndpointPath: "/responses",
});
assert.equal(JSON.stringify(result.input).includes('"type":"output_text"'), false);
});