From 7ddb0334d1cc7b702533aec004b249ef5b1654b7 Mon Sep 17 00:00:00 2001 From: Andris Reinman Date: Mon, 27 Nov 2023 15:44:52 +0200 Subject: [PATCH] feat(list-models): new method to list available models --- examples/list-models.js | 14 +++++ index.js | 2 + lib/list-models.js | 130 ++++++++++++++++++++++++++++++++++++++++ package-lock.json | 70 ++++++++++++---------- package.json | 4 +- 5 files changed, 188 insertions(+), 32 deletions(-) create mode 100644 examples/list-models.js create mode 100644 lib/list-models.js diff --git a/examples/list-models.js b/examples/list-models.js new file mode 100644 index 0000000..fca5650 --- /dev/null +++ b/examples/list-models.js @@ -0,0 +1,14 @@ +'use strict'; + +const { listModels } = require('../lib/list-models'); +const util = require('util'); + +async function main() { + const result = await listModels(process.env.OPENAI_API_KEY, { + verbose: true + }); + + console.log(util.inspect(result, false, 22, true)); +} + +main(); diff --git a/index.js b/index.js index e771db9..5a8295b 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,7 @@ const { generateSummary, DEFAULT_SYSTEM_PROMPT, DEFAULT_USER_PROMPT } = require( const riskAnalysis = require('./lib/risk-analysis'); const { generateEmbeddings, getChunkEmbeddings } = require('./lib/generate-embeddings'); const { embeddingsQuery, questionQuery } = require('./lib/embeddings-query'); +const { listModels } = require('./lib/list-models'); module.exports = { generateSummary, @@ -12,6 +13,7 @@ module.exports = { embeddingsQuery, questionQuery, riskAnalysis, + listModels, DEFAULT_SYSTEM_PROMPT, DEFAULT_USER_PROMPT }; diff --git a/lib/list-models.js b/lib/list-models.js new file mode 100644 index 0000000..24be926 --- /dev/null +++ b/lib/list-models.js @@ -0,0 +1,130 @@ +'use strict'; + +const packageData = require('../package.json'); +const { fetch: fetchCmd, Agent } = require('undici'); +const fetchAgent = new Agent({ connect: { timeout: 90 * 1000 } }); +const util = require('util'); +const crypto = require('crypto'); + +const OPENAI_API_BASE_URL = 'https://api.openai.com'; +const OPENAI_API_MODELS = '/v1/models'; + +function getApiUrl(baseApiUrl, path) { + let url = new URL(path || '/', baseApiUrl || OPENAI_API_BASE_URL); + return url.href; +} + +async function listModels(apiToken, opts) { + opts = opts || {}; + + let baseApiUrl = opts.baseApiUrl || OPENAI_API_BASE_URL; + + let openAiAPIURL = getApiUrl(baseApiUrl, OPENAI_API_MODELS); + + let headers = { + 'User-Agent': `${packageData.name}/${packageData.version}`, + Authorization: `Bearer ${apiToken}`, + 'Content-Type': 'application/json' + }; + + const requestId = crypto.randomBytes(8).toString('base64'); + + let res; + let data; + let retries = 0; + + if (opts.verbose) { + console.error(util.inspect({ requestId, apiUrl: openAiAPIURL }, false, 8, true)); + } + + let run = async () => { + res = await fetchCmd(openAiAPIURL, { + method: 'get', + headers, + dispatcher: fetchAgent + }); + + data = await res.json(); + + if (!res.ok) { + if (res.status === 429 && ++retries < 5) { + // try again + await new Promise(r => setTimeout(r, 1000)); + return await run(); + } + + if (data && data.error) { + let error = new Error(data.error.message || data.error); + if (data.error.code) { + error.code = data.error.code; + } + + error.statusCode = res.status; + throw error; + } + + let error = new Error('Failed to run API request'); + error.statusCode = res.status; + throw error; + } + + if (!data) { + throw new Error(`Failed to POST API request`); + } + }; + + const reqStartTime = Date.now(); + await run(); + const reqEndTime = Date.now(); + + const response = { models: [].concat((data && data.data) || []).filter(entry => !['openai-dev'].includes(entry.owned_by)) }; + + if (opts.verbose) { + response._time = reqEndTime - reqStartTime; + + console.error(util.inspect({ requestId, response }, false, 8, true)); + } + + response.models.sort((a, b) => { + if (/^gpt/.test(a.id) && !/^gpt/.test(b.id)) { + return -1; + } + + if (/^gpt/.test(b.id) && !/^gpt/.test(a.id)) { + return 1; + } + + if (/-\d{4,}$/.test(b.id) && !/-\d{4,}$/.test(a.id)) { + return -1; + } + + if (/-\d{4,}$/.test(a.id) && !/-\d{4,}$/.test(b.id)) { + return 1; + } + + if (/-preview/.test(b.id) && !/-preview/.test(a.id)) { + return -1; + } + + if (/-preview/.test(a.id) && !/-preview/.test(b.id)) { + return 1; + } + + return a.id.localeCompare(b.id); + }); + + response.models.forEach(entry => { + entry.name = entry.id + .replace(/-/g, ' ') + .replace(/^.| ./g, c => c.toUpperCase()) + .replace(/\bhd\b/gi, c => c.toUpperCase()) + .replace(/\b\d+k\b/gi, c => c.toUpperCase()) + .replace(/Dall E/g, 'Dall-E') + .replace(/^Whisper /g, 'Whisper-') + .replace(/^(gpt|tts)\s/gi, (o, n) => `${n.toUpperCase()}-`); + }); + + return response; +} + +module.exports = { listModels }; diff --git a/package-lock.json b/package-lock.json index a4be19d..57a0af2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,8 +15,8 @@ "linkify-it": "4.0.1", "nodemailer": "6.9.7", "punycode": "2.3.1", - "tlds": "1.244.0", - "undici": "5.27.0" + "tlds": "1.247.0", + "undici": "5.28.1" }, "devDependencies": { "eslint-config-nodemailer": "1.2.0", @@ -61,9 +61,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", - "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", + "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", "dev": true, "peer": true, "dependencies": { @@ -85,9 +85,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.52.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.52.0.tgz", - "integrity": "sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz", + "integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==", "dev": true, "peer": true, "engines": { @@ -95,9 +95,9 @@ } }, "node_modules/@fastify/busboy": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.0.0.tgz", - "integrity": "sha512-JUFJad5lv7jxj926GPgymrWQxxjPYuJNiNjNMzqT+HiuP6Vl3dk5xzG+8sTX96np0ZAluvaMzPsjhHZ5rNuNQQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz", + "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==", "engines": { "node": ">=14" } @@ -198,6 +198,14 @@ "tlds": "1.244.0" } }, + "node_modules/@postalsys/email-text-tools/node_modules/tlds": { + "version": "1.244.0", + "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.244.0.tgz", + "integrity": "sha512-nkacnxHmN5USM/cpmPx29sc2/AnmvUA9han0tNtAJ9yOhh4bPmZm4dGhyg/isWBIES4a70mjd0Q8FSaof6Vf0g==", + "bin": { + "tlds": "bin.js" + } + }, "node_modules/@selderee/plugin-htmlparser2": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", @@ -228,7 +236,8 @@ "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==" + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead" }, "node_modules/abbrev": { "version": "1.1.1", @@ -626,6 +635,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", "dependencies": { "webidl-conversions": "^7.0.0" }, @@ -748,16 +758,16 @@ } }, "node_modules/eslint": { - "version": "8.52.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.52.0.tgz", - "integrity": "sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.54.0.tgz", + "integrity": "sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==", "dev": true, "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.52.0", + "@eslint/eslintrc": "^2.1.3", + "@eslint/js": "8.54.0", "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -977,9 +987,9 @@ } }, "node_modules/flat-cache": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz", - "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, "peer": true, "dependencies": { @@ -988,7 +998,7 @@ "rimraf": "^3.0.2" }, "engines": { - "node": ">=12.0.0" + "node": "^10.12.0 || >=12.0.0" } }, "node_modules/flatted": { @@ -1199,9 +1209,9 @@ } }, "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", + "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", "dev": true, "peer": true, "engines": { @@ -2150,9 +2160,9 @@ "peer": true }, "node_modules/tlds": { - "version": "1.244.0", - "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.244.0.tgz", - "integrity": "sha512-nkacnxHmN5USM/cpmPx29sc2/AnmvUA9han0tNtAJ9yOhh4bPmZm4dGhyg/isWBIES4a70mjd0Q8FSaof6Vf0g==", + "version": "1.247.0", + "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.247.0.tgz", + "integrity": "sha512-m9G0j6euzOucY3Auzl2/SAnGXJVFA1ibJDRBqUdAx7o4jzMidCCKZXWNQVBDcx5zHISJl1r5yA8aaS6WfWuKMQ==", "bin": { "tlds": "bin.js" } @@ -2214,9 +2224,9 @@ "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" }, "node_modules/undici": { - "version": "5.27.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.27.0.tgz", - "integrity": "sha512-l3ydWhlhOJzMVOYkymLykcRRXqbUaQriERtR70B9LzNkZ4bX52Fc8wbTDneMiwo8T+AemZXvXaTx+9o5ROxrXg==", + "version": "5.28.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.1.tgz", + "integrity": "sha512-xcIIvj1LOQH9zAL54iWFkuDEaIVEjLrru7qRpa3GrEEHk6OBhb/LycuUY2m7VCcTuDeLziXCxobQVyKExyGeIA==", "dependencies": { "@fastify/busboy": "^2.0.0" }, diff --git a/package.json b/package.json index 50338d7..5db8b7e 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "linkify-it": "4.0.1", "nodemailer": "6.9.7", "punycode": "2.3.1", - "tlds": "1.244.0", - "undici": "5.27.0" + "tlds": "1.247.0", + "undici": "5.28.1" } }