mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-07 07:42:13 +03:00
[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:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.")
|
||||
);
|
||||
|
||||
35
tests/unit/encrypted-reasoning-summary-7243.test.ts
Normal file
35
tests/unit/encrypted-reasoning-summary-7243.test.ts
Normal 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: [] }), "");
|
||||
});
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user