From e4124047b74024db8c002e25b423df382d4c1bac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Sep 2023 13:32:21 +0000 Subject: [PATCH 1/4] build(deps): bump openai from 3.3.0 to 4.4.0 Bumps [openai](https://github.com/openai/openai-node) from 3.3.0 to 4.4.0. - [Release notes](https://github.com/openai/openai-node/releases) - [Changelog](https://github.com/openai/openai-node/blob/master/CHANGELOG.md) - [Commits](https://github.com/openai/openai-node/compare/v3.3.0...v4.4.0) --- updated-dependencies: - dependency-name: openai dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- package-lock.json | 180 +++++++++++++++++++++++++++++++++++----------- package.json | 2 +- 2 files changed, 141 insertions(+), 41 deletions(-) diff --git a/package-lock.json b/package-lock.json index 55cd2ea..e5d4fea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "dotenv": "^16.3.1", "gpt-3-encoder": "^1.1.4", "knex": "^2.5.1", - "openai": "^3.3.0", + "openai": "^4.4.0", "sqlite3": "^5.1.6", "winston": "^3.10.0" }, @@ -1622,6 +1622,15 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.9.tgz", "integrity": "sha512-PcGNd//40kHAS3sTlzKB9C9XL4K0sTup8nbG5lC14kzEteTNuAFh9u5nA0o5TWnSG2r/JNPRXFVcHJIIeRlmqQ==" }, + "node_modules/@types/node-fetch": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.4.tgz", + "integrity": "sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==", + "dependencies": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, "node_modules/@types/semver": { "version": "7.5.1", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.1.tgz", @@ -2048,6 +2057,17 @@ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, + "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/acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", @@ -2093,7 +2113,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.3.0.tgz", "integrity": "sha512-7Epl1Blf4Sy37j4v9f9FjICCh4+KAQOyXgHEwlyBiAQLbhKdq/i2QQU3amQalS/wPhdPzDXPL5DMR5bkn+YeWg==", - "optional": true, "dependencies": { "debug": "^4.1.0", "depd": "^2.0.0", @@ -2243,14 +2262,6 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, - "node_modules/axios": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", - "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", - "dependencies": { - "follow-redirects": "^1.14.8" - } - }, "node_modules/babel-jest": { "version": "29.6.4", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.6.4.tgz", @@ -2347,6 +2358,11 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base-64": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", + "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==" + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -2553,6 +2569,14 @@ "node": ">=10" } }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "engines": { + "node": "*" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -2785,6 +2809,14 @@ "node": ">= 8" } }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "engines": { + "node": "*" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -2847,7 +2879,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "optional": true, "engines": { "node": ">= 0.8" } @@ -2887,6 +2918,15 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/digest-fetch": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/digest-fetch/-/digest-fetch-1.3.0.tgz", + "integrity": "sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA==", + "dependencies": { + "base-64": "^0.1.0", + "md5": "^2.3.0" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -3194,6 +3234,14 @@ "node": ">=0.10.0" } }, + "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/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -3374,29 +3422,10 @@ "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, - "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -3406,6 +3435,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/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -3662,7 +3708,6 @@ "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==", - "optional": true, "dependencies": { "ms": "^2.0.0" } @@ -3799,6 +3844,11 @@ "node": ">=8" } }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, "node_modules/is-core-module": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", @@ -4841,6 +4891,16 @@ "tmpl": "1.0.5" } }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -5042,6 +5102,24 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==" }, + "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==", + "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.6.9", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", @@ -5304,14 +5382,28 @@ } }, "node_modules/openai": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-3.3.0.tgz", - "integrity": "sha512-uqxI/Au+aPRnsaQRe8CojU0eCR7I0mBiKjD3sNMzY6DaC1ZVrc85u98mtJW6voDug8fgGN+DIZmTDxTthxb7dQ==", - "dependencies": { - "axios": "^0.26.0", - "form-data": "^4.0.0" + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.4.0.tgz", + "integrity": "sha512-JN0t628Kh95T0IrXl0HdBqnlJg+4Vq0Bnh55tio+dfCnyzHvMLiWyCM9m726MAJD2YkDU4/8RQB6rNbEq9ct2w==", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "digest-fetch": "^1.3.0", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + }, + "bin": { + "openai": "bin/cli" } }, + "node_modules/openai/node_modules/@types/node": { + "version": "18.17.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.14.tgz", + "integrity": "sha512-ZE/5aB73CyGqgQULkLG87N9GnyGe5TcQjv34pwS8tfBs1IkCh0ASM69mydb2znqd6v0eX+9Ytvk6oQRqu8T1Vw==" + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -6588,6 +6680,14 @@ "makeerror": "1.0.12" } }, + "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": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/package.json b/package.json index 446ac91..89fbc08 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "dotenv": "^16.3.1", "gpt-3-encoder": "^1.1.4", "knex": "^2.5.1", - "openai": "^3.3.0", + "openai": "^4.4.0", "sqlite3": "^5.1.6", "winston": "^3.10.0" }, From 6e8c2eae10a36d75e9f9c5c8134e93ff8eba7ff4 Mon Sep 17 00:00:00 2001 From: Elliott deLaunay Date: Thu, 7 Sep 2023 02:33:00 +0000 Subject: [PATCH 2/4] refact(openai): implementing v4 types and functions --- src/__test__/handlers/chunk.test.ts | 3 ++- src/__test__/svcs/openai.test.ts | 19 +++++++------------ src/config/openai.ts | 5 ++--- src/handlers/chunk.ts | 6 ++---- src/svcs/openai.ts | 8 +++----- 5 files changed, 16 insertions(+), 25 deletions(-) diff --git a/src/__test__/handlers/chunk.test.ts b/src/__test__/handlers/chunk.test.ts index 481faf2..4fdfe27 100644 --- a/src/__test__/handlers/chunk.test.ts +++ b/src/__test__/handlers/chunk.test.ts @@ -3,6 +3,7 @@ import { DISCORD_MAX_CHARS } from '../../config/app'; import { chunkHandler, clearActiveChunks } from '../../handlers/chunk'; import { runners } from '../../svcs/runner'; import { send } from '../../util/send'; +import { ChatCompletionChunk } from 'openai/resources/chat'; jest.mock('../../util/send', () => ({ send: jest.fn(), @@ -38,7 +39,7 @@ describe('chunkHandler', () => { }); it('should handle multiple chunks', () => { - const payload1 = { referenceId, data: { role: 'assistant' } }; + const payload1 = { referenceId, data: { role: 'assistant' } as ChatCompletionChunk.Choice.Delta }; const longString = 'a'.repeat(2 * DISCORD_MAX_CHARS); const payload2 = { referenceId, data: { content: longString } }; chunkHandler(payload1); diff --git a/src/__test__/svcs/openai.test.ts b/src/__test__/svcs/openai.test.ts index bc1daba..9bf3c59 100644 --- a/src/__test__/svcs/openai.test.ts +++ b/src/__test__/svcs/openai.test.ts @@ -1,10 +1,11 @@ -import { AxiosResponse } from 'axios'; import { openai } from '../../config/openai'; import { completionMessage } from '../../svcs/openai'; import { streamHandler } from '../../handlers/openai/stream'; import { runners } from '../../svcs/runner'; import type { Message } from 'discord.js'; import { messages } from '../../util/openai/messages'; +import { Stream } from 'openai/streaming'; +import { ChatCompletionChunk } from 'openai/resources/chat'; jest.mock('../../handlers/openai/stream'); jest.mock('../../util/openai/messages'); @@ -15,11 +16,9 @@ describe('completionMessage', () => { test('calls the provided callback with chat completions', async () => { const mockStream = jest.fn(); - const mockResponse = { - data: mockStream, - }; - const mockCreateChatCompletion = jest.spyOn(openai, 'createChatCompletion').mockResolvedValue( - mockResponse as unknown as AxiosResponse, + + const mockCreateChatCompletion = jest.spyOn(openai.chat.completions, 'create').mockResolvedValue( + mockStream as unknown as Stream, ); const mockStreamHandler = streamHandler as jest.MockedFunction; const mockRunners = runners as jest.MockedObject; @@ -38,8 +37,6 @@ describe('completionMessage', () => { 'role': 'system', }], stream: true, - }, { - responseType: 'stream', }); expect(mockStreamHandler).toHaveBeenCalledWith(mockStream, 'ref-123'); }); @@ -48,8 +45,8 @@ describe('completionMessage', () => { const mockResponse = { data: mockStream, }; - const mockCreateChatCompletion = jest.spyOn(openai, 'createChatCompletion').mockResolvedValue( - mockResponse as unknown as AxiosResponse, + const mockCreateChatCompletion = jest.spyOn(openai.chat.completions, 'create').mockResolvedValue( + mockResponse as unknown as Stream, ); const mockStreamHandler = streamHandler as jest.MockedFunction; const mockRunners = runners as jest.MockedObject; @@ -68,8 +65,6 @@ describe('completionMessage', () => { 'role': 'system', }], stream: true, - }, { - responseType: 'stream', }); expect(mockStreamHandler).toHaveBeenCalledTimes(0); }); diff --git a/src/config/openai.ts b/src/config/openai.ts index 21356bf..a6b0a88 100644 --- a/src/config/openai.ts +++ b/src/config/openai.ts @@ -1,9 +1,8 @@ -import { OpenAIApi, Configuration } from 'openai'; +import OpenAI from 'openai'; import { openaiApiKey } from '../config/app'; -const configuration = new Configuration({ +const openai = new OpenAI({ apiKey: openaiApiKey, }); -const openai = new OpenAIApi(configuration); export { openai }; diff --git a/src/handlers/chunk.ts b/src/handlers/chunk.ts index f1093bd..3f363f3 100644 --- a/src/handlers/chunk.ts +++ b/src/handlers/chunk.ts @@ -4,15 +4,13 @@ import { send, sendTyping } from '../util/send'; import { messageHandler } from './openai/message'; import { endMessage } from '../util/openai/messages'; import { ReferenceId, runners } from '../svcs/runner'; +import { ChatCompletionChunk } from 'openai/resources/chat'; interface ChunkHandler { // want to limit interactions to one channel at a time referenceId: ReferenceId, last?: boolean, - data?: { - role?: string - content?: string - } + data?: ChatCompletionChunk.Choice.Delta } const activeChunks = new Map(); diff --git a/src/svcs/openai.ts b/src/svcs/openai.ts index 3de450d..6ad2ede 100644 --- a/src/svcs/openai.ts +++ b/src/svcs/openai.ts @@ -1,4 +1,3 @@ -import { IncomingMessage } from 'node:http'; import { openai } from '../config/openai'; import { messages } from '../util/openai/messages'; import { streamHandler } from '../handlers/openai/stream'; @@ -9,19 +8,18 @@ import { logger } from '../util/log'; const completionMessage = async (referenceId: ReferenceId) => { const { channelId } = runners[referenceId].message; const startTime = Date.now(); - const response = await openai.createChatCompletion({ + const stream = await openai.chat.completions.create({ model: 'gpt-3.5-turbo-16k', messages: messages[channelId], stream: true, - }, { responseType: 'stream' }); + }); const responseTime = Date.now() - startTime; // Sometimes this gets really slow... typically mornings EST. logger.info( - `openai.createChatCompletion time until response.data stream:${((responseTime % 60000) / 1000).toFixed(1)}s, ${responseTime % 1000}ms`, + `openai.chat.completions.create time until response.data stream:${((responseTime % 60000) / 1000).toFixed(1)}s, ${responseTime % 1000}ms`, { referenceId }, ); if (runners[referenceId].status == 'aborted') { return; } - const stream = response.data as unknown as IncomingMessage; await streamHandler(stream, referenceId); }; From 5d87ef11b6f918cf5d756724478e0ac8e8e64466 Mon Sep 17 00:00:00 2001 From: Elliott deLaunay Date: Thu, 7 Sep 2023 02:34:32 +0000 Subject: [PATCH 3/4] refact(openai): refactoring to use openai new streaming functionality --- src/__test__/handlers/openai/stream.test.ts | 153 ++++---------------- src/handlers/openai/stream.ts | 58 ++------ 2 files changed, 44 insertions(+), 167 deletions(-) diff --git a/src/__test__/handlers/openai/stream.test.ts b/src/__test__/handlers/openai/stream.test.ts index a405094..8c2ac00 100644 --- a/src/__test__/handlers/openai/stream.test.ts +++ b/src/__test__/handlers/openai/stream.test.ts @@ -1,161 +1,64 @@ -import { IncomingMessage } from 'http'; import { streamHandler } from '../../../handlers/openai/stream'; -import { chunkHandler } from '../../../handlers/chunk'; +import * as chunk from '../../../handlers/chunk'; import { runners } from '../../../svcs/runner'; import { Message } from 'discord.js'; +import { Stream } from 'openai/streaming'; +import { ChatCompletionChunk } from 'openai/resources/chat'; jest.mock('../../../handlers/chunk'); jest.mock('../../../svcs/runner'); +class StreamMock { + controller; + + constructor(controller = { abort: jest.fn() }) { + this.controller = controller; + } + + // mock implementation for the asyncIterator method + async *[Symbol.asyncIterator]() { + yield { choices: [{ delta: 'the' }] }; + yield { choices: [{ delta: 'data' }] }; + } +} describe('streamHandler', () => { afterEach(() => { jest.clearAllMocks(); }); it('parses valid JSON data and calls the callback', async () => { - const mockStream = { - on: jest.fn(), - destroy: jest.fn(), - }; - const mockCallback = chunkHandler as jest.MockedFunction; - mockCallback.mockResolvedValue(undefined); + const mockStream = new StreamMock() as unknown as Stream; + const mockCallback = jest.spyOn(chunk, 'chunkHandler').mockResolvedValue(undefined); const referenceId = 'ref1'; const mockRunners = runners as jest.MockedObject; mockRunners[referenceId] = { status: 'running', message: { channelId: 'channel1' } as Message }; - const testData = ['data: {"choices":[{"delta":5}]}', 'data: {"choices":[{"delta":10}]}', 'data: [DONE]']; - - // Create a Promise that resolves after the "end" event is emitted. - const streamPromise = new Promise((resolve) => { - mockStream.on.mockImplementation((eventName, listener) => { - if (eventName === 'data') { - for (const data of testData) { - listener(Buffer.from(`${data}\n\n`)); - } - } - else if (eventName === 'end') { - listener(); - resolve(); - } - }); - }); - // Call the streamHandler function. - await streamHandler(mockStream as unknown as IncomingMessage, referenceId); - - // Assert that the mockStream.on function was called with the correct arguments. - expect(mockStream.on).toHaveBeenCalledTimes(3); - expect(mockStream.on).toHaveBeenCalledWith('data', expect.any(Function)); - expect(mockStream.on).toHaveBeenCalledWith('end', expect.any(Function)); - expect(mockStream.on).toHaveBeenCalledWith('error', expect.any(Function)); + await streamHandler(mockStream, referenceId); // Assert that the mockCallback function was called with the correct arguments. expect(mockCallback).toHaveBeenCalledTimes(3); - expect(mockCallback).toHaveBeenCalledWith({ referenceId, data: 5 }); - expect(mockCallback).toHaveBeenCalledWith({ referenceId, data: 10 }); + expect(mockCallback).toHaveBeenCalledWith({ referenceId, data: 'the' }); + expect(mockCallback).toHaveBeenCalledWith({ referenceId, data: 'data' }); expect(mockCallback).toHaveBeenCalledWith({ referenceId, last: true }); - - // Assert that the Promise resolves without errors. - await expect(streamPromise).resolves.not.toThrow(); }); + it('destroys the stream if runner is aborted', async () => { - const mockStream = { - on: jest.fn(), - destroy: jest.fn(), + const mockController = { + abort: jest.fn(), }; + const mockStream = new StreamMock(mockController) as unknown as Stream; const referenceId = 'ref1'; - const mockCallback = chunkHandler as jest.MockedFunction; - mockCallback.mockResolvedValue(undefined); + const mockCallback = jest.spyOn(chunk, 'chunkHandler').mockResolvedValue(undefined); const mockRunners = runners as jest.MockedObject; mockRunners[referenceId] = { status: 'aborted', message: { channelId: 'channel1' } as Message }; - const testData = ['data: {"choices":[{"delta":5}]}', 'data: {"choices":[{"delta":10}]}', 'data: [DONE]']; - - // Create a Promise that resolves after the "end" event is emitted. - const streamPromise = new Promise((resolve) => { - let error: Error; - mockStream.on.mockImplementation((eventName, listener) => { - if (eventName === 'data') { - for (const data of testData) { - listener(Buffer.from(`${data}\n\n`)); - } - } - else if (eventName === 'error') { - listener(error); - resolve(); - } - }); - mockStream.destroy.mockImplementation((e) => { - error = e; - }); - }); - await streamHandler(mockStream as unknown as IncomingMessage, referenceId); - - // Assert that the mockStream.on function was called with the correct arguments. - expect(mockStream.on).toHaveBeenCalledTimes(3); - expect(mockStream.on).toHaveBeenCalledWith('data', expect.any(Function)); - expect(mockStream.on).toHaveBeenCalledWith('end', expect.any(Function)); - expect(mockStream.on).toHaveBeenCalledWith('error', expect.any(Function)); - - // Assert that the mockCallback function was called with the correct arguments. - expect(mockCallback).toHaveBeenCalledTimes(1); - expect(mockCallback).toHaveBeenCalledWith({ referenceId, last: true }); - - // Assert that the Promise resolves without errors. - await expect(streamPromise).resolves.not.toThrow(); - }); - - it.skip('handles JSON parsing errors and calls the callback with an error object', async () => { - const mockStream = { - on: jest.fn(), - emit: jest.fn(), - destroy: jest.fn(), - }; - const mockCallback = chunkHandler as jest.MockedFunction; - mockCallback.mockResolvedValue(); - const referenceId = 'ref1'; - const mockRunners = runners as jest.MockedObject; - mockRunners[referenceId] = { status: 'running', message: { channelId: 'channel1' } as Message }; - const testData = 'data: {invalid JSON}'; - - // Create a Promise that resolves after the "end" event is emitted. - const streamPromise = new Promise((resolve) => { - let error: Error; - mockStream.on.mockImplementation((eventName, listener) => { - if (eventName === 'data') { - listener(Buffer.from(`${testData}\n\n`)); - } - else if (eventName === 'error') { - listener(error); - resolve(); - } - }); - mockStream.emit.mockImplementation((eventName, arg) => { - if (eventName === 'error') { - error = arg; - } - }); - }); - - try { - // Call the streamHandler function. - await streamHandler(mockStream as unknown as IncomingMessage, referenceId); - } - catch (error) { - expect(error).toBeInstanceOf(Error); - expect((error as Error).message).toMatch(/Error with JSON.parse and data: {invalid JSON}./); - } - - // Assert that the mockStream.on function was called with the correct arguments. - expect(mockStream.on).toHaveBeenCalledTimes(3); - expect(mockStream.on).toHaveBeenCalledWith('data', expect.any(Function)); - expect(mockStream.on).toHaveBeenCalledWith('end', expect.any(Function)); - expect(mockStream.on).toHaveBeenCalledWith('error', expect.any(Function)); + await streamHandler(mockStream, referenceId); // Assert that the mockCallback function was called with the correct arguments. expect(mockCallback).toHaveBeenCalledTimes(1); expect(mockCallback).toHaveBeenCalledWith({ referenceId, last: true }); // Assert that the Promise resolves without errors. - await expect(streamPromise).resolves.not.toThrow(); + await expect(mockController.abort).toHaveBeenCalledTimes(1); }); }); diff --git a/src/handlers/openai/stream.ts b/src/handlers/openai/stream.ts index bb57d5e..d8e15b1 100644 --- a/src/handlers/openai/stream.ts +++ b/src/handlers/openai/stream.ts @@ -1,50 +1,24 @@ -import { IncomingMessage } from 'http'; import { chunkHandler } from '../chunk'; -import { logger } from '../../util/log'; -import { StreamInterruptedError } from '../../errors/stream'; import { ReferenceId, runners } from '../../svcs/runner'; +import type { Stream } from 'openai/streaming'; +import type { ChatCompletionChunk } from 'openai/resources/chat'; -const streamHandler = ( - stream: IncomingMessage, +const streamHandler = async ( + stream: Stream, referenceId: ReferenceId, -) => new Promise((resolve, reject) => { - stream.on('data', async (chunk: Buffer) => { +) => { + let aborted = false; + for await (const part of stream) { const { status } = runners[referenceId]; - if (status == 'aborted') { return stream.destroy(new StreamInterruptedError('Stream aborted')); } - // Messages in the event stream are separated by a pair of newline characters. - const payloads = chunk.toString().split('\n\n'); - for (const payload of payloads) { - // signalling to chunkHandler to finish via stream.on('end') - if (payload.includes('[DONE]')) return; - if (payload.startsWith('data:')) { - // in case there's multiline data event - const data = payload.replaceAll(/(\n)?^data:\s*/g, ''); - try { - const delta = JSON.parse(data.trim()); - chunkHandler({ referenceId, data: delta.choices[0].delta }); - } - catch (error) { - const msg = `Error with JSON.parse and ${payloads}.`; - logger.error(`${msg}\n${error}`, { referenceId }); - /** - * Swallow the errors for now, receiving partial JSON - * is happening more and more frequently - * TODO: revamp and actually stream data into json. - */ - // stream.emit('error', new StreamDataError(msg, error as Error)); - } - } + if (status == 'aborted') { + await chunkHandler({ referenceId, last: true }); + stream.controller.abort(); + aborted = true; + break; } - }); - stream.on('end', async () => { - await chunkHandler({ referenceId, last: true }); - resolve(); - }); - stream.on('error', async (error: Error) => { - await chunkHandler({ referenceId, last: true }); - if (error instanceof StreamInterruptedError) { resolve(); } - else { reject(error); } - }); -}); + await chunkHandler({ referenceId, data: part.choices[0].delta }); + } + if (!aborted) await chunkHandler({ referenceId, last: true }); +}; export { streamHandler }; From 92ebdd956174be3fccf7e265e1251985a344dafa Mon Sep 17 00:00:00 2001 From: Elliott deLaunay Date: Thu, 7 Sep 2023 02:38:54 +0000 Subject: [PATCH 4/4] fix(app): setting openai api key placeholder --- src/config/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/app.ts b/src/config/app.ts index 27c8144..ef5f479 100644 --- a/src/config/app.ts +++ b/src/config/app.ts @@ -8,7 +8,7 @@ export const clientId = process.env.CLIENT_ID; export const guildId = process.env.GUILD_ID; -export const openaiApiKey = process.env.OPENAI_API_KEY; +export const openaiApiKey = process.env.OPENAI_API_KEY || 'sk-placeholder'; export const environment = process.env.NODE_ENV || 'development';