[v3.8.50] fix(sse): stop fabricating encrypted Codex reasoning summary text (#8807)

Validated in local merge-train T6 (ungrouped batch 1)
This commit is contained in:
Prudhvi Vuda
2026-08-06 04:23:53 -04:00
committed by GitHub
parent 39220a1a5c
commit 7c0dba222c
6 changed files with 70 additions and 48 deletions

View File

@@ -1185,10 +1185,8 @@ function openaiResponsesToOpenAIResponseStream(chunk, state) {
!(state.reasoningItemsWithDelta instanceof Set && state.reasoningItemsWithDelta.size > 0);
if (emittedForItem || emittedWithoutItemId) return null;
// #7095/#7176 reconciliation: computed WITHOUT mutating `item`, so an
// encrypted-only reasoning item (and its `encrypted_content`) is never
// rewritten with a fabricated `summary` — the placeholder only feeds this
// synthetic client-facing delta chunk.
// #7176/#7243: only synthesize from real upstream plaintext — never mutate
// `item` and never fabricate placeholder text for encrypted-only reasoning.
const summaryText = getVisibleResponsesReasoningSummaryText(item);
if (!summaryText) return null;
return buildResponsesReasoningDeltaChunk(state, summaryText);

View File

@@ -177,27 +177,19 @@ export function extractResponsesReasoningSummaryText(item) {
.join("\n\n");
}
// #7095/#7176 — when Codex exposes a reasoning item only as encrypted private
// reasoning (no plaintext summary), chat clients would otherwise see nothing in
// their thinking panel. Reconciles two goals that used to be in tension:
// - #7095 wants a visible placeholder in the chat client.
// - #7176 wants the upstream response item left untouched, so `encrypted_content`
// (needed by Codex for subsequent requests) is never overwritten by a
// fabricated `summary`.
// This function computes the placeholder text WITHOUT mutating `item` — callers
// use the returned text for synthetic client-facing events only.
const ENCRYPTED_REASONING_PLACEHOLDER =
"Codex is reasoning, but the upstream Responses API exposed this reasoning block only as encrypted private reasoning. OmniRoute cannot recover the plaintext.";
// #7095/#7176/#7243 — when Codex exposes a reasoning item only as encrypted
// private reasoning (no plaintext summary), callers may synthesize client-facing
// reasoning summary events from this helper. Reconciles three goals:
// - #7176: never mutate the upstream item — `encrypted_content` (needed by
// Codex for subsequent requests) must not be overwritten with a fabricated
// `summary`.
// - #7095: real plaintext summaries from upstream are forwarded to chat
// clients that render a thinking panel.
// - #7243: when upstream provides no plaintext summary, do NOT fabricate an
// alarming error-like paragraph into `reasoning_summary_text.delta` — clients
// would display it as if it were real reasoning. Return empty so synthetic
// summary events are suppressed; the reasoning item (with `encrypted_content`)
// still arrives on `response.output_item.done`.
export function getVisibleResponsesReasoningSummaryText(item) {
const existingSummary = extractResponsesReasoningSummaryText(item);
if (existingSummary) return existingSummary;
const hasEncryptedReasoning =
item &&
item.type === "reasoning" &&
typeof item.encrypted_content === "string" &&
item.encrypted_content.length > 0;
return hasEncryptedReasoning ? ENCRYPTED_REASONING_PLACEHOLDER : "";
return extractResponsesReasoningSummaryText(item);
}

View File

@@ -1047,9 +1047,9 @@ export function createSSEStream(options: StreamOptions = {}) {
return;
}
// #7095/#7176 reconciliation: compute the visible placeholder WITHOUT
// mutating `item` — the encrypted reasoning item (and its `encrypted_content`,
// required by Codex for subsequent requests) is forwarded to the client intact.
// #7176/#7243: only synthesize summary events from real upstream plaintext —
// never mutate `item` and never fabricate alarming placeholder text for
// encrypted-only reasoning (`encrypted_content` still forwards intact).
const visibleSummary = getVisibleResponsesReasoningSummaryText(item);
if (!visibleSummary) {

View File

@@ -285,14 +285,16 @@ test("chat completions streams Codex Responses reasoning through real route HTTP
const chunks = parseSse(raw);
assert.equal(chunks.at(-1), "[DONE]");
const payloads = chunks.slice(0, -1).map((chunk) => JSON.parse(chunk));
// #7243: encrypted-only reasoning must not fabricate client-visible
// reasoning_content (old #7095/#7304 placeholder is gone).
const reasoningContentDeltas = payloads
.map((payload) => payload.choices?.[0]?.delta?.reasoning_content)
.filter((content): content is string => Boolean(content));
assert.equal(reasoningContentDeltas.length, 1);
assert.equal(reasoningContentDeltas.length, 0);
const reasoningContent = reasoningContentDeltas.join("");
assert.match(reasoningContent, /encrypted (?:state|private reasoning)/i);
assert.doesNotMatch(reasoningContent, /encrypted (?:state|private reasoning)/i);
assert.doesNotMatch(raw, /OmniRoute cannot recover|Codex is reasoning/i);
assert(!raw.includes(ENCRYPTED_CONTENT_SENTINEL), raw);
assert(!reasoningContent.includes(ENCRYPTED_CONTENT_SENTINEL), reasoningContent);
assert(
payloads.some((payload) => payload.choices?.[0]?.delta?.content === "The answer is 42.")
);

View File

@@ -0,0 +1,35 @@
import { test } from "node:test";
import assert from "node:assert/strict";
const { getVisibleResponsesReasoningSummaryText } = await import(
"../../open-sse/translator/response/openai-responses/pureHelpers.ts"
);
test("#7243 getVisibleResponsesReasoningSummaryText returns upstream plaintext summary", () => {
const item = {
type: "reasoning",
summary: [{ type: "summary_text", text: "Planning the next edit." }],
encrypted_content: "opaque-blob",
};
assert.equal(getVisibleResponsesReasoningSummaryText(item), "Planning the next edit.");
});
test("#7243 getVisibleResponsesReasoningSummaryText suppresses synthetic text for encrypted-only reasoning", () => {
const item = {
type: "reasoning",
encrypted_content: "opaque-blob",
};
assert.equal(getVisibleResponsesReasoningSummaryText(item), "");
assert.doesNotMatch(
getVisibleResponsesReasoningSummaryText(item),
/OmniRoute cannot recover|encrypted private reasoning/i
);
});
test("#7243 getVisibleResponsesReasoningSummaryText returns empty for non-reasoning or empty summary", () => {
assert.equal(getVisibleResponsesReasoningSummaryText(null), "");
assert.equal(getVisibleResponsesReasoningSummaryText({ type: "message" }), "");
assert.equal(getVisibleResponsesReasoningSummaryText({ type: "reasoning", summary: [] }), "");
});

View File

@@ -230,16 +230,11 @@ test("createPassthroughStreamWithLogger synthesizes reasoning summary events fro
assert.match(result, /event: response\.output_item\.done/);
});
// Reconciles #7095 (xz-dev — chat clients never saw ANY signal that Codex was
// reasoning when the upstream Responses API exposed only encrypted private
// reasoning) with #7176 (JxnLexn — mutating `item.summary` with a fabricated
// placeholder corrupted the response item forwarded downstream, discarding the
// `encrypted_content` shape Codex needs for follow-up requests). Both goals are
// satisfied simultaneously: the client-facing synthetic delta/part events still
// carry the placeholder text, but the forwarded `response.output_item.done`
// payload is untouched — `encrypted_content` survives and no `summary` is
// fabricated onto the wire item.
test("createPassthroughStreamWithLogger shows a placeholder for encrypted reasoning items without mutating the forwarded item", async () => {
// #7176/#7243 — encrypted-only reasoning must not mutate the forwarded item and
// must not fabricate alarming placeholder text into client-visible reasoning
// summary streams. The reasoning item (with `encrypted_content`) still reaches
// the client on `response.output_item.done` for continuation.
test("createPassthroughStreamWithLogger suppresses synthetic summary for encrypted-only reasoning without mutating the forwarded item", async () => {
const transform = createPassthroughStreamWithLogger(
"codex",
null,
@@ -275,9 +270,9 @@ test("createPassthroughStreamWithLogger shows a placeholder for encrypted reason
result += decoder.decode(value);
}
// #7095: chat clients still see the placeholder via the synthetic events.
assert.match(result, /event: response\.reasoning_summary_text\.delta/);
assert.match(result, /Codex is reasoning/);
// #7243: no fabricated reasoning summary deltas when upstream has no plaintext.
assert.doesNotMatch(result, /event: response\.reasoning_summary_text\.delta/);
assert.doesNotMatch(result, /Codex is reasoning/);
// #7176: the forwarded response.output_item.done payload is untouched —
// encrypted_content survives intact and no fabricated `summary` is present.
@@ -332,9 +327,9 @@ test("createPassthroughStreamWithLogger backfills completed output with encrypte
result += decoder.decode(value);
}
// The placeholder still reached the client (#7095).
assert.match(result, /event: response\.reasoning_summary_text\.delta/);
assert.match(result, /Codex is reasoning/);
// #7243: no fabricated reasoning summary deltas when upstream has no plaintext.
assert.doesNotMatch(result, /event: response\.reasoning_summary_text\.delta/);
assert.doesNotMatch(result, /Codex is reasoning/);
// The item re-serialized into the completed snapshot carries the original
// encrypted_content and never a fabricated summary (#7176).