audit: fix 18 defects across gateway SSE streaming, voice-call security, and telephony

Gateway (pipecat compatibility):
- openai-http: add finish_reason:"stop" on final SSE chunk, fix ID format
  (chatcmpl- not chatcmpl_), capture timestamp once, use delta only, add
  writable checks and flush after writes
- http-common: add TCP_NODELAY, X-Accel-Buffering:no, flush after writes,
  writable checks on writeDone
- agent-events: fix seqByRun memory leak in clearAgentRunContext

Voice-call security:
- manager.ts, twiml.ts, twilio.ts: escape voice/language XML attributes
  to prevent XML injection
- voice-mapping: strip control characters in escapeXml

Voice-call bugs:
- tts-openai: fix broken resample24kTo8k (interpolation frac always 0)
- stt-openai-realtime: close zombie WebSocket on connection timeout
- telnyx: extract direction/from/to for inbound calls (were silently dropped)
- plivo: clean up 5 internal maps on terminal call states (memory leak)
- twilio: clean up callWebhookUrls on terminal call states

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Tarun Sukhani
2026-02-10 04:05:22 +08:00
parent 806c5e2d13
commit d4e3549ed2
11 changed files with 203 additions and 26 deletions

View File

@@ -77,7 +77,11 @@ export async function readJsonBodyOrError(
}
export function writeDone(res: ServerResponse) {
if (res.writableEnded || res.destroyed) {
return;
}
res.write("data: [DONE]\n\n");
(res as unknown as { flush?: () => void }).flush?.();
}
export function setSseHeaders(res: ServerResponse) {
@@ -85,5 +89,7 @@ export function setSseHeaders(res: ServerResponse) {
res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
res.setHeader("X-Accel-Buffering", "no");
res.socket?.setNoDelay?.(true);
res.flushHeaders?.();
}

View File

@@ -37,7 +37,11 @@ type OpenAiChatCompletionRequest = {
};
function writeSse(res: ServerResponse, data: unknown) {
if (res.writableEnded || res.destroyed) {
return;
}
res.write(`data: ${JSON.stringify(data)}\n\n`);
(res as unknown as { flush?: () => void }).flush?.();
}
function asMessages(val: unknown): OpenAiChatMessage[] {
@@ -178,7 +182,7 @@ export async function handleOpenAiHttpRequest(
return true;
}
const runId = `chatcmpl_${randomUUID()}`;
const runId = `chatcmpl-${randomUUID()}`;
const deps = createDefaultDeps();
if (!stream) {
@@ -231,10 +235,27 @@ export async function handleOpenAiHttpRequest(
setSseHeaders(res);
const created = Math.floor(Date.now() / 1000);
let wroteRole = false;
let sawAssistantDelta = false;
let closed = false;
/** Send a final chunk with finish_reason and then [DONE]. */
function finishStream(finishReason: string = "stop") {
if (res.writableEnded || res.destroyed) {
return;
}
writeSse(res, {
id: runId,
object: "chat.completion.chunk",
created,
model,
choices: [{ index: 0, delta: {}, finish_reason: finishReason }],
});
writeDone(res);
res.end();
}
const unsubscribe = onAgentEvent((evt) => {
if (evt.runId !== runId) {
return;
@@ -254,7 +275,7 @@ export async function handleOpenAiHttpRequest(
writeSse(res, {
id: runId,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
created,
model,
choices: [{ index: 0, delta: { role: "assistant" } }],
});
@@ -264,7 +285,7 @@ export async function handleOpenAiHttpRequest(
writeSse(res, {
id: runId,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
created,
model,
choices: [
{
@@ -282,8 +303,7 @@ export async function handleOpenAiHttpRequest(
if (phase === "end" || phase === "error") {
closed = true;
unsubscribe();
writeDone(res);
res.end();
finishStream(phase === "error" ? "stop" : "stop");
}
}
});
@@ -319,7 +339,7 @@ export async function handleOpenAiHttpRequest(
writeSse(res, {
id: runId,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
created,
model,
choices: [{ index: 0, delta: { role: "assistant" } }],
});
@@ -338,7 +358,7 @@ export async function handleOpenAiHttpRequest(
writeSse(res, {
id: runId,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
created,
model,
choices: [
{
@@ -357,7 +377,7 @@ export async function handleOpenAiHttpRequest(
writeSse(res, {
id: runId,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
created,
model,
choices: [
{
@@ -376,8 +396,7 @@ export async function handleOpenAiHttpRequest(
if (!closed) {
closed = true;
unsubscribe();
writeDone(res);
res.end();
finishStream();
}
}
})();

View File

@@ -48,6 +48,7 @@ export function getAgentRunContext(runId: string) {
export function clearAgentRunContext(runId: string) {
runContextById.delete(runId);
seqByRun.delete(runId);
}
export function resetAgentRunContextForTest() {