| | 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<string, Function>; |
| |
|
| | constructor() { |
| | this.duckAI = new DuckAI(); |
| | this.toolService = new ToolService(); |
| | this.availableFunctions = this.initializeBuiltInFunctions(); |
| | } |
| |
|
| | private initializeBuiltInFunctions(): Record<string, Function> { |
| | return { |
| | |
| | get_current_time: () => new Date().toISOString(), |
| | calculate: (args: { expression: string }) => { |
| | try { |
| | |
| | const result = Function( |
| | `"use strict"; return (${args.expression})` |
| | )(); |
| | return { result }; |
| | } catch (error) { |
| | return { error: "Invalid expression" }; |
| | } |
| | }, |
| | get_weather: (args: { location: string }) => { |
| | |
| | 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 { |
| | |
| | return Math.ceil(text.length / 4); |
| | } |
| |
|
| | private transformToDuckAIRequest( |
| | request: ChatCompletionRequest, |
| | vqd?: string |
| | ): DuckAIRequest { |
| | |
| | |
| | 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") { |
| | |
| | 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") { |
| | |
| | transformedMessages.push({ |
| | role: "assistant" as const, |
| | content: message.content || "", |
| | }); |
| | } |
| | } |
| |
|
| | |
| | if (!firstUserMessageProcessed && systemContent) { |
| | transformedMessages.push({ |
| | role: "user" as const, |
| | content: systemContent, |
| | }); |
| | } |
| |
|
| | |
| | const model = request.model || "gpt-4o-mini"; |
| | |
| | return { |
| | model, |
| | messages: transformedMessages, |
| | vqd |
| | }; |
| | } |
| |
|
| | async createChatCompletion( |
| | request: ChatCompletionRequest, |
| | vqd?: string |
| | ): Promise<{ completion: ChatCompletionResponse, vqd: string | null }> { |
| | |
| | if ( |
| | this.toolService.shouldUseFunctionCalling( |
| | request.tools, |
| | request.tool_choice |
| | ) |
| | ) { |
| | |
| | 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(); |
| |
|
| | |
| | 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(); |
| |
|
| | |
| | if (request.tools) { |
| | const validation = this.toolService.validateTools(request.tools); |
| | if (!validation.valid) { |
| | throw new Error(`Invalid tools: ${validation.errors.join(", ")}`); |
| | } |
| | } |
| |
|
| | |
| | const modifiedMessages = [...request.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; |
| |
|
| | |
| | if (this.toolService.detectFunctionCalls(content)) { |
| | const toolCalls = this.toolService.extractFunctionCalls(content); |
| |
|
| | if (toolCalls.length > 0) { |
| | |
| | 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 |
| | }; |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| |
|
| | |
| | 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<Uint8Array>, vqd: string | null }> { |
| | |
| | if ( |
| | this.toolService.shouldUseFunctionCalling( |
| | request.tools, |
| | request.tool_choice |
| | ) |
| | ) { |
| | |
| | const result = await this.createChatCompletionWithTools(request, vqd); |
| | |
| | |
| | } |
| |
|
| | 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<void> { |
| | return reader.read().then(({ done, value }) => { |
| | if (done) { |
| | |
| | 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<ReadableStream<Uint8Array>> { |
| | |
| | |
| | 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) { |
| | |
| | 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)); |
| |
|
| | |
| | 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 { |
| | |
| | const content = choice.message.content || ""; |
| |
|
| | |
| | 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)); |
| |
|
| | |
| | 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)); |
| | } |
| |
|
| | |
| | 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)" |
| | ); |
| | } |
| |
|
| | |
| | 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 { |
| | |
| | if ( |
| | message.content === undefined || |
| | (message.content !== null && typeof message.content !== "string") |
| | ) { |
| | throw new Error("Each message must have content as a string or null"); |
| | } |
| | } |
| | } |
| |
|
| | |
| | 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<string> { |
| | return this.toolService.executeFunctionCall( |
| | toolCall, |
| | this.availableFunctions |
| | ); |
| | } |
| |
|
| | |
| | |
| | |
| | getRateLimitStatus() { |
| | return this.duckAI.getRateLimitStatus(); |
| | } |
| | } |
| |
|