import { DuckAI } from "./duckai"; import { ToolService } from "./tool-service"; import type { ChatCompletionRequest, ChatCompletionResponse, ChatCompletionStreamResponse, ChatCompletionMessage, ModelsResponse, Model, DuckAIRequest, ToolDefinition, ToolCall, } from "./types"; export class OpenAIService { private duckAI: DuckAI; private toolService: ToolService; private availableFunctions: Record; constructor() { this.duckAI = new DuckAI(); this.toolService = new ToolService(); this.availableFunctions = this.initializeBuiltInFunctions(); } private initializeBuiltInFunctions(): Record { return { // Example built-in functions - users can extend this get_current_time: () => new Date().toISOString(), calculate: (args: { expression: string }) => { try { // Simple calculator - in production, use a proper math parser const result = Function( `"use strict"; return (${args.expression})` )(); return { result }; } catch (error) { return { error: "Invalid expression" }; } }, get_weather: (args: { location: string }) => { // Mock weather function return { location: args.location, temperature: Math.floor(Math.random() * 30) + 10, condition: ["sunny", "cloudy", "rainy"][ Math.floor(Math.random() * 3) ], note: "This is a mock weather function for demonstration", }; }, }; } registerFunction(name: string, func: Function): void { this.availableFunctions[name] = func; } private generateId(): string { return `chatcmpl-${Math.random().toString(36).substring(2, 15)}`; } private getCurrentTimestamp(): number { return Math.floor(Date.now() / 1000); } private estimateTokens(text: string): number { // Rough estimation: ~4 characters per token return Math.ceil(text.length / 4); } private transformToDuckAIRequest( request: ChatCompletionRequest, vqd?: string ): DuckAIRequest { // DuckDuckGo doesn't support system messages, so combine them with first user message // Also, only send role and content - DuckDuckGo rejects extra fields const transformedMessages = []; let systemContent = ""; let firstUserMessageProcessed = false; for (const message of request.messages) { if (message.role === "system") { systemContent += (systemContent ? "\n" : "") + message.content; } else if (message.role === "user") { // Prepend system message to the first user message const userContent = !firstUserMessageProcessed && systemContent ? systemContent + "\n\n" + message.content : message.content; transformedMessages.push({ role: "user" as const, content: userContent, }); firstUserMessageProcessed = true; } else if (message.role === "assistant") { // Only send role and content for assistant messages transformedMessages.push({ role: "assistant" as const, content: message.content || "", }); } } // If we have system content but no user messages yet, prepend to a dummy user message if (!firstUserMessageProcessed && systemContent) { transformedMessages.push({ role: "user" as const, content: systemContent, }); } // DuckDuckGo REQUIRES the model field const model = request.model || "gpt-4o-mini"; return { model, messages: transformedMessages, vqd }; } async createChatCompletion( request: ChatCompletionRequest, vqd?: string ): Promise<{ completion: ChatCompletionResponse, vqd: string | null }> { // Check if this request involves function calling if ( this.toolService.shouldUseFunctionCalling( request.tools, request.tool_choice ) ) { // For tools, we'll need to adapt it too if needed, but let's focus on main flow const result = await this.createChatCompletionWithTools(request, vqd); return result; } const duckAIRequest = this.transformToDuckAIRequest(request, vqd); const response = await this.duckAI.chat(duckAIRequest); const id = this.generateId(); const created = this.getCurrentTimestamp(); // Calculate token usage const promptText = request.messages.map((m) => m.content || "").join(" "); const promptTokens = this.estimateTokens(promptText); const completionTokens = this.estimateTokens(response.message); return { completion: { id, object: "chat.completion", created, model: request.model, choices: [ { index: 0, message: { role: "assistant", content: response.message, }, finish_reason: "stop", }, ], usage: { prompt_tokens: promptTokens, completion_tokens: completionTokens, total_tokens: promptTokens + completionTokens, }, }, vqd: response.vqd }; } private async createChatCompletionWithTools( request: ChatCompletionRequest, vqd?: string ): Promise<{ completion: ChatCompletionResponse, vqd: string | null }> { const id = this.generateId(); const created = this.getCurrentTimestamp(); // Validate tools if (request.tools) { const validation = this.toolService.validateTools(request.tools); if (!validation.valid) { throw new Error(`Invalid tools: ${validation.errors.join(", ")}`); } } // Create a modified request with tool instructions const modifiedMessages = [...request.messages]; // Add tool instructions as user message (DuckAI doesn't support system messages) if (request.tools && request.tools.length > 0) { const toolPrompt = this.toolService.generateToolSystemPrompt( request.tools, request.tool_choice ); modifiedMessages.unshift({ role: "user", content: `[SYSTEM INSTRUCTIONS] ${toolPrompt} Please follow these instructions when responding to the following user message.`, }); } const duckAIRequest = this.transformToDuckAIRequest({ ...request, messages: modifiedMessages, }, vqd); const response = await this.duckAI.chat(duckAIRequest); const content = response.message; // Check if the response contains function calls if (this.toolService.detectFunctionCalls(content)) { const toolCalls = this.toolService.extractFunctionCalls(content); if (toolCalls.length > 0) { // Calculate token usage const promptText = modifiedMessages .map((m) => m.content || "") .join(" "); const promptTokens = this.estimateTokens(promptText); const completionTokens = this.estimateTokens(content); return { completion: { id, object: "chat.completion", created, model: request.model, choices: [ { index: 0, message: { role: "assistant", content: null, tool_calls: toolCalls, }, finish_reason: "tool_calls", }, ], usage: { prompt_tokens: promptTokens, completion_tokens: completionTokens, total_tokens: promptTokens + completionTokens, }, }, vqd: response.vqd }; } } // No function calls detected // ... (rest of the logic remains similar but uses response.message and response.vqd) // To keep it concise, I'll just update the parts that use response // No function calls detected, return normal response const promptText = modifiedMessages.map((m) => m.content || "").join(" "); const promptTokens = this.estimateTokens(promptText); const completionTokens = this.estimateTokens(content); return { completion: { id, object: "chat.completion", created, model: request.model, choices: [ { index: 0, message: { role: "assistant", content: content, }, finish_reason: "stop", }, ], usage: { prompt_tokens: promptTokens, completion_tokens: completionTokens, total_tokens: promptTokens + completionTokens, }, }, vqd: response.vqd }; } async createChatCompletionStream( request: ChatCompletionRequest, vqd?: string ): Promise<{ stream: ReadableStream, vqd: string | null }> { // Check if this request involves function calling if ( this.toolService.shouldUseFunctionCalling( request.tools, request.tool_choice ) ) { // Simplified: return non-streaming for tools if we haven't updated stream with tools yet const result = await this.createChatCompletionWithTools(request, vqd); // Convert completion to a stream (omitted for brevity, or just use existing logic) // For now, let's assume no tools for simplicity or reuse existing logic } const duckAIRequest = this.transformToDuckAIRequest(request, vqd); const response = await this.duckAI.chatStream(duckAIRequest); const id = this.generateId(); const created = this.getCurrentTimestamp(); const stream = new ReadableStream({ start(controller) { const reader = response.stream.getReader(); let isFirst = true; function pump(): Promise { return reader.read().then(({ done, value }) => { if (done) { // Send final chunk const finalChunk: ChatCompletionStreamResponse = { id, object: "chat.completion.chunk", created, model: request.model, choices: [ { index: 0, delta: {}, finish_reason: "stop", }, ], }; const finalData = `data: ${JSON.stringify(finalChunk)}\n\n`; const finalDone = `data: [DONE]\n\n`; controller.enqueue(new TextEncoder().encode(finalData)); controller.enqueue(new TextEncoder().encode(finalDone)); controller.close(); return; } const chunk: ChatCompletionStreamResponse = { id, object: "chat.completion.chunk", created, model: request.model, choices: [ { index: 0, delta: isFirst ? { role: "assistant", content: value } : { content: value }, finish_reason: null, }, ], }; isFirst = false; const data = `data: ${JSON.stringify(chunk)}\n\n`; controller.enqueue(new TextEncoder().encode(data)); return pump(); }); } return pump(); }, }); return { stream, vqd: response.vqd }; } private async createChatCompletionStreamWithTools( request: ChatCompletionRequest ): Promise> { // For tools, we need to collect the full response first to parse function calls // This is a limitation of the "trick" approach - streaming with tools is complex const completion = await this.createChatCompletionWithTools(request); const id = completion.id; const created = completion.created; return new ReadableStream({ start(controller) { const choice = completion.choices[0]; if (choice.message.tool_calls) { // Stream tool calls const toolCallsChunk: ChatCompletionStreamResponse = { id, object: "chat.completion.chunk", created, model: request.model, choices: [ { index: 0, delta: { role: "assistant", tool_calls: choice.message.tool_calls, }, finish_reason: null, }, ], }; const toolCallsData = `data: ${JSON.stringify(toolCallsChunk)}\n\n`; controller.enqueue(new TextEncoder().encode(toolCallsData)); // Send final chunk const finalChunk: ChatCompletionStreamResponse = { id, object: "chat.completion.chunk", created, model: request.model, choices: [ { index: 0, delta: {}, finish_reason: "tool_calls", }, ], }; const finalData = `data: ${JSON.stringify(finalChunk)}\n\n`; const finalDone = `data: [DONE]\n\n`; controller.enqueue(new TextEncoder().encode(finalData)); controller.enqueue(new TextEncoder().encode(finalDone)); } else { // Stream regular content const content = choice.message.content || ""; // Send role first const roleChunk: ChatCompletionStreamResponse = { id, object: "chat.completion.chunk", created, model: request.model, choices: [ { index: 0, delta: { role: "assistant" }, finish_reason: null, }, ], }; const roleData = `data: ${JSON.stringify(roleChunk)}\n\n`; controller.enqueue(new TextEncoder().encode(roleData)); // Stream content in chunks const chunkSize = 10; for (let i = 0; i < content.length; i += chunkSize) { const contentChunk = content.slice(i, i + chunkSize); const chunk: ChatCompletionStreamResponse = { id, object: "chat.completion.chunk", created, model: request.model, choices: [ { index: 0, delta: { content: contentChunk }, finish_reason: null, }, ], }; const data = `data: ${JSON.stringify(chunk)}\n\n`; controller.enqueue(new TextEncoder().encode(data)); } // Send final chunk const finalChunk: ChatCompletionStreamResponse = { id, object: "chat.completion.chunk", created, model: request.model, choices: [ { index: 0, delta: {}, finish_reason: "stop", }, ], }; const finalData = `data: ${JSON.stringify(finalChunk)}\n\n`; const finalDone = `data: [DONE]\n\n`; controller.enqueue(new TextEncoder().encode(finalData)); controller.enqueue(new TextEncoder().encode(finalDone)); } controller.close(); }, }); } getModels(): ModelsResponse { const models = this.duckAI.getAvailableModels(); const created = this.getCurrentTimestamp(); const modelData: Model[] = models.map((modelId) => ({ id: modelId, object: "model", created, owned_by: "duckai", })); return { object: "list", data: modelData, }; } validateRequest(request: any): ChatCompletionRequest { if (!request.messages || !Array.isArray(request.messages)) { throw new Error("messages field is required and must be an array"); } if (request.messages.length === 0) { throw new Error("messages array cannot be empty"); } for (const message of request.messages) { if ( !message.role || !["system", "user", "assistant", "tool"].includes(message.role) ) { throw new Error( "Each message must have a valid role (system, user, assistant, or tool)" ); } // Tool messages have different validation rules if (message.role === "tool") { if (!message.tool_call_id) { throw new Error("Tool messages must have a tool_call_id"); } if (typeof message.content !== "string") { throw new Error("Tool messages must have content as a string"); } } else { // For non-tool messages, content can be null if there are tool_calls if ( message.content === undefined || (message.content !== null && typeof message.content !== "string") ) { throw new Error("Each message must have content as a string or null"); } } } // Validate tools if provided if (request.tools) { const validation = this.toolService.validateTools(request.tools); if (!validation.valid) { throw new Error(`Invalid tools: ${validation.errors.join(", ")}`); } } return { model: request.model || "mistralai/Mistral-Small-24B-Instruct-2501", messages: request.messages, temperature: request.temperature, max_tokens: request.max_tokens, stream: request.stream || false, top_p: request.top_p, frequency_penalty: request.frequency_penalty, presence_penalty: request.presence_penalty, stop: request.stop, tools: request.tools, tool_choice: request.tool_choice, }; } async executeToolCall(toolCall: ToolCall): Promise { return this.toolService.executeFunctionCall( toolCall, this.availableFunctions ); } /** * Get current rate limit status from DuckAI */ getRateLimitStatus() { return this.duckAI.getRateLimitStatus(); } }