UNPKG

openai-nodejs

Version:

A non-official OpenAI API wrapper for node.

619 lines (554 loc) 22.4 kB
const gpt3encoder = require('gpt-3-encoder'); const { typeCheck } = require('type-check'); const FormData = require('form-data'); const axios = require('axios').create({ baseURL: 'https://api.openai.com/v1' }); const stream = require('stream'); const assert = require('assert'); const parameters = require('./utils/parameters.json'); const RequestError = require('./utils/RequestError'); const { ReadStream } = require('fs'); /** * OpenAI client library. */ class OpenAI { #api_key; #organization_id; /** * @param {String} key API key * @param {String} [organization=null] Organization ID * @param {String} [engine=davinci] The engine you will use in your requests * @see https://beta.openai.com/docs/api-reference/authentication */ constructor(key, organization = null, engine = 'davinci') { assert.strictEqual(typeof key, 'string', 'key must be a string'); assert.ok(organization ? typeof organization === 'string' : true, 'organization must be a string'); assert.ok(engine ? typeof engine === 'string' : true, 'engine must be a string'); this.#api_key = key; this.#organization_id = organization; this.engine = engine; } _request(endpoint = '', request_content = {}, request_type = 'POST', custom_header = {}) { assert.strictEqual(typeof endpoint, 'string', 'endpoint must be a string'); assert.strictEqual(typeof request_content, 'object', 'request_content must be an object'); assert.strictEqual(typeof request_type, 'string', 'request_type must be a string'); assert.strictEqual(typeof custom_header, 'object', 'custom_header must be a string'); request_type = request_type.toUpperCase(); assert.ok(request_type === 'GET' || request_type === 'POST' || request_type === 'DELETE', 'invalid request_type'); let headers = { 'Authorization': 'Bearer ' + this.#api_key, } if (this.#organization_id) headers['OpenAI-Organization'] = this.#organization_id; if (request_type === 'POST') headers['content-type'] = 'application/json'; headers = {...headers, ...custom_header}; if (request_type === 'POST') return axios.post(endpoint, request_content, {headers}); else if (request_type === 'GET') return axios.get(endpoint, {headers, params: request_content}); else if (request_type === 'DELETE') return axios.delete(endpoint, {headers, request_content}); } /** * @typedef {Object} Engine * * @property {String} id * @property {String} object * @property {Number|null} created * @property {Number|null} max_replicas * @property {String} owner * @property {any} permissions * @property {boolean} ready * @property {boolean|null} ready_replicas * @property {any} ready_replicas * @see https://beta.openai.com/docs/api-reference/engines */ /** * Lists the currently available engines, and provides basic information about each one such as the owner and availability. * @returns {Promise<Array<Engine>>} * @example * const OpenAI = require('openai-nodejs'); * const client = new OpenAI('YOUR_API_KEY'); * * client.getEngines() * .then(engines => { * console.log(`Engines: ${engines.map(engine => engine.id).join(', ')}`); * }) * .catch(console.error); * * @see https://beta.openai.com/docs/api-reference/engines/list */ async getEngines() { return this._request('/engines', {}, 'GET') .then(res => res.data.data) .catch(err => { throw new RequestError(err.response.data.error); }); } /** * Retrieves an engine instance, providing basic information about the engine such as the owner and availability. * @param {String} engine The ID of the engine to use for this request * @returns {Promise<Engine>} * @example * const OpenAI = require('openai-nodejs'); * const client = new OpenAI('YOUR_API_KEY'); * * client.getEngine('davinci') * .then(engine => { * console.log(`Engine owner: ${engine.owner}`); * }) * .catch(console.error); * * @see https://beta.openai.com/docs/api-reference/engines/retrieve */ async getEngine(engine) { assert.strictEqual(typeof engine, 'string', 'engine must be a string'); return this._request('/engines/' + engine, {}, 'GET') .then(res => res.data) .catch(err => { throw new RequestError(err.response.data.error); }); } /** * @typedef {Object} Completion * * @property {String} id * @property {String} object * @property {Number|null} created * @property {String} model * @property {array<CompletionChoice>} choices * @see https://beta.openai.com/docs/api-reference/completions */ /** * @typedef {Object} CompletionBody * * @property {Number} [max_tokens] The maximum number of tokens to generate. * @property {Number} [temperature] What sampling temperature to use. * @property {Number} [top_p] An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. * @property {Number} [n] How many completions to generate for each prompt. * @property {Boolean} [stream] Whether to stream back partial progress. * @property {Number} [logprobs] Include the log probabilities on the logprobs most likely tokens, as well the chosen tokens. * @property {Boolean} [echo] Echo back the prompt in addition to the completion * @property {String|Array<String>} [stop] Up to 4 sequences where the API will stop generating further tokens. * @property {Number} [presence_penalty] Number between 0 and 1 that penalizes new tokens based on whether they appear in the text so far. * @property {Number} [frequency_penalty] Number between 0 and 1 that penalizes new tokens based on their existing frequency in the text so far. * @property {Number} [best_of] Generates best_of completions server-side and returns the "best" (the one with the lowest log probability per token). * @property {Object} [logit_bias] Modify the likelihood of specified tokens appearing in the completion. * @see https://beta.openai.com/docs/api-reference/completions */ /** * @typedef {Object} CompletionChoice * * @property {String} text * @property {Number} index * @property {Number|null} logprobs * @property {String} finish_reason * @see https://beta.openai.com/docs/api-reference/completions */ /** * Creates a new completion for the provided prompt and parameters. * @param {string|array} [prompt=<|endoftext|>] The prompt(s) to generate completions for, encoded as a string, a list of strings, or a list of token lists. * @param {CompletionBody} [body={}] * @returns {Promise<Completion>} * @example * const OpenAI = require('openai-nodejs'); * const client = new OpenAI('YOUR_API_KEY'); * * var prompt = 'My name is Bond'; * client.complete(prompt, {stop: ['\n', '"'], temperature: 0}) * .then(completion => { * console.log(`Result: ${prompt}${completion.choices[0].text}`); * }) * .catch(console.error); * * @see https://beta.openai.com/docs/api-reference/completions/create */ async complete(prompt, body = {}) { assert.ok(typeof prompt === 'string' || Array.isArray(prompt), 'prompt must be a string or array'); assert.ok(typeCheck(parameters.completion, body), 'invalid body content'); if (body.engine) { var engine = body.engine; delete body.engine; } return this._request(`/engines/${engine || this.engine}/completions`, {prompt, ...body}) .then(res => res.data) .catch(err => { throw new RequestError(err.response.data.error); }); } /** * @typedef {Object} Search * * @property {Number} document * @property {String} object * @property {Number} score * @see https://beta.openai.com/docs/api-reference/searches */ /** * @typedef {Object} SearchBody * * @property {Array<String>} [documents] Up to 200 documents to search over, provided as a list of strings. * @property {String} [file] The ID of an uploaded file that contains documents to search over. * @property {Number} [max_rerank] The maximum number of documents to be re-ranked and returned by search. * @property {Boolean} [return_metadata] A special boolean flag for showing metadata. * @see https://beta.openai.com/docs/api-reference/searches */ /** * Given a query and a set of documents or labels, the model ranks each document based on its semantic similarity to the provided query. * @param {string|array} query * @param {SearchBody} [body={}] * @returns {Promise<Search>} * @example * const OpenAI = require('openai-nodejs'); * const client = new OpenAI('YOUR_API_KEY'); * * const documents = ['Dancing', 'Programming', 'Skating', 'Drawing']; * * client.search('I do not like CSS', {documents}) * .then(result => { * var max_score = Math.max(...result.map(e => e.score), 0); * * console.log(`Likely subject: ${documents[result.find(e => e.score === max_score).document]} (${max_score})`); * }) * .catch(console.error); * * @see https://beta.openai.com/docs/guides/search/create */ async search(query, body = {}) { assert.strictEqual(typeof query, 'string', 'query must be a string'); assert.ok(typeCheck(parameters.search, body), 'invalid body content'); if (body.engine) { var engine = body.engine; delete body.engine; } return this._request(`/engines/${engine || this.engine}/search`, {query, ...body}) .then(res => res.data.data) .catch(err => { throw new RequestError(err.response.data.error); }); } /** * @typedef {Object} Classification * * @property {String|Completion} completion * @property {String} label * @property {String} model * @property {String} object * @property {String} search_model * @property {String} [prompt] * @property {Array<ClassificationExample>} selected_examples * @see https://beta.openai.com/docs/api-reference/classifications */ /** * @typedef {Object} ClassificationBody * * @property {Array<Array<String>>} [examples] A list of examples with labels. * @property {String} [file] The ID of the uploaded file that contains training examples. * @property {Array<String>} [labels] The set of categories being classified. * @property {String} [search_model] ID of the engine to use for Search. * @property {Number} [temperature] What sampling temperature to use. * @property {Number} [logprobs] Include the log probabilities on the logprobs most likely tokens, as well the chosen tokens. * @property {Number} [max_examples] The maximum number of examples to be ranked by Search when using file. * @property {Object} [logit_bias] Modify the likelihood of specified tokens appearing in the completion. * @property {Boolean} [return_prompt] If set to true, the returned JSON will include a "prompt" field containing the final prompt that was used to request a completion. * @property {Boolean} [return_metadata] A special boolean flag for showing metadata. * @property {Array<String>} [expand] If an object name is in the list, we provide the full information of the object; otherwise, we only provide the object ID. * @see https://beta.openai.com/docs/api-reference/classifications */ /** * @typedef {Object} ClassificationExample * * @property {Number} document * @property {String} label * @property {String} text * @see https://beta.openai.com/docs/api-reference/classifications */ /** * [BETA] Classifies the specified query using provided examples. * @param {string|array} query * @param {ClassificationBody} [body={}] * @returns {Promise<Classification>} * @example * const OpenAI = require('openai-nodejs'); * const client = new OpenAI('YOUR_API_KEY'); * * const examples = [ * ['A happy moment', 'Positive'], * ['I am sad.', 'Negative'], * ['I am feeling awesome', 'Positive'] * ]; * * const labels = ['Positive', 'Negative', 'Neutral']; * * client.classificate('It is a raining day :(', {examples, labels, 'search_model': 'ada', 'engine': 'curie'}) * .then(classification => { * console.log(`Classification: ${classification.label}`); * }) * .catch(console.error); * * @see https://beta.openai.com/docs/api-reference/classifications/create */ async classificate(query, body = {}) { assert.strictEqual(typeof query, 'string', 'query must be a string'); assert.ok(typeCheck(parameters.classification, body), 'invalid body content'); if (body.model) { var model = body.model; delete body.model; } return this._request(`/classifications`, {model: model || this.engine, query, ...body}) .then(res => res.data) .catch(err => { throw new RequestError(err.response.data.error); }); } /** * @typedef {Object} Answer * * @property {Array<String>} answers * @property {String|Completion} completion * @property {String} model * @property {String} object * @property {String} search_model * @property {String} [prompt] * @property {Array<AnswerDocument>} selected_documents * @see https://beta.openai.com/docs/api-reference/answers */ /** * @typedef {Object} AnswerBody * * @property {Array<Array<String>>} examples List of (question, answer) pairs that will help steer the model towards the tone and answer format you'd like. * @property {String} examples_context A text snippet containing the contextual information used to generate the answers for the examples you provide. * @property {Array<String>} [documents] List of documents from which the answer for the input question should be derived. * @property {String} [file] The ID of an uploaded file that contains documents to search over. * @property {String} [search_model] ID of the engine to use for Search. * @property {Boolean} [return_prompt] If set to true, the returned JSON will include a "prompt" field containing the final prompt that was used to request a completion. * @property {Array<String>} [expand] If an object name is in the list, we provide the full information of the object; otherwise, we only provide the object ID. * @property {Number} [max_rerank] The maximum number of documents to be ranked by Search when using file. * @property {Boolean} [return_metadata] A special boolean flag for showing metadata. * @property {Number} [max_tokens] The maximum number of tokens allowed for the generated answer. * @property {Number} [temperature] What sampling temperature to use. * @property {Number} [n] How many answers to generate for each question. * @property {Number} [logprobs] Include the log probabilities on the logprobs most likely tokens, as well the chosen tokens. * @property {String|Array<String>} [stop] Up to 4 sequences where the API will stop generating further tokens. * @property {Object} [logit_bias] Modify the likelihood of specified tokens appearing in the completion. * @see https://beta.openai.com/docs/api-reference/answers */ /** * @typedef {Object} AnswerDocument * * @property {Number} document * @property {String} text * @see https://beta.openai.com/docs/api-reference/answers */ /** * [BETA] Answers the specified question using the provided documents and examples. * @param {string|array} question * @param {AnswerBody} [body={}] * @returns {Promise<Answer>} * @example * const OpenAI = require('openai-nodejs'); * const client = new OpenAI('YOUR_API_KEY'); * * const documents = ['Puppy A is happy.', 'Puppy B is sad.']; * const examples_context = 'In 2017, U.S. life expectancy was 78.6 years.'; * const examples = [ * ['What is human life expectancy in the United States?', '78 years.'] * ]; * * const stop = ['\n', '<|endoftext|>']; * * client.answer('Which puppy is happy?', {documents, examples_context, examples, stop}) * .then(answer => { * console.log(`Answer: ${answer.answers[0]}`); * }) * .catch(console.error); * * @see https://beta.openai.com/docs/api-reference/answers/create */ async answer(question, body = {}) { assert.strictEqual(typeof question, 'string', 'question must be a string'); assert.ok(typeCheck(parameters.answer, body), 'invalid body content'); if (body.model) { var model = body.model; delete body.model; } return this._request(`/answers`, {model: model || this.engine, question: question, ...body}) .then(res => res.data) .catch(err => { throw new RequestError(err.response.data.error); }); } /** * @typedef {Object} File * * @property {String} id * @property {String} object * @property {Number} bytes * @property {Number|null} created_at * @property {String} filename * @property {String} purpose * @property {String} status * @property {String|null} status_details * @see https://beta.openai.com/docs/api-reference/files */ /** * Returns a list of files that belong to the user's organization. * @returns {Promise<Array<File>>} * @example * const OpenAI = require('openai-nodejs'); * const client = new OpenAI('YOUR_API_KEY'); * * client.getFiles() * .then(console.log) * .catch(console.error); * @see https://beta.openai.com/docs/api-reference/files/list */ async getFiles() { return this._request('/files', {}, 'GET') .then(res => res.data.data) .catch(err => { throw new RequestError(err.response.data.error); }); } /** * Returns information about a specific file. * @param {String} filename * @returns {Promise<File>} * @example * const OpenAI = require('openai-nodejs'); * const client = new OpenAI('YOUR_API_KEY'); * * client.getFile('FILE_ID') * .then(console.log) * .catch(console.error); * @see https://beta.openai.com/docs/api-reference/files/retrieve */ async getFile(fileId) { assert.strictEqual(typeof fileId, 'string', 'fileId must be a string'); return this._request('/files/' + fileId, {}, 'GET') .then(res => res.data) .catch(err => { throw new RequestError(err.response.data.error); }); } /** * @typedef {Object} DeletedFile * * @property {String} id * @property {String} object * @property {Boolean} deleted * @see https://beta.openai.com/docs/api-reference/files */ /** * Delete a File. * @param {String} fileId * @returns {DeletedFile} * @example * const OpenAI = require('openai-nodejs'); * const client = new OpenAI('YOUR_API_KEY'); * * client.deleteFile('FILE_ID') * .then(console.log) * .catch(console.error); * @see https://beta.openai.com/docs/api-reference/files/delete */ async deleteFile(fileId) { assert.strictEqual(typeof fileId, 'string', 'fileId must be a string'); return this._request('/files/' + fileId, {}, 'DELETE') .then(res => res.data) .catch(err => { throw new RequestError(err.response.data.error); }); } /** * Upload a file that contains document(s) to be used across various endpoints/features. * @param {string|ReadStream} file The content of the JSON to be uploaded. * @param {String} purpose The intended purpose of the uploaded documents. * @returns {File} * @example * const fs = require('fs'); * const OpenAI = require('openai-nodejs'); * const client = new OpenAI('YOUR_API_KEY'); * * client.uploadFile(fs.createReadStream('file.jsonl'), 'answers') * .then(console.log) * .catch(console.error); * * client.uploadFile('{"text": "A text here"}', 'answers') * .then(console.log) * .catch(console.error); * @see https://beta.openai.com/docs/api-reference/files/upload */ async uploadFile(file, purpose) { assert.ok(typeof file === 'string' || file instanceof stream.Readable, 'file must be a string or readableStream'); assert.strictEqual(typeof purpose, 'string', 'purpose must be a string'); assert.ok(purpose === 'search' || purpose === 'answers' || purpose === 'classifications', 'invalid purpose'); if (typeof file === 'string') { let content = file; file = new stream.Readable(); file.push(content); file.push(null); file.path = 'file.jsonl'; } let data = new FormData(); data.append('file', file); data.append('purpose', purpose); return this._request('/files', data, 'POST', data.getHeaders()) .then(res => res.data) .catch(err => { throw new RequestError(err.response.data.error); }); } /** * Split text by keys. * @param {String} text The string to be encoded * @returns {Array<String>} * @example * const OpenAI = require('openai-nodejs'); * const client = new OpenAI('YOUR_API_KEY'); * * var encoded = client.encode('Hello, world!'); * console.log(encoded); * @see https://beta.openai.com/docs/introduction/key-concepts */ encode(text) { assert.strictEqual(typeof text, 'string', 'text must be a string'); return gpt3encoder.encode(text); } /** * Decode keys to text. * @param {Array<String>} text The encoded text * @returns {string} * @example * const OpenAI = require('openai-nodejs'); * const client = new OpenAI('YOUR_API_KEY'); * * var encoded = client.encode('Hello, world!'); * var decoded = client.decode(encoded); * console.log(`Decoded: ${decoded}`); * @see https://beta.openai.com/docs/introduction/key-concepts */ decode(encoded_text) { assert.ok(Array.isArray(encoded_text), 'encoded_text must be an array'); return gpt3encoder.decode(encoded_text); } /** * Shows the token amount of the inserted text. * @param {String} text The string to get the token amount * @returns {Number} * @example * const OpenAI = require('openai-nodejs'); * const client = new OpenAI('YOUR_API_KEY'); * * var tokens = client.tokens('Hello, world!'); * console.log(`Tokens count: ${tokens}`); * @see https://beta.openai.com/docs/introduction/key-concepts */ tokens(text) { assert.strictEqual(typeof text, 'string', 'text must be a string'); return this.encode(text).length; } } module.exports = OpenAI;