From a2c81409f0f8d2ccaf8d5493bf974f3c9be3d428 Mon Sep 17 00:00:00 2001 From: NekoMonci12 Date: Tue, 3 Jun 2025 13:21:54 +0700 Subject: [PATCH] Voyage Embedding Cache --- .env.example | 10 ++- cosineSimilarityWorker.js | 27 ++++++ embedding.js | 37 ++++++++ hybridCacheManager.js | 77 ++++++++++++++--- index.js | 2 +- migration.js | 94 ++++++++++++++++++++ mongoCacheManager.js | 55 ++++++++---- package-lock.json | 178 +++++++++++++++++++++++++++++++++++--- package.json | 2 +- 9 files changed, 433 insertions(+), 49 deletions(-) create mode 100644 cosineSimilarityWorker.js create mode 100644 embedding.js create mode 100644 migration.js diff --git a/.env.example b/.env.example index 095ea57..7cce275 100644 --- a/.env.example +++ b/.env.example @@ -1,12 +1,18 @@ DISCORD_TOKEN=DISCORD_BOT_TOKEN CLIENT_ID=DISCORD_BOT_CLIENT_ID +API_PORT=3003 OPENAI_API_KEY=sk-*********************************** -OPENAI_BASE_URL=https://api.deepseek.com/chat/completions +OPENAI_BASE_URL=https://api.openai.com/v1/chat/completions +OPENAI_DEFAULT_MODEL=gpt-4.1-nano + +VOYAGE_EMBEDDING_URL=https://api.voyageai.com/v1/embeddings +OPENAI_EMBEDDINVOYAGE_EMBEDDING_MODELG_MODEL=voyage-3.5-lite +VOYAGE_API_KEY=pa-*********************************** MONGO_URL=mongodb://username:password@localhost:27000 MONGO_DB_NAME=RakunNakun - +MONGO_COLLECTION_NAME=cache MYSQL_HOST=localhost MYSQL_PORT=3306 diff --git a/cosineSimilarityWorker.js b/cosineSimilarityWorker.js new file mode 100644 index 0000000..f483c33 --- /dev/null +++ b/cosineSimilarityWorker.js @@ -0,0 +1,27 @@ +// cosineSimilarityWorker.js + +const { parentPort } = require('worker_threads'); + +function cosineSimilarity(a, b) { + const dot = a.reduce((sum, v, i) => sum + v * b[i], 0); + const magA = Math.sqrt(a.reduce((sum, v) => sum + v * v, 0)); + const magB = Math.sqrt(b.reduce((sum, v) => sum + v * v, 0)); + return dot / (magA * magB); +} + +parentPort.on('message', (data) => { + const { inputEmbedding, cachedEntries, threshold } = data; + + let bestMatch = null; + let bestScore = threshold; + + for (const item of cachedEntries) { + const score = cosineSimilarity(inputEmbedding, item.embedding); + if (score > bestScore) { + bestScore = score; + bestMatch = item; + } + } + + parentPort.postMessage({ bestMatch, bestScore }); +}); diff --git a/embedding.js b/embedding.js new file mode 100644 index 0000000..4b3c1c6 --- /dev/null +++ b/embedding.js @@ -0,0 +1,37 @@ +require('dotenv').config(); +const axios = require('axios'); + +const VOYAGE_API_KEY = process.env.VOYAGE_API_KEY; +const VOYAGE_EMBEDDING_URL = process.env.VOYAGE_EMBEDDING_URL || 'https://api.voyageai.com/v1/embeddings'; +const VOYAGE_EMBEDDING_MODEL = process.env.VOYAGE_EMBEDDING_MODEL || 'voyage-3.5-lite'; + +async function getVoyageEmbeddings(texts) { + if (!Array.isArray(texts)) { + throw new Error('Input must be an array of strings'); + } + + try { + const response = await axios.post( + VOYAGE_EMBEDDING_URL, + { + model: VOYAGE_EMBEDDING_MODEL, + input: texts, + output_dimension: 1024, + }, + { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${VOYAGE_API_KEY}`, + }, + } + ); + + // Map over the response data to return an array of embeddings + return response.data.data.map(item => item.embedding); + } catch (error) { + console.error('Error fetching voyage embeddings:', error.response?.data || error.message); + throw error; + } +} + +module.exports = { getVoyageEmbeddings }; diff --git a/hybridCacheManager.js b/hybridCacheManager.js index 9b99efa..cc0a240 100644 --- a/hybridCacheManager.js +++ b/hybridCacheManager.js @@ -1,30 +1,79 @@ // hybridCacheManager.js +const crypto = require('crypto'); const MongoCacheManager = require('./mongoCacheManager'); +const { Worker } = require('worker_threads'); +const path = require('path'); +const { getVoyageEmbeddings } = require('./embedding'); + +function hashInput(input) { + return crypto.createHash('sha256').update(input.trim().toLowerCase()).digest('hex'); +} + +async function getEmbedding(text) { + const embeddings = await getVoyageEmbeddings([text]); + return embeddings[0]; +} + class HybridCacheManager { - /** - * @param {string} mongoUrl - Connection URL for MongoDB. - * @param {string} dbName - MongoDB database name. - * @param {string} collectionName - MongoDB collection name for cache. - */ constructor(mongoUrl, dbName, collectionName = 'cache') { this.mongoCache = new MongoCacheManager(mongoUrl, dbName, collectionName); } - async getCachedResult(input) { - let result = await this.mongoCache.getCachedResult(input); - if (result) { - console.log("Hybrid Cache: Found result in MongoDB."); - return result; + async getCachedResult(input, threshold = 0.8) { + const inputHash = hashInput(input); + + // 🔍 Fast exact-match hash lookup + const exactMatch = await this.mongoCache.getByHash(inputHash); + if (exactMatch) { + console.log("[HybridCache] Exact hash match found."); + return exactMatch.value; } - console.log("Hybrid Cache: No cached result found."); - return null; + + // 🤖 Embedding-based semantic search + const inputEmbedding = await getEmbedding(input); + const cachedEntries = await this.mongoCache.getAllEmbeddings(); + + if (cachedEntries.length === 0) { + console.log("[HybridCache] No cache entries with embeddings found."); + return null; + } + + // Return a Promise that resolves with the worker's result + return new Promise((resolve, reject) => { + const worker = new Worker(path.resolve(__dirname, './cosineSimilarityWorker.js')); + + worker.postMessage({ inputEmbedding, cachedEntries, threshold }); + + worker.on('message', ({ bestMatch, bestScore }) => { + if (bestMatch) { + console.log(`[HybridCache] Semantic match found with similarity ${bestScore.toFixed(2)}`); + resolve(bestMatch.value); + } else { + console.log("[HybridCache] No suitable semantic cache match found."); + resolve(null); + } + worker.terminate(); + }); + + worker.on('error', (err) => { + console.error("[HybridCache] Worker thread error:", err); + reject(err); + }); + + worker.on('exit', (code) => { + if (code !== 0) + console.warn(`[HybridCache] Worker stopped with exit code ${code}`); + }); + }); } async setCache(input, value) { - await this.mongoCache.setCache(input, value); - console.log("Hybrid Cache: Stored value in both caches for key:", input); + const embedding = await getEmbedding(input); + const hash = hashInput(input); + await this.mongoCache.setCache(input, value, embedding, hash); + console.log("[HybridCache] Stored new cache entry with embedding and hash."); } } diff --git a/index.js b/index.js index d4b6325..279be32 100644 --- a/index.js +++ b/index.js @@ -17,7 +17,7 @@ const logDebug = (msg, ...args) => console.log(`[DEBUG] ${msg}`, ...args); const logError = (msg, ...args) => console.error(`[ERROR] ${msg}`, ...args); // ——— Globals / Defaults ——— -const defaultModelId = 'gpt-4.1-nano'; +const defaultModelId = process.env.OPENAI_DEFAULT_MODEL; const defaultPersona = 'You are a helpful assistant.'; const defaultApiUrl = process.env.OPENAI_BASE_URL; const MIN_TOKEN_THRESH = 1000; diff --git a/migration.js b/migration.js new file mode 100644 index 0000000..5d75a77 --- /dev/null +++ b/migration.js @@ -0,0 +1,94 @@ +require('dotenv').config(); +const crypto = require('crypto'); +const { MongoClient } = require('mongodb'); +const { getVoyageEmbeddings } = require('./embedding'); + +const MONGO_URL = process.env.MONGO_URL; +const DB_NAME = process.env.MONGO_DB_NAME; +const COLLECTION_NAME = process.env.MONGO_COLLECTION_NAME; +const BATCH_SIZE = 500; + +// Toggle: set true to overwrite all embeddings, false to update only missing ones +const OVERWRITE_EMBEDDINGS = true; + +function computeHash(input) { + return crypto.createHash('sha256').update(input.trim().toLowerCase()).digest('hex'); +} + +(async () => { + const client = new MongoClient(MONGO_URL); + await client.connect(); + const db = client.db(DB_NAME); + const collection = db.collection(COLLECTION_NAME); + + // Select documents based on the toggle + const query = OVERWRITE_EMBEDDINGS + ? {} // all documents + : { + $or: [ + { embedding: { $exists: false } }, + { hash: { $exists: false } } + ] + }; + + const cursor = collection.find(query); + + let updatedCount = 0; + let batchDocs = []; + + while (await cursor.hasNext()) { + const doc = await cursor.next(); + + if (!OVERWRITE_EMBEDDINGS) { + // Only push docs missing embedding or hash + const needsEmbedding = !doc.embedding; + const needsHash = !doc.hash; + if (!needsEmbedding && !needsHash) continue; + } + + batchDocs.push(doc); + + if (batchDocs.length === BATCH_SIZE) { + const texts = batchDocs.map(d => d.key); + try { + const embeddings = await getVoyageEmbeddings(texts); + + for (let i = 0; i < batchDocs.length; i++) { + const hash = computeHash(batchDocs[i].key); + await collection.updateOne( + { _id: batchDocs[i]._id }, + { $set: { embedding: embeddings[i], hash } } + ); + console.log(`✅ Updated embedding & hash for: ${batchDocs[i].key}`); + updatedCount++; + } + } catch (err) { + console.warn(`⚠️ Failed batch: ${err.message}`); + } + + batchDocs = []; + await new Promise(r => setTimeout(r, 1000)); + } + } + + if (batchDocs.length > 0) { + const texts = batchDocs.map(d => d.key); + try { + const embeddings = await getVoyageEmbeddings(texts); + for (let i = 0; i < batchDocs.length; i++) { + const hash = computeHash(batchDocs[i].key); + await collection.updateOne( + { _id: batchDocs[i]._id }, + { $set: { embedding: embeddings[i], hash } } + ); + console.log(`✅ Updated embedding & hash for: ${batchDocs[i].key}`); + updatedCount++; + } + } catch (err) { + console.warn(`⚠️ Failed batch: ${err.message}`); + } + } + + console.log(`🎉 Migration complete. ${updatedCount} entries updated.`); + await client.close(); +})(); diff --git a/mongoCacheManager.js b/mongoCacheManager.js index 26ea104..d3ce50f 100644 --- a/mongoCacheManager.js +++ b/mongoCacheManager.js @@ -1,3 +1,4 @@ +require('dotenv').config(); const { MongoClient } = require('mongodb'); class MongoCacheManager { @@ -6,10 +7,10 @@ class MongoCacheManager { * @param {string} dbName - Database name. * @param {string} collectionName - Collection name for caching. */ - constructor(mongoUrl, dbName, collectionName = 'cache') { + constructor(mongoUrl, dbName, collectionName) { this.mongoUrl = mongoUrl; this.dbName = dbName; - this.collectionName = collectionName; + this.collectionName = collectionName || process.env.MONGO_COLLECTION_NAME || 'cache'; this.client = new MongoClient(mongoUrl, { useUnifiedTopology: true }); this.connected = false; this.readOnly = false; @@ -63,23 +64,41 @@ class MongoCacheManager { return null; } - async setCache(input, value) { - try { - await this.connect(); - if (!this.connected || !this.collection || this.readOnly) { - console.warn("[MongoCache] Skipping cache store due to read-only mode or no connection."); - return; - } - const key = this.normalize(input); - const result = await this.collection.updateOne( - { key }, - { $set: { value, updatedAt: new Date() } }, - { upsert: true } - ); - console.log("[MongoCache] Stored value for key:", key, "Update result:", result.result); - } catch (error) { - console.error("[MongoCache] Error storing cache for key:", this.normalize(input), error); + async getByHash(hash) { + await this.connect(); + return await this.collection.findOne({ hash }); + } + + async getAllEmbeddings() { + await this.connect(); + return await this.collection.find({ embedding: { $exists: true } }).toArray(); + } + + async setCache(input, value, embedding, hash) { + await this.connect(); + if (!this.connected || !this.collection || this.readOnly) return; + + const key = this.normalize(input); + + const count = await this.collection.estimatedDocumentCount(); + const maxSize = 5000; + if (count >= maxSize) { + await this.collection.deleteOne({}, { sort: { updatedAt: 1 } }); } + + await this.collection.updateOne( + { key }, + { + $set: { + key, + value, + updatedAt: new Date(), + embedding, + hash + } + }, + { upsert: true } + ); } } diff --git a/package-lock.json b/package-lock.json index da4091a..68a7800 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "express": "^4.18.2", "mongodb": "^5.7.0", "mysql2": "^3.2.0", - "openai": "^3.2.1", + "openai": "^4.104.0", "qs": "^6.11.2", "redis": "^4.6.7", "uuid": "^9.0.0" @@ -255,6 +255,15 @@ "undici-types": "~6.20.0" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, "node_modules/@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", @@ -290,6 +299,17 @@ "npm": ">=7.0.0" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -303,6 +323,17 @@ "node": ">= 0.6" } }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -669,6 +700,14 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, "node_modules/express": { "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", @@ -789,6 +828,23 @@ "node": ">= 6" } }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -938,6 +994,14 @@ "node": ">= 0.8" } }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -1212,6 +1276,63 @@ "node": ">= 0.6" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -1237,24 +1358,47 @@ } }, "node_modules/openai": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-3.3.0.tgz", - "integrity": "sha512-uqxI/Au+aPRnsaQRe8CojU0eCR7I0mBiKjD3sNMzY6DaC1ZVrc85u98mtJW6voDug8fgGN+DIZmTDxTthxb7dQ==", - "license": "MIT", + "version": "4.104.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", + "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==", "dependencies": { - "axios": "^0.26.0", - "form-data": "^4.0.0" + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + }, + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } } }, - "node_modules/openai/node_modules/axios": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", - "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", - "license": "MIT", + "node_modules/openai/node_modules/@types/node": { + "version": "18.19.110", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.110.tgz", + "integrity": "sha512-WW2o4gTmREtSnqKty9nhqF/vA0GKd0V/rbC0OyjSk9Bz6bzlsXKT+i7WDdS/a0z74rfT2PO4dArVCSnapNLA5Q==", "dependencies": { - "follow-redirects": "^1.14.8" + "undici-types": "~5.26.4" } }, + "node_modules/openai/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1688,6 +1832,14 @@ "node": ">= 0.8" } }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "engines": { + "node": ">= 14" + } + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", diff --git a/package.json b/package.json index 98df629..c0dee09 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "discord.js": "^14.11.0", "dotenv": "^16.0.0", "mysql2": "^3.2.0", - "openai": "^3.2.1", + "openai": "^4.104.0", "axios": "^1.4.0", "qs": "^6.11.2", "redis": "^4.6.7",