const OpenAI = require("openai"); const Provider = require("./ai-provider.js"); const { RetryError } = require("../error.js"); /** * The agent provider for the OpenAI API. * By default, the model is set to 'gpt-3.5-turbo'. */ class OpenAIProvider extends Provider { model; static COST_PER_TOKEN = { "gpt-3.5-turbo": { input: 0.0015, output: 0.002, }, "gpt-3.5-turbo-16k": { input: 0.003, output: 0.004, }, "gpt-4": { input: 0.03, output: 0.06, }, "gpt-4-turbo": { input: 0.01, output: 0.03, }, "gpt-4o": { input: 0.005, output: 0.015, }, "gpt-4-32k": { input: 0.06, output: 0.12, }, "gpt-4o-mini": { input: 0.00015, output: 0.0006, }, }; constructor(config = {}) { const { options = { apiKey: process.env.OPEN_AI_KEY, maxRetries: 3, }, model = "gpt-4o", } = config; const client = new OpenAI(options); super(client); this.model = model; } /** * Create a completion based on the received messages. * * @param messages A list of messages to send to the OpenAI API. * @param functions * @returns The completion. */ async complete(messages, functions = null) { try { const response = await this.client.chat.completions.create({ model: this.model, // stream: true, messages, ...(Array.isArray(functions) && functions?.length > 0 ? { functions } : {}), }); // Right now, we only support one completion, // so we just take the first one in the list const completion = response.choices[0].message; const cost = this.getCost(response.usage); // treat function calls if (completion.function_call) { let functionArgs = {}; try { functionArgs = JSON.parse(completion.function_call.arguments); } catch (error) { // call the complete function again in case it gets a json error return this.complete( [ ...messages, { role: "function", name: completion.function_call.name, function_call: completion.function_call, content: error?.message, }, ], functions ); } // console.log(completion, { functionArgs }) return { result: null, functionCall: { name: completion.function_call.name, arguments: functionArgs, }, cost, }; } return { result: completion.content, cost, }; } catch (error) { // If invalid Auth error we need to abort because no amount of waiting // will make auth better. if (error instanceof OpenAI.AuthenticationError) throw error; if ( error instanceof OpenAI.RateLimitError || error instanceof OpenAI.InternalServerError || error instanceof OpenAI.APIError // Also will catch AuthenticationError!!! ) { throw new RetryError(error.message); } throw error; } } /** * Get the cost of the completion. * * @param usage The completion to get the cost for. * @returns The cost of the completion. */ getCost(usage) { if (!usage) { return Number.NaN; } // regex to remove the version number from the model const modelBase = this.model.replace(/-(\d{4})$/, ""); if (!(modelBase in OpenAIProvider.COST_PER_TOKEN)) { return Number.NaN; } const costPerToken = OpenAIProvider.COST_PER_TOKEN?.[modelBase]; const inputCost = (usage.prompt_tokens / 1000) * costPerToken.input; const outputCost = (usage.completion_tokens / 1000) * costPerToken.output; return inputCost + outputCost; } } module.exports = OpenAIProvider;