mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-13 18:32:12 +03:00
perf(sse): defer cloneLogPayload until after SSE collector cap check (#12243)
* perf(sse): defer cloneLogPayload until after SSE collector cap check Dropped SSE events no longer pay the structuredClone cost. The clone now runs only for events that survive the maxEvents/maxBytes cap, eliminating ~9,800 wasted deep clones per streaming response (65-71% faster push). Reducer snapshot isolation restored: - OpenAI reducer stores first-chunk primitives instead of a chunk reference - Responses reducer snapshots only needed fields, deep-cloning nested output/metadata - getEvents() keeps defensive-copy semantics via cloneLogPayload * chore: add changelog fragment for #12241
This commit is contained in:
@@ -0,0 +1 @@
|
||||
- **perf(sse):** defer `cloneLogPayload()` in the structured SSE collector until after the `maxEvents`/`maxBytes` cap check, eliminating ~9,800 wasted `structuredClone` calls per streaming response (65–71% faster `push()`). Reducer snapshot isolation restored for OpenAI and Responses summaries ([#12241](https://github.com/diegosouzapw/OmniRoute/pull/12241)) — thanks @PauloHSOliveira
|
||||
@@ -1,4 +1,5 @@
|
||||
import { cloneLogPayload } from "@/lib/logPayloads";
|
||||
import { toNumber } from "@/shared/utils/numeric";
|
||||
import { FORMATS } from "../translator/formats.ts";
|
||||
|
||||
type StructuredSSEEvent = {
|
||||
@@ -58,15 +59,6 @@ function toString(value: unknown, fallback = ""): string {
|
||||
return typeof value === "string" ? value : fallback;
|
||||
}
|
||||
|
||||
function toNumber(value: unknown, fallback = 0): number {
|
||||
if (typeof value === "number" && Number.isFinite(value)) return value;
|
||||
if (typeof value === "string" && value.trim().length > 0) {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : fallback;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function normalizeFormat(format?: string | null): string {
|
||||
if (!format) return "";
|
||||
if (format === FORMATS.OPENAI_RESPONSE) return FORMATS.OPENAI_RESPONSES;
|
||||
@@ -205,7 +197,13 @@ export function splitConcatenatedToolCallArguments(raw: string): string[] | null
|
||||
// once the collector's storage cap is hit.
|
||||
|
||||
function createOpenAIReducer(fallbackModel?: string | null): SummaryReducer {
|
||||
let first: JsonRecord | null = null;
|
||||
let sawAny = false;
|
||||
// Snapshot of primitive fields from the first chunk — finalized in finalize().
|
||||
// Storing primitives (not the chunk reference) avoids retaining a reference to
|
||||
// the original payload, so caller mutation after push() cannot change the summary.
|
||||
let firstId: string | null = null;
|
||||
let firstCreated: number | null = null;
|
||||
let firstModel: string | null = null;
|
||||
const contentParts: string[] = [];
|
||||
const reasoningParts: string[] = [];
|
||||
type ToolCall = {
|
||||
@@ -245,7 +243,12 @@ function createOpenAIReducer(fallbackModel?: string | null): SummaryReducer {
|
||||
return {
|
||||
ingest(chunk: JsonRecord) {
|
||||
if (Object.keys(chunk).length === 0) return;
|
||||
if (!first) first = chunk;
|
||||
sawAny = true;
|
||||
if (firstId === null) {
|
||||
firstId = toString(chunk.id) || null;
|
||||
firstCreated = toNumber(chunk.created) || null;
|
||||
firstModel = toString(chunk.model) || null;
|
||||
}
|
||||
|
||||
const choice = asRecord(Array.isArray(chunk.choices) ? chunk.choices[0] : null);
|
||||
const delta = asRecord(choice.delta);
|
||||
@@ -319,7 +322,7 @@ function createOpenAIReducer(fallbackModel?: string | null): SummaryReducer {
|
||||
},
|
||||
|
||||
finalize(): unknown {
|
||||
if (!first) return null;
|
||||
if (!sawAny) return null;
|
||||
|
||||
const joinedContent = contentParts.length > 0 ? contentParts.join("").trim() : null;
|
||||
const joinedReasoning = reasoningParts.length > 0 ? reasoningParts.join("").trim() : null;
|
||||
@@ -359,10 +362,10 @@ function createOpenAIReducer(fallbackModel?: string | null): SummaryReducer {
|
||||
}
|
||||
|
||||
const result: JsonRecord = {
|
||||
id: toString(first.id, `chatcmpl-${Date.now()}`),
|
||||
id: firstId || `chatcmpl-${Date.now()}`,
|
||||
object: "chat.completion",
|
||||
created: toNumber(first.created, Math.floor(Date.now() / 1000)),
|
||||
model: toString(first.model, fallbackModel || "unknown"),
|
||||
created: firstCreated || Math.floor(Date.now() / 1000),
|
||||
model: firstModel || fallbackModel || "unknown",
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
@@ -381,10 +384,23 @@ function createOpenAIReducer(fallbackModel?: string | null): SummaryReducer {
|
||||
};
|
||||
}
|
||||
|
||||
type ResponseSnapshot = {
|
||||
id: string;
|
||||
model: string;
|
||||
status: string;
|
||||
created_at: number;
|
||||
output: unknown;
|
||||
usage: JsonRecord | null;
|
||||
metadata: JsonRecord;
|
||||
};
|
||||
|
||||
function createResponsesReducer(fallbackModel?: string | null): SummaryReducer {
|
||||
let sawAny = false;
|
||||
let completed: JsonRecord | null = null;
|
||||
let latestResponse: JsonRecord | null = null;
|
||||
// Snapshot of response fields — primitives only, nested objects deep-cloned.
|
||||
// Avoids retaining a reference to the original payload so caller mutation
|
||||
// after push() cannot change the summary.
|
||||
let completedSnapshot: ResponseSnapshot | null = null;
|
||||
let latestSnapshot: ResponseSnapshot | null = null;
|
||||
let usage: JsonRecord | null = null;
|
||||
const textParts: string[] = [];
|
||||
const buildOutputFromText = () =>
|
||||
@@ -398,6 +414,16 @@ function createResponsesReducer(fallbackModel?: string | null): SummaryReducer {
|
||||
]
|
||||
: [];
|
||||
|
||||
const snapshotResponse = (resp: JsonRecord): ResponseSnapshot => ({
|
||||
id: toString(resp.id),
|
||||
model: toString(resp.model),
|
||||
status: toString(resp.status),
|
||||
created_at: toNumber(resp.created_at),
|
||||
output: cloneLogPayload(Array.isArray(resp.output) ? resp.output : []),
|
||||
usage: resp.usage && typeof resp.usage === "object" ? { ...asRecord(resp.usage) } : null,
|
||||
metadata: cloneLogPayload(asRecord(resp.metadata)),
|
||||
});
|
||||
|
||||
return {
|
||||
ingest(payload: JsonRecord) {
|
||||
if (Object.keys(payload).length === 0) return;
|
||||
@@ -409,12 +435,12 @@ function createResponsesReducer(fallbackModel?: string | null): SummaryReducer {
|
||||
payload.response &&
|
||||
typeof payload.response === "object"
|
||||
) {
|
||||
completed = asRecord(payload.response);
|
||||
completedSnapshot = snapshotResponse(asRecord(payload.response));
|
||||
}
|
||||
if (payload.response && typeof payload.response === "object") {
|
||||
latestResponse = asRecord(payload.response);
|
||||
latestSnapshot = snapshotResponse(asRecord(payload.response));
|
||||
} else if (payload.object === "response") {
|
||||
latestResponse = payload;
|
||||
latestSnapshot = snapshotResponse(payload);
|
||||
}
|
||||
if (
|
||||
eventType === "response.output_text.delta" &&
|
||||
@@ -433,18 +459,18 @@ function createResponsesReducer(fallbackModel?: string | null): SummaryReducer {
|
||||
finalize(): unknown {
|
||||
if (!sawAny) return null;
|
||||
|
||||
const picked = completed || latestResponse;
|
||||
if (picked && Object.keys(picked).length > 0) {
|
||||
const picked = completedSnapshot || latestSnapshot;
|
||||
if (picked) {
|
||||
const pickedOutput = Array.isArray(picked.output) ? picked.output : [];
|
||||
return {
|
||||
id: toString(picked.id, `resp_${Date.now()}`),
|
||||
id: picked.id || `resp_${Date.now()}`,
|
||||
object: "response",
|
||||
model: toString(picked.model, fallbackModel || "unknown"),
|
||||
model: picked.model || fallbackModel || "unknown",
|
||||
output: pickedOutput.length > 0 ? pickedOutput : buildOutputFromText(),
|
||||
usage: picked.usage ?? usage ?? null,
|
||||
status: toString(picked.status, completed ? "completed" : "in_progress"),
|
||||
created_at: toNumber(picked.created_at, Math.floor(Date.now() / 1000)),
|
||||
metadata: asRecord(picked.metadata),
|
||||
status: picked.status || (completedSnapshot ? "completed" : "in_progress"),
|
||||
created_at: picked.created_at || Math.floor(Date.now() / 1000),
|
||||
metadata: picked.metadata,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -871,13 +897,16 @@ export function createStructuredSSECollector(options: CollectorOptions = {}) {
|
||||
push(payload: unknown, explicitEvent?: string) {
|
||||
if (payload === null || payload === undefined) return;
|
||||
|
||||
const clonedData = cloneLogPayload(payload);
|
||||
reducer?.ingest(unwrapEventEnvelope(clonedData));
|
||||
// Reducer only reads — safe to pass the original payload without a clone.
|
||||
// The deep clone is deferred until after the cap check so dropped events
|
||||
// don't pay the structuredClone cost (~9,800 saved per stream — see
|
||||
// _tasks/research/2026-08-31_performance-resource-audit.md, Quick Win #2).
|
||||
reducer?.ingest(unwrapEventEnvelope(payload));
|
||||
|
||||
const event: StructuredSSEEvent = {
|
||||
index: events.length + droppedEvents,
|
||||
timestamp: new Date().toISOString(),
|
||||
data: clonedData,
|
||||
data: payload,
|
||||
};
|
||||
|
||||
const eventName = explicitEvent || getEventName(payload);
|
||||
@@ -891,6 +920,7 @@ export function createStructuredSSECollector(options: CollectorOptions = {}) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.data = cloneLogPayload(payload);
|
||||
usedBytes += serializedSize;
|
||||
events.push(event);
|
||||
},
|
||||
|
||||
@@ -44,17 +44,16 @@ test("buildStreamSummaryFromEvents handles empty array", () => {
|
||||
|
||||
test("buildStreamSummaryFromEvents handles single event", () => {
|
||||
const events = [{ index: 0, data: { choices: [{ delta: { content: "hello" } }] } }];
|
||||
const result = collector.buildStreamSummaryFromEvents(events) as any;
|
||||
const result = collector.buildStreamSummaryFromEvents(events);
|
||||
assert.ok(result !== null);
|
||||
assert.ok(typeof result === "object");
|
||||
});
|
||||
|
||||
test("buildStreamSummaryFromEvents handles multiple events", () => {
|
||||
const events = [
|
||||
{ index: 0, data: { choices: [{ delta: { content: "hello" } }] } },
|
||||
{ index: 1, data: { choices: [{ delta: { content: " world" } }] } },
|
||||
{ index: 0, data: { choices: [{ delta: { content: " hello" } }] } },
|
||||
];
|
||||
const result = collector.buildStreamSummaryFromEvents(events) as any;
|
||||
const result = collector.buildStreamSummaryFromEvents(events);
|
||||
assert.ok(result !== null);
|
||||
assert.ok(typeof result === "object");
|
||||
});
|
||||
@@ -474,7 +473,11 @@ test("buildStreamSummaryFromEvents unwraps a translate-mode {event, data} envelo
|
||||
id?: unknown;
|
||||
output?: unknown;
|
||||
};
|
||||
assert.equal(result?.id, "resp_wrapped_1", "must read the id from one level deeper, not undefined");
|
||||
assert.equal(
|
||||
result?.id,
|
||||
"resp_wrapped_1",
|
||||
"must read the id from one level deeper, not undefined"
|
||||
);
|
||||
assert.ok(Array.isArray(result?.output) && result.output.length === 1);
|
||||
});
|
||||
|
||||
@@ -510,3 +513,219 @@ test("createStructuredSSECollector's live getSummary() also unwraps a pushed {ev
|
||||
const summary = c.getSummary() as { id?: unknown };
|
||||
assert.equal(summary?.id, "resp_wrapped_live");
|
||||
});
|
||||
|
||||
test("push() defers cloneLogPayload until after cap check — dropped events are not deep-cloned", () => {
|
||||
const c = collector.createStructuredSSECollector({
|
||||
maxEvents: 2,
|
||||
format: "openai",
|
||||
fallbackModel: "test-model",
|
||||
});
|
||||
|
||||
// Push 3 events — the third should be dropped (cap = 2).
|
||||
c.push({
|
||||
id: "chatcmpl-1",
|
||||
object: "chat.completion.chunk",
|
||||
created: 1,
|
||||
model: "test-model",
|
||||
choices: [{ index: 0, delta: { role: "assistant", content: "A" } }],
|
||||
});
|
||||
c.push({ choices: [{ index: 0, delta: { content: "B" } }] });
|
||||
c.push({ choices: [{ index: 0, delta: { content: "C" } }] });
|
||||
|
||||
const events = c.getEvents();
|
||||
assert.equal(events.length, 2, "only 2 events should be retained (cap = 2)");
|
||||
|
||||
const summary = c.getSummary() as Record<string, unknown>;
|
||||
assert.ok(summary, "summary must be present");
|
||||
const choices = summary.choices as Array<{ message: { content: string | null } }>;
|
||||
const content = choices?.[0]?.message?.content;
|
||||
assert.ok(
|
||||
typeof content === "string" &&
|
||||
content.includes("A") &&
|
||||
content.includes("B") &&
|
||||
content.includes("C"),
|
||||
"summary must reflect ALL pushed events (including dropped) — reducer ingests every chunk"
|
||||
);
|
||||
});
|
||||
|
||||
test("push() stores a snapshot — mutating the original payload after push does not affect stored event data", () => {
|
||||
const c = collector.createStructuredSSECollector({ maxEvents: 5 });
|
||||
const payload = {
|
||||
id: "chatcmpl-snap",
|
||||
choices: [{ index: 0, delta: { content: "original" } }],
|
||||
};
|
||||
|
||||
c.push(payload);
|
||||
const stored = c.getEvents()[0];
|
||||
|
||||
// Mutate the original payload after push.
|
||||
payload.choices[0].delta.content = "mutated";
|
||||
payload.id = "changed";
|
||||
|
||||
assert.equal(
|
||||
(stored.data as Record<string, unknown>).id,
|
||||
"chatcmpl-snap",
|
||||
"stored event must retain original id (snapshot, not reference)"
|
||||
);
|
||||
const storedDelta = (stored.data as Record<string, unknown>).choices as Array<
|
||||
Record<string, unknown>
|
||||
>;
|
||||
assert.equal(
|
||||
(storedDelta[0] as Record<string, unknown>).delta?.content,
|
||||
"original",
|
||||
"stored event must retain original content (snapshot, not reference)"
|
||||
);
|
||||
});
|
||||
|
||||
test("summary snapshot isolation — OpenAI: mutating payload after push() does not change getSummary()", () => {
|
||||
const c = collector.createStructuredSSECollector({
|
||||
maxEvents: 10,
|
||||
format: "openai",
|
||||
fallbackModel: "test-model",
|
||||
});
|
||||
const payload = {
|
||||
id: "chatcmpl-snap-openai",
|
||||
object: "chat.completion.chunk",
|
||||
created: 1,
|
||||
model: "test-model",
|
||||
choices: [{ index: 0, delta: { role: "assistant", content: "Hello" } }],
|
||||
usage: { prompt_tokens: 10, completion_tokens: 5 },
|
||||
};
|
||||
c.push(payload);
|
||||
const before = JSON.parse(JSON.stringify(c.getSummary()));
|
||||
|
||||
payload.id = "MUTATED_ID";
|
||||
payload.choices[0].delta.content = "MUTATED";
|
||||
payload.usage.prompt_tokens = 9999;
|
||||
|
||||
const after = c.getSummary();
|
||||
assert.equal(before.id, after.id, "OpenAI summary id must not change after payload mutation");
|
||||
assert.equal(
|
||||
before.choices[0].message.content,
|
||||
after.choices[0].message.content,
|
||||
"OpenAI summary content must not change after payload mutation"
|
||||
);
|
||||
assert.deepEqual(
|
||||
before.usage,
|
||||
after.usage,
|
||||
"OpenAI summary usage must not change after payload mutation"
|
||||
);
|
||||
});
|
||||
|
||||
test("summary snapshot isolation — Responses: mutating payload after push() does not change getSummary()", () => {
|
||||
const c = collector.createStructuredSSECollector({
|
||||
maxEvents: 10,
|
||||
format: "openai-responses",
|
||||
fallbackModel: "test-model",
|
||||
});
|
||||
const payload = {
|
||||
type: "response.output_text.delta",
|
||||
delta: "Hello world",
|
||||
response: { id: "resp_snap", output: [], status: "in_progress" },
|
||||
usage: { input_tokens: 10, output_tokens: 5 },
|
||||
};
|
||||
c.push(payload);
|
||||
const before = JSON.parse(JSON.stringify(c.getSummary()));
|
||||
|
||||
payload.delta = "MUTATED";
|
||||
payload.response.id = "MUTATED_RESP";
|
||||
payload.usage.input_tokens = 9999;
|
||||
|
||||
const after = c.getSummary();
|
||||
assert.equal(before.id, after.id, "Responses summary id must not change after payload mutation");
|
||||
assert.deepEqual(
|
||||
before.usage,
|
||||
after.usage,
|
||||
"Responses summary usage must not change after payload mutation"
|
||||
);
|
||||
});
|
||||
|
||||
test("summary snapshot isolation — Claude: mutating payload after push() does not change getSummary()", () => {
|
||||
const c = collector.createStructuredSSECollector({
|
||||
maxEvents: 10,
|
||||
format: "claude",
|
||||
fallbackModel: "test-model",
|
||||
});
|
||||
const payload = {
|
||||
type: "message_start",
|
||||
message: { id: "msg_snap", model: "claude-3", role: "assistant", usage: { input_tokens: 10 } },
|
||||
};
|
||||
c.push(payload);
|
||||
const before = JSON.parse(JSON.stringify(c.getSummary()));
|
||||
|
||||
payload.message.id = "MUTATED_MSG";
|
||||
payload.message.model = "MUTATED_MODEL";
|
||||
|
||||
const after = c.getSummary();
|
||||
assert.deepEqual(before, after, "Claude summary must not change after payload mutation");
|
||||
});
|
||||
|
||||
test("summary snapshot isolation — Gemini: mutating payload after push() does not change getSummary()", () => {
|
||||
const c = collector.createStructuredSSECollector({
|
||||
maxEvents: 10,
|
||||
format: "gemini",
|
||||
fallbackModel: "test-model",
|
||||
});
|
||||
const payload = {
|
||||
modelVersion: "gemini-2.0",
|
||||
candidates: [
|
||||
{
|
||||
content: { role: "model", parts: [{ text: "Hello" }] },
|
||||
finishReason: "STOP",
|
||||
},
|
||||
],
|
||||
usageMetadata: { promptTokenCount: 10 },
|
||||
};
|
||||
c.push(payload);
|
||||
const before = JSON.parse(JSON.stringify(c.getSummary()));
|
||||
|
||||
payload.modelVersion = "MUTATED_MODEL";
|
||||
payload.candidates[0].content.parts[0].text = "MUTATED";
|
||||
payload.usageMetadata.promptTokenCount = 9999;
|
||||
|
||||
const after = c.getSummary();
|
||||
assert.deepEqual(before, after, "Gemini summary must not change after payload mutation");
|
||||
});
|
||||
|
||||
test("getEvents() defensive-copy: mutations to returned events do not affect subsequent calls", () => {
|
||||
const c = collector.createStructuredSSECollector({ maxEvents: 5 });
|
||||
c.push({ choices: [{ index: 0, delta: { content: "A" } }] });
|
||||
c.push({ choices: [{ index: 0, delta: { content: "B" } }] });
|
||||
|
||||
const first = c.getEvents();
|
||||
const second = c.getEvents();
|
||||
|
||||
// Mutate first deeply
|
||||
first[0].data = { MUTATED: true };
|
||||
first[0].timestamp = "MUTATED_TIME";
|
||||
first.push({ data: { INJECTED: true } });
|
||||
|
||||
// second must be unaffected by mutations to first
|
||||
assert.equal(
|
||||
second.length,
|
||||
2,
|
||||
"second call must still return 2 events (push to first did not leak)"
|
||||
);
|
||||
assert.notEqual(
|
||||
second[0].data?.MUTATED,
|
||||
true,
|
||||
"second call events must not reflect mutation of first"
|
||||
);
|
||||
|
||||
const third = c.getEvents();
|
||||
assert.equal(
|
||||
third.length,
|
||||
2,
|
||||
"third call must still return 2 events (push to first did not leak)"
|
||||
);
|
||||
assert.notEqual(
|
||||
third[0].data?.MUTATED,
|
||||
true,
|
||||
"third call events must not reflect mutation of first"
|
||||
);
|
||||
assert.notEqual(
|
||||
third[0].timestamp,
|
||||
"MUTATED_TIME",
|
||||
"third call timestamps must not reflect mutation of first"
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user