mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-04 06:12:10 +03:00
fix(combo): preserve omniModel tag in streaming output for round-trip context pinning (#2646)
Integrated into release/v3.8.3
This commit is contained in:
committed by
GitHub
parent
9c94be6597
commit
40cc555b68
@@ -1611,47 +1611,7 @@ export async function handleComboChat({
|
||||
},
|
||||
});
|
||||
|
||||
// FIX #585: Sanitize outbound stream — strip <omniModel> tags from
|
||||
// visible content so they don't leak to the user. The tag is still
|
||||
// present in the full response for round-trip context pinning, but
|
||||
// we clean it from each SSE chunk's content field before delivery.
|
||||
//
|
||||
// IMPORTANT: Use a SEPARATE TextDecoder from the transform stream above.
|
||||
// The transform stream's decoder accumulates UTF-8 state; reusing it here
|
||||
// would corrupt multi-byte characters split across chunk boundaries.
|
||||
const sanitizeDecoder = new TextDecoder();
|
||||
const sanitize = new TransformStream({
|
||||
transform(chunk, controller) {
|
||||
const text = sanitizeDecoder.decode(chunk, { stream: true });
|
||||
if (text) {
|
||||
if (text.includes("<omniModel>")) {
|
||||
const cleaned = text.replaceAll(
|
||||
/(?:\\n|\n|\r)*<omniModel>[^<]+<\/omniModel>(?:\\n|\n|\r)*/g,
|
||||
""
|
||||
);
|
||||
if (cleaned) controller.enqueue(encoder.encode(cleaned));
|
||||
} else {
|
||||
controller.enqueue(encoder.encode(text));
|
||||
}
|
||||
}
|
||||
},
|
||||
flush(controller) {
|
||||
const tail = sanitizeDecoder.decode();
|
||||
if (tail) {
|
||||
if (tail.includes("<omniModel>")) {
|
||||
const cleaned = tail.replaceAll(
|
||||
/(?:\\n|\n|\r)*<omniModel>[^<]+<\/omniModel>(?:\\n|\n|\r)*/g,
|
||||
""
|
||||
);
|
||||
if (cleaned) controller.enqueue(encoder.encode(cleaned));
|
||||
} else {
|
||||
controller.enqueue(encoder.encode(tail));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const transformedStream = res.body.pipeThrough(transform).pipeThrough(sanitize);
|
||||
const transformedStream = res.body.pipeThrough(transform);
|
||||
// Add model info as response header for clients that support it
|
||||
const headers = new Headers(res.headers);
|
||||
headers.set("X-OmniRoute-Model", modelStr);
|
||||
|
||||
@@ -1764,7 +1764,7 @@ test("handleComboChat context cache protection pins the model and tags tool-call
|
||||
);
|
||||
});
|
||||
|
||||
test("handleComboChat context cache protection sanitizes streamed text tags from client output", async () => {
|
||||
test("handleComboChat context cache protection preserves omniModel tag in streamed output for round-trip pinning", async () => {
|
||||
const result = await handleComboChat({
|
||||
body: { stream: true, messages: [{ role: "user", content: "stream it" }] },
|
||||
combo: {
|
||||
@@ -1790,7 +1790,7 @@ test("handleComboChat context cache protection sanitizes streamed text tags from
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.headers.get("X-OmniRoute-Model"), "openai/gpt-4o-mini");
|
||||
assert.match(text, /hello world/);
|
||||
assert.doesNotMatch(text, /<omniModel>/);
|
||||
assert.match(text, /<omniModel>openai\/gpt-4o-mini<\/omniModel>/);
|
||||
});
|
||||
|
||||
test("handleComboChat context cache protection injects a hidden tag for tool-call-only streams", async () => {
|
||||
@@ -1818,7 +1818,7 @@ test("handleComboChat context cache protection injects a hidden tag for tool-cal
|
||||
const text = await result.text();
|
||||
assert.equal(result.ok, true);
|
||||
assert.match(text, /"finish_reason":"tool_calls"/);
|
||||
assert.doesNotMatch(text, /<omniModel>/);
|
||||
assert.match(text, /<omniModel>openai\/gpt-4o-mini<\/omniModel>/);
|
||||
});
|
||||
|
||||
test("handleComboChat context cache protection flushes cleanly when a stream ends without content", async () => {
|
||||
@@ -1842,8 +1842,7 @@ test("handleComboChat context cache protection flushes cleanly when a stream end
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.headers.get("X-OmniRoute-Model"), "openai/gpt-4o-mini");
|
||||
assert.match(text, /data: \[DONE\]/);
|
||||
assert.match(text, /"content":""/);
|
||||
assert.doesNotMatch(text, /<omniModel>/);
|
||||
assert.match(text, /"content":"<omniModel>openai\/gpt-4o-mini<\/omniModel>"/);
|
||||
});
|
||||
|
||||
test("handleComboChat round-robin resolves nested combos and returns inactive when every target is skipped", async () => {
|
||||
|
||||
Reference in New Issue
Block a user