import { describe, expect, it } from "bun:test";
import { Effort } from "../src/model-thinking";
import { encodeResponse, encodeStream, parseRequest } from "../src/providers/openai-responses-server";
import type { AssistantMessage } from "../src/types";
import { AssistantMessageEventStream } from "../src/utils/event-stream";
function zeroUsage(): AssistantMessage["usage"] {
return {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
};
}
async function collectStream(stream: ReadableStream<Uint8Array>): Promise<string> {
const reader = stream.getReader();
const decoder = new TextDecoder();
let out = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
out += decoder.decode(value);
}
return out;
}
interface SseFrame {
event: string;
data: Record<string, unknown> | string;
}
function parseSse(raw: string): SseFrame[] {
const frames: SseFrame[] = [];
for (const chunk of raw.split("\n\n")) {
if (!chunk.trim()) continue;
let event = "";
let dataLine = "";
for (const line of chunk.split("\n")) {
if (line.startsWith("event: ")) event = line.slice("event: ".length);
else if (line.startsWith("data: ")) dataLine = line.slice("data: ".length);
}
if (dataLine === "[DONE]") {
frames.push({ event: event || "done_sentinel", data: "[DONE]" });
} else if (dataLine) {
const parsed: unknown = JSON.parse(dataLine);
if (parsed && typeof parsed === "object") {
frames.push({ event, data: parsed as Record<string, unknown> });
}
}
}
return frames;
}
describe("openai-responses parseRequest", () => {
it("parses an input array with mixed message + reasoning + function_call + function_call_output", () => {
const reasoningItem = {
type: "reasoning",
id: "rs_abc",
summary: [{ type: "summary_text", text: "The user wants arithmetic." }],
};
const parsed = parseRequest({
model: "gpt-5.3-codex-spark",
instructions: "You are X",
input: [
{ type: "message", role: "user", content: [{ type: "input_text", text: "what's 2+2?" }] },
{
type: "message",
role: "assistant",
content: [{ type: "output_text", text: "Let me think." }],
},
reasoningItem,
{
type: "function_call",
id: "fc_item_999",
call_id: "call_42",
name: "math",
arguments: '{"a":2,"b":2}',
},
{ type: "function_call_output", call_id: "call_42", output: "4" },
],
tools: [
{
type: "function",
name: "math",
description: "Do arithmetic",
parameters: { type: "object", properties: { a: { type: "number" }, b: { type: "number" } } },
strict: true,
},
],
tool_choice: { type: "function", name: "math" },
max_output_tokens: 1024,
temperature: 0.1,
top_p: 0.9,
reasoning: { effort: "high", summary: "detailed" },
store: true,
previous_response_id: "resp_prev",
stream: true,
});
expect(parsed.modelId).toBe("gpt-5.3-codex-spark");
expect(parsed.stream).toBe(true);
expect(parsed.context.systemPrompt).toEqual(["You are X"]);
const msgs = parsed.context.messages;
expect(msgs).toHaveLength(3);
expect(msgs[0]!.role).toBe("user");
const u = msgs[0]!;
if (u.role !== "user") throw new Error("expected user");
expect(u.content).toBe("what's 2+2?");
const a = msgs[1]!;
if (a.role !== "assistant") throw new Error("expected assistant");
expect(a.api).toBe("openai-responses");
expect(a.provider).toBe("openai");
expect(a.model).toBe("gpt-5.3-codex-spark");
expect(a.content).toHaveLength(3);
expect(a.content[0]).toMatchObject({ type: "text", text: "Let me think." });
expect(a.content[1]).toMatchObject({
type: "thinking",
thinking: "The user wants arithmetic.",
thinkingSignature: JSON.stringify(reasoningItem),
itemId: "rs_abc",
});
expect(a.content[2]).toMatchObject({
type: "toolCall",
id: "call_42",
name: "math",
arguments: { a: 2, b: 2 },
thoughtSignature: "fc_item_999",
});
const tr = msgs[2]!;
if (tr.role !== "toolResult") throw new Error("expected toolResult");
expect(tr.toolCallId).toBe("call_42");
expect(tr.toolName).toBe("math");
expect(tr.content).toEqual([{ type: "text", text: "4" }]);
expect(tr.isError).toBe(false);
expect(parsed.context.tools).toHaveLength(1);
expect(parsed.context.tools![0]).toMatchObject({ name: "math", strict: true });
expect(parsed.options.maxOutputTokens).toBe(1024);
expect(parsed.options.temperature).toBe(0.1);
expect(parsed.options.topP).toBe(0.9);
expect(parsed.options.toolChoice).toEqual({ name: "math" });
expect(parsed.options.reasoning).toBe(Effort.High);
expect(parsed.options.hideThinkingSummary).toBeUndefined();
expect(parsed.options.extra).toBeUndefined();
});
it("accepts a bare string input and rejects a missing model", () => {
const parsed = parseRequest({ model: "m", input: "hi" });
expect(parsed.context.messages).toHaveLength(1);
const m = parsed.context.messages[0]!;
if (m.role !== "user") throw new Error("expected user");
expect(m.content).toBe("hi");
expect(() => parseRequest({ input: "hi" })).toThrow(/model/);
});
it("preserves string message content and system input items", () => {
const parsed = parseRequest({
model: "m",
instructions: "top-level instructions",
input: [
{ role: "system", content: "system from easy input" },
{ role: "user", content: "hello" },
{ role: "assistant", content: "hi there" },
{
type: "message",
role: "system",
content: [{ type: "input_text", text: "structured system" }],
},
],
});
expect(parsed.context.systemPrompt).toEqual([
"top-level instructions",
"system from easy input",
"structured system",
]);
expect(parsed.context.messages).toHaveLength(2);
const user = parsed.context.messages[0]!;
const assistant = parsed.context.messages[1]!;
if (user.role !== "user") throw new Error("expected user");
if (assistant.role !== "assistant") throw new Error("expected assistant");
expect(user.content).toBe("hello");
expect(assistant.content).toEqual([{ type: "text", text: "hi there" }]);
});
it("creates a synthetic assistant when reasoning comes before any assistant message", () => {
const reasoningItem = {
type: "reasoning",
id: "rs_x",
content: [{ type: "reasoning_text", text: "hmm" }],
};
const parsed = parseRequest({
model: "m",
input: [reasoningItem],
});
expect(parsed.context.messages).toHaveLength(1);
const a = parsed.context.messages[0]!;
if (a.role !== "assistant") throw new Error("expected synthetic assistant");
expect(a.content).toHaveLength(1);
expect(a.content[0]).toMatchObject({
type: "thinking",
thinking: "hmm",
thinkingSignature: JSON.stringify(reasoningItem),
itemId: "rs_x",
});
});
});
describe("openai-responses encodeResponse", () => {
it("encodes reasoning + message + function_call output items", () => {
const reasoningItem = {
type: "reasoning",
id: "rs_signed",
summary: [{ type: "summary_text", text: "thinking aloud" }],
};
const message: AssistantMessage = {
role: "assistant",
api: "openai-responses",
provider: "openai",
model: "gpt-5",
content: [
{
type: "thinking",
thinking: "thinking aloud",
thinkingSignature: JSON.stringify(reasoningItem),
itemId: "rs_signed",
},
{ type: "text", text: "Hello " },
{ type: "text", text: "world" },
{
type: "toolCall",
id: "call_t1",
name: "math",
arguments: { a: 1, b: 2 },
thoughtSignature: "fc_item_t1",
},
],
usage: {
...zeroUsage(),
input: 10,
output: 20,
cacheRead: 4,
cacheWrite: 6,
reasoningTokens: 5,
},
stopReason: "toolUse",
timestamp: 1_700_000_000_000,
};
const body = encodeResponse(message, "gpt-5-requested");
expect(body.object).toBe("response");
expect(body.status).toBe("completed");
expect(body.model).toBe("gpt-5-requested");
expect(body.created_at).toBe(1_700_000_000);
expect(typeof body.id).toBe("string");
expect((body.id as string).startsWith("resp_")).toBe(true);
const output = body.output as Array<Record<string, unknown>>;
expect(output).toHaveLength(3);
expect(output[0]).toEqual(reasoningItem);
expect(output[1]!.type).toBe("message");
expect(output[1]!.role).toBe("assistant");
const parts = output[1]!.content as Array<{ type: string; text: string; annotations: never[] }>;
expect(parts).toEqual([
{ type: "output_text", text: "Hello ", annotations: [] },
{ type: "output_text", text: "world", annotations: [] },
]);
expect(output[2]).toMatchObject({
type: "function_call",
id: "fc_item_t1",
call_id: "call_t1",
name: "math",
arguments: '{"a":1,"b":2}',
status: "completed",
});
expect(body.usage).toEqual({
input_tokens: 20,
input_tokens_details: { cached_tokens: 4 },
output_tokens: 20,
output_tokens_details: { reasoning_tokens: 5 },
total_tokens: 40,
});
});
it("marks length-limited responses incomplete", () => {
const message: AssistantMessage = {
role: "assistant",
api: "openai-responses",
provider: "openai",
model: "gpt-5",
content: [{ type: "text", text: "partial" }],
usage: zeroUsage(),
stopReason: "length",
timestamp: 1_700_000_000_000,
};
const body = encodeResponse(message, "gpt-5-requested");
expect(body.status).toBe("incomplete");
expect(body.incomplete_details).toEqual({ reason: "max_output_tokens" });
});
});
describe("openai-responses encodeStream", () => {
it("emits response.created, reasoning_summary_text.delta, output_text.delta, function_call_arguments.delta, response.completed, [DONE]", async () => {
const stream = new AssistantMessageEventStream();
const partial: AssistantMessage = {
role: "assistant",
api: "openai-responses",
provider: "openai",
model: "gpt-5",
content: [],
usage: zeroUsage(),
stopReason: "stop",
timestamp: 1_700_000_000_000,
};
const finalMessage: AssistantMessage = {
role: "assistant",
api: "openai-responses",
provider: "openai",
model: "gpt-5",
content: [
{ type: "thinking", thinking: "step 1", thinkingSignature: "rs_s1", itemId: "rs_s1" },
{ type: "text", text: "Hi!" },
{
type: "toolCall",
id: "call_x",
name: "math",
arguments: { a: 1 },
thoughtSignature: "fc_x",
},
],
usage: { ...zeroUsage(), input: 1, output: 2 },
stopReason: "toolUse",
timestamp: 1_700_000_000_000,
};
const partialWithThinking: AssistantMessage = {
...partial,
content: [{ type: "thinking", thinking: "", thinkingSignature: "rs_s1", itemId: "rs_s1" }],
};
const partialWithToolCall: AssistantMessage = {
...partial,
content: [
{ type: "thinking", thinking: "step 1", thinkingSignature: "rs_s1", itemId: "rs_s1" },
{ type: "text", text: "Hi!" },
{ type: "toolCall", id: "call_x", name: "math", arguments: {}, thoughtSignature: "fc_x" },
],
};
queueMicrotask(() => {
stream.push({ type: "start", partial });
stream.push({ type: "thinking_start", contentIndex: 0, partial: partialWithThinking });
stream.push({ type: "thinking_delta", contentIndex: 0, delta: "step ", partial: partialWithThinking });
stream.push({ type: "thinking_delta", contentIndex: 0, delta: "1", partial: partialWithThinking });
stream.push({ type: "thinking_end", contentIndex: 0, content: "step 1", partial: partialWithThinking });
stream.push({ type: "text_start", contentIndex: 1, partial });
stream.push({ type: "text_delta", contentIndex: 1, delta: "Hi", partial });
stream.push({ type: "text_delta", contentIndex: 1, delta: "!", partial });
stream.push({ type: "text_end", contentIndex: 1, content: "Hi!", partial });
stream.push({ type: "toolcall_start", contentIndex: 2, partial: partialWithToolCall });
stream.push({ type: "toolcall_delta", contentIndex: 2, delta: '{"a":', partial: partialWithToolCall });
stream.push({ type: "toolcall_delta", contentIndex: 2, delta: "1}", partial: partialWithToolCall });
stream.push({
type: "toolcall_end",
contentIndex: 2,
toolCall: {
type: "toolCall",
id: "call_x",
name: "math",
arguments: { a: 1 },
thoughtSignature: "fc_x",
},
partial: partialWithToolCall,
});
stream.push({ type: "done", reason: "toolUse", message: finalMessage });
});
const raw = await collectStream(encodeStream(stream, "gpt-5-requested"));
const frames = parseSse(raw);
const names = frames.map(f => f.event);
expect(names[0]).toBe("response.created");
expect(names[names.length - 1]).toBe("done_sentinel");
expect(frames[frames.length - 1]!.data).toBe("[DONE]");
const idxCreated = names.indexOf("response.created");
const idxReasoningDelta = names.indexOf("response.reasoning_summary_text.delta");
const idxReasoningDone = names.indexOf("response.reasoning_summary_text.done");
const idxTextDelta = names.indexOf("response.output_text.delta");
const idxTextDone = names.indexOf("response.output_text.done");
const idxArgsDelta = names.indexOf("response.function_call_arguments.delta");
const idxMessageDone = frames.findIndex(
f =>
f.event === "response.output_item.done" &&
(f.data as Record<string, unknown>).item &&
((f.data as Record<string, unknown>).item as Record<string, unknown>).type === "message",
);
const idxArgsDone = names.indexOf("response.function_call_arguments.done");
const idxCompleted = names.indexOf("response.completed");
expect(idxCreated).toBeGreaterThanOrEqual(0);
expect(idxReasoningDelta).toBeGreaterThan(idxCreated);
expect(idxReasoningDone).toBeGreaterThan(idxReasoningDelta);
expect(idxTextDelta).toBeGreaterThan(idxReasoningDone);
expect(idxTextDone).toBeGreaterThan(idxTextDelta);
expect(idxArgsDelta).toBeGreaterThan(idxTextDone);
expect(idxArgsDone).toBeGreaterThan(idxArgsDelta);
expect(idxCompleted).toBeGreaterThan(idxArgsDone);
const reasoningDelta = frames[idxReasoningDelta]!.data as Record<string, unknown>;
expect(reasoningDelta.item_id).toBe("rs_s1");
expect(reasoningDelta.output_index).toBe(0);
expect(reasoningDelta.delta).toBe("step ");
const textDelta = frames[idxTextDelta]!.data as Record<string, unknown>;
expect(typeof textDelta.item_id).toBe("string");
expect((textDelta.item_id as string).startsWith("msg_")).toBe(true);
expect(textDelta.output_index).toBe(1);
expect(textDelta.delta).toBe("Hi");
expect(textDelta.logprobs).toEqual([]);
const textDone = frames[idxTextDone]!.data as Record<string, unknown>;
expect(textDone.text).toBe("Hi!");
expect(textDone.logprobs).toEqual([]);
const messageDone = frames[idxMessageDone]!.data as Record<string, unknown>;
expect(messageDone.output_index).toBe(1);
expect(messageDone.item).toMatchObject({
type: "message",
status: "completed",
content: [{ type: "output_text", text: "Hi!", annotations: [] }],
});
const argsDelta = frames[idxArgsDelta]!.data as Record<string, unknown>;
expect(argsDelta.item_id).toBe("fc_x");
expect(argsDelta.output_index).toBe(2);
expect(argsDelta.delta).toBe('{"a":');
const argsDone = frames[idxArgsDone]!.data as Record<string, unknown>;
expect(argsDone.item_id).toBe("fc_x");
expect(argsDone.arguments).toBe('{"a":1}');
expect(argsDone.name).toBe("math");
const completed = frames[idxCompleted]!.data as Record<string, unknown>;
const response = completed.response as Record<string, unknown>;
expect(response.status).toBe("completed");
expect(response.model).toBe("gpt-5-requested");
const output = response.output as Array<Record<string, unknown>>;
expect(output).toHaveLength(3);
expect(output[0]!.type).toBe("reasoning");
expect(output[1]!.type).toBe("message");
expect(output[2]).toMatchObject({
type: "function_call",
id: "fc_x",
call_id: "call_x",
name: "math",
arguments: '{"a":1}',
});
expect(output[2]!.id).not.toBe(output[2]!.call_id);
});
it("emits response.incomplete for length-limited streams", async () => {
const stream = new AssistantMessageEventStream();
const message: AssistantMessage = {
role: "assistant",
api: "openai-responses",
provider: "openai",
model: "gpt-5",
content: [{ type: "text", text: "partial" }],
usage: { ...zeroUsage(), output: 1 },
stopReason: "length",
timestamp: 1_700_000_000_000,
};
queueMicrotask(() => {
stream.push({ type: "start", partial: { ...message, content: [] } });
stream.push({ type: "text_start", contentIndex: 0, partial: message });
stream.push({ type: "text_delta", contentIndex: 0, delta: "partial", partial: message });
stream.push({ type: "text_end", contentIndex: 0, content: "partial", partial: message });
stream.push({ type: "done", reason: "length", message });
});
const raw = await collectStream(encodeStream(stream, "gpt-5-requested"));
const frames = parseSse(raw);
const names = frames.map(f => f.event);
const idxIncomplete = names.indexOf("response.incomplete");
expect(idxIncomplete).toBeGreaterThan(-1);
expect(names).not.toContain("response.completed");
const incomplete = frames[idxIncomplete]!.data as Record<string, unknown>;
const response = incomplete.response as Record<string, unknown>;
expect(response.status).toBe("incomplete");
expect(response.incomplete_details).toEqual({ reason: "max_output_tokens" });
});
});