import { describe, expect, it } from "bun:test";
import { encodeResponse, encodeStream, parseRequest } from "../src/providers/openai-chat-server";
import type { AssistantMessage, AssistantMessageEvent, AssistantMessageEventStream } from "../src/types";
function makeEventStream(events: AssistantMessageEvent[], final: AssistantMessage): AssistantMessageEventStream {
async function* iter() {
for (const e of events) yield e;
}
const stream = iter() as unknown as AssistantMessageEventStream;
(stream as { result(): Promise<AssistantMessage> }).result = async () => final;
return stream;
}
async function collectStream(stream: ReadableStream<Uint8Array>): Promise<string[]> {
const reader = stream.getReader();
const decoder = new TextDecoder();
let buf = "";
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
}
buf += decoder.decode();
return buf.split("\n\n").filter(s => s.length > 0);
}
function parseSseLine(line: string): unknown {
const stripped = line.replace(/^data: /, "");
if (stripped === "[DONE]") return "[DONE]";
return JSON.parse(stripped);
}
const baseUsage = {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
};
function emptyAssistant(): AssistantMessage {
return {
role: "assistant",
content: [],
api: "openai-completions",
provider: "openai",
model: "gpt-test",
usage: baseUsage,
stopReason: "stop",
timestamp: 0,
};
}
describe("auth-gateway openai-chat: parseRequest", () => {
it("converts a full request into a Context", () => {
const parsed = parseRequest({
model: "gpt-5.2",
messages: [
{ role: "system", content: "you are X" },
{ role: "system", content: "also Y" },
{ role: "user", content: "hi" },
{
role: "assistant",
content: "hello",
tool_calls: [
{
id: "call_1",
type: "function",
function: { name: "lookup", arguments: '{"q":"a"}' },
},
{
id: "call_2",
type: "function",
function: { name: "broken", arguments: "not-json" },
},
],
},
{ role: "tool", tool_call_id: "call_1", content: "result-text" },
],
tools: [
{
type: "function",
function: {
name: "lookup",
description: "look something up",
parameters: { type: "object", properties: { q: { type: "string" } } },
},
},
],
stream: true,
max_tokens: 512,
max_completion_tokens: 1024,
temperature: 0.2,
top_p: 0.9,
stop: ["\n\n"],
tool_choice: { type: "function", function: { name: "lookup" } },
response_format: { type: "json_object" },
stream_options: { include_usage: true },
});
expect(parsed.modelId).toBe("gpt-5.2");
expect(parsed.stream).toBe(true);
expect(parsed.context.systemPrompt).toEqual(["you are X\n\nalso Y"]);
expect(parsed.context.messages).toHaveLength(3);
const [user, assistant, tool] = parsed.context.messages;
expect(user.role).toBe("user");
expect(assistant.role).toBe("assistant");
if (assistant.role !== "assistant") throw new Error("unreachable");
expect(assistant.api).toBe("openai-completions");
expect(assistant.provider).toBe("openai");
expect(assistant.model).toBe("gpt-5.2");
expect(assistant.content[0]).toEqual({ type: "text", text: "hello" });
const call1 = assistant.content[1];
const call2 = assistant.content[2];
if (call1.type !== "toolCall" || call2.type !== "toolCall") throw new Error("unreachable");
expect(call1.id).toBe("call_1");
expect(call1.name).toBe("lookup");
expect(call1.arguments).toEqual({ q: "a" });
expect(call2.arguments).toEqual({ __raw: "not-json" });
expect(tool.role).toBe("toolResult");
if (tool.role !== "toolResult") throw new Error("unreachable");
expect(tool.toolCallId).toBe("call_1");
expect(tool.toolName).toBe("lookup");
expect(tool.content).toEqual([{ type: "text", text: "result-text" }]);
expect(parsed.context.tools).toHaveLength(1);
expect(parsed.context.tools?.[0].name).toBe("lookup");
expect(parsed.options.maxOutputTokens).toBe(1024);
expect(parsed.options.temperature).toBe(0.2);
expect(parsed.options.topP).toBe(0.9);
expect(parsed.options.stopSequences).toEqual(["\n\n"]);
expect(parsed.options.toolChoice).toEqual({ name: "lookup" });
expect(parsed.options.responseFormat).toEqual({ type: "json_object" });
expect(parsed.options.extra).toEqual({ includeStreamingUsage: true });
});
it("rejects missing required fields", () => {
expect(() => parseRequest({ messages: [] })).toThrow(/model/);
expect(() => parseRequest({ model: "x" })).toThrow(/messages/);
});
it("falls back to max_tokens when max_completion_tokens is absent", () => {
const parsed = parseRequest({ model: "m", messages: [], max_tokens: 256 });
expect(parsed.options.maxOutputTokens).toBe(256);
expect(parsed.stream).toBe(false);
});
it("honours an explicit wire `name` on a tool message over back-resolution", () => {
const parsed = parseRequest({
model: "m",
messages: [
{ role: "user", content: "go" },
{
role: "assistant",
tool_calls: [{ id: "c1", type: "function", function: { name: "lookup", arguments: "{}" } }],
},
{ role: "tool", tool_call_id: "c1", name: "submit_move", content: "ok" },
],
});
const tool = parsed.context.messages.find(m => m.role === "toolResult");
if (tool?.role !== "toolResult") throw new Error("expected toolResult");
expect(tool.toolName).toBe("submit_move");
});
it("treats an empty wire `name` as absent and falls back to map lookup", () => {
const parsed = parseRequest({
model: "m",
messages: [
{ role: "user", content: "go" },
{
role: "assistant",
tool_calls: [{ id: "c1", type: "function", function: { name: "submit_move", arguments: "{}" } }],
},
{ role: "tool", tool_call_id: "c1", name: "", content: "ok" },
],
});
const tool = parsed.context.messages.find(m => m.role === "toolResult");
if (tool?.role !== "toolResult") throw new Error("expected toolResult");
expect(tool.toolName).toBe("submit_move");
});
it("leaves toolName empty when no matching tool_call_id and no wire name", () => {
const parsed = parseRequest({
model: "m",
messages: [
{ role: "user", content: "go" },
{ role: "tool", tool_call_id: "orphan", content: "ok" },
],
});
const tool = parsed.context.messages.find(m => m.role === "toolResult");
if (tool?.role !== "toolResult") throw new Error("expected toolResult");
expect(tool.toolName).toBe("");
});
});
describe("auth-gateway openai-chat: encodeResponse", () => {
it("serializes text + tool calls with finish_reason=tool_calls", () => {
const message: AssistantMessage = {
...emptyAssistant(),
content: [
{ type: "text", text: "the answer is " },
{ type: "thinking", thinking: "private reasoning" },
{ type: "toolCall", id: "call_42", name: "compute", arguments: { x: 1 } },
],
usage: { ...baseUsage, input: 10, output: 20, cacheRead: 4, cacheWrite: 6, totalTokens: 40 },
stopReason: "toolUse",
};
const out = encodeResponse(message, "gpt-5.2");
expect(out.object).toBe("chat.completion");
expect(out.model).toBe("gpt-5.2");
expect(typeof out.id).toBe("string");
expect(String(out.id).startsWith("chatcmpl-")).toBe(true);
const choices = out.choices as Array<{
index: number;
message: { role: string; content: string | null; tool_calls?: unknown };
finish_reason: string;
}>;
expect(choices).toHaveLength(1);
expect(choices[0].finish_reason).toBe("tool_calls");
expect(choices[0].message.role).toBe("assistant");
expect(choices[0].message.content).toBe("the answer is ");
expect(choices[0].message.tool_calls).toEqual([
{ id: "call_42", type: "function", function: { name: "compute", arguments: '{"x":1}' } },
]);
expect(out.usage).toEqual({
prompt_tokens: 20,
prompt_tokens_details: { cached_tokens: 4 },
completion_tokens: 20,
total_tokens: 40,
});
});
it("maps length stop reason and emits null content when text is empty", () => {
const message: AssistantMessage = { ...emptyAssistant(), stopReason: "length" };
const out = encodeResponse(message, "gpt-test");
const choices = out.choices as Array<{ finish_reason: string; message: { content: string | null } }>;
expect(choices[0].finish_reason).toBe("length");
expect(choices[0].message.content).toBeNull();
});
});
describe("auth-gateway openai-chat: encodeStream", () => {
it("emits role chunk, text deltas, tool_call deltas with sequential indexes, then [DONE]", async () => {
const partial = emptyAssistant();
partial.content = [
{ type: "text", text: "" },
{ type: "toolCall", id: "call_A", name: "tool_a", arguments: {} },
{ type: "toolCall", id: "call_B", name: "tool_b", arguments: {} },
];
const events: AssistantMessageEvent[] = [
{ type: "text_start", contentIndex: 0, partial },
{ type: "text_delta", contentIndex: 0, delta: "Hi ", partial },
{ type: "text_delta", contentIndex: 0, delta: "there", partial },
{ type: "text_end", contentIndex: 0, content: "Hi there", partial },
{ type: "toolcall_start", contentIndex: 1, partial },
{ type: "toolcall_delta", contentIndex: 1, delta: '{"a":', partial },
{ type: "toolcall_delta", contentIndex: 1, delta: "1}", partial },
{ type: "toolcall_start", contentIndex: 2, partial },
{ type: "toolcall_delta", contentIndex: 2, delta: "{}", partial },
{
type: "done",
reason: "toolUse",
message: { ...partial, stopReason: "toolUse" },
},
];
const stream = encodeStream(makeEventStream(events, partial), "gpt-5.2");
const lines = await collectStream(stream);
const payloads = lines.map(parseSseLine);
expect(payloads[payloads.length - 1]).toBe("[DONE]");
const chunks = payloads.slice(0, -1) as Array<{
id: string;
object: string;
model: string;
choices: Array<{ delta: Record<string, unknown>; finish_reason: string | null }>;
}>;
expect(chunks[0].object).toBe("chat.completion.chunk");
expect(chunks[0].model).toBe("gpt-5.2");
expect(chunks[0].choices[0].delta).toEqual({ role: "assistant" });
expect(chunks[0].choices[0].finish_reason).toBeNull();
const id = chunks[0].id;
for (const c of chunks) expect(c.id).toBe(id);
const textDeltas = chunks.map(c => c.choices[0].delta.content).filter((v): v is string => typeof v === "string");
expect(textDeltas.join("")).toBe("Hi there");
const toolDeltas: Array<{ index: number; id?: string; function?: { name?: string; arguments?: string } }> = [];
for (const c of chunks) {
const tc = c.choices[0].delta.tool_calls;
if (Array.isArray(tc)) toolDeltas.push(...(tc as typeof toolDeltas));
}
const starts = toolDeltas.filter(t => typeof t.id === "string" && t.id.length > 0);
expect(starts.map(s => s.index)).toEqual([0, 1]);
expect(starts[0].id).toBe("call_A");
expect(starts[0].function?.name).toBe("tool_a");
expect(starts[1].id).toBe("call_B");
expect(starts[1].function?.name).toBe("tool_b");
const argDeltas = toolDeltas.filter(t => typeof t.function?.arguments === "string" && !t.id);
expect(argDeltas.map(d => [d.index, d.function?.arguments])).toEqual([
[0, '{"a":'],
[0, "1}"],
[1, "{}"],
]);
const finishChunk = chunks[chunks.length - 1];
expect(finishChunk.choices[0].delta).toEqual({});
expect(finishChunk.choices[0].finish_reason).toBe("tool_calls");
});
it("emits an error envelope when the stream errors", async () => {
const partial = emptyAssistant();
const errorMessage: AssistantMessage = { ...partial, errorMessage: "upstream went away" };
const events: AssistantMessageEvent[] = [{ type: "error", reason: "error", error: errorMessage }];
const stream = encodeStream(makeEventStream(events, partial), "gpt-test");
const lines = await collectStream(stream);
expect(lines).toHaveLength(2);
const payloads = lines.map(parseSseLine) as Array<Record<string, unknown>>;
expect(payloads[1]).toEqual({ error: { message: "upstream went away", type: "upstream_error" } });
});
});