fix(sse): normalize numeric provider ids to strings (#3451)

Integrated into release/v3.8.18
This commit is contained in:
Dmitrii Safronov
2026-06-09 17:40:25 +04:00
committed by GitHub
parent 212fe8ed04
commit c5b9b2b0db
6 changed files with 741 additions and 53 deletions

View File

@@ -130,7 +130,7 @@ export function parseSSEToOpenAIResponse(rawSSE, fallbackModel) {
const getToolCallKey = (toolCall: Record<string, unknown>) => {
if (Number.isInteger(toolCall?.index)) return `idx:${toolCall.index}`;
if (toolCall?.id) return `id:${toolCall.id}`;
if (toolCall?.id != null) return `id:${String(toolCall.id)}`;
unknownToolCallSeq += 1;
return `seq:${unknownToolCallSeq}`;
};
@@ -448,11 +448,16 @@ function toOutputIndex(value) {
return null;
}
function toIdString(value) {
return value === null || value === undefined ? "" : String(value);
}
function cloneResponseItem(item) {
const record = toRecord(item);
return {
...record,
id: record.id != null ? String(record.id) : record.id,
call_id: record.call_id != null ? String(record.call_id) : record.call_id,
...(Array.isArray(record.content)
? {
content: record.content.map((contentPart) => {
@@ -517,18 +522,28 @@ function ensureResponsesReasoningItem(outputItems, outputIndex, itemId) {
function ensureResponsesFunctionCallItem(outputItems, outputIndex, itemId, callId, name) {
const existing = outputItems.get(outputIndex);
const normalizedItemId = toIdString(itemId);
const normalizedCallId = toIdString(callId);
const existingId = existing?.id != null ? String(existing.id) : "";
const existingCallId = existing?.call_id != null ? String(existing.call_id) : "";
if (existing?.type === "function_call") {
if (callId && !existing.call_id) existing.call_id = callId;
if (existing.call_id != null) existing.call_id = String(existing.call_id);
if (existing.id != null) existing.id = String(existing.id);
if (normalizedCallId && !existing.call_id) existing.call_id = normalizedCallId;
if (name && !existing.name) existing.name = name;
if (itemId && !existing.id) existing.id = itemId;
if (normalizedItemId && !existing.id) existing.id = normalizedItemId;
return existing;
}
const next = {
...(existing && typeof existing === "object" ? existing : {}),
id: itemId || (existing?.id != null ? String(existing.id) : null) || `fc_${callId || `${Date.now()}_${outputIndex}`}`,
id:
normalizedItemId ||
existingId ||
`fc_${normalizedCallId || `${Date.now()}_${outputIndex}`}`,
type: "function_call",
call_id: callId || existing?.call_id || "",
call_id: normalizedCallId || existingCallId || "",
name: name || existing?.name || "",
arguments: typeof existing?.arguments === "string" ? existing.arguments : "",
};
@@ -626,7 +641,7 @@ export function parseSSEToResponsesOutput(rawSSE, fallbackModel) {
const reasoningItem = ensureResponsesReasoningItem(
outputItems,
outputIndex,
toString(evt.item_id)
toIdString(evt.item_id)
);
const summary = Array.isArray(reasoningItem.summary) ? reasoningItem.summary : [];
const firstPart =
@@ -641,7 +656,7 @@ export function parseSSEToResponsesOutput(rawSSE, fallbackModel) {
const reasoningItem = ensureResponsesReasoningItem(
outputItems,
outputIndex,
toString(evt.item_id)
toIdString(evt.item_id)
);
const summary = Array.isArray(reasoningItem.summary) ? reasoningItem.summary : [];
const firstPart =
@@ -656,7 +671,7 @@ export function parseSSEToResponsesOutput(rawSSE, fallbackModel) {
const functionCallItem = ensureResponsesFunctionCallItem(
outputItems,
outputIndex,
toString(evt.item_id),
toIdString(evt.item_id),
"",
""
);
@@ -667,7 +682,7 @@ export function parseSSEToResponsesOutput(rawSSE, fallbackModel) {
const functionCallItem = ensureResponsesFunctionCallItem(
outputItems,
outputIndex,
toString(evt.item_id),
toIdString(evt.item_id),
"",
""
);
@@ -709,10 +724,14 @@ export function parseSSEToResponsesOutput(rawSSE, fallbackModel) {
id: picked.id != null ? String(picked.id) : `resp_${Date.now()}`,
object: picked.object || "response",
model: picked.model || fallbackModel || "unknown",
output: (pickedOutput.length > 0 ? pickedOutput : reconstructedOutput).map((item) => ({
...item,
id: item.id != null ? String(item.id) : item.id,
})),
output: (pickedOutput.length > 0 ? pickedOutput : reconstructedOutput).map((item) => {
const record = toRecord(item);
return {
...record,
id: record.id != null ? String(record.id) : record.id,
call_id: record.call_id != null ? String(record.call_id) : record.call_id,
};
}),
usage: picked.usage || null,
status: picked.status || statusFallback,
created_at: picked.created_at || Math.floor(Date.now() / 1000),

View File

@@ -44,14 +44,19 @@ export function transformToOllama(response, model) {
for (const tc of toolCalls) {
const idx = tc.index;
const toolCallId = tc.id != null ? String(tc.id) : tc.id;
// T37: Prevent merging tool_calls on same index if ID changes
if (pendingToolCalls[idx] && tc.id && pendingToolCalls[idx].id !== tc.id) {
if (pendingToolCalls[idx] && toolCallId && pendingToolCalls[idx].id !== toolCallId) {
completedToolCalls.push(pendingToolCalls[idx]);
delete pendingToolCalls[idx];
}
if (!pendingToolCalls[idx]) {
pendingToolCalls[idx] = { id: tc.id != null ? String(tc.id) : tc.id, function: { name: "", arguments: "" } };
pendingToolCalls[idx] = {
id: toolCallId,
function: { name: "", arguments: "" },
};
}
if (tc.function?.name) pendingToolCalls[idx].function.name += tc.function.name;
if (tc.function?.arguments)

View File

@@ -62,6 +62,81 @@ export { COLORS, formatSSE };
type JsonRecord = Record<string, unknown>;
function stringifyIdValue(value: unknown): string | null {
return value === null || value === undefined ? null : String(value);
}
function normalizeResponsesOutputItemIds(item: unknown): unknown {
if (!item || typeof item !== "object" || Array.isArray(item)) {
return item;
}
const record = item as JsonRecord;
let changed = false;
const normalized = { ...record };
const id = stringifyIdValue(record.id);
if (id !== null && record.id !== id) {
normalized.id = id;
changed = true;
}
const callId = stringifyIdValue(record.call_id);
if (callId !== null && record.call_id !== callId) {
normalized.call_id = callId;
changed = true;
}
return changed ? normalized : item;
}
function normalizeResponsesSseIds(payload: JsonRecord): boolean {
let changed = false;
for (const key of ["response_id", "item_id", "call_id"] as const) {
const value = stringifyIdValue(payload[key]);
if (value !== null && payload[key] !== value) {
payload[key] = value;
changed = true;
}
}
if (payload.item && typeof payload.item === "object" && !Array.isArray(payload.item)) {
const normalizedItem = normalizeResponsesOutputItemIds(payload.item);
if (normalizedItem !== payload.item) {
payload.item = normalizedItem;
changed = true;
}
}
if (payload.response && typeof payload.response === "object" && !Array.isArray(payload.response)) {
const response = payload.response as JsonRecord;
let responseChanged = false;
const normalizedResponse = { ...response };
const responseId = stringifyIdValue(response.id);
if (responseId !== null && response.id !== responseId) {
normalizedResponse.id = responseId;
responseChanged = true;
}
if (Array.isArray(response.output)) {
const normalizedOutput = response.output.map(normalizeResponsesOutputItemIds);
if (normalizedOutput.some((item, index) => item !== response.output[index])) {
normalizedResponse.output = normalizedOutput;
responseChanged = true;
}
}
if (responseChanged) {
payload.response = normalizedResponse;
changed = true;
}
}
return changed;
}
export const PENDING_REQUEST_CLEARED_MARKER = "__omniroutePendingRequestCleared";
function markPendingRequestCleared(error: Error): Error {
@@ -76,8 +151,8 @@ function buildResponsesOutputItemKey(item: unknown): string | null {
const record = item as JsonRecord;
const type = typeof record.type === "string" ? record.type : "";
const id = typeof record.id === "string" ? record.id : "";
const callId = typeof record.call_id === "string" ? record.call_id : "";
const id = stringifyIdValue(record.id) ?? "";
const callId = stringifyIdValue(record.call_id) ?? "";
const outputIndex = typeof record.output_index === "number" ? record.output_index : "";
const name = typeof record.name === "string" ? record.name : "";
@@ -1025,22 +1100,21 @@ export function createSSEStream(options: StreamOptions = {}) {
};
const getResponsesReasoningKey = (payload: Record<string, unknown>): string | null => {
if (typeof payload.item_id === "string" && payload.item_id) {
return payload.item_id;
const itemId = stringifyIdValue(payload.item_id);
if (itemId) {
return itemId;
}
const item =
payload.item && typeof payload.item === "object" && !Array.isArray(payload.item)
? (payload.item as Record<string, unknown>)
: null;
if (item && typeof item.id === "string" && item.id) {
return item.id;
const outputItemId = item ? stringifyIdValue(item.id) : null;
if (outputItemId) {
return outputItemId;
}
const responseId =
typeof payload.response_id === "string" && payload.response_id
? payload.response_id
: passthroughResponsesId;
const responseId = stringifyIdValue(payload.response_id) || passthroughResponsesId;
const outputIndex =
typeof payload.output_index === "number" && Number.isInteger(payload.output_index)
? payload.output_index
@@ -1319,12 +1393,16 @@ export function createSSEStream(options: StreamOptions = {}) {
parsed.type === "error");
if (isResponsesSSE) {
const responsesIdsNormalized = normalizeResponsesSseIds(parsed as JsonRecord);
const parsedResponse =
parsed.response &&
typeof parsed.response === "object" &&
!Array.isArray(parsed.response)
? (parsed.response as JsonRecord)
: null;
const responseId =
typeof parsed.response?.id === "string"
? parsed.response.id
: typeof parsed.response_id === "string"
? parsed.response_id
: null;
(parsedResponse ? stringifyIdValue(parsedResponse.id) : null) ||
stringifyIdValue(parsed.response_id);
if (responseId) {
passthroughResponsesId = responseId;
}
@@ -1530,7 +1608,7 @@ export function createSSEStream(options: StreamOptions = {}) {
parsed,
passthroughResponsesOutputItems
);
if (stripped || backfilled || textualToolCallBackfilled) {
if (stripped || backfilled || textualToolCallBackfilled || responsesIdsNormalized) {
output = `data: ${JSON.stringify(parsed)}\n`;
injectedUsage = true;
}
@@ -1655,6 +1733,7 @@ export function createSSEStream(options: StreamOptions = {}) {
)
)
: false;
const hadNonStringTopLevelId = parsed?.id != null && typeof parsed.id !== "string";
parsed = sanitizeStreamingChunk(parsed);
if (
@@ -1666,7 +1745,7 @@ export function createSSEStream(options: StreamOptions = {}) {
continue;
}
const idFixed = fixInvalidId(parsed);
const idFixed = hadNonStringTopLevelId ? false : fixInvalidId(parsed);
if (!hasValuableContent(parsed, FORMATS.OPENAI)) {
continue;
@@ -1718,18 +1797,18 @@ export function createSSEStream(options: StreamOptions = {}) {
passthroughHasToolCalls = true;
lastToolCallChunkTime = Date.now();
for (const tc of delta.tool_calls) {
if (tc?.id != null) {
const stringId = String(tc.id);
if (tc.id !== stringId) {
tc.id = stringId;
toolCallIdCoerced = true;
}
// Note: sanitizeStreamingChunk above already coerces non-string
// tool_call IDs, but this defensive check catches edge cases
// where sanitize didn't run (e.g. flush path shortcuts).
if (tc?.id != null && typeof tc.id !== "string") {
tc.id = String(tc.id);
toolCallIdCoerced = true;
}
// Key by index first — id only appears on the first delta in OpenAI streaming
let key: string;
if (Number.isInteger(tc?.index)) {
key = `idx:${tc.index}`;
} else if (tc?.id) {
} else if (tc?.id != null) {
key = `id:${tc.id}`;
} else {
key = `seq:${++passthroughToolCallSeq}`;
@@ -1839,7 +1918,8 @@ export function createSSEStream(options: StreamOptions = {}) {
idFixed ||
needsReserialization ||
toolCallIdCoerced ||
hadNonStringToolCallId
hadNonStringToolCallId ||
hadNonStringTopLevelId
) {
output = `data: ${JSON.stringify(parsed)}\n`;
injectedUsage = true;
@@ -2098,6 +2178,43 @@ export function createSSEStream(options: StreamOptions = {}) {
updateClaudeEmptyResponseLifecycle(claudeEmptyResponseLifecycle, bufferedPayload);
}
clientPayloadCollector.push(bufferedPayload);
// Normalize numeric IDs for final buffered data: chunk (same as transform path)
if (typeof bufferedPayload === "object" && !Array.isArray(bufferedPayload)) {
const flushedParsed = bufferedPayload as JsonRecord;
const flushedType = typeof flushedParsed.type === "string" ? flushedParsed.type : "";
const isResponses = flushedType.startsWith("response.");
const isClaude = isClaudeEventPayload(flushedParsed);
if (isResponses) {
if (normalizeResponsesSseIds(flushedParsed)) {
output = `data: ${JSON.stringify(flushedParsed)}\n`;
}
} else if (!isClaude) {
let flushChanged = false;
const flushedHadNonStringTopLevelId =
flushedParsed?.id != null && typeof flushedParsed.id !== "string";
if (flushedHadNonStringTopLevelId) {
flushedParsed.id = String(flushedParsed.id);
flushChanged = true;
}
if (Array.isArray(flushedParsed.choices)) {
for (const choice of flushedParsed.choices as JsonRecord[]) {
const tcs = (choice as JsonRecord | undefined)?.delta as JsonRecord | undefined;
if (Array.isArray(tcs?.tool_calls)) {
for (const tc of tcs.tool_calls as JsonRecord[]) {
if (tc?.id != null && typeof tc.id !== "string") {
tc.id = String(tc.id);
flushChanged = true;
}
}
}
}
}
if (flushChanged) {
output = `data: ${JSON.stringify(flushedParsed)}\n`;
}
}
}
}
if (!bufferedLine && pendingPassthroughEventLine && !pendingPassthroughEventEmitted) {
output = `${pendingPassthroughEventLine}\n${output}`;

View File

@@ -55,18 +55,22 @@ test("transformToOllama handles string tool_call id normally", async () => {
object: "chat.completion.chunk",
created: 1,
model: "gpt-4",
choices: [{
index: 0,
delta: {
tool_calls: [{
index: 0,
id: "call_abc",
type: "function",
function: { name: "test", arguments: "{}" }
}]
choices: [
{
index: 0,
delta: {
tool_calls: [
{
index: 0,
id: "call_abc",
type: "function",
function: { name: "test", arguments: "{}" },
},
],
},
finish_reason: "tool_calls",
},
finish_reason: "tool_calls"
}]
],
})}\n`,
].join("");
@@ -83,8 +87,77 @@ test("transformToOllama handles string tool_call id normally", async () => {
const result = transformToOllama(mockResponse, "test-model");
const text = await result.text();
const lines = text.trim().split("\n").map(l => JSON.parse(l));
const lines = text.trim().split("\n").map((line) => JSON.parse(line));
const toolCallLine = lines.find(l => l.message?.tool_calls);
const toolCallLine = lines.find((line) => line.message?.tool_calls);
assert.ok(toolCallLine, "Should produce a tool call line");
});
test("transformToOllama merges multi-chunk numeric tool_call id", async () => {
const inputSSE = [
`data: ${JSON.stringify({
id: "chatcmpl_1",
object: "chat.completion.chunk",
created: 1,
model: "gpt-4",
choices: [
{
index: 0,
delta: {
tool_calls: [
{
index: 0,
id: 12345,
type: "function",
function: { name: "test", arguments: '{"a":' },
},
],
},
},
],
})}\n`,
`data: ${JSON.stringify({
id: "chatcmpl_1",
object: "chat.completion.chunk",
created: 1,
model: "gpt-4",
choices: [
{
index: 0,
delta: {
tool_calls: [
{
index: 0,
id: 12345,
type: "function",
function: { arguments: "1}" },
},
],
},
finish_reason: "tool_calls",
},
],
})}\n`,
].join("");
const inputStream = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode(inputSSE));
controller.close();
},
});
const mockResponse = new Response(inputStream, {
headers: { "Content-Type": "text/event-stream" },
});
const result = transformToOllama(mockResponse, "test-model");
const text = await result.text();
const lines = text.trim().split("\n").map((line) => JSON.parse(line));
const toolCallLines = lines.filter((line) => line.message?.tool_calls);
assert.equal(toolCallLines.length, 1);
assert.equal(toolCallLines[0].message.tool_calls.length, 1);
assert.equal(toolCallLines[0].message.tool_calls[0].function.name, "test");
assert.deepEqual(toolCallLines[0].message.tool_calls[0].function.arguments, { a: 1 });
});

View File

@@ -77,3 +77,80 @@ test("parseSSEToResponsesOutput coerces numeric reasoning item id from increment
assert.equal(typeof parsed.output[0].id, "string");
assert.equal(parsed.output[0].id, "101");
});
test("parseSSEToResponsesOutput coerces numeric function call call_id to string", () => {
const rawSSE = [
'data: {"type":"response.completed","response":{"id":"resp_call","model":"gpt-4.1","status":"completed","output":[{"id":789,"call_id":456,"type":"function_call","name":"foo","arguments":"{}"}]}}',
"data: [DONE]",
].join("\n");
const parsed = parseSSEToResponsesOutput(rawSSE, "fallback-model");
assert.equal(typeof parsed.output[0].id, "string");
assert.equal(parsed.output[0].id, "789");
assert.equal(typeof parsed.output[0].call_id, "string");
assert.equal(parsed.output[0].call_id, "456");
});
test("parseSSEToResponsesOutput preserves numeric item_id from function call deltas", () => {
const rawSSE = [
'data: {"type":"response.created","response":{"id":"resp_item","model":"gpt-4.1","status":"in_progress","output":[]}}',
'data: {"type":"response.function_call_arguments.delta","output_index":0,"item_id":321,"delta":"{\\"a\\":"}',
'data: {"type":"response.function_call_arguments.done","output_index":0,"item_id":321,"arguments":"{\\"a\\":1}","status":"completed"}',
'data: {"type":"response.completed","response":{"id":"resp_item","model":"gpt-4.1","status":"completed","output":null}}',
"data: [DONE]",
].join("\n");
const parsed = parseSSEToResponsesOutput(rawSSE, "fallback-model");
assert.equal(typeof parsed.output[0].id, "string");
assert.equal(parsed.output[0].id, "321");
});
test("parseSSEToOpenAIResponse merges tool_call with id 0 and no index", () => {
const rawSSE = [
'data: {"id":"chatcmpl_zero","choices":[{"index":0,"delta":{"tool_calls":[{"id":0,"function":{"name":"foo","arguments":"{\\"a\\":"}}]},"finish_reason":null}]}',
'data: {"id":"chatcmpl_zero","choices":[{"index":0,"delta":{"tool_calls":[{"id":0,"function":{"arguments":"1}"}}]},"finish_reason":"tool_calls"}]}',
"data: [DONE]",
].join("\n");
const parsed = parseSSEToOpenAIResponse(rawSSE, "fallback-model");
assert.equal(parsed.choices[0].message.tool_calls.length, 1);
assert.equal(typeof parsed.choices[0].message.tool_calls[0].id, "string");
assert.equal(parsed.choices[0].message.tool_calls[0].id, "0");
assert.equal(parsed.choices[0].message.tool_calls[0].function.name, "foo");
assert.equal(parsed.choices[0].message.tool_calls[0].function.arguments, '{"a":1}');
});
test("parseSSEToOpenAIResponse handles tool_call with negative numeric id", () => {
const rawSSE = [
'data: {"id":"chatcmpl_neg","choices":[{"index":0,"delta":{"tool_calls":[{"id":-1,"index":0,"function":{"name":"bar","arguments":"{}"}}]},"finish_reason":"tool_calls"}]}',
"",
"data: [DONE]",
"",
].join("\n");
const parsed = parseSSEToOpenAIResponse(rawSSE, "fallback-model");
assert.equal(typeof parsed.choices[0].message.tool_calls[0].id, "string");
assert.equal(parsed.choices[0].message.tool_calls[0].id, "-1");
});
test("parseSSEToResponsesOutput coerces numeric call_id and item_id together", () => {
const rawSSE = [
'data: {"type":"response.created","response":{"id":"resp_comb","model":"gpt-4.1","status":"in_progress","output":[]}}',
'data: {"type":"response.output_item.added","output_index":0,"item":{"id":111,"call_id":222,"type":"function_call","status":"in_progress","name":"both","arguments":""}}',
'data: {"type":"response.function_call_arguments.delta","output_index":0,"item_id":111,"delta":"{\\"x\\":"}',
'data: {"type":"response.function_call_arguments.done","output_index":0,"item_id":111,"arguments":"{\\"x\\":1}","status":"completed"}',
'data: {"type":"response.completed","response":{"id":"resp_comb","model":"gpt-4.1","status":"completed","output":null}}',
"data: [DONE]",
].join("\n");
const parsed = parseSSEToResponsesOutput(rawSSE, "fallback-model");
assert.equal(typeof parsed.output[0].id, "string");
assert.equal(parsed.output[0].id, "111");
assert.equal(typeof parsed.output[0].call_id, "string");
assert.equal(parsed.output[0].call_id, "222");
});

View File

@@ -105,3 +105,400 @@ test("createSSEStream passthrough coerces numeric tool_call id to string", async
assert.equal(typeof finalId, "string", "tool_call.id in final message should be a string");
assert.equal(finalId, "12345");
});
test("createSSEStream passthrough preserves numeric top-level id as string", async () => {
const text = await readTransformed(
[
`data: ${JSON.stringify({
id: 123,
object: "chat.completion.chunk",
created: 1,
model: "gpt-4.1-mini",
choices: [{ index: 0, delta: { content: "Hello" } }],
})}\n\n`,
`data: ${JSON.stringify({
id: 123,
object: "chat.completion.chunk",
created: 1,
model: "gpt-4.1-mini",
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
})}\n\n`,
],
{
mode: "passthrough",
sourceFormat: FORMATS.OPENAI,
provider: "openai",
model: "gpt-4.1-mini",
body: { messages: [{ role: "user", content: "hello" }] },
}
);
const lines = text
.trim()
.split("\n")
.filter((line) => line.startsWith("data: ") && !line.includes("[DONE]"));
assert.ok(lines.length > 0, "expected streamed chunks");
for (const line of lines) {
const payload = JSON.parse(line.slice(6));
assert.equal(typeof payload.id, "string", "top-level chunk id should be a string");
assert.equal(payload.id, "123");
assert.notEqual(payload.id.startsWith("chatcmpl-"), true);
}
});
test("createSSEStream responses passthrough coerces numeric ids to strings", async () => {
const text = await readTransformed(
[
`data: ${JSON.stringify({
type: "response.created",
response: {
id: 987,
model: "gpt-4.1-mini",
status: "in_progress",
output: [],
},
})}\n\n`,
`data: ${JSON.stringify({
type: "response.output_item.added",
response_id: 987,
output_index: 0,
item: {
id: 456,
call_id: 654,
type: "function_call",
status: "in_progress",
name: "lookup",
arguments: "",
},
})}\n\n`,
`data: ${JSON.stringify({
type: "response.function_call_arguments.delta",
response_id: 987,
item_id: 456,
output_index: 0,
delta: '{"q":',
})}\n\n`,
`data: ${JSON.stringify({
type: "response.function_call_arguments.done",
response_id: 987,
item_id: 456,
output_index: 0,
arguments: '{"q":1}',
})}\n\n`,
`data: ${JSON.stringify({
type: "response.completed",
response: {
id: 987,
model: "gpt-4.1-mini",
status: "completed",
output: [
{
id: 456,
call_id: 654,
type: "function_call",
status: "completed",
name: "lookup",
arguments: '{"q":1}',
},
],
},
})}\n\n`,
],
{
mode: "passthrough",
sourceFormat: FORMATS.OPENAI,
provider: "openai",
model: "gpt-4.1-mini",
body: { input: "hello" },
}
);
const payloads = text
.trim()
.split("\n")
.filter((line) => line.startsWith("data: ") && !line.includes("[DONE]"))
.map((line) => JSON.parse(line.slice(6)));
const created = payloads.find((payload) => payload.type === "response.created");
assert.equal(typeof created.response.id, "string");
assert.equal(created.response.id, "987");
const added = payloads.find((payload) => payload.type === "response.output_item.added");
assert.equal(typeof added.response_id, "string");
assert.equal(added.response_id, "987");
assert.equal(typeof added.item.id, "string");
assert.equal(added.item.id, "456");
assert.equal(typeof added.item.call_id, "string");
assert.equal(added.item.call_id, "654");
const delta = payloads.find((payload) => payload.type === "response.function_call_arguments.delta");
assert.equal(typeof delta.response_id, "string");
assert.equal(delta.response_id, "987");
assert.equal(typeof delta.item_id, "string");
assert.equal(delta.item_id, "456");
const completed = payloads.find((payload) => payload.type === "response.completed");
assert.equal(typeof completed.response.id, "string");
assert.equal(completed.response.id, "987");
assert.equal(typeof completed.response.output[0].id, "string");
assert.equal(completed.response.output[0].id, "456");
assert.equal(typeof completed.response.output[0].call_id, "string");
assert.equal(completed.response.output[0].call_id, "654");
});
test("createSSEStream responses passthrough does not normalize unrelated top-level id", async () => {
const text = await readTransformed(
[
`data: ${JSON.stringify({
type: "response.output_text.delta",
id: 1,
response_id: 987,
item_id: 456,
delta: "x",
})}\n\n`,
],
{
mode: "passthrough",
sourceFormat: FORMATS.OPENAI,
provider: "openai",
model: "gpt-4.1-mini",
body: { input: "hello" },
}
);
const payloads = text
.trim()
.split("\n")
.filter((line) => line.startsWith("data: ") && !line.includes("[DONE]"))
.map((line) => JSON.parse(line.slice(6)));
const delta = payloads.find((p) => p.type === "response.output_text.delta");
assert.equal(typeof delta.id, "number", "unrelated top-level id should stay numeric");
assert.equal(delta.id, 1);
assert.equal(typeof delta.response_id, "string");
assert.equal(delta.response_id, "987");
assert.equal(typeof delta.item_id, "string");
assert.equal(delta.item_id, "456");
});
test("createSSEStream passthrough normalizes numeric id in final chunk without trailing newline", async () => {
const text = await readTransformed(
[
`data: ${JSON.stringify({
id: 123,
object: "chat.completion.chunk",
created: 1,
model: "gpt-4.1-mini",
choices: [{ index: 0, delta: { content: "Hello" } }],
})}\n\n`,
`data: ${JSON.stringify({
id: 123,
object: "chat.completion.chunk",
created: 1,
model: "gpt-4.1-mini",
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
})}`,
],
{
mode: "passthrough",
sourceFormat: FORMATS.OPENAI,
provider: "openai",
model: "gpt-4.1-mini",
body: { messages: [{ role: "user", content: "hello" }] },
}
);
const lines = text
.trim()
.split("\n")
.filter((line) => line.startsWith("data: ") && !line.includes("[DONE]"));
for (const line of lines) {
const payload = JSON.parse(line.slice(6));
if (payload.id) {
assert.equal(typeof payload.id, "string", "top-level chunk id should be a string");
assert.equal(payload.id, "123");
}
}
});
test("createSSEStream responses passthrough normalizes numeric ids in final chunk without trailing newline", async () => {
const text = await readTransformed(
[
`data: ${JSON.stringify({
type: "response.created",
response: { id: 987, model: "gpt-4.1-mini", status: "in_progress", output: [] },
})}\n\n`,
`data: ${JSON.stringify({
type: "response.completed",
response: {
id: 987,
model: "gpt-4.1-mini",
status: "completed",
output: [
{
id: 456,
call_id: 654,
type: "function_call",
status: "completed",
name: "lookup",
arguments: "{}",
},
],
},
})}`,
],
{
mode: "passthrough",
sourceFormat: FORMATS.OPENAI,
provider: "openai",
model: "gpt-4.1-mini",
body: { input: "hello" },
}
);
const payloads = text
.trim()
.split("\n")
.filter((line) => line.startsWith("data: ") && !line.includes("[DONE]"))
.map((line) => JSON.parse(line.slice(6)));
const completed = payloads.find((p) => p.type === "response.completed");
assert.equal(typeof completed.response.id, "string");
assert.equal(completed.response.id, "987");
assert.equal(typeof completed.response.output[0].id, "string");
assert.equal(completed.response.output[0].id, "456");
assert.equal(typeof completed.response.output[0].call_id, "string");
assert.equal(completed.response.output[0].call_id, "654");
});
test("createSSEStream passthrough coerces tool_call id 0 without index", async () => {
let onCompletePayload = null;
const text = await readTransformed(
[
`data: ${JSON.stringify({
id: "cmpl_0",
object: "chat.completion.chunk",
created: 1,
model: "gpt-4.1-mini",
choices: [{ index: 0, delta: { role: "assistant", content: "" } }],
})}\n\n`,
`data: ${JSON.stringify({
id: "cmpl_0",
object: "chat.completion.chunk",
created: 1,
model: "gpt-4.1-mini",
choices: [
{
index: 0,
delta: {
tool_calls: [
{
id: 0,
type: "function",
function: { name: "zero", arguments: '{"path":"/tmp"}' },
},
],
},
},
],
})}\n\n`,
`data: ${JSON.stringify({
id: "cmpl_0",
object: "chat.completion.chunk",
created: 1,
model: "gpt-4.1-mini",
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
})}\n\n`,
],
{
mode: "passthrough",
sourceFormat: FORMATS.OPENAI,
provider: "openai",
model: "gpt-4.1-mini",
body: { messages: [{ role: "user", content: "hello" }] },
onComplete(payload) {
onCompletePayload = payload;
},
}
);
const lines = text
.trim()
.split("\n")
.filter((line) => line.startsWith("data: ") && !line.includes("[DONE]"));
let sawStreamedToolCall = false;
for (const line of lines) {
const payload = JSON.parse(line.slice(6));
const tc = payload?.choices?.[0]?.delta?.tool_calls?.[0];
if (tc?.id != null) {
assert.equal(typeof tc.id, "string", "tool_call.id should be a string");
assert.equal(tc.id, "0");
sawStreamedToolCall = true;
}
}
assert.equal(sawStreamedToolCall, true, "expected streamed tool_call.id to be present");
const finalId = onCompletePayload?.responseBody?.choices?.[0]?.message?.tool_calls?.[0]?.id;
assert.equal(typeof finalId, "string", "tool_call.id in final message should be a string");
assert.equal(finalId, "0");
});
test("createSSEStream Claude passthrough does not normalize numeric ids", async () => {
const text = await readTransformed(
[
`data: ${JSON.stringify({
type: "message_start",
message: {
id: 987,
type: "message",
role: "assistant",
content: [],
model: "claude-3-5-sonnet",
},
})}\n\n`,
`data: ${JSON.stringify({
type: "content_block_start",
index: 0,
content_block: {
id: 456,
type: "text",
text: "",
},
})}\n\n`,
`data: ${JSON.stringify({
type: "content_block_delta",
index: 0,
delta: {
type: "text_delta",
text: "Hello",
},
})}\n\n`,
`data: ${JSON.stringify({
type: "message_stop",
})}\n`,
],
{
mode: "passthrough",
sourceFormat: FORMATS.ANTHROPIC,
provider: "anthropic",
model: "claude-3-5-sonnet-20241022",
body: { messages: [{ role: "user", content: "hello" }] },
}
);
const payloads = text
.trim()
.split("\n")
.filter((line) => line.startsWith("data: ") && !line.includes("[DONE]"))
.map((line) => JSON.parse(line.slice(6)));
const start = payloads.find((p) => p.type === "message_start");
assert.equal(start.message.id, 987, "Claude message id should remain numeric");
assert.equal(typeof start.message.id, "number");
const contentStart = payloads.find((p) => p.type === "content_block_start");
assert.equal(contentStart.content_block.id, 456, "Claude content block id should remain numeric");
assert.equal(typeof contentStart.content_block.id, "number");
});