| import { describe, it, expect, beforeAll, afterAll } from "bun:test"; |
| import type { Server } from "bun"; |
| import { OpenAIService } from "../src/openai-service"; |
|
|
| describe("End-to-End Tool Calling Tests", () => { |
| let server: Server; |
| let baseUrl: string; |
|
|
| beforeAll(async () => { |
| |
| const openAIService = new OpenAIService(); |
| const testPort = 3001; |
|
|
| server = Bun.serve({ |
| port: testPort, |
| async fetch(req) { |
| const url = new URL(req.url); |
|
|
| |
| const corsHeaders = { |
| "Access-Control-Allow-Origin": "*", |
| "Access-Control-Allow-Methods": "GET, POST, OPTIONS", |
| "Access-Control-Allow-Headers": "Content-Type, Authorization", |
| }; |
|
|
| |
| if (req.method === "OPTIONS") { |
| return new Response(null, { headers: corsHeaders }); |
| } |
|
|
| try { |
| |
| if (url.pathname === "/health" && req.method === "GET") { |
| return new Response(JSON.stringify({ status: "ok" }), { |
| headers: { "Content-Type": "application/json", ...corsHeaders }, |
| }); |
| } |
|
|
| |
| if (url.pathname === "/v1/models" && req.method === "GET") { |
| const models = openAIService.getModels(); |
| return new Response(JSON.stringify(models), { |
| headers: { "Content-Type": "application/json", ...corsHeaders }, |
| }); |
| } |
|
|
| |
| if ( |
| url.pathname === "/v1/chat/completions" && |
| req.method === "POST" |
| ) { |
| const body = await req.json(); |
| const validatedRequest = openAIService.validateRequest(body); |
|
|
| |
| if (validatedRequest.stream) { |
| const stream = |
| await openAIService.createChatCompletionStream( |
| validatedRequest |
| ); |
| return new Response(stream, { |
| headers: { |
| "Content-Type": "text/event-stream", |
| "Cache-Control": "no-cache", |
| Connection: "keep-alive", |
| ...corsHeaders, |
| }, |
| }); |
| } |
|
|
| |
| const completion = |
| await openAIService.createChatCompletion(validatedRequest); |
| return new Response(JSON.stringify(completion), { |
| headers: { "Content-Type": "application/json", ...corsHeaders }, |
| }); |
| } |
|
|
| |
| return new Response( |
| JSON.stringify({ |
| error: { |
| message: "Not found", |
| type: "invalid_request_error", |
| }, |
| }), |
| { |
| status: 404, |
| headers: { "Content-Type": "application/json", ...corsHeaders }, |
| } |
| ); |
| } catch (error) { |
| console.error("Server error:", error); |
|
|
| const errorMessage = |
| error instanceof Error ? error.message : "Internal server error"; |
| const statusCode = |
| errorMessage.includes("required") || errorMessage.includes("must") |
| ? 400 |
| : 500; |
|
|
| return new Response( |
| JSON.stringify({ |
| error: { |
| message: errorMessage, |
| type: |
| statusCode === 400 |
| ? "invalid_request_error" |
| : "internal_server_error", |
| }, |
| }), |
| { |
| status: statusCode, |
| headers: { "Content-Type": "application/json", ...corsHeaders }, |
| } |
| ); |
| } |
| }, |
| }); |
|
|
| baseUrl = `http://localhost:${testPort}`; |
|
|
| |
| await new Promise((resolve) => setTimeout(resolve, 100)); |
| }); |
|
|
| afterAll(() => { |
| if (server) { |
| server.stop(); |
| } |
| }); |
|
|
| describe("Function Calling API", () => { |
| it("should handle basic function calling request", async () => { |
| const request = { |
| model: "gpt-4o-mini", |
| messages: [{ role: "user", content: "What time is it?" }], |
| tools: [ |
| { |
| type: "function", |
| function: { |
| name: "get_current_time", |
| description: "Get the current time", |
| }, |
| }, |
| ], |
| }; |
|
|
| const response = await fetch(`${baseUrl}/v1/chat/completions`, { |
| method: "POST", |
| headers: { |
| "Content-Type": "application/json", |
| }, |
| body: JSON.stringify(request), |
| }); |
|
|
| expect(response.status).toBe(200); |
| const data = await response.json(); |
|
|
| expect(data.object).toBe("chat.completion"); |
| expect(data.choices).toHaveLength(1); |
| expect(data.choices[0].message.role).toBe("assistant"); |
|
|
| |
| |
| if (data.choices[0].finish_reason === "tool_calls") { |
| expect(data.choices[0].message.tool_calls).toBeDefined(); |
| expect(data.choices[0].message.content).toBe(null); |
| } else { |
| expect(data.choices[0].message.content).toBeTypeOf("string"); |
| } |
| }); |
|
|
| it("should handle calculate function", async () => { |
| const request = { |
| model: "gpt-4o-mini", |
| messages: [{ role: "user", content: "Calculate 15 + 27" }], |
| tools: [ |
| { |
| type: "function", |
| function: { |
| name: "calculate", |
| description: "Perform mathematical calculations", |
| parameters: { |
| type: "object", |
| properties: { |
| expression: { |
| type: "string", |
| description: "Mathematical expression to evaluate", |
| }, |
| }, |
| required: ["expression"], |
| }, |
| }, |
| }, |
| ], |
| tool_choice: "required", |
| }; |
|
|
| const response = await fetch(`${baseUrl}/v1/chat/completions`, { |
| method: "POST", |
| headers: { |
| "Content-Type": "application/json", |
| }, |
| body: JSON.stringify(request), |
| }); |
|
|
| expect(response.status).toBe(200); |
| const data = await response.json(); |
|
|
| |
| expect(data.choices[0].finish_reason).toBe("tool_calls"); |
| expect(data.choices[0].message.tool_calls).toHaveLength(1); |
| expect(data.choices[0].message.tool_calls[0].function.name).toBe( |
| "calculate" |
| ); |
| }); |
|
|
| it("should handle weather function", async () => { |
| const request = { |
| model: "gpt-4o-mini", |
| messages: [ |
| { |
| role: "user", |
| content: "What's the weather like in San Francisco?", |
| }, |
| ], |
| tools: [ |
| { |
| type: "function", |
| function: { |
| name: "get_weather", |
| description: "Get weather information for a location", |
| parameters: { |
| type: "object", |
| properties: { |
| location: { |
| type: "string", |
| description: "The city and state, e.g. San Francisco, CA", |
| }, |
| }, |
| required: ["location"], |
| }, |
| }, |
| }, |
| ], |
| }; |
|
|
| const response = await fetch(`${baseUrl}/v1/chat/completions`, { |
| method: "POST", |
| headers: { |
| "Content-Type": "application/json", |
| }, |
| body: JSON.stringify(request), |
| }); |
|
|
| expect(response.status).toBe(200); |
| const data = await response.json(); |
|
|
| expect(data.object).toBe("chat.completion"); |
| expect(data.choices[0].message.role).toBe("assistant"); |
| }); |
|
|
| it("should handle streaming with tools", async () => { |
| const request = { |
| model: "gpt-4o-mini", |
| messages: [{ role: "user", content: "What time is it?" }], |
| tools: [ |
| { |
| type: "function", |
| function: { |
| name: "get_current_time", |
| description: "Get the current time", |
| }, |
| }, |
| ], |
| stream: true, |
| }; |
|
|
| const response = await fetch(`${baseUrl}/v1/chat/completions`, { |
| method: "POST", |
| headers: { |
| "Content-Type": "application/json", |
| }, |
| body: JSON.stringify(request), |
| }); |
|
|
| expect(response.status).toBe(200); |
| expect(response.headers.get("content-type")).toBe("text/event-stream"); |
|
|
| const reader = response.body?.getReader(); |
| expect(reader).toBeDefined(); |
|
|
| let chunks: string[] = []; |
| let done = false; |
|
|
| while (!done && chunks.length < 10) { |
| |
| const { value, done: streamDone } = await reader!.read(); |
| done = streamDone; |
|
|
| if (value) { |
| const text = new TextDecoder().decode(value); |
| chunks.push(text); |
| } |
| } |
|
|
| const fullResponse = chunks.join(""); |
| expect(fullResponse).toContain("data:"); |
| expect(fullResponse).toContain("[DONE]"); |
| }); |
|
|
| it("should reject invalid tool definitions", async () => { |
| const request = { |
| model: "gpt-4o-mini", |
| messages: [{ role: "user", content: "Hello" }], |
| tools: [ |
| { |
| type: "invalid_type", |
| function: { |
| name: "test", |
| }, |
| }, |
| ], |
| }; |
|
|
| const response = await fetch(`${baseUrl}/v1/chat/completions`, { |
| method: "POST", |
| headers: { |
| "Content-Type": "application/json", |
| }, |
| body: JSON.stringify(request), |
| }); |
|
|
| expect(response.status).toBe(400); |
| const data = await response.json(); |
| expect(data.error.message).toContain("Invalid tools"); |
| }); |
|
|
| it("should handle tool_choice none", async () => { |
| const request = { |
| model: "gpt-4o-mini", |
| messages: [{ role: "user", content: "What time is it?" }], |
| tools: [ |
| { |
| type: "function", |
| function: { |
| name: "get_current_time", |
| description: "Get the current time", |
| }, |
| }, |
| ], |
| tool_choice: "none", |
| }; |
|
|
| const response = await fetch(`${baseUrl}/v1/chat/completions`, { |
| method: "POST", |
| headers: { |
| "Content-Type": "application/json", |
| }, |
| body: JSON.stringify(request), |
| }); |
|
|
| expect(response.status).toBe(200); |
| const data = await response.json(); |
|
|
| |
| expect(data.choices[0].message.content).toBeTypeOf("string"); |
| expect(data.choices[0].finish_reason).toBe("stop"); |
| }); |
|
|
| it("should handle multi-turn conversation with tools", async () => { |
| const request = { |
| model: "gpt-4o-mini", |
| messages: [ |
| { role: "user", content: "What time is it?" }, |
| { |
| role: "assistant", |
| content: null, |
| tool_calls: [ |
| { |
| id: "call_1", |
| type: "function", |
| function: { |
| name: "get_current_time", |
| arguments: "{}", |
| }, |
| }, |
| ], |
| }, |
| { |
| role: "tool", |
| content: "2024-01-15T10:30:00Z", |
| tool_call_id: "call_1", |
| }, |
| { |
| role: "user", |
| content: "Thanks! Can you also calculate 10 + 5?", |
| }, |
| ], |
| tools: [ |
| { |
| type: "function", |
| function: { |
| name: "get_current_time", |
| description: "Get the current time", |
| }, |
| }, |
| { |
| type: "function", |
| function: { |
| name: "calculate", |
| description: "Perform mathematical calculations", |
| parameters: { |
| type: "object", |
| properties: { |
| expression: { |
| type: "string", |
| description: "Mathematical expression to evaluate", |
| }, |
| }, |
| required: ["expression"], |
| }, |
| }, |
| }, |
| ], |
| }; |
|
|
| const response = await fetch(`${baseUrl}/v1/chat/completions`, { |
| method: "POST", |
| headers: { |
| "Content-Type": "application/json", |
| }, |
| body: JSON.stringify(request), |
| }); |
|
|
| expect(response.status).toBe(200); |
| const data = await response.json(); |
|
|
| expect(data.object).toBe("chat.completion"); |
| expect(data.choices[0].message.role).toBe("assistant"); |
| }); |
| }); |
|
|
| describe("Error Handling", () => { |
| it("should handle malformed tool messages", async () => { |
| const request = { |
| model: "gpt-4o-mini", |
| messages: [ |
| { |
| role: "tool", |
| content: "Some result", |
| |
| }, |
| ], |
| }; |
|
|
| const response = await fetch(`${baseUrl}/v1/chat/completions`, { |
| method: "POST", |
| headers: { |
| "Content-Type": "application/json", |
| }, |
| body: JSON.stringify(request), |
| }); |
|
|
| expect(response.status).toBe(400); |
| const data = await response.json(); |
| expect(data.error.message).toContain("tool_call_id"); |
| }); |
|
|
| it("should handle missing function parameters", async () => { |
| const request = { |
| model: "gpt-4o-mini", |
| messages: [{ role: "user", content: "Hello" }], |
| tools: [ |
| { |
| type: "function", |
| function: { |
| |
| description: "A test function", |
| }, |
| }, |
| ], |
| }; |
|
|
| const response = await fetch(`${baseUrl}/v1/chat/completions`, { |
| method: "POST", |
| headers: { |
| "Content-Type": "application/json", |
| }, |
| body: JSON.stringify(request), |
| }); |
|
|
| expect(response.status).toBe(400); |
| const data = await response.json(); |
| expect(data.error.message).toContain("function name is required"); |
| }); |
| }); |
| }); |
|
|