Files
OmniRoute/open-sse/utils/passthroughTailProcessor.ts
Diego Rodrigues de Sa e Souza 07e1011d3b fix(stream): reconcile encrypted Codex reasoning visibility without mutating upstream item (#7304)
* fix(stream): reconcile encrypted Codex reasoning visibility without mutating upstream item

Resolves the collision between two open PRs on ensureVisibleResponsesReasoningSummary:
#7095 (xz-dev) found that chat clients see nothing when Codex exposes reasoning
only as encrypted_content, and added a visible placeholder — but did so by
mutating item.summary in place. #7176 (JxnLexn) found that same mutation
corrupts the forwarded response item, discarding the encrypted_content shape
Codex needs for follow-up requests, and removed the mutation — but that also
silently dropped the placeholder, so chat clients went back to seeing nothing.

The mutation existed only so a later line could read the summary text back off
the same item. getVisibleResponsesReasoningSummaryText() computes that text
without touching the item, so:
  - synthetic response.reasoning_summary_text.delta / .part.done events still
    carry the placeholder for chat clients (#7095's goal), and
  - the forwarded response.output_item.done payload keeps its original
    encrypted_content intact with no fabricated summary field (#7176's goal).

Applied at both call sites #7095 identified: the native Responses passthrough
in stream.ts/passthroughTailProcessor.ts, and the Responses-to-Chat-Completions
translator in openai-responses.ts.

Closes #7095, closes #7176.

Co-authored-by: Xiangzhe <xiangzhedev@gmail.com>
Co-authored-by: Jan Leon <Jan.gaschler@gmail.com>

* test(stream): guard the encrypted-reasoning mutation via the completed backfill path

The output_item.done line is echoed verbatim on the wire, so a re-introduced
item.summary mutation does NOT surface in that event — verified by re-injecting
the mutation, which left the existing assertion green. The mutation does surface
in the response.completed snapshot, where the captured reasoning item is
re-serialized when upstream sends an empty output (store: false).

Adds that case, which fails as expected when the mutation is re-introduced,
making the #7176 half of the reconciliation an enforced regression guard rather
than an incidental property of the current code path.

Co-authored-by: Xiangzhe <xiangzhedev@gmail.com>
Co-authored-by: Jan Leon <Jan.gaschler@gmail.com>

---------

Co-authored-by: Xiangzhe <xiangzhedev@gmail.com>
Co-authored-by: Jan Leon <Jan.gaschler@gmail.com>
2026-07-15 23:49:22 -03:00

295 lines
10 KiB
TypeScript

import { extractUsage } from "./usageTracking.ts";
import { parseSSEDataPayload } from "./streamHelpers.ts";
import {
backfillResponsesCompletedOutput,
normalizeResponsesSseIds,
pushUniqueResponsesOutputItems,
stringifyIdValue,
stripResponsesLifecycleEcho,
} from "./responsesStreamHelpers.ts";
import { getAnyReasoningValue } from "./reasoningFields.ts";
type JsonRecord = Record<string, unknown>;
type EventPrefixBuffer = {
eventType: () => string;
flush: () => string;
prefixData: (output: string, line: string) => string;
remember: (line: string) => void;
};
export type PassthroughTailProcessorContext = {
getSkipPassthroughEvent: () => boolean;
setSkipPassthroughEvent: (value: boolean) => void;
clearPendingPassthroughEvent: () => void;
shouldAbortOnClaudeLifecycle: (payload: unknown) => boolean;
emitClaudeEmptyStreamErrorAndAbort: () => void;
isClaudeEventPayload: (payload: unknown) => boolean;
updateClaudeEmptyResponseLifecycle: (payload: unknown) => void;
passthroughEventPrefix: EventPrefixBuffer;
emitConvertedOutput: (output: string) => void;
pushProviderPayload: (payload: unknown) => void;
pushClientPayload: (payload: unknown) => void;
setPassthroughResponsesId: (value: string) => void;
setUsage: (value: unknown) => void;
addTotalContentLength: (value: number) => void;
appendPassthroughContent: (value: string) => void;
appendPassthroughReasoning: (value: string) => void;
getResponsesReasoningKey: (payload: Record<string, unknown>) => string | null;
markResponsesReasoningSummarySeen: (key: string) => void;
emitSyntheticResponsesReasoningSummary: (payload: Record<string, unknown>) => void;
passthroughResponsesOutputItems: unknown[];
passthroughResponsesPendingFunctionCalls: Map<string, JsonRecord>;
getPassthroughResponsesCurrentFunctionCallKey: () => string | null;
setPassthroughResponsesCurrentFunctionCallKey: (value: string | null) => void;
hasPassthroughToolCalls: () => boolean;
toResponsesCompletedWithToolCalls: (parsed: JsonRecord) => JsonRecord;
};
function asRecord(value: unknown): JsonRecord {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {};
}
function getFunctionCallPendingKey(item: JsonRecord | null): string | null {
if (!item) return null;
if (typeof item.id === "string") return item.id;
if (typeof item.call_id === "string") return item.call_id;
return null;
}
function handleResponsesTailPayload(
parsed: JsonRecord,
output: string,
context: PassthroughTailProcessorContext
): string {
const responsesIdsNormalized = normalizeResponsesSseIds(parsed);
const parsedResponse = asRecord(parsed.response);
const responseId =
(parsedResponse ? stringifyIdValue(parsedResponse.id) : null) ||
stringifyIdValue(parsed.response_id);
if (responseId) {
context.setPassthroughResponsesId(responseId);
}
const extracted = extractUsage(parsed);
if (extracted) {
context.setUsage(extracted);
}
if (typeof parsed.delta === "string") {
context.addTotalContentLength(parsed.delta.length);
}
if (parsed.type === "response.output_text.delta" && typeof parsed.delta === "string") {
context.appendPassthroughContent(parsed.delta);
}
if (
parsed.type === "response.reasoning_summary_text.delta" ||
parsed.type === "response.reasoning_summary_text.done" ||
parsed.type === "response.reasoning_summary_part.done"
) {
const reasoningKey = context.getResponsesReasoningKey(parsed);
if (reasoningKey) {
context.markResponsesReasoningSummarySeen(reasoningKey);
}
}
if (
parsed.type === "response.output_item.added" &&
asRecord(parsed.item).type === "function_call"
) {
const item = { ...(parsed.item as JsonRecord) };
const pendingKey = getFunctionCallPendingKey(item);
if (pendingKey) {
if (typeof item.arguments !== "string") {
item.arguments = "";
}
context.passthroughResponsesPendingFunctionCalls.set(pendingKey, item);
context.setPassthroughResponsesCurrentFunctionCallKey(pendingKey);
}
}
if (parsed.type === "response.function_call_arguments.delta") {
const pendingKey =
typeof parsed.item_id === "string"
? parsed.item_id
: context.getPassthroughResponsesCurrentFunctionCallKey();
const pending = pendingKey
? context.passthroughResponsesPendingFunctionCalls.get(pendingKey)
: undefined;
if (pending && typeof parsed.delta === "string") {
const previousArgs = typeof pending.arguments === "string" ? pending.arguments : "";
pending.arguments = previousArgs + parsed.delta;
}
}
if (parsed.type === "response.function_call_arguments.done") {
const pendingKey =
typeof parsed.item_id === "string"
? parsed.item_id
: context.getPassthroughResponsesCurrentFunctionCallKey();
const pending = pendingKey
? context.passthroughResponsesPendingFunctionCalls.get(pendingKey)
: undefined;
if (pending) {
if (typeof parsed.arguments === "string") {
pending.arguments = parsed.arguments;
}
pushUniqueResponsesOutputItems(context.passthroughResponsesOutputItems, [pending]);
}
}
if (parsed.type === "response.output_item.done" && parsed.item) {
context.emitSyntheticResponsesReasoningSummary(parsed);
pushUniqueResponsesOutputItems(context.passthroughResponsesOutputItems, [parsed.item]);
const item = asRecord(parsed.item);
if (item.type === "function_call") {
const pendingKey = getFunctionCallPendingKey(item);
if (pendingKey) {
context.passthroughResponsesPendingFunctionCalls.delete(pendingKey);
if (context.getPassthroughResponsesCurrentFunctionCallKey() === pendingKey) {
context.setPassthroughResponsesCurrentFunctionCallKey(null);
}
}
}
}
if (
parsed.type === "response.completed" &&
Array.isArray(asRecord(parsed.response).output) &&
(asRecord(parsed.response).output as unknown[]).length > 0
) {
pushUniqueResponsesOutputItems(
context.passthroughResponsesOutputItems,
asRecord(parsed.response).output as unknown[]
);
}
if (
parsed.type === "response.completed" &&
context.passthroughResponsesPendingFunctionCalls.size > 0
) {
pushUniqueResponsesOutputItems(context.passthroughResponsesOutputItems, [
...context.passthroughResponsesPendingFunctionCalls.values(),
]);
context.passthroughResponsesPendingFunctionCalls.clear();
context.setPassthroughResponsesCurrentFunctionCallKey(null);
}
const textualToolCallBackfilled =
parsed.type === "response.completed" && context.hasPassthroughToolCalls();
const outputPayload = textualToolCallBackfilled
? context.toResponsesCompletedWithToolCalls(parsed)
: parsed;
const stripped = stripResponsesLifecycleEcho(outputPayload);
const backfilled = backfillResponsesCompletedOutput(
outputPayload,
context.passthroughResponsesOutputItems
);
if (stripped || backfilled || textualToolCallBackfilled || responsesIdsNormalized) {
output = `data: ${JSON.stringify(outputPayload)}\n\n`;
}
return output;
}
function handleOpenAiTailPayload(parsed: JsonRecord, context: PassthroughTailProcessorContext) {
const firstChoice = Array.isArray(parsed.choices)
? (parsed.choices[0] as JsonRecord | undefined)
: undefined;
const delta = asRecord(firstChoice?.delta);
if (typeof delta.content === "string") {
context.appendPassthroughContent(delta.content);
context.addTotalContentLength(delta.content.length);
}
const reasoningDelta = getAnyReasoningValue(delta);
if (reasoningDelta) {
context.appendPassthroughReasoning(reasoningDelta);
}
}
export function processBufferedPassthroughLine(
line: string,
context: PassthroughTailProcessorContext
): boolean {
const trimmed = line.trim();
if (context.getSkipPassthroughEvent()) {
if (!trimmed) {
context.setSkipPassthroughEvent(false);
context.clearPendingPassthroughEvent();
}
return false;
}
if (/^event:\s*keepalive\b/i.test(trimmed)) {
context.setSkipPassthroughEvent(true);
context.clearPendingPassthroughEvent();
return false;
}
if (/^event:/i.test(trimmed)) {
const eventType = trimmed.replace(/^event:\s*/i, "");
if (context.shouldAbortOnClaudeLifecycle({ type: eventType })) {
context.emitClaudeEmptyStreamErrorAndAbort();
return true;
}
context.passthroughEventPrefix.remember(line);
return false;
}
if (/^(?::|id:|retry:)/i.test(trimmed)) {
context.passthroughEventPrefix.remember(line);
return false;
}
if (!trimmed) {
const pendingOutput = context.passthroughEventPrefix.flush();
if (pendingOutput) {
context.emitConvertedOutput(pendingOutput);
}
context.clearPendingPassthroughEvent();
return false;
}
if (!trimmed.startsWith("data:")) {
context.passthroughEventPrefix.remember(line);
return false;
}
const parsedPassthroughData = parseSSEDataPayload(trimmed.slice(5), {
eventType: context.passthroughEventPrefix.eventType(),
});
if (parsedPassthroughData?.done === true) {
return false;
}
let output =
line.startsWith("data:") && !line.startsWith("data: ")
? `data: ${line.slice(5)}\n\n`
: `${line}\n\n`;
if (parsedPassthroughData) {
context.pushProviderPayload(parsedPassthroughData);
if (context.shouldAbortOnClaudeLifecycle(parsedPassthroughData)) {
context.emitClaudeEmptyStreamErrorAndAbort();
return true;
}
if (context.isClaudeEventPayload(parsedPassthroughData)) {
context.updateClaudeEmptyResponseLifecycle(parsedPassthroughData);
}
const parsed = parsedPassthroughData as JsonRecord;
const parsedType = typeof parsed.type === "string" ? parsed.type : "";
const isResponses = parsedType.startsWith("response.");
const isClaude = context.isClaudeEventPayload(parsed);
if (isResponses) {
output = handleResponsesTailPayload(parsed, output, context);
} else if (!isClaude) {
handleOpenAiTailPayload(parsed, context);
}
context.pushClientPayload(parsed);
}
output = context.passthroughEventPrefix.prefixData(output, line);
context.emitConvertedOutput(output);
return false;
}