refactor(open-sse): extract chunkBuilder, dedup chat.completion.chunk (B1)

Add helpers/chunkBuilder.js; apply to claude/gemini/kiro/ollama/commandcode/
openai-responses response translators. Caller supplies id/created/model so each
keeps exact id-generation + usage semantics. Extend golden response stream to
openai-responses (codex). No behavior change; gate: no regression.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
decolua
2026-06-13 17:05:42 +07:00
parent 0e34358e74
commit 17202f7111
10 changed files with 270 additions and 295 deletions

View File

@@ -0,0 +1,11 @@
// Build OpenAI chat.completion.chunk. Caller supplies id/created/model so each
// translator keeps its exact id-generation + created semantics (no Date.now here).
export function buildChunk({ id, created, model }, delta, finishReason = null) {
return {
id,
object: "chat.completion.chunk",
created,
model,
choices: [{ index: 0, delta, finish_reason: finishReason }],
};
}

View File

@@ -1,19 +1,14 @@
import { register } from "../index.js";
import { FORMATS } from "../formats.js";
import { buildChunk } from "../helpers/chunkBuilder.js";
// Create OpenAI chunk helper
function createChunk(state, delta, finishReason = null) {
return {
id: `chatcmpl-${state.messageId}`,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: state.model,
choices: [{
index: 0,
delta,
finish_reason: finishReason
}]
};
return buildChunk(
{ id: `chatcmpl-${state.messageId}`, created: Math.floor(Date.now() / 1000), model: state.model },
delta,
finishReason
);
}
// Convert Claude stream chunk to OpenAI format
@@ -129,13 +124,7 @@ export function claudeToOpenAIResponse(chunk, state) {
if (chunk.delta?.stop_reason) {
state.finishReason = convertStopReason(chunk.delta.stop_reason);
const finalChunk = {
id: `chatcmpl-${state.messageId}`,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: state.model,
choices: [{ index: 0, delta: {}, finish_reason: state.finishReason }]
};
const finalChunk = createChunk(state, {}, state.finishReason);
if (state.usage) {
finalChunk.usage = {
@@ -169,18 +158,7 @@ export function claudeToOpenAIResponse(chunk, state) {
total_tokens: (state.usage.input_tokens || 0) + (state.usage.output_tokens || 0)
}
} : {};
results.push({
id: `chatcmpl-${state.messageId}`,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: state.model,
choices: [{
index: 0,
delta: {},
finish_reason: finishReason
}],
...usageObj
});
results.push({ ...createChunk(state, {}, finishReason), ...usageObj });
state.finishReasonSent = true;
}
break;

View File

@@ -17,6 +17,7 @@
*/
import { register } from "../index.js";
import { FORMATS } from "../formats.js";
import { buildChunk } from "../helpers/chunkBuilder.js";
function ensureState(state, model) {
if (!state.responseId) {
@@ -34,13 +35,11 @@ function ensureState(state, model) {
}
function makeChunk(state, delta, finishReason = null) {
return {
id: state.responseId,
object: "chat.completion.chunk",
created: state.created,
model: state.model,
choices: [{ index: 0, delta, finish_reason: finishReason }],
};
return buildChunk(
{ id: state.responseId, created: state.created, model: state.model },
delta,
finishReason
);
}
function mapFinishReason(reason) {

View File

@@ -1,5 +1,11 @@
import { register } from "../index.js";
import { FORMATS } from "../formats.js";
import { buildChunk } from "../helpers/chunkBuilder.js";
// Build chunk meta for current gemini state
function chunkMeta(state) {
return { id: `chatcmpl-${state.messageId}`, created: Math.floor(Date.now() / 1000), model: state.model };
}
// Convert Gemini response chunk to OpenAI format
export function geminiToOpenAIResponse(chunk, state) {
@@ -18,17 +24,7 @@ export function geminiToOpenAIResponse(chunk, state) {
state.messageId = response.responseId || `msg_${Date.now()}`;
state.model = response.modelVersion || "gemini";
state.functionIndex = 0;
results.push({
id: `chatcmpl-${state.messageId}`,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: state.model,
choices: [{
index: 0,
delta: { role: "assistant" },
finish_reason: null
}]
});
results.push(buildChunk(chunkMeta(state), { role: "assistant" }, null));
}
// Process parts
@@ -43,19 +39,11 @@ export function geminiToOpenAIResponse(chunk, state) {
const hasFunctionCall = !!part.functionCall;
if (hasTextContent) {
results.push({
id: `chatcmpl-${state.messageId}`,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: state.model,
choices: [{
index: 0,
delta: isThought
? { reasoning_content: part.text }
: { content: part.text },
finish_reason: null
}]
});
results.push(buildChunk(
chunkMeta(state),
isThought ? { reasoning_content: part.text } : { content: part.text },
null
));
}
if (hasFunctionCall) {
@@ -77,17 +65,7 @@ export function geminiToOpenAIResponse(chunk, state) {
state.toolCalls.set(toolCallIndex, toolCall);
results.push({
id: `chatcmpl-${state.messageId}`,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: state.model,
choices: [{
index: 0,
delta: { tool_calls: [toolCall] },
finish_reason: null
}]
});
results.push(buildChunk(chunkMeta(state), { tool_calls: [toolCall] }, null));
}
continue;
}
@@ -97,19 +75,11 @@ export function geminiToOpenAIResponse(chunk, state) {
// can also stream thought parts without a signature; those must not be
// surfaced as normal assistant content in OpenAI-compatible clients.
if (part.text !== undefined && part.text !== "") {
results.push({
id: `chatcmpl-${state.messageId}`,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: state.model,
choices: [{
index: 0,
delta: isThought
? { reasoning_content: part.text }
: { content: part.text },
finish_reason: null
}]
});
results.push(buildChunk(
chunkMeta(state),
isThought ? { reasoning_content: part.text } : { content: part.text },
null
));
}
// Function call
@@ -132,39 +102,23 @@ export function geminiToOpenAIResponse(chunk, state) {
state.toolCalls.set(toolCallIndex, toolCall);
results.push({
id: `chatcmpl-${state.messageId}`,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: state.model,
choices: [{
index: 0,
delta: { tool_calls: [toolCall] },
finish_reason: null
}]
});
results.push(buildChunk(chunkMeta(state), { tool_calls: [toolCall] }, null));
}
// Inline data (images)
const inlineData = part.inlineData || part.inline_data;
if (inlineData?.data) {
const mimeType = inlineData.mimeType || inlineData.mime_type || "image/png";
results.push({
id: `chatcmpl-${state.messageId}`,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: state.model,
choices: [{
index: 0,
delta: {
images: [{
type: "image_url",
image_url: { url: `data:${mimeType};base64,${inlineData.data}` }
}]
},
finish_reason: null
}]
});
results.push(buildChunk(
chunkMeta(state),
{
images: [{
type: "image_url",
image_url: { url: `data:${mimeType};base64,${inlineData.data}` }
}]
},
null
));
}
}
}
@@ -218,17 +172,7 @@ export function geminiToOpenAIResponse(chunk, state) {
finishReason = "tool_calls";
}
const finalChunk = {
id: `chatcmpl-${state.messageId}`,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: state.model,
choices: [{
index: 0,
delta: {},
finish_reason: finishReason
}]
};
const finalChunk = buildChunk(chunkMeta(state), {}, finishReason);
// Include usage in final chunk for downstream translators
if (state.usage) {

View File

@@ -4,6 +4,12 @@
*/
import { register } from "../index.js";
import { FORMATS } from "../formats.js";
import { buildChunk } from "../helpers/chunkBuilder.js";
// Build chunk meta for current kiro state
function chunkMeta(state) {
return { id: state.responseId, created: state.created, model: state.model || "kiro" };
}
/**
* Parse Kiro SSE event and convert to OpenAI format
@@ -66,20 +72,10 @@ export function convertKiroToOpenAI(chunk, state) {
const content = data.assistantResponseEvent?.content || data.content || "";
if (!content) return null;
const openaiChunk = {
id: state.responseId,
object: "chat.completion.chunk",
created: state.created,
model: state.model || "kiro",
choices: [{
index: 0,
delta: {
...(state.chunkIndex === 0 ? { role: "assistant" } : {}),
content: content
},
finish_reason: null
}]
};
const openaiChunk = buildChunk(chunkMeta(state), {
...(state.chunkIndex === 0 ? { role: "assistant" } : {}),
content: content
}, null);
state.chunkIndex++;
return openaiChunk;
@@ -97,20 +93,10 @@ export function convertKiroToOpenAI(chunk, state) {
: (reasoning.text || reasoning.content || data.content || "");
if (!content) return null;
const openaiChunk = {
id: state.responseId,
object: "chat.completion.chunk",
created: state.created,
model: state.model || "kiro",
choices: [{
index: 0,
delta: {
...(state.chunkIndex === 0 ? { role: "assistant" } : {}),
reasoning_content: content
},
finish_reason: null
}]
};
const openaiChunk = buildChunk(chunkMeta(state), {
...(state.chunkIndex === 0 ? { role: "assistant" } : {}),
reasoning_content: content
}, null);
state.chunkIndex++;
return openaiChunk;
@@ -123,28 +109,18 @@ export function convertKiroToOpenAI(chunk, state) {
const toolName = toolUse.name || "";
const toolInput = toolUse.input || {};
const openaiChunk = {
id: state.responseId,
object: "chat.completion.chunk",
created: state.created,
model: state.model || "kiro",
choices: [{
const openaiChunk = buildChunk(chunkMeta(state), {
...(state.chunkIndex === 0 ? { role: "assistant" } : {}),
tool_calls: [{
index: 0,
delta: {
...(state.chunkIndex === 0 ? { role: "assistant" } : {}),
tool_calls: [{
index: 0,
id: toolCallId,
type: "function",
function: {
name: toolName,
arguments: JSON.stringify(toolInput)
}
}]
},
finish_reason: null
id: toolCallId,
type: "function",
function: {
name: toolName,
arguments: JSON.stringify(toolInput)
}
}]
};
}, null);
state.chunkIndex++;
return openaiChunk;
@@ -154,17 +130,7 @@ export function convertKiroToOpenAI(chunk, state) {
if (eventType === "messageStopEvent" || eventType === "done" || data.messageStopEvent) {
state.finishReason = "stop"; // Mark for usage injection in stream.js
const openaiChunk = {
id: state.responseId,
object: "chat.completion.chunk",
created: state.created,
model: state.model || "kiro",
choices: [{
index: 0,
delta: {},
finish_reason: "stop"
}]
};
const openaiChunk = buildChunk(chunkMeta(state), {}, "stop");
// Include usage in final chunk if available
if (state.usage && typeof state.usage === "object") {

View File

@@ -1,5 +1,6 @@
import { register } from "../index.js";
import { FORMATS } from "../formats.js";
import { buildChunk } from "../helpers/chunkBuilder.js";
/**
* Convert Ollama NDJSON response to OpenAI SSE format
@@ -36,18 +37,9 @@ export function ollamaToOpenAI(chunk, state) {
finishReason = "tool_calls";
}
return {
id: id,
object: "chat.completion.chunk",
created: created,
model: model,
choices: [{
index: 0,
delta: {},
finish_reason: finishReason
}],
usage: usage
};
const doneChunk = buildChunk({ id, created, model }, {}, finishReason);
doneChunk.usage = usage;
return doneChunk;
}
// Content chunk
@@ -79,17 +71,7 @@ export function ollamaToOpenAI(chunk, state) {
delta.tool_calls = convertToolCalls(toolCalls);
}
return {
id: id,
object: "chat.completion.chunk",
created: created,
model: model,
choices: [{
index: 0,
delta: delta,
finish_reason: null
}]
};
return buildChunk({ id, created, model }, delta, null);
}
/**

View File

@@ -4,6 +4,7 @@
*/
import { register } from "../index.js";
import { FORMATS } from "../formats.js";
import { buildChunk } from "../helpers/chunkBuilder.js";
/**
* Translate OpenAI chunk to Responses API events
@@ -377,17 +378,11 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
state.finishReasonSent = true;
state.finishReason = finishReason;
const finalChunk = {
id: state.chatId || `chatcmpl-${Date.now()}`,
object: "chat.completion.chunk",
created: state.created || Math.floor(Date.now() / 1000),
model: state.model || "unknown",
choices: [{
index: 0,
delta: {},
finish_reason: finishReason
}]
};
const finalChunk = buildChunk(
{ id: state.chatId || `chatcmpl-${Date.now()}`, created: state.created || Math.floor(Date.now() / 1000), model: state.model || "unknown" },
{},
finishReason
);
if (state.usage && typeof state.usage === "object") {
finalChunk.usage = state.usage;
@@ -414,17 +409,10 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
const delta = data.delta || "";
if (!delta) return null;
return {
id: state.chatId,
object: "chat.completion.chunk",
created: state.created,
model: state.model || "unknown",
choices: [{
index: 0,
delta: { content: delta },
finish_reason: null
}]
};
return buildChunk(
{ id: state.chatId, created: state.created, model: state.model || "unknown" },
{ content: delta }
);
}
// Text content done (ignore, we handle via delta)
@@ -437,27 +425,17 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
const item = data.item;
state.currentToolCallId = item.call_id || `call_${Date.now()}`;
return {
id: state.chatId,
object: "chat.completion.chunk",
created: state.created,
model: state.model || "unknown",
choices: [{
index: 0,
delta: {
tool_calls: [{
index: state.toolCallIndex,
id: state.currentToolCallId,
type: "function",
function: {
name: item.name || "",
arguments: ""
}
}]
},
finish_reason: null
}]
};
return buildChunk(
{ id: state.chatId, created: state.created, model: state.model || "unknown" },
{
tool_calls: [{
index: state.toolCallIndex,
id: state.currentToolCallId,
type: "function",
function: { name: item.name || "", arguments: "" }
}]
}
);
}
// Function call arguments delta (standard or custom_tool_call variant)
@@ -465,22 +443,10 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
const argsDelta = data.delta || "";
if (!argsDelta) return null;
return {
id: state.chatId,
object: "chat.completion.chunk",
created: state.created,
model: state.model || "unknown",
choices: [{
index: 0,
delta: {
tool_calls: [{
index: state.toolCallIndex,
function: { arguments: argsDelta }
}]
},
finish_reason: null
}]
};
return buildChunk(
{ id: state.chatId, created: state.created, model: state.model || "unknown" },
{ tool_calls: [{ index: state.toolCallIndex, function: { arguments: argsDelta } }] }
);
}
// Function call done (standard or custom_tool_call variant)
@@ -520,18 +486,12 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
state.finishReasonSent = true;
state.finishReason = finishReason; // Mark for usage injection in stream.js
const finalChunk = {
id: state.chatId,
object: "chat.completion.chunk",
created: state.created,
model: state.model || "unknown",
choices: [{
index: 0,
delta: {},
finish_reason: finishReason
}]
};
const finalChunk = buildChunk(
{ id: state.chatId, created: state.created, model: state.model || "unknown" },
{},
finishReason
);
// Include usage in final chunk if available
if (state.usage && typeof state.usage === "object") {
finalChunk.usage = state.usage;
@@ -553,17 +513,11 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
state.finishReasonSent = true;
// Surface the error as an OpenAI-compatible error chunk
return {
id: state.chatId || `chatcmpl-${Date.now()}`,
object: "chat.completion.chunk",
created: state.created || Math.floor(Date.now() / 1000),
model: state.model || "unknown",
choices: [{
index: 0,
delta: { content: `[Error] ${error.message || JSON.stringify(error)}` },
finish_reason: "stop"
}]
};
return buildChunk(
{ id: state.chatId || `chatcmpl-${Date.now()}`, created: state.created || Math.floor(Date.now() / 1000), model: state.model || "unknown" },
{ content: `[Error] ${error.message || JSON.stringify(error)}` },
"stop"
);
}
return null;
}
@@ -572,13 +526,10 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
if (eventType === "response.reasoning_summary_text.delta") {
const delta = data.delta || "";
if (!delta) return null;
return {
id: state.chatId,
object: "chat.completion.chunk",
created: state.created,
model: state.model || "unknown",
choices: [{ index: 0, delta: { reasoning_content: delta }, finish_reason: null }]
};
return buildChunk(
{ id: state.chatId, created: state.created, model: state.model || "unknown" },
{ reasoning_content: delta }
);
}
// Ignore other events

File diff suppressed because one or more lines are too long

View File

@@ -445,3 +445,126 @@ exports[`GOLDEN response stream: Ollama → OpenAI > content + thinking + tool_c
},
]
`;
exports[`GOLDEN response stream: OpenAI-Responses (codex) → OpenAI > error event → error chunk (fallback id/created) 1`] = `
[
{
"choices": [
{
"delta": {
"content": "[Error] model_not_found",
},
"finish_reason": "stop",
"index": 0,
},
],
"created": 0,
"id": "chatcmpl-<TS>",
"model": "unknown",
"object": "chat.completion.chunk",
},
]
`;
exports[`GOLDEN response stream: OpenAI-Responses (codex) → OpenAI > text + reasoning + tool_call + completed usage 1`] = `
[
{
"choices": [
{
"delta": {
"content": "Hello",
},
"finish_reason": null,
"index": 0,
},
],
"created": 0,
"id": "chatcmpl-<TS>",
"model": "unknown",
"object": "chat.completion.chunk",
},
{
"choices": [
{
"delta": {
"reasoning_content": "thinking",
},
"finish_reason": null,
"index": 0,
},
],
"created": 0,
"id": "chatcmpl-<TS>",
"model": "unknown",
"object": "chat.completion.chunk",
},
{
"choices": [
{
"delta": {
"tool_calls": [
{
"function": {
"arguments": "",
"name": "get_weather",
},
"id": "call_1",
"index": 0,
"type": "function",
},
],
},
"finish_reason": null,
"index": 0,
},
],
"created": 0,
"id": "chatcmpl-<TS>",
"model": "unknown",
"object": "chat.completion.chunk",
},
{
"choices": [
{
"delta": {
"tool_calls": [
{
"function": {
"arguments": "{"city":"NYC"}",
},
"index": 0,
},
],
},
"finish_reason": null,
"index": 0,
},
],
"created": 0,
"id": "chatcmpl-<TS>",
"model": "unknown",
"object": "chat.completion.chunk",
},
{
"choices": [
{
"delta": {},
"finish_reason": "tool_calls",
"index": 0,
},
],
"created": 0,
"id": "chatcmpl-<TS>",
"model": "unknown",
"object": "chat.completion.chunk",
"usage": {
"completion_tokens": 5,
"prompt_tokens": 10,
"prompt_tokens_details": {
"cached_tokens": 3,
},
"total_tokens": 15,
},
},
]
`;

View File

@@ -95,3 +95,24 @@ describe("GOLDEN response stream: Ollama → OpenAI", () => {
expect(runStream(FORMATS.OLLAMA, FORMATS.OPENAI, events)).toMatchSnapshot();
});
});
describe("GOLDEN response stream: OpenAI-Responses (codex) → OpenAI", () => {
it("text + reasoning + tool_call + completed usage", () => {
const events = [
{ type: "response.output_text.delta", delta: "Hello" },
{ type: "response.reasoning_summary_text.delta", delta: "thinking" },
{ type: "response.output_item.added", item: { type: "function_call", call_id: "call_1", name: "get_weather" } },
{ type: "response.function_call_arguments.delta", delta: '{"city":"NYC"}' },
{ type: "response.output_item.done", item: { type: "function_call" } },
{ type: "response.completed", response: { usage: { input_tokens: 10, output_tokens: 5, input_tokens_details: { cached_tokens: 3 } } } },
];
expect(runStream(FORMATS.OPENAI_RESPONSES, FORMATS.OPENAI, events)).toMatchSnapshot();
});
it("error event → error chunk (fallback id/created)", () => {
const events = [
{ type: "error", error: { message: "model_not_found" } },
];
expect(runStream(FORMATS.OPENAI_RESPONSES, FORMATS.OPENAI, events)).toMatchSnapshot();
});
});