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:
parent
0e34358e74
commit
17202f7111
10 changed files with 270 additions and 295 deletions
11
open-sse/translator/helpers/chunkBuilder.js
Normal file
11
open-sse/translator/helpers/chunkBuilder.js
Normal 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 }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,19 +1,14 @@
|
||||||
import { register } from "../index.js";
|
import { register } from "../index.js";
|
||||||
import { FORMATS } from "../formats.js";
|
import { FORMATS } from "../formats.js";
|
||||||
|
import { buildChunk } from "../helpers/chunkBuilder.js";
|
||||||
|
|
||||||
// Create OpenAI chunk helper
|
// Create OpenAI chunk helper
|
||||||
function createChunk(state, delta, finishReason = null) {
|
function createChunk(state, delta, finishReason = null) {
|
||||||
return {
|
return buildChunk(
|
||||||
id: `chatcmpl-${state.messageId}`,
|
{ id: `chatcmpl-${state.messageId}`, created: Math.floor(Date.now() / 1000), model: state.model },
|
||||||
object: "chat.completion.chunk",
|
delta,
|
||||||
created: Math.floor(Date.now() / 1000),
|
finishReason
|
||||||
model: state.model,
|
);
|
||||||
choices: [{
|
|
||||||
index: 0,
|
|
||||||
delta,
|
|
||||||
finish_reason: finishReason
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert Claude stream chunk to OpenAI format
|
// Convert Claude stream chunk to OpenAI format
|
||||||
|
|
@ -129,13 +124,7 @@ export function claudeToOpenAIResponse(chunk, state) {
|
||||||
|
|
||||||
if (chunk.delta?.stop_reason) {
|
if (chunk.delta?.stop_reason) {
|
||||||
state.finishReason = convertStopReason(chunk.delta.stop_reason);
|
state.finishReason = convertStopReason(chunk.delta.stop_reason);
|
||||||
const finalChunk = {
|
const finalChunk = createChunk(state, {}, state.finishReason);
|
||||||
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 }]
|
|
||||||
};
|
|
||||||
|
|
||||||
if (state.usage) {
|
if (state.usage) {
|
||||||
finalChunk.usage = {
|
finalChunk.usage = {
|
||||||
|
|
@ -169,18 +158,7 @@ export function claudeToOpenAIResponse(chunk, state) {
|
||||||
total_tokens: (state.usage.input_tokens || 0) + (state.usage.output_tokens || 0)
|
total_tokens: (state.usage.input_tokens || 0) + (state.usage.output_tokens || 0)
|
||||||
}
|
}
|
||||||
} : {};
|
} : {};
|
||||||
results.push({
|
results.push({ ...createChunk(state, {}, finishReason), ...usageObj });
|
||||||
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
|
|
||||||
});
|
|
||||||
state.finishReasonSent = true;
|
state.finishReasonSent = true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
*/
|
*/
|
||||||
import { register } from "../index.js";
|
import { register } from "../index.js";
|
||||||
import { FORMATS } from "../formats.js";
|
import { FORMATS } from "../formats.js";
|
||||||
|
import { buildChunk } from "../helpers/chunkBuilder.js";
|
||||||
|
|
||||||
function ensureState(state, model) {
|
function ensureState(state, model) {
|
||||||
if (!state.responseId) {
|
if (!state.responseId) {
|
||||||
|
|
@ -34,13 +35,11 @@ function ensureState(state, model) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeChunk(state, delta, finishReason = null) {
|
function makeChunk(state, delta, finishReason = null) {
|
||||||
return {
|
return buildChunk(
|
||||||
id: state.responseId,
|
{ id: state.responseId, created: state.created, model: state.model },
|
||||||
object: "chat.completion.chunk",
|
delta,
|
||||||
created: state.created,
|
finishReason
|
||||||
model: state.model,
|
);
|
||||||
choices: [{ index: 0, delta, finish_reason: finishReason }],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapFinishReason(reason) {
|
function mapFinishReason(reason) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
import { register } from "../index.js";
|
import { register } from "../index.js";
|
||||||
import { FORMATS } from "../formats.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
|
// Convert Gemini response chunk to OpenAI format
|
||||||
export function geminiToOpenAIResponse(chunk, state) {
|
export function geminiToOpenAIResponse(chunk, state) {
|
||||||
|
|
@ -18,17 +24,7 @@ export function geminiToOpenAIResponse(chunk, state) {
|
||||||
state.messageId = response.responseId || `msg_${Date.now()}`;
|
state.messageId = response.responseId || `msg_${Date.now()}`;
|
||||||
state.model = response.modelVersion || "gemini";
|
state.model = response.modelVersion || "gemini";
|
||||||
state.functionIndex = 0;
|
state.functionIndex = 0;
|
||||||
results.push({
|
results.push(buildChunk(chunkMeta(state), { role: "assistant" }, null));
|
||||||
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
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process parts
|
// Process parts
|
||||||
|
|
@ -43,19 +39,11 @@ export function geminiToOpenAIResponse(chunk, state) {
|
||||||
const hasFunctionCall = !!part.functionCall;
|
const hasFunctionCall = !!part.functionCall;
|
||||||
|
|
||||||
if (hasTextContent) {
|
if (hasTextContent) {
|
||||||
results.push({
|
results.push(buildChunk(
|
||||||
id: `chatcmpl-${state.messageId}`,
|
chunkMeta(state),
|
||||||
object: "chat.completion.chunk",
|
isThought ? { reasoning_content: part.text } : { content: part.text },
|
||||||
created: Math.floor(Date.now() / 1000),
|
null
|
||||||
model: state.model,
|
));
|
||||||
choices: [{
|
|
||||||
index: 0,
|
|
||||||
delta: isThought
|
|
||||||
? { reasoning_content: part.text }
|
|
||||||
: { content: part.text },
|
|
||||||
finish_reason: null
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasFunctionCall) {
|
if (hasFunctionCall) {
|
||||||
|
|
@ -77,17 +65,7 @@ export function geminiToOpenAIResponse(chunk, state) {
|
||||||
|
|
||||||
state.toolCalls.set(toolCallIndex, toolCall);
|
state.toolCalls.set(toolCallIndex, toolCall);
|
||||||
|
|
||||||
results.push({
|
results.push(buildChunk(chunkMeta(state), { tool_calls: [toolCall] }, null));
|
||||||
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
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -97,19 +75,11 @@ export function geminiToOpenAIResponse(chunk, state) {
|
||||||
// can also stream thought parts without a signature; those must not be
|
// can also stream thought parts without a signature; those must not be
|
||||||
// surfaced as normal assistant content in OpenAI-compatible clients.
|
// surfaced as normal assistant content in OpenAI-compatible clients.
|
||||||
if (part.text !== undefined && part.text !== "") {
|
if (part.text !== undefined && part.text !== "") {
|
||||||
results.push({
|
results.push(buildChunk(
|
||||||
id: `chatcmpl-${state.messageId}`,
|
chunkMeta(state),
|
||||||
object: "chat.completion.chunk",
|
isThought ? { reasoning_content: part.text } : { content: part.text },
|
||||||
created: Math.floor(Date.now() / 1000),
|
null
|
||||||
model: state.model,
|
));
|
||||||
choices: [{
|
|
||||||
index: 0,
|
|
||||||
delta: isThought
|
|
||||||
? { reasoning_content: part.text }
|
|
||||||
: { content: part.text },
|
|
||||||
finish_reason: null
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function call
|
// Function call
|
||||||
|
|
@ -132,39 +102,23 @@ export function geminiToOpenAIResponse(chunk, state) {
|
||||||
|
|
||||||
state.toolCalls.set(toolCallIndex, toolCall);
|
state.toolCalls.set(toolCallIndex, toolCall);
|
||||||
|
|
||||||
results.push({
|
results.push(buildChunk(chunkMeta(state), { tool_calls: [toolCall] }, null));
|
||||||
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
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inline data (images)
|
// Inline data (images)
|
||||||
const inlineData = part.inlineData || part.inline_data;
|
const inlineData = part.inlineData || part.inline_data;
|
||||||
if (inlineData?.data) {
|
if (inlineData?.data) {
|
||||||
const mimeType = inlineData.mimeType || inlineData.mime_type || "image/png";
|
const mimeType = inlineData.mimeType || inlineData.mime_type || "image/png";
|
||||||
results.push({
|
results.push(buildChunk(
|
||||||
id: `chatcmpl-${state.messageId}`,
|
chunkMeta(state),
|
||||||
object: "chat.completion.chunk",
|
{
|
||||||
created: Math.floor(Date.now() / 1000),
|
images: [{
|
||||||
model: state.model,
|
type: "image_url",
|
||||||
choices: [{
|
image_url: { url: `data:${mimeType};base64,${inlineData.data}` }
|
||||||
index: 0,
|
}]
|
||||||
delta: {
|
},
|
||||||
images: [{
|
null
|
||||||
type: "image_url",
|
));
|
||||||
image_url: { url: `data:${mimeType};base64,${inlineData.data}` }
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
finish_reason: null
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -218,17 +172,7 @@ export function geminiToOpenAIResponse(chunk, state) {
|
||||||
finishReason = "tool_calls";
|
finishReason = "tool_calls";
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalChunk = {
|
const finalChunk = buildChunk(chunkMeta(state), {}, finishReason);
|
||||||
id: `chatcmpl-${state.messageId}`,
|
|
||||||
object: "chat.completion.chunk",
|
|
||||||
created: Math.floor(Date.now() / 1000),
|
|
||||||
model: state.model,
|
|
||||||
choices: [{
|
|
||||||
index: 0,
|
|
||||||
delta: {},
|
|
||||||
finish_reason: finishReason
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
|
|
||||||
// Include usage in final chunk for downstream translators
|
// Include usage in final chunk for downstream translators
|
||||||
if (state.usage) {
|
if (state.usage) {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,12 @@
|
||||||
*/
|
*/
|
||||||
import { register } from "../index.js";
|
import { register } from "../index.js";
|
||||||
import { FORMATS } from "../formats.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
|
* 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 || "";
|
const content = data.assistantResponseEvent?.content || data.content || "";
|
||||||
if (!content) return null;
|
if (!content) return null;
|
||||||
|
|
||||||
const openaiChunk = {
|
const openaiChunk = buildChunk(chunkMeta(state), {
|
||||||
id: state.responseId,
|
...(state.chunkIndex === 0 ? { role: "assistant" } : {}),
|
||||||
object: "chat.completion.chunk",
|
content: content
|
||||||
created: state.created,
|
}, null);
|
||||||
model: state.model || "kiro",
|
|
||||||
choices: [{
|
|
||||||
index: 0,
|
|
||||||
delta: {
|
|
||||||
...(state.chunkIndex === 0 ? { role: "assistant" } : {}),
|
|
||||||
content: content
|
|
||||||
},
|
|
||||||
finish_reason: null
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
|
|
||||||
state.chunkIndex++;
|
state.chunkIndex++;
|
||||||
return openaiChunk;
|
return openaiChunk;
|
||||||
|
|
@ -97,20 +93,10 @@ export function convertKiroToOpenAI(chunk, state) {
|
||||||
: (reasoning.text || reasoning.content || data.content || "");
|
: (reasoning.text || reasoning.content || data.content || "");
|
||||||
if (!content) return null;
|
if (!content) return null;
|
||||||
|
|
||||||
const openaiChunk = {
|
const openaiChunk = buildChunk(chunkMeta(state), {
|
||||||
id: state.responseId,
|
...(state.chunkIndex === 0 ? { role: "assistant" } : {}),
|
||||||
object: "chat.completion.chunk",
|
reasoning_content: content
|
||||||
created: state.created,
|
}, null);
|
||||||
model: state.model || "kiro",
|
|
||||||
choices: [{
|
|
||||||
index: 0,
|
|
||||||
delta: {
|
|
||||||
...(state.chunkIndex === 0 ? { role: "assistant" } : {}),
|
|
||||||
reasoning_content: content
|
|
||||||
},
|
|
||||||
finish_reason: null
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
|
|
||||||
state.chunkIndex++;
|
state.chunkIndex++;
|
||||||
return openaiChunk;
|
return openaiChunk;
|
||||||
|
|
@ -123,28 +109,18 @@ export function convertKiroToOpenAI(chunk, state) {
|
||||||
const toolName = toolUse.name || "";
|
const toolName = toolUse.name || "";
|
||||||
const toolInput = toolUse.input || {};
|
const toolInput = toolUse.input || {};
|
||||||
|
|
||||||
const openaiChunk = {
|
const openaiChunk = buildChunk(chunkMeta(state), {
|
||||||
id: state.responseId,
|
...(state.chunkIndex === 0 ? { role: "assistant" } : {}),
|
||||||
object: "chat.completion.chunk",
|
tool_calls: [{
|
||||||
created: state.created,
|
|
||||||
model: state.model || "kiro",
|
|
||||||
choices: [{
|
|
||||||
index: 0,
|
index: 0,
|
||||||
delta: {
|
id: toolCallId,
|
||||||
...(state.chunkIndex === 0 ? { role: "assistant" } : {}),
|
type: "function",
|
||||||
tool_calls: [{
|
function: {
|
||||||
index: 0,
|
name: toolName,
|
||||||
id: toolCallId,
|
arguments: JSON.stringify(toolInput)
|
||||||
type: "function",
|
}
|
||||||
function: {
|
|
||||||
name: toolName,
|
|
||||||
arguments: JSON.stringify(toolInput)
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
finish_reason: null
|
|
||||||
}]
|
}]
|
||||||
};
|
}, null);
|
||||||
|
|
||||||
state.chunkIndex++;
|
state.chunkIndex++;
|
||||||
return openaiChunk;
|
return openaiChunk;
|
||||||
|
|
@ -154,17 +130,7 @@ export function convertKiroToOpenAI(chunk, state) {
|
||||||
if (eventType === "messageStopEvent" || eventType === "done" || data.messageStopEvent) {
|
if (eventType === "messageStopEvent" || eventType === "done" || data.messageStopEvent) {
|
||||||
state.finishReason = "stop"; // Mark for usage injection in stream.js
|
state.finishReason = "stop"; // Mark for usage injection in stream.js
|
||||||
|
|
||||||
const openaiChunk = {
|
const openaiChunk = buildChunk(chunkMeta(state), {}, "stop");
|
||||||
id: state.responseId,
|
|
||||||
object: "chat.completion.chunk",
|
|
||||||
created: state.created,
|
|
||||||
model: state.model || "kiro",
|
|
||||||
choices: [{
|
|
||||||
index: 0,
|
|
||||||
delta: {},
|
|
||||||
finish_reason: "stop"
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
|
|
||||||
// Include usage in final chunk if available
|
// Include usage in final chunk if available
|
||||||
if (state.usage && typeof state.usage === "object") {
|
if (state.usage && typeof state.usage === "object") {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { register } from "../index.js";
|
import { register } from "../index.js";
|
||||||
import { FORMATS } from "../formats.js";
|
import { FORMATS } from "../formats.js";
|
||||||
|
import { buildChunk } from "../helpers/chunkBuilder.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert Ollama NDJSON response to OpenAI SSE format
|
* Convert Ollama NDJSON response to OpenAI SSE format
|
||||||
|
|
@ -36,18 +37,9 @@ export function ollamaToOpenAI(chunk, state) {
|
||||||
finishReason = "tool_calls";
|
finishReason = "tool_calls";
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const doneChunk = buildChunk({ id, created, model }, {}, finishReason);
|
||||||
id: id,
|
doneChunk.usage = usage;
|
||||||
object: "chat.completion.chunk",
|
return doneChunk;
|
||||||
created: created,
|
|
||||||
model: model,
|
|
||||||
choices: [{
|
|
||||||
index: 0,
|
|
||||||
delta: {},
|
|
||||||
finish_reason: finishReason
|
|
||||||
}],
|
|
||||||
usage: usage
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Content chunk
|
// Content chunk
|
||||||
|
|
@ -79,17 +71,7 @@ export function ollamaToOpenAI(chunk, state) {
|
||||||
delta.tool_calls = convertToolCalls(toolCalls);
|
delta.tool_calls = convertToolCalls(toolCalls);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return buildChunk({ id, created, model }, delta, null);
|
||||||
id: id,
|
|
||||||
object: "chat.completion.chunk",
|
|
||||||
created: created,
|
|
||||||
model: model,
|
|
||||||
choices: [{
|
|
||||||
index: 0,
|
|
||||||
delta: delta,
|
|
||||||
finish_reason: null
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
*/
|
*/
|
||||||
import { register } from "../index.js";
|
import { register } from "../index.js";
|
||||||
import { FORMATS } from "../formats.js";
|
import { FORMATS } from "../formats.js";
|
||||||
|
import { buildChunk } from "../helpers/chunkBuilder.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Translate OpenAI chunk to Responses API events
|
* Translate OpenAI chunk to Responses API events
|
||||||
|
|
@ -377,17 +378,11 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
|
||||||
state.finishReasonSent = true;
|
state.finishReasonSent = true;
|
||||||
state.finishReason = finishReason;
|
state.finishReason = finishReason;
|
||||||
|
|
||||||
const finalChunk = {
|
const finalChunk = buildChunk(
|
||||||
id: state.chatId || `chatcmpl-${Date.now()}`,
|
{ id: state.chatId || `chatcmpl-${Date.now()}`, created: state.created || Math.floor(Date.now() / 1000), model: state.model || "unknown" },
|
||||||
object: "chat.completion.chunk",
|
{},
|
||||||
created: state.created || Math.floor(Date.now() / 1000),
|
finishReason
|
||||||
model: state.model || "unknown",
|
);
|
||||||
choices: [{
|
|
||||||
index: 0,
|
|
||||||
delta: {},
|
|
||||||
finish_reason: finishReason
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
|
|
||||||
if (state.usage && typeof state.usage === "object") {
|
if (state.usage && typeof state.usage === "object") {
|
||||||
finalChunk.usage = state.usage;
|
finalChunk.usage = state.usage;
|
||||||
|
|
@ -414,17 +409,10 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
|
||||||
const delta = data.delta || "";
|
const delta = data.delta || "";
|
||||||
if (!delta) return null;
|
if (!delta) return null;
|
||||||
|
|
||||||
return {
|
return buildChunk(
|
||||||
id: state.chatId,
|
{ id: state.chatId, created: state.created, model: state.model || "unknown" },
|
||||||
object: "chat.completion.chunk",
|
{ content: delta }
|
||||||
created: state.created,
|
);
|
||||||
model: state.model || "unknown",
|
|
||||||
choices: [{
|
|
||||||
index: 0,
|
|
||||||
delta: { content: delta },
|
|
||||||
finish_reason: null
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Text content done (ignore, we handle via delta)
|
// Text content done (ignore, we handle via delta)
|
||||||
|
|
@ -437,27 +425,17 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
|
||||||
const item = data.item;
|
const item = data.item;
|
||||||
state.currentToolCallId = item.call_id || `call_${Date.now()}`;
|
state.currentToolCallId = item.call_id || `call_${Date.now()}`;
|
||||||
|
|
||||||
return {
|
return buildChunk(
|
||||||
id: state.chatId,
|
{ id: state.chatId, created: state.created, model: state.model || "unknown" },
|
||||||
object: "chat.completion.chunk",
|
{
|
||||||
created: state.created,
|
tool_calls: [{
|
||||||
model: state.model || "unknown",
|
index: state.toolCallIndex,
|
||||||
choices: [{
|
id: state.currentToolCallId,
|
||||||
index: 0,
|
type: "function",
|
||||||
delta: {
|
function: { name: item.name || "", arguments: "" }
|
||||||
tool_calls: [{
|
}]
|
||||||
index: state.toolCallIndex,
|
}
|
||||||
id: state.currentToolCallId,
|
);
|
||||||
type: "function",
|
|
||||||
function: {
|
|
||||||
name: item.name || "",
|
|
||||||
arguments: ""
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
finish_reason: null
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function call arguments delta (standard or custom_tool_call variant)
|
// Function call arguments delta (standard or custom_tool_call variant)
|
||||||
|
|
@ -465,22 +443,10 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
|
||||||
const argsDelta = data.delta || "";
|
const argsDelta = data.delta || "";
|
||||||
if (!argsDelta) return null;
|
if (!argsDelta) return null;
|
||||||
|
|
||||||
return {
|
return buildChunk(
|
||||||
id: state.chatId,
|
{ id: state.chatId, created: state.created, model: state.model || "unknown" },
|
||||||
object: "chat.completion.chunk",
|
{ tool_calls: [{ index: state.toolCallIndex, function: { arguments: argsDelta } }] }
|
||||||
created: state.created,
|
);
|
||||||
model: state.model || "unknown",
|
|
||||||
choices: [{
|
|
||||||
index: 0,
|
|
||||||
delta: {
|
|
||||||
tool_calls: [{
|
|
||||||
index: state.toolCallIndex,
|
|
||||||
function: { arguments: argsDelta }
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
finish_reason: null
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function call done (standard or custom_tool_call variant)
|
// Function call done (standard or custom_tool_call variant)
|
||||||
|
|
@ -520,18 +486,12 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
|
||||||
state.finishReasonSent = true;
|
state.finishReasonSent = true;
|
||||||
state.finishReason = finishReason; // Mark for usage injection in stream.js
|
state.finishReason = finishReason; // Mark for usage injection in stream.js
|
||||||
|
|
||||||
const finalChunk = {
|
const finalChunk = buildChunk(
|
||||||
id: state.chatId,
|
{ id: state.chatId, created: state.created, model: state.model || "unknown" },
|
||||||
object: "chat.completion.chunk",
|
{},
|
||||||
created: state.created,
|
finishReason
|
||||||
model: state.model || "unknown",
|
);
|
||||||
choices: [{
|
|
||||||
index: 0,
|
|
||||||
delta: {},
|
|
||||||
finish_reason: finishReason
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
|
|
||||||
// Include usage in final chunk if available
|
// Include usage in final chunk if available
|
||||||
if (state.usage && typeof state.usage === "object") {
|
if (state.usage && typeof state.usage === "object") {
|
||||||
finalChunk.usage = state.usage;
|
finalChunk.usage = state.usage;
|
||||||
|
|
@ -553,17 +513,11 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
|
||||||
state.finishReasonSent = true;
|
state.finishReasonSent = true;
|
||||||
|
|
||||||
// Surface the error as an OpenAI-compatible error chunk
|
// Surface the error as an OpenAI-compatible error chunk
|
||||||
return {
|
return buildChunk(
|
||||||
id: state.chatId || `chatcmpl-${Date.now()}`,
|
{ id: state.chatId || `chatcmpl-${Date.now()}`, created: state.created || Math.floor(Date.now() / 1000), model: state.model || "unknown" },
|
||||||
object: "chat.completion.chunk",
|
{ content: `[Error] ${error.message || JSON.stringify(error)}` },
|
||||||
created: state.created || Math.floor(Date.now() / 1000),
|
"stop"
|
||||||
model: state.model || "unknown",
|
);
|
||||||
choices: [{
|
|
||||||
index: 0,
|
|
||||||
delta: { content: `[Error] ${error.message || JSON.stringify(error)}` },
|
|
||||||
finish_reason: "stop"
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -572,13 +526,10 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
|
||||||
if (eventType === "response.reasoning_summary_text.delta") {
|
if (eventType === "response.reasoning_summary_text.delta") {
|
||||||
const delta = data.delta || "";
|
const delta = data.delta || "";
|
||||||
if (!delta) return null;
|
if (!delta) return null;
|
||||||
return {
|
return buildChunk(
|
||||||
id: state.chatId,
|
{ id: state.chatId, created: state.created, model: state.model || "unknown" },
|
||||||
object: "chat.completion.chunk",
|
{ reasoning_content: delta }
|
||||||
created: state.created,
|
);
|
||||||
model: state.model || "unknown",
|
|
||||||
choices: [{ index: 0, delta: { reasoning_content: delta }, finish_reason: null }]
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore other events
|
// Ignore other events
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
|
||||||
|
|
@ -95,3 +95,24 @@ describe("GOLDEN response stream: Ollama → OpenAI", () => {
|
||||||
expect(runStream(FORMATS.OLLAMA, FORMATS.OPENAI, events)).toMatchSnapshot();
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue