import { describe, it, expect, vi, beforeEach } from "vitest"; const mockCreate = vi.fn(); const mockList = vi.fn(); const mockRetrieve = vi.fn(); vi.mock("openai", () => { return { default: class MockOpenAI { chat = { completions: { create: mockCreate, }, }; models = { list: mockList, retrieve: mockRetrieve, }; }, }; }); import { OpenAiApi } from "./openai"; const mockLogger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), }; const mockEnv = { NODE_ENV: "test", services: {} }; const mockProvider = { _id: "provider-openai", name: "OpenAI Compatible", sdk: "openai" as const, baseUrl: "https://example.test/v1", apiKey: "test-key", }; async function* streamChunks(chunks: unknown[]) { for (const chunk of chunks) { yield chunk; } } describe("OpenAiApi", () => { let api: OpenAiApi; beforeEach(() => { vi.clearAllMocks(); api = new OpenAiApi(mockEnv as any, mockProvider as any, mockLogger as any); }); it("rejects an empty streaming and fallback response", async () => { mockCreate .mockResolvedValueOnce(streamChunks([ { choices: [{ delta: {}, finish_reason: "stop" }] }, ])) .mockResolvedValueOnce({ choices: [{ message: { content: "", tool_calls: [] }, finish_reason: "stop" }], }); await expect(api.chat( { provider: mockProvider as any, modelId: "test-model", params: { reasoning: false, temperature: 0.8, topP: 0.9, topK: 40, numPredict: -1, numCtx: 131072, maxCompletionTokens: 16384 }, }, { userPrompt: "Hello", context: [], tools: [] }, vi.fn(), )).rejects.toThrow("Provider returned an empty chat response"); }); it("assembles streamed tool-call argument fragments and returns them", async () => { mockCreate.mockResolvedValueOnce(streamChunks([ { choices: [{ delta: { tool_calls: [{ index: 0, id: "call_1", type: "function", function: { name: "file_read", arguments: '{"path"' }, }], }, finish_reason: null, }], }, { choices: [{ delta: { tool_calls: [{ index: 0, function: { arguments: ':"index.html"}' }, }], }, finish_reason: "tool_calls", }], }, ])); const streamCallback = vi.fn(); const response = await api.chat( { provider: mockProvider as any, modelId: "test-model", params: { reasoning: false, temperature: 0.8, topP: 0.9, topK: 40, numPredict: -1, numCtx: 131072, maxCompletionTokens: 16384 }, }, { userPrompt: "Read index.html", context: [], tools: [] }, streamCallback, ); // Tool calls are returned, not executed expect(response.toolCalls).toBeDefined(); expect(response.toolCalls!.length).toBe(1); expect(response.toolCalls![0].function.name).toBe("file_read"); expect(response.toolCalls![0].function.arguments).toBe('{"path":"index.html"}'); expect(mockCreate).toHaveBeenCalledTimes(1); }); it("falls back to non-streaming response when stream has no deltas", async () => { mockCreate .mockResolvedValueOnce(streamChunks([ { choices: [{ delta: {}, finish_reason: "stop" }] }, ])) .mockResolvedValueOnce({ choices: [{ message: { content: "Fallback answer", tool_calls: [] }, finish_reason: "stop" }], }); const streamCallback = vi.fn(); const response = await api.chat( { provider: mockProvider as any, modelId: "test-model", params: { reasoning: false, temperature: 0.8, topP: 0.9, topK: 40, numPredict: -1, numCtx: 131072, maxCompletionTokens: 16384 }, }, { userPrompt: "Hello", context: [], tools: [] }, streamCallback, ); expect(response.response).toBe("Fallback answer"); expect(streamCallback).toHaveBeenCalledWith({ type: "response", data: "Fallback answer" }); }); it("returns conversational responses without forcing another iteration", async () => { mockCreate.mockResolvedValueOnce(streamChunks([ { choices: [{ delta: { content: "I can talk this through." }, finish_reason: "stop" }] }, ])); const streamCallback = vi.fn(); const response = await api.chat( { provider: mockProvider as any, modelId: "test-model", params: { reasoning: false, temperature: 0.8, topP: 0.9, topK: 40, numPredict: -1, numCtx: 131072, maxCompletionTokens: 16384 }, }, { userPrompt: "Don't edit code. Just talk to me.", context: [], tools: [{ name: "file_edit", category: "file", definition: { type: "function" as const, function: { name: "file_edit", description: "Edit file", parameters: { type: "object", properties: {} }, }, }, execute: vi.fn(), }], }, streamCallback, ); expect(response.response).toBe("I can talk this through."); expect(mockCreate).toHaveBeenCalledTimes(1); }); });