fix(sse): reconstruct Claude-format content in synthetic bypass responses

handleBypassRequest() returns a canned response for CLI warmup/title-
extraction patterns without calling the provider. For Claude-format
clients (e.g. Claude Code CLI), the non-streaming path merged translated
SSE chunks by taking message_start.message as-is — but the
openai-to-claude translator always initializes that message with
content: [] and streams the actual text via separate
content_block_start/delta events. Every synthetic Claude-format
bypass response therefore silently returned empty content.

mergeChunksToResponse() now rebuilds the content array from
content_block_start/delta events (mirroring the streaming path) and
carries over stop_reason/stop_sequence from message_delta. Extracted
the response-builder helpers (createOpenAIResponse,
create{Non}StreamingResponse, mergeChunksToResponse) out of
bypassHandler.ts into a new open-sse/utils/bypassResponse.ts module so
this logic has a single owner instead of being duplicated inline.

Co-authored-by: KunN-21 <kunn21.nv@gmail.com>
Inspired-by: https://github.com/decolua/9router/pull/2404
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-15 03:32:57 -03:00
parent abe686dab9
commit 0e90b4d058
3 changed files with 284 additions and 211 deletions

View File

@@ -1,9 +1,7 @@
import { CORS_HEADERS } from "./cors.ts";
import { detectFormat } from "../services/provider.ts";
import { translateResponse, initState } from "../translator/index.ts";
import { FORMATS } from "../translator/formats.ts";
import { SKIP_PATTERNS } from "../config/constants.ts";
import { formatSSE } from "./stream.ts";
import { createNonStreamingResponse, createStreamingResponse } from "./bypassResponse.ts";
/**
* Check for bypass patterns — return fake response without calling provider.
@@ -90,211 +88,3 @@ export function handleBypassRequest(body, model, userAgent = "") {
? createStreamingResponse(sourceFormat, model)
: createNonStreamingResponse(sourceFormat, model);
}
/**
* Create OpenAI standard format response
*/
function createOpenAIResponse(model) {
const id = `chatcmpl-${Date.now()}`;
const created = Math.floor(Date.now() / 1000);
const text = "CLI Command Execution: Clear Terminal";
return {
id,
object: "chat.completion",
created,
model,
choices: [
{
index: 0,
message: {
role: "assistant",
content: text,
},
finish_reason: "stop",
},
],
usage: {
prompt_tokens: 1,
completion_tokens: 1,
total_tokens: 2,
},
};
}
/**
* Create non-streaming response with translation
* Use translator to convert OpenAI → sourceFormat
*/
function createNonStreamingResponse(sourceFormat, model) {
const openaiResponse = createOpenAIResponse(model);
// If sourceFormat is OpenAI, return directly
if (sourceFormat === FORMATS.OPENAI) {
return {
success: true,
response: new Response(JSON.stringify(openaiResponse), {
headers: {
"Content-Type": "application/json",
},
}),
};
}
// Use translator to convert: simulate streaming then collect all chunks
const state = initState(sourceFormat);
state.model = model;
const openaiChunks = createOpenAIStreamingChunks(openaiResponse);
const allTranslated = [];
for (const chunk of openaiChunks) {
const translated = translateResponse(FORMATS.OPENAI, sourceFormat, chunk, state);
if (translated?.length > 0) {
allTranslated.push(...translated);
}
}
// Flush remaining
const flushed = translateResponse(FORMATS.OPENAI, sourceFormat, null, state);
if (flushed?.length > 0) {
allTranslated.push(...flushed);
}
// For non-streaming, merge all chunks into final response
const finalResponse = mergeChunksToResponse(allTranslated, sourceFormat);
return {
success: true,
response: new Response(JSON.stringify(finalResponse), {
headers: {
"Content-Type": "application/json",
},
}),
};
}
/**
* Create streaming response with translation
* Use translator to convert OpenAI chunks → sourceFormat
*/
function createStreamingResponse(sourceFormat, model) {
const openaiResponse = createOpenAIResponse(model);
const state = initState(sourceFormat);
state.model = model;
// Create OpenAI streaming chunks
const openaiChunks = createOpenAIStreamingChunks(openaiResponse);
// Translate each chunk to sourceFormat using translator
const translatedChunks = [];
for (const chunk of openaiChunks) {
const translated = translateResponse(FORMATS.OPENAI, sourceFormat, chunk, state);
if (translated?.length > 0) {
for (const item of translated) {
translatedChunks.push(formatSSE(item, sourceFormat));
}
}
}
// Flush remaining events
const flushed = translateResponse(FORMATS.OPENAI, sourceFormat, null, state);
if (flushed?.length > 0) {
for (const item of flushed) {
translatedChunks.push(formatSSE(item, sourceFormat));
}
}
// Add [DONE]
translatedChunks.push("data: [DONE]\n\n");
return {
success: true,
response: new Response(translatedChunks.join(""), {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
}),
};
}
/**
* Merge translated chunks into final response object (for non-streaming)
* Takes the last complete chunk as the final response
*/
function mergeChunksToResponse(chunks, sourceFormat) {
if (!chunks || chunks.length === 0) {
return createOpenAIResponse("unknown");
}
// For most formats, the last chunk before done contains the complete response
// Find the most complete chunk (usually the last one with content)
let finalChunk = chunks[chunks.length - 1];
// For Claude format, find the message_stop or final message
if (sourceFormat === FORMATS.CLAUDE) {
const messageStop = chunks.find((c) => c.type === "message_stop");
if (messageStop) {
// Reconstruct complete message from chunks
const contentDelta = chunks.find((c) => c.type === "content_block_delta");
const messageDelta = chunks.find((c) => c.type === "message_delta");
const messageStart = chunks.find((c) => c.type === "message_start");
if (messageStart?.message) {
finalChunk = messageStart.message;
// Merge usage if available
if (messageDelta?.usage) {
finalChunk.usage = messageDelta.usage;
}
}
}
}
return finalChunk;
}
/**
* Create OpenAI streaming chunks from complete response
*/
function createOpenAIStreamingChunks(completeResponse) {
const { id, created, model, choices } = completeResponse;
const content = choices[0].message.content;
return [
// Chunk with content
{
id,
object: "chat.completion.chunk",
created,
model,
choices: [
{
index: 0,
delta: {
role: "assistant",
content,
},
finish_reason: null,
},
],
},
// Final chunk with finish_reason
{
id,
object: "chat.completion.chunk",
created,
model,
choices: [
{
index: 0,
delta: {},
finish_reason: "stop",
},
],
usage: completeResponse.usage,
},
];
}

View File

@@ -0,0 +1,209 @@
import { translateResponse, initState } from "../translator/index.ts";
import { FORMATS } from "../translator/formats.ts";
import { formatSSE } from "./stream.ts";
/**
* Shared synthetic-response builders for the various "answer without calling
* the provider" code paths (CLI bypass patterns today; any future canned/
* synthetic response can reuse these instead of re-deriving format
* translation). Extracted out of bypassHandler.ts so the logic has exactly
* one owner. Ported from upstream decolua/9router#2404 (bypassResponse.js),
* with the Claude-format content reconstruction fixed — see
* mergeChunksToResponse() below.
*/
const DEFAULT_BYPASS_TEXT = "CLI Command Execution: Clear Terminal";
/** Build a complete (non-chunked) OpenAI chat-completion response object. */
export function createOpenAIResponse(model, text = DEFAULT_BYPASS_TEXT) {
const id = `chatcmpl-${Date.now()}`;
const created = Math.floor(Date.now() / 1000);
return {
id,
object: "chat.completion",
created,
model,
choices: [
{
index: 0,
message: {
role: "assistant",
content: text,
},
finish_reason: "stop",
},
],
usage: {
prompt_tokens: 1,
completion_tokens: 1,
total_tokens: 2,
},
};
}
/** Split a complete OpenAI response into the two streaming chunks a client expects. */
export function createOpenAIStreamingChunks(completeResponse) {
const { id, created, model, choices } = completeResponse;
const content = choices[0].message.content;
return [
{
id,
object: "chat.completion.chunk",
created,
model,
choices: [
{
index: 0,
delta: { role: "assistant", content },
finish_reason: null,
},
],
},
{
id,
object: "chat.completion.chunk",
created,
model,
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
usage: completeResponse.usage,
},
];
}
/**
* Merge translated chunks into a final response object (for non-streaming
* callers). For most formats the last chunk is already complete. Claude
* format is chunk-oriented even for "one-shot" synthetic responses, so the
* final message has to be reconstructed from content_block_start/delta
* events — taking the raw message_start.message would return an empty
* `content: []` (the translator always starts it empty and streams blocks
* in via separate events).
*/
export function mergeChunksToResponse(chunks, sourceFormat) {
if (!chunks || chunks.length === 0) {
return createOpenAIResponse("unknown");
}
let finalChunk = chunks[chunks.length - 1];
if (sourceFormat === FORMATS.CLAUDE) {
const messageStop = chunks.find((c) => c.type === "message_stop");
if (messageStop) {
const messageDelta = chunks.find((c) => c.type === "message_delta");
const messageStart = chunks.find((c) => c.type === "message_start");
if (messageStart?.message) {
finalChunk = { ...messageStart.message, content: [] };
const blockMap = new Map();
for (const chunk of chunks) {
if (chunk?.type === "content_block_start" && typeof chunk.index === "number") {
blockMap.set(chunk.index, { ...(chunk.content_block || {}) });
}
if (chunk?.type === "content_block_delta" && typeof chunk.index === "number") {
const current = blockMap.get(chunk.index) || { type: "text", text: "" };
if (chunk.delta?.type === "text_delta") {
current.type = current.type || "text";
current.text = `${current.text || ""}${chunk.delta.text || ""}`;
}
blockMap.set(chunk.index, current);
}
}
finalChunk.content = [...blockMap.entries()]
.sort((a, b) => a[0] - b[0])
.map(([, block]) => block);
const startUsage = messageStart.message.usage;
const deltaUsage = messageDelta?.usage;
if (startUsage || deltaUsage) {
finalChunk.usage = {
...(startUsage || {}),
...(deltaUsage || {}),
};
}
if (messageDelta?.delta?.stop_reason !== undefined) {
finalChunk.stop_reason = messageDelta.delta.stop_reason;
}
if (messageDelta?.delta?.stop_sequence !== undefined) {
finalChunk.stop_sequence = messageDelta.delta.stop_sequence;
}
}
}
}
return finalChunk;
}
/** Build a non-streaming Response translated from OpenAI into `sourceFormat`. */
export function createNonStreamingResponse(sourceFormat, model, text?: string) {
const openaiResponse = createOpenAIResponse(model, text);
if (sourceFormat === FORMATS.OPENAI) {
return {
success: true,
response: new Response(JSON.stringify(openaiResponse), {
headers: { "Content-Type": "application/json" },
}),
};
}
const state = initState(sourceFormat);
state.model = model;
const openaiChunks = createOpenAIStreamingChunks(openaiResponse);
const allTranslated: unknown[] = [];
for (const chunk of openaiChunks) {
const translated = translateResponse(FORMATS.OPENAI, sourceFormat, chunk, state);
if (translated?.length > 0) allTranslated.push(...translated);
}
const flushed = translateResponse(FORMATS.OPENAI, sourceFormat, null, state);
if (flushed?.length > 0) allTranslated.push(...flushed);
const finalResponse = mergeChunksToResponse(allTranslated, sourceFormat);
return {
success: true,
response: new Response(JSON.stringify(finalResponse), {
headers: { "Content-Type": "application/json" },
}),
};
}
/** Build a streaming (SSE) Response translated from OpenAI into `sourceFormat`. */
export function createStreamingResponse(sourceFormat, model, text?: string) {
const openaiResponse = createOpenAIResponse(model, text);
const state = initState(sourceFormat);
state.model = model;
const openaiChunks = createOpenAIStreamingChunks(openaiResponse);
const translatedChunks: string[] = [];
for (const chunk of openaiChunks) {
const translated = translateResponse(FORMATS.OPENAI, sourceFormat, chunk, state);
if (translated?.length > 0) {
for (const item of translated) translatedChunks.push(formatSSE(item, sourceFormat));
}
}
const flushed = translateResponse(FORMATS.OPENAI, sourceFormat, null, state);
if (flushed?.length > 0) {
for (const item of flushed) translatedChunks.push(formatSSE(item, sourceFormat));
}
translatedChunks.push("data: [DONE]\n\n");
return {
success: true,
response: new Response(translatedChunks.join(""), {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
}),
};
}

View File

@@ -0,0 +1,74 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { mergeChunksToResponse } from "../../open-sse/utils/bypassResponse.ts";
/**
* Regression guard for the Claude-format non-streaming bypass response bug:
* mergeChunksToResponse() used to return `messageStart.message` as-is, which
* the openai-to-claude translator always initializes with `content: []` —
* the actual text only exists in the separate content_block_start/delta
* events. A synthetic (non-streaming) Claude-format bypass response
* therefore always came back with an empty `content` array, silently
* dropping the bypass text ("CLI Command Execution: Clear Terminal", etc.)
* from every Claude-format client (e.g. the Claude Code CLI).
*/
describe("mergeChunksToResponse (Claude format content reconstruction)", () => {
const chunks = [
{
type: "message_start",
message: {
id: "msg_1",
type: "message",
role: "assistant",
model: "demo",
content: [],
stop_reason: null,
stop_sequence: null,
usage: { input_tokens: 1, cache_read_input_tokens: 2 },
},
},
{ type: "content_block_start", index: 0, content_block: { type: "text", text: "" } },
{ type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "hello world" } },
{ type: "content_block_stop", index: 0 },
{
type: "message_delta",
delta: { stop_reason: "end_turn", stop_sequence: null },
usage: { output_tokens: 3 },
},
{ type: "message_stop" },
];
it("reconstructs the message content from content_block_start/delta chunks", () => {
const result = mergeChunksToResponse(chunks, "claude") as Record<string, unknown>;
assert.equal(result.type, "message");
assert.equal(result.role, "assistant");
assert.deepEqual(result.content, [{ type: "text", text: "hello world" }]);
});
it("merges start + delta usage and carries the final stop_reason", () => {
const result = mergeChunksToResponse(chunks, "claude") as Record<string, unknown>;
assert.equal(result.stop_reason, "end_turn");
assert.deepEqual(result.usage, {
input_tokens: 1,
cache_read_input_tokens: 2,
output_tokens: 3,
});
});
it("falls back to the last chunk untouched for non-Claude formats", () => {
const openaiChunks = [{ type: "chat.completion.chunk", choices: [] }];
assert.equal(mergeChunksToResponse(openaiChunks, "openai"), openaiChunks[0]);
});
it("falls back to a canned unknown response for an empty chunk list", () => {
const result = mergeChunksToResponse([], "claude") as {
model: string;
choices: Array<{ message: { role: string } }>;
};
assert.equal(result.model, "unknown");
assert.equal(result.choices[0].message.role, "assistant");
});
});