mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
* 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>
386 lines
13 KiB
TypeScript
386 lines
13 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import {
|
|
pipeWithDisconnect,
|
|
createStreamController,
|
|
createDisconnectAwareStream,
|
|
} from "../../open-sse/utils/streamHandler.ts";
|
|
import { createPassthroughStreamWithLogger } from "../../open-sse/utils/stream.ts";
|
|
|
|
import { wantsProgress, createProgressTransform } from "../../open-sse/utils/progressTracker.ts";
|
|
|
|
test("wantsProgress detects X-OmniRoute-Progress header correctly", () => {
|
|
const headersObj = { "x-omniroute-progress": "true" };
|
|
assert.equal(wantsProgress(new Headers(headersObj)), true);
|
|
|
|
const headersMap = new Map([["x-omniroute-progress", "true"]]);
|
|
assert.equal(wantsProgress(headersMap), true);
|
|
|
|
const headersPlain = { "x-omniroute-progress": "true" };
|
|
assert.equal(wantsProgress(headersPlain), true);
|
|
|
|
assert.equal(wantsProgress({ "x-omniroute-progress": "false" }), false);
|
|
assert.equal(wantsProgress(null), false);
|
|
assert.equal(wantsProgress({}), false);
|
|
});
|
|
|
|
test("createProgressTransform maps SSE text output to valid byte stream with progress", async () => {
|
|
const transform = createProgressTransform({ signal: new AbortController().signal });
|
|
const writer = transform.writable.getWriter();
|
|
|
|
writer.write(new TextEncoder().encode('data: {"chunk":"one"}\n\n'));
|
|
writer.write(new TextEncoder().encode('data: {"chunk":"two"}\n\n'));
|
|
writer.write(new TextEncoder().encode("data: [DONE]\n\n"));
|
|
writer.close();
|
|
|
|
const reader = transform.readable.getReader();
|
|
const decoder = new TextDecoder();
|
|
let result = "";
|
|
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
result += decoder.decode(value);
|
|
}
|
|
|
|
assert.match(result, /data: \{"chunk":"one"\}/);
|
|
assert.match(result, /data: \{"chunk":"two"\}/);
|
|
assert.match(result, /data: \[DONE\]/);
|
|
assert.match(result, /event: progress/); // progress check
|
|
assert.match(result, /done":true/);
|
|
});
|
|
|
|
test("createPassthroughStreamWithLogger omits [DONE] for Responses clients", async () => {
|
|
const transform = createPassthroughStreamWithLogger(
|
|
"codex",
|
|
null,
|
|
null,
|
|
"gpt-5.5-low",
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
"openai-responses"
|
|
);
|
|
|
|
const writer = transform.writable.getWriter();
|
|
await writer.write(
|
|
new TextEncoder().encode(
|
|
[
|
|
"event: response.completed",
|
|
'data: {"type":"response.completed","response":{"id":"resp_1","model":"gpt-5.5-low","status":"completed","output":[],"usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}}',
|
|
"",
|
|
].join("\n")
|
|
)
|
|
);
|
|
await writer.close();
|
|
|
|
const reader = transform.readable.getReader();
|
|
const decoder = new TextDecoder();
|
|
let result = "";
|
|
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
result += decoder.decode(value);
|
|
}
|
|
|
|
assert.match(result, /event: response\.completed/);
|
|
assert.doesNotMatch(result, /data: \[DONE\]/);
|
|
});
|
|
|
|
test("createPassthroughStreamWithLogger synthesizes reasoning summary events from reasoning output items", async () => {
|
|
const transform = createPassthroughStreamWithLogger(
|
|
"codex",
|
|
null,
|
|
null,
|
|
"gpt-5.5-low",
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
"openai-responses"
|
|
);
|
|
|
|
const writer = transform.writable.getWriter();
|
|
await writer.write(
|
|
new TextEncoder().encode(
|
|
[
|
|
"event: response.output_item.done",
|
|
'data: {"type":"response.output_item.done","response_id":"resp_reasoning_1","output_index":0,"item":{"id":"rs_resp_reasoning_1_0","type":"reasoning","summary":[{"type":"summary_text","text":"Reasoning summary text"}]}}',
|
|
"",
|
|
].join("\n")
|
|
)
|
|
);
|
|
await writer.close();
|
|
|
|
const reader = transform.readable.getReader();
|
|
const decoder = new TextDecoder();
|
|
let result = "";
|
|
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
result += decoder.decode(value);
|
|
}
|
|
|
|
assert.match(result, /event: response\.reasoning_summary_text\.delta/);
|
|
assert.match(result, /"delta":"Reasoning summary text"/);
|
|
assert.match(result, /event: response\.reasoning_summary_part\.done/);
|
|
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 () => {
|
|
const transform = createPassthroughStreamWithLogger(
|
|
"codex",
|
|
null,
|
|
null,
|
|
"gpt-5.5-low",
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
"openai-responses"
|
|
);
|
|
|
|
const writer = transform.writable.getWriter();
|
|
await writer.write(
|
|
new TextEncoder().encode(
|
|
[
|
|
"event: response.output_item.done",
|
|
'data: {"type":"response.output_item.done","response_id":"resp_reasoning_2","output_index":0,"item":{"id":"rs_resp_reasoning_2_0","type":"reasoning","encrypted_content":"enc_opaque_state"}}',
|
|
"",
|
|
].join("\n")
|
|
)
|
|
);
|
|
await writer.close();
|
|
|
|
const reader = transform.readable.getReader();
|
|
const decoder = new TextDecoder();
|
|
let result = "";
|
|
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
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/);
|
|
|
|
// #7176: the forwarded response.output_item.done payload is untouched —
|
|
// encrypted_content survives intact and no fabricated `summary` is present.
|
|
assert.match(result, /"encrypted_content":"enc_opaque_state"/);
|
|
assert.doesNotMatch(result, /"summary":/);
|
|
assert.match(result, /event: response\.output_item\.done/);
|
|
});
|
|
|
|
// Companion guard for the test above. The output_item.done line is echoed
|
|
// verbatim, so a re-introduced `item.summary` mutation would NOT surface there.
|
|
// It does surface here: the reasoning item is captured into
|
|
// passthroughResponsesOutputItems and re-serialized into the response.completed
|
|
// snapshot when upstream sends an empty `output` (store: false). This is the
|
|
// path that actually fails if the mutation comes back — it is what makes the
|
|
// #7176 half of the reconciliation enforceable rather than incidental.
|
|
test("createPassthroughStreamWithLogger backfills completed output with encrypted reasoning unmutated", async () => {
|
|
const transform = createPassthroughStreamWithLogger(
|
|
"codex",
|
|
null,
|
|
null,
|
|
"gpt-5.5-low",
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
"openai-responses"
|
|
);
|
|
|
|
const writer = transform.writable.getWriter();
|
|
await writer.write(
|
|
new TextEncoder().encode(
|
|
[
|
|
"event: response.output_item.done",
|
|
'data: {"type":"response.output_item.done","response_id":"resp_reasoning_3","output_index":0,"item":{"id":"rs_resp_reasoning_3_0","type":"reasoning","encrypted_content":"enc_opaque_state"}}',
|
|
"",
|
|
"event: response.completed",
|
|
'data: {"type":"response.completed","response":{"id":"resp_reasoning_3","model":"gpt-5.5-low","status":"completed","output":[],"usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}}',
|
|
"",
|
|
].join("\n")
|
|
)
|
|
);
|
|
await writer.close();
|
|
|
|
const reader = transform.readable.getReader();
|
|
const decoder = new TextDecoder();
|
|
let result = "";
|
|
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
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/);
|
|
|
|
// The item re-serialized into the completed snapshot carries the original
|
|
// encrypted_content and never a fabricated summary (#7176).
|
|
const completed = result
|
|
.split(/\n\n+/)
|
|
.find((block) => block.includes('"type":"response.completed"'));
|
|
assert.ok(completed, "expected a response.completed event on the wire");
|
|
assert.match(completed, /"encrypted_content":"enc_opaque_state"/);
|
|
assert.doesNotMatch(completed, /"summary":/);
|
|
});
|
|
|
|
test("createPassthroughStreamWithLogger backfills completed output from function_call arguments events", async () => {
|
|
const transform = createPassthroughStreamWithLogger(
|
|
"codex",
|
|
null,
|
|
null,
|
|
"gpt-5.5-low",
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
"openai-responses"
|
|
);
|
|
|
|
const writer = transform.writable.getWriter();
|
|
await writer.write(
|
|
new TextEncoder().encode(
|
|
[
|
|
'data: {"type":"response.created","response":{"id":"resp_fc_1"}}',
|
|
"event: response.output_item.added",
|
|
'data: {"type":"response.output_item.added","response_id":"resp_fc_1","output_index":0,"item":{"id":"fc_call_1","type":"function_call","call_id":"call_1","name":"workspace_read_file","arguments":""}}',
|
|
"event: response.function_call_arguments.done",
|
|
'data: {"type":"response.function_call_arguments.done","response_id":"resp_fc_1","output_index":0,"item_id":"fc_call_1","arguments":"{\\"path\\":\\"README.md\\"}"}',
|
|
"event: response.completed",
|
|
'data: {"type":"response.completed","response":{"id":"resp_fc_1","status":"completed","output":[],"usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}}',
|
|
"",
|
|
].join("\n")
|
|
)
|
|
);
|
|
await writer.close();
|
|
|
|
const reader = transform.readable.getReader();
|
|
const decoder = new TextDecoder();
|
|
let result = "";
|
|
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
result += decoder.decode(value);
|
|
}
|
|
|
|
assert.match(result, /event: response\.completed/);
|
|
assert.match(result, /"type":"function_call"/);
|
|
assert.match(result, /"call_id":"call_1"/);
|
|
assert.ok(result.includes('"arguments":"{\\"path\\":\\"README.md\\"}"'));
|
|
});
|
|
|
|
test("createPassthroughStreamWithLogger keeps reasoning deltas out of logged assistant content", async () => {
|
|
let completePayload = null;
|
|
const transform = createPassthroughStreamWithLogger(
|
|
"codex",
|
|
null,
|
|
null,
|
|
"gpt-5.5-low",
|
|
null,
|
|
{
|
|
input: [
|
|
{
|
|
type: "message",
|
|
role: "user",
|
|
content: [{ type: "input_text", text: "Say OK." }],
|
|
},
|
|
],
|
|
},
|
|
(payload) => {
|
|
completePayload = payload;
|
|
},
|
|
null,
|
|
null,
|
|
"openai-responses"
|
|
);
|
|
|
|
const writer = transform.writable.getWriter();
|
|
await writer.write(
|
|
new TextEncoder().encode(
|
|
[
|
|
'data: {"type":"response.created","response":{"id":"resp_reasoning_delta_1"}}',
|
|
"event: response.reasoning_summary_text.delta",
|
|
'data: {"type":"response.reasoning_summary_text.delta","response_id":"resp_reasoning_delta_1","delta":"Internal reasoning should not be content."}',
|
|
"event: response.function_call_arguments.delta",
|
|
'data: {"type":"response.function_call_arguments.delta","response_id":"resp_reasoning_delta_1","delta":"{\\"path\\":\\"secret.txt\\"}"}',
|
|
"event: response.output_text.delta",
|
|
'data: {"type":"response.output_text.delta","response_id":"resp_reasoning_delta_1","delta":"OK"}',
|
|
"event: response.completed",
|
|
'data: {"type":"response.completed","response":{"id":"resp_reasoning_delta_1","status":"completed","output":[],"usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}}',
|
|
"",
|
|
].join("\n")
|
|
)
|
|
);
|
|
await writer.close();
|
|
|
|
const reader = transform.readable.getReader();
|
|
while (true) {
|
|
const { done } = await reader.read();
|
|
if (done) break;
|
|
}
|
|
|
|
assert.ok(completePayload, "expected onComplete payload");
|
|
assert.equal(completePayload.responseBody.choices[0].message.content, "OK");
|
|
assert.equal(
|
|
JSON.stringify(completePayload.responseBody).includes(
|
|
"Internal reasoning should not be content"
|
|
),
|
|
false
|
|
);
|
|
assert.equal(JSON.stringify(completePayload.responseBody).includes("secret.txt"), false);
|
|
});
|
|
|
|
test("createStreamController returns valid controller", () => {
|
|
let completeLogged = false;
|
|
let disconnectLogged = false;
|
|
|
|
const originalLog = console.log;
|
|
console.log = (msg) => {
|
|
if (msg.includes("complete")) completeLogged = true;
|
|
if (msg.includes("disconnect")) disconnectLogged = true;
|
|
};
|
|
|
|
const sc = createStreamController({
|
|
provider: "test",
|
|
model: "conn_1",
|
|
});
|
|
|
|
assert.equal(typeof sc.signal, "object");
|
|
|
|
sc.handleComplete();
|
|
assert.equal(completeLogged, true);
|
|
assert.equal(sc.isConnected(), false);
|
|
|
|
sc.handleDisconnect();
|
|
assert.equal(disconnectLogged, false);
|
|
|
|
console.log = originalLog;
|
|
});
|