Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions open-sse/handlers/rerank.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ function transformRequestForProvider(providerConfig, body) {
/**
* Transform response from provider-specific formats back to Cohere format
*/
function transformResponseFromProvider(providerConfig, data) {
/* @testonly */ export function transformResponseFromProvider(providerConfig, data) {
if (providerConfig.format === "nvidia") {
return {
id: data.id || `rerank-${Date.now()}`,
id: data.id != null ? String(data.id) : `rerank-${Date.now()}`,
results: (data.rankings || []).map((r) => ({
index: r.index,
relevance_score: r.logit || r.score || 0,
Expand Down
19 changes: 16 additions & 3 deletions open-sse/handlers/responseSanitizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -985,8 +985,10 @@ export function sanitizeStreamingChunk(parsed: unknown): unknown {
// Build sanitized chunk
const sanitized: JsonRecord = {};

// Keep only standard fields
if (parsedRecord.id !== undefined) sanitized.id = parsedRecord.id;
// Keep only standard fields — normalize id to string to avoid AI_InvalidResponseDataError
if (parsedRecord.id !== undefined && parsedRecord.id !== null) {
sanitized.id = normalizeResponseId(typeof parsedRecord.id === "string" ? parsedRecord.id : String(parsedRecord.id));
}
sanitized.object = toString(parsedRecord.object) || "chat.completion.chunk";
if (parsedRecord.created !== undefined) sanitized.created = parsedRecord.created;
if (parsedRecord.model !== undefined) sanitized.model = parsedRecord.model;
Expand Down Expand Up @@ -1037,7 +1039,18 @@ export function sanitizeStreamingChunk(parsed: unknown): unknown {
delta.reasoning_content = parts.join("");
}
}
if (deltaRecord.tool_calls !== undefined) delta.tool_calls = deltaRecord.tool_calls;
if (deltaRecord.tool_calls !== undefined) {
delta.tool_calls = Array.isArray(deltaRecord.tool_calls)
? deltaRecord.tool_calls.map((tc) => {
const t = toRecord(tc);
if (!t) return tc;
if (t.id !== undefined && t.id !== null && typeof t.id !== "string") {
return { ...t, id: String(t.id) };
}
return t;
})
: deltaRecord.tool_calls;
}
if (deltaRecord.function_call !== undefined)
delta.function_call = deltaRecord.function_call;
c.delta = delta;
Expand Down
20 changes: 12 additions & 8 deletions open-sse/handlers/sseParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ export function parseSSEToOpenAIResponse(rawSSE, fallbackModel) {

if (!existing) {
accumulatedToolCalls.set(key, {
id: tc?.id ?? null,
id: tc?.id != null ? String(tc.id) : null,
index: Number.isInteger(tc?.index) ? tc.index : accumulatedToolCalls.size,
type: tc?.type || "function",
function: {
Expand All @@ -172,7 +172,7 @@ export function parseSSEToOpenAIResponse(rawSSE, fallbackModel) {
},
});
} else {
existing.id = existing.id || tc?.id || null;
existing.id = existing.id || (tc?.id != null ? String(tc.id) : null);
if (!Number.isInteger(existing.index) && Number.isInteger(tc?.index)) {
existing.index = tc.index;
}
Expand Down Expand Up @@ -216,7 +216,7 @@ export function parseSSEToOpenAIResponse(rawSSE, fallbackModel) {
}

const result: Record<string, unknown> = {
id: first.id || `chatcmpl-${Date.now()}`,
id: first.id != null ? String(first.id) : `chatcmpl-${Date.now()}`,
object: "chat.completion",
created: first.created || Math.floor(Date.now() / 1000),
model: first.model || fallbackModel || "unknown",
Expand Down Expand Up @@ -452,6 +452,7 @@ function cloneResponseItem(item) {
const record = toRecord(item);
return {
...record,
id: record.id != null ? String(record.id) : record.id,
...(Array.isArray(record.content)
? {
content: record.content.map((contentPart) => {
Expand All @@ -477,7 +478,7 @@ function ensureResponsesMessageItem(outputItems, outputIndex) {

const next = {
...(existing && typeof existing === "object" ? existing : {}),
id: existing?.id || `msg_${Date.now()}_${outputIndex}`,
id: existing?.id != null ? String(existing.id) : `msg_${Date.now()}_${outputIndex}`,
type: "message",
role: "assistant",
content: Array.isArray(existing?.content)
Expand All @@ -499,7 +500,7 @@ function ensureResponsesReasoningItem(outputItems, outputIndex, itemId) {

const next = {
...(existing && typeof existing === "object" ? existing : {}),
id: itemId || existing?.id || `rs_${Date.now()}_${outputIndex}`,
id: itemId || (existing?.id != null ? String(existing.id) : null) || `rs_${Date.now()}_${outputIndex}`,
type: "reasoning",
summary: Array.isArray(existing?.summary)
? existing.summary.map((summaryPart) => ({ ...toRecord(summaryPart) }))
Expand All @@ -525,7 +526,7 @@ function ensureResponsesFunctionCallItem(outputItems, outputIndex, itemId, callI

const next = {
...(existing && typeof existing === "object" ? existing : {}),
id: itemId || existing?.id || `fc_${callId || `${Date.now()}_${outputIndex}`}`,
id: itemId || (existing?.id != null ? String(existing.id) : null) || `fc_${callId || `${Date.now()}_${outputIndex}`}`,
type: "function_call",
call_id: callId || existing?.call_id || "",
name: name || existing?.name || "",
Expand Down Expand Up @@ -705,10 +706,13 @@ export function parseSSEToResponsesOutput(rawSSE, fallbackModel) {
: "in_progress";

return {
id: picked.id || `resp_${Date.now()}`,
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,
output: (pickedOutput.length > 0 ? pickedOutput : reconstructedOutput).map((item) => ({
...item,
id: item.id != null ? String(item.id) : item.id,
})),
usage: picked.usage || null,
status: picked.status || statusFallback,
created_at: picked.created_at || Math.floor(Date.now() / 1000),
Expand Down
2 changes: 1 addition & 1 deletion open-sse/utils/ollamaTransform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export function transformToOllama(response, model) {
}

if (!pendingToolCalls[idx]) {
pendingToolCalls[idx] = { id: tc.id, function: { name: "", arguments: "" } };
pendingToolCalls[idx] = { id: tc.id != null ? String(tc.id) : tc.id, function: { name: "", arguments: "" } };
}
if (tc.function?.name) pendingToolCalls[idx].function.name += tc.function.name;
if (tc.function?.arguments)
Expand Down
39 changes: 30 additions & 9 deletions open-sse/utils/stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,10 +269,10 @@ function collectPassthroughTextualToolCall(
return toolCall;
}

function toStreamingToolCallDelta(toolCall: ToolCall) {
/* @testonly */ export function toStreamingToolCallDelta(toolCall: ToolCall) {
return {
index: toolCall.index,
id: toolCall.id,
id: toolCall.id != null ? String(toolCall.id) : null,
type: toolCall.type,
function: {
name: toolCall.function.name,
Expand All @@ -281,11 +281,11 @@ function toStreamingToolCallDelta(toolCall: ToolCall) {
};
}

function toResponsesFunctionCallItem(toolCall: ToolCall) {
/* @testonly */ export function toResponsesFunctionCallItem(toolCall: ToolCall) {
return {
type: "function_call",
id: toolCall.id || `fc_${toolCall.index}`,
call_id: toolCall.id || `call_${toolCall.index}`,
id: (toolCall.id != null ? String(toolCall.id) : null) || `fc_${toolCall.index}`,
call_id: (toolCall.id != null ? String(toolCall.id) : null) || `call_${toolCall.index}`,
name: toolCall.function.name,
arguments: toolCall.function.arguments,
status: "completed",
Expand Down Expand Up @@ -1604,6 +1604,14 @@ export function createSSEStream(options: StreamOptions = {}) {
typeof parsed.choices[0].delta.reasoning === "string" &&
!parsed.choices[0].delta.reasoning_content
);
const hadNonStringToolCallId = Array.isArray(parsed.choices)
? parsed.choices.some((choice) =>
Array.isArray(choice?.delta?.tool_calls) &&
choice.delta.tool_calls.some(
(tc) => tc?.id != null && typeof tc.id !== "string"
)
)
: false;

parsed = sanitizeStreamingChunk(parsed);
if (
Expand All @@ -1623,6 +1631,7 @@ export function createSSEStream(options: StreamOptions = {}) {

const delta = parsed.choices?.[0]?.delta;
let textualToolCallConverted = false;
let toolCallIdCoerced = false;

// Extract <think> tags from streaming content
if (delta?.content && typeof delta.content === "string") {
Expand Down Expand Up @@ -1666,6 +1675,13 @@ 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;
}
}
// Key by index first — id only appears on the first delta in OpenAI streaming
let key: string;
if (Number.isInteger(tc?.index)) {
Expand All @@ -1680,7 +1696,7 @@ export function createSSEStream(options: StreamOptions = {}) {
typeof tc?.function?.arguments === "string" ? tc.function.arguments : "";
if (!existing) {
passthroughToolCalls.set(key, {
id: tc?.id ?? null,
id: tc?.id != null ? String(tc.id) : null,
index: Number.isInteger(tc?.index) ? tc.index : passthroughToolCalls.size,
type: tc?.type || "function",
function: {
Expand All @@ -1689,7 +1705,7 @@ export function createSSEStream(options: StreamOptions = {}) {
},
});
} else {
if (tc?.id) existing.id = existing.id || tc.id;
if (tc?.id) existing.id = existing.id || String(tc.id);
if (tc?.function?.name && !existing.function.name)
existing.function.name = tc.function.name;
existing.function.arguments += deltaArgs;
Expand Down Expand Up @@ -1772,7 +1788,12 @@ export function createSSEStream(options: StreamOptions = {}) {
} else if (textualToolCallConverted) {
output = `data: ${JSON.stringify(parsed)}\n`;
injectedUsage = true;
} else if (idFixed || needsReserialization) {
} else if (
idFixed ||
needsReserialization ||
toolCallIdCoerced ||
hadNonStringToolCallId
) {
output = `data: ${JSON.stringify(parsed)}\n`;
injectedUsage = true;
}
Expand Down Expand Up @@ -2385,7 +2406,7 @@ export function createSSEStream(options: StreamOptions = {}) {
? [...state.toolCalls.values()]
.map(
(tc: Record<string, unknown>): ToolCall => ({
id: (tc.id as string) ?? null,
id: tc.id != null ? String(tc.id) : null,
index: (tc.index as number) ?? (tc.blockIndex as number) ?? 0,
type: (tc.type as string) ?? "function",
function: (tc.function as ToolCall["function"]) ?? {
Expand Down
90 changes: 90 additions & 0 deletions tests/unit/ollama-transform.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import test from "node:test";
import assert from "node:assert/strict";

const { transformToOllama } = await import("../../open-sse/utils/ollamaTransform.ts");

test("transformToOllama coerces numeric tool_call id to string without crashing", 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: "{}" }
}]
},
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();

// Should produce valid JSON lines without crashing
const lines = text.trim().split("\n");
assert.ok(lines.length > 0, "Should produce at least one line of output");
for (const line of lines) {
const parsed = JSON.parse(line);
assert.ok(parsed, "Each line should be valid JSON");
}
});

test("transformToOllama handles string tool_call id normally", 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: "call_abc",
type: "function",
function: { name: "test", arguments: "{}" }
}]
},
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(l => JSON.parse(l));

const toolCallLine = lines.find(l => l.message?.tool_calls);
assert.ok(toolCallLine, "Should produce a tool call line");
});
52 changes: 52 additions & 0 deletions tests/unit/rerank.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import test from "node:test";
import assert from "node:assert/strict";

const { transformResponseFromProvider } = await import("../../open-sse/handlers/rerank.ts");

test("transformResponseFromProvider coerces numeric data.id to string for nvidia format", () => {
const result = transformResponseFromProvider(
{ format: "nvidia" },
{ id: 12345, rankings: [] }
);

assert.equal(typeof result.id, "string");
assert.equal(result.id, "12345");
});

test("transformResponseFromProvider coerces numeric zero data.id to string", () => {
const result = transformResponseFromProvider(
{ format: "nvidia" },
{ id: 0, rankings: [] }
);

assert.equal(typeof result.id, "string");
assert.equal(result.id, "0");
});

test("transformResponseFromProvider uses fallback when data.id is null", () => {
const result = transformResponseFromProvider(
{ format: "nvidia" },
{ id: null, rankings: [] }
);

assert.ok(result.id.startsWith("rerank-"));
});

test("transformResponseFromProvider uses fallback when data.id is undefined", () => {
const result = transformResponseFromProvider(
{ format: "nvidia" },
{ rankings: [] }
);

assert.ok(result.id.startsWith("rerank-"));
});

test("transformResponseFromProvider passes through string id unchanged", () => {
const result = transformResponseFromProvider(
{ format: "nvidia" },
{ id: "nvidia-abc", rankings: [] }
);

assert.equal(typeof result.id, "string");
assert.equal(result.id, "nvidia-abc");
});
Loading