OpenCut/apps/web/src/agent/providers/__tests__/openai-compatible.test.ts

296 lines
7.7 KiB
TypeScript

import { beforeEach, describe, expect, jest, test } from "bun:test";
import type { ProviderConfig } from "@/agent/providers/types";
import type { ToolDefinition } from "@/agent/types";
// ---------------------------------------------------------------------------
// Mock the openai SDK before any module that imports it is loaded
// ---------------------------------------------------------------------------
const mockCreate = jest.fn();
jest.mock("openai", () => ({
default: class MockOpenAI {
chat = {
completions: {
create: mockCreate,
},
};
constructor(_opts: { apiKey: string; baseURL?: string }) {}
},
}));
import { OpenAICompatibleAdapter } from "@/agent/providers/openai-compatible";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const TEST_CONFIG: ProviderConfig = {
provider: "openai-compatible",
apiKey: "sk-test",
model: "gpt-4o-mini",
};
const SIMPLE_TOOLS: ToolDefinition[] = [
{
name: "test_tool",
description: "A test tool",
parameters: [
{ key: "input", type: "string", required: true },
],
execute: async () => ({}),
},
];
function makeOpenAIResponse(overrides: {
content?: string;
toolCalls?: Array<{ id: string; name: string; arguments: string }>;
}) {
return {
choices: [
{
message: {
content: overrides.content ?? "Hello!",
tool_calls: overrides.toolCalls?.map((tc) => ({
id: tc.id,
type: "function",
function: { name: tc.name, arguments: tc.arguments },
})),
},
},
],
};
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe("OpenAICompatibleAdapter.chat()", () => {
beforeEach(() => {
mockCreate.mockReset();
});
test("returns content-only response", async () => {
mockCreate.mockResolvedValueOnce(
makeOpenAIResponse({ content: "Hi there!" }),
);
const adapter = new OpenAICompatibleAdapter(TEST_CONFIG);
const result = await adapter.chat({
messages: [{ id: "1", role: "user", content: "Hey", timestamp: 0 }],
systemPrompt: "You are helpful.",
tools: [],
});
expect(result.content).toBe("Hi there!");
expect(result.toolCalls).toBeUndefined();
});
test("returns tool calls from provider response", async () => {
mockCreate.mockResolvedValueOnce(
makeOpenAIResponse({
content: "",
toolCalls: [
{
id: "tc_1",
name: "test_tool",
arguments: '{"input":"hello"}',
},
],
}),
);
const adapter = new OpenAICompatibleAdapter(TEST_CONFIG);
const result = await adapter.chat({
messages: [{ id: "1", role: "user", content: "Run tool", timestamp: 0 }],
systemPrompt: "System",
tools: SIMPLE_TOOLS,
});
expect(result.content).toBe("");
expect(result.toolCalls).toHaveLength(1);
expect(result.toolCalls?.[0]).toEqual({
id: "tc_1",
name: "test_tool",
args: { input: "hello" },
});
});
test("prepends system prompt as first message", async () => {
mockCreate.mockResolvedValueOnce(makeOpenAIResponse({ content: "ok" }));
const adapter = new OpenAICompatibleAdapter(TEST_CONFIG);
await adapter.chat({
messages: [{ id: "1", role: "user", content: "Hi", timestamp: 0 }],
systemPrompt: "Custom system prompt",
tools: [],
});
const callArgs = mockCreate.mock.calls[0][0] as {
messages: Array<Record<string, unknown>>;
};
expect(callArgs.messages[0]).toEqual({
role: "system",
content: "Custom system prompt",
});
});
test("converts user messages to OpenAI format", async () => {
mockCreate.mockResolvedValueOnce(makeOpenAIResponse({ content: "ok" }));
const adapter = new OpenAICompatibleAdapter(TEST_CONFIG);
await adapter.chat({
messages: [
{ id: "1", role: "user", content: "Hello", timestamp: 0 },
],
systemPrompt: "System",
tools: [],
});
const callArgs = mockCreate.mock.calls[0][0] as {
messages: Array<Record<string, unknown>>;
};
// messages[0] is system, messages[1] is user
expect(callArgs.messages[1]).toEqual({
role: "user",
content: "Hello",
});
});
test("converts assistant messages with tool calls", async () => {
mockCreate.mockResolvedValueOnce(makeOpenAIResponse({ content: "ok" }));
const adapter = new OpenAICompatibleAdapter(TEST_CONFIG);
await adapter.chat({
messages: [
{
id: "1",
role: "assistant",
content: "Let me check.",
toolCalls: [
{ id: "tc_1", name: "test_tool", args: { input: "x" } },
],
timestamp: 0,
},
],
systemPrompt: "System",
tools: [],
});
const callArgs = mockCreate.mock.calls[0][0] as {
messages: Array<Record<string, unknown>>;
};
const assistantMsg = callArgs.messages[1];
expect(assistantMsg.role).toBe("assistant");
expect(assistantMsg.content).toBe("Let me check.");
expect(assistantMsg.tool_calls).toEqual([
{
id: "tc_1",
type: "function",
function: { name: "test_tool", arguments: '{"input":"x"}' },
},
]);
});
test("converts tool_result messages to OpenAI tool format", async () => {
mockCreate.mockResolvedValueOnce(makeOpenAIResponse({ content: "ok" }));
const adapter = new OpenAICompatibleAdapter(TEST_CONFIG);
await adapter.chat({
messages: [
{
id: "1",
role: "tool_result",
content: '{"result":"data"}',
toolCallId: "tc_1",
timestamp: 0,
},
],
systemPrompt: "System",
tools: [],
});
const callArgs = mockCreate.mock.calls[0][0] as {
messages: Array<Record<string, unknown>>;
};
expect(callArgs.messages[1]).toEqual({
role: "tool",
tool_call_id: "tc_1",
content: '{"result":"data"}',
});
});
test("converts tool definitions to OpenAI function format", async () => {
mockCreate.mockResolvedValueOnce(makeOpenAIResponse({ content: "ok" }));
const adapter = new OpenAICompatibleAdapter(TEST_CONFIG);
await adapter.chat({
messages: [{ id: "1", role: "user", content: "Go", timestamp: 0 }],
systemPrompt: "System",
tools: SIMPLE_TOOLS,
});
const callArgs = mockCreate.mock.calls[0][0] as {
tools: Array<Record<string, unknown>>;
};
expect(callArgs.tools).toHaveLength(1);
expect(callArgs.tools[0]).toEqual({
type: "function",
function: {
name: "test_tool",
description: "A test tool",
parameters: {
type: "object",
properties: { input: { type: "string" } },
required: ["input"],
},
},
});
});
test("throws when provider returns empty choices", async () => {
mockCreate.mockResolvedValueOnce({ choices: [] });
const adapter = new OpenAICompatibleAdapter(TEST_CONFIG);
expect(
adapter.chat({
messages: [{ id: "1", role: "user", content: "Hi", timestamp: 0 }],
systemPrompt: "System",
tools: [],
}),
).rejects.toThrow("Empty response from AI provider");
});
test("defaults content to empty string when null", async () => {
mockCreate.mockResolvedValueOnce({
choices: [{ message: { content: null } }],
});
const adapter = new OpenAICompatibleAdapter(TEST_CONFIG);
const result = await adapter.chat({
messages: [{ id: "1", role: "user", content: "Hi", timestamp: 0 }],
systemPrompt: "System",
tools: [],
});
expect(result.content).toBe("");
expect(result.toolCalls).toBeUndefined();
});
test("passes model from config to API call", async () => {
mockCreate.mockResolvedValueOnce(makeOpenAIResponse({ content: "ok" }));
const adapter = new OpenAICompatibleAdapter(TEST_CONFIG);
await adapter.chat({
messages: [{ id: "1", role: "user", content: "Hi", timestamp: 0 }],
systemPrompt: "System",
tools: [],
});
const callArgs = mockCreate.mock.calls[0][0] as { model: string };
expect(callArgs.model).toBe("gpt-4o-mini");
});
});