From f6cc31a0401581276c940a00569fb3fbc18a2c1c Mon Sep 17 00:00:00 2001 From: Eric allen Date: Sun, 29 Oct 2023 22:45:36 -0400 Subject: [PATCH] feat: add gpt-3.5-turbo-instruct, gpt-4-32k, and gpt-3.5-turbo-16k --- src/components/SidebarView.tsx | 20 +- src/services/openai/adapters/chat.ts | 9 + src/services/openai/adapters/completion.ts | 9 + src/services/openai/index.ts | 171 ++++++++++++++---- .../openai/models/gpt-3.5-turbo-16k.ts | 13 ++ .../openai/models/gpt-3.5-turbo-instruct.ts | 13 ++ src/services/openai/models/gpt-3.5-turbo.ts | 14 +- src/services/openai/models/gpt-4-32k.ts | 13 ++ src/services/openai/models/gpt-4.ts | 12 +- src/services/openai/models/index.ts | 8 +- src/services/openai/types.ts | 9 +- src/types.ts | 2 +- 12 files changed, 233 insertions(+), 60 deletions(-) create mode 100644 src/services/openai/adapters/chat.ts create mode 100644 src/services/openai/adapters/completion.ts create mode 100644 src/services/openai/models/gpt-3.5-turbo-16k.ts create mode 100644 src/services/openai/models/gpt-3.5-turbo-instruct.ts create mode 100644 src/services/openai/models/gpt-4-32k.ts diff --git a/src/components/SidebarView.tsx b/src/components/SidebarView.tsx index 4ecb4ea..1d8a9d5 100644 --- a/src/components/SidebarView.tsx +++ b/src/components/SidebarView.tsx @@ -10,7 +10,10 @@ import { useApp } from '../hooks/useApp' import type { Conversation } from '../services/conversation' -import { OPEN_AI_CHAT_COMPLETION_OBJECT_TYPE } from '../services/openai/constants' +import { + OPEN_AI_CHAT_COMPLETION_OBJECT_TYPE, + OPEN_AI_COMPLETION_OBJECT_TYPE +} from '../services/openai/constants' export interface ChatFormProps { onChatUpdate?: () => Promise @@ -41,7 +44,9 @@ const SidebarView = ({ onChatUpdate }: ChatFormProps): React.ReactElement => { setPrompt('') chat - ?.send(prompt, { signal: cancelPromptController.signal }) + ?.send(prompt, { + signal: cancelPromptController.signal + }) .then(async (responseStream: Stream) => { let accumulatedMessage = '' @@ -72,7 +77,10 @@ const SidebarView = ({ onChatUpdate }: ChatFormProps): React.ReactElement => { id, model, created, - object: OPEN_AI_CHAT_COMPLETION_OBJECT_TYPE, + object: + conversation?.model?.adapter?.engine === 'chat' + ? OPEN_AI_CHAT_COMPLETION_OBJECT_TYPE + : OPEN_AI_COMPLETION_OBJECT_TYPE, choices: [ { message: { @@ -111,7 +119,11 @@ const SidebarView = ({ onChatUpdate }: ChatFormProps): React.ReactElement => { } const cancelPromptSubmit = (event: React.FormEvent): void => { - logger.debug(`Cancelling streaming response from ${conversation?.model?.adapter?.name as string}...`) + logger.debug( + `Cancelling streaming response from ${ + conversation?.model?.adapter?.name as string + }...` + ) cancelPromptController.abort() } diff --git a/src/services/openai/adapters/chat.ts b/src/services/openai/adapters/chat.ts new file mode 100644 index 0000000..92d2462 --- /dev/null +++ b/src/services/openai/adapters/chat.ts @@ -0,0 +1,9 @@ +import type { ChatAdapter } from 'src/types' + +const OpenAIModelChatAdapter: ChatAdapter = { + name: 'openai', + engine: 'chat', + endpoint: '/v1/chat/completions' +} + +export default OpenAIModelChatAdapter diff --git a/src/services/openai/adapters/completion.ts b/src/services/openai/adapters/completion.ts new file mode 100644 index 0000000..6957848 --- /dev/null +++ b/src/services/openai/adapters/completion.ts @@ -0,0 +1,9 @@ +import type { ChatAdapter } from 'src/types' + +const OpenAIModelCompletionAdapter: ChatAdapter = { + name: 'openai', + engine: 'completion', + endpoint: '/v1/completions' +} + +export default OpenAIModelCompletionAdapter diff --git a/src/services/openai/index.ts b/src/services/openai/index.ts index eb8a13b..ae85bb6 100644 --- a/src/services/openai/index.ts +++ b/src/services/openai/index.ts @@ -1,17 +1,25 @@ +import { requestUrl as obsidianRequest, type RequestUrlParam } from 'obsidian' + import OpenAI from 'openai' import type { Stream } from 'openai/streaming' import formatChat from './utils/formatChat' import { + OPEN_AI_BASE_URL, OPEN_AI_DEFAULT_MODEL, OPEN_AI_RESPONSE_TOKENS, OPEN_AI_DEFAULT_TEMPERATURE } from './constants' +import { + OPEN_AI_GPT_START_WORD, + OPEN_AI_GPT3_STOP_WORD +} from './models/constants' + import { PLUGIN_SETTINGS } from '../../constants' -import type { OpenAICompletionRequest } from './types' +import type { OpenAICompletionRequest, OpenAICompletion } from './types' import type { Conversation } from '../conversation' import type { PluginSettings } from '../../types' import type Logger from '../logger' @@ -36,45 +44,142 @@ export const openAICompletion = async ( { signal }: { signal?: AbortSignal }, settings: PluginSettings = PLUGIN_SETTINGS, logger: Logger -): Promise> => { - let { openAiApiKey: apiKey } = settings + // @ts-expect-error +): Promise> => { + let { openAiApiKey: apiKey, userHandle, botHandle } = settings if (safeStorage.isEncryptionAvailable() === true) { apiKey = await safeStorage.decryptString(Buffer.from(apiKey)) } - try { - const openai = new OpenAI({ - apiKey, - dangerouslyAllowBrowser: true - }) - - const messages = formatChat(input as Conversation) - - const stream = await openai.chat.completions.create( - { - model: model.model, - messages, - stream: true, - temperature, - max_tokens: maxTokens, - top_p: topP, - frequency_penalty: frequencyPenalty, - presence_penalty: presencePenalty - }, - { - signal + if (model.adapter.engine === 'chat') { + try { + const openai = new OpenAI({ + apiKey, + dangerouslyAllowBrowser: true + }) + + const messages = formatChat(input as Conversation) + + const stream = await openai.chat.completions.create( + { + model: model.model, + messages, + stream: true, + temperature, + max_tokens: maxTokens, + top_p: topP, + frequency_penalty: frequencyPenalty, + presence_penalty: presencePenalty + }, + { + signal + } + ) + + return stream + } catch (error) { + if (typeof error?.response !== 'undefined') { + logger.error(error.response.status, error.response.data) + } else { + logger.error(error.message) } - ) - - return stream - } catch (error) { - if (typeof error?.response !== 'undefined') { - logger.error(error.response.status, error.response.data) - } else { - logger.error(error.message) + + throw error + } + } else if (typeof model?.adapter?.endpoint !== 'undefined') { + // TODO: remove this now that non-chat models are being deprecated + const requestUrl = new URL(model?.adapter?.endpoint, OPEN_AI_BASE_URL) + + const requestHeaders = { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + Accept: 'text/event-stream' + } + + const prompt = formatChat(input as Conversation) + + const stopWords: string[] = [] + + if (typeof model.stopWord !== 'undefined' && model.stopWord !== '') { + stopWords.push(model.stopWord) + } + + if (typeof userHandle !== 'undefined' && userHandle !== '') { + stopWords.push(userHandle) + } + + if (typeof botHandle !== 'undefined' && botHandle !== '') { + stopWords.push(botHandle) + } + + const requestBody = { + prompt: prompt + .reduce((promptString, message) => { + return ( + `${promptString}${OPEN_AI_GPT_START_WORD}[${message.role[0].toUpperCase()}${message.role.slice( + 1 + )}]\n` + + `${message?.content as string}\n${OPEN_AI_GPT3_STOP_WORD}`.trim() + ) + }, '') + .trim(), + model: model.model, + stream: true, + temperature, + max_tokens: maxTokens, + stop: stopWords, + top_p: topP, + frequency_penalty: frequencyPenalty, + presence_penalty: presencePenalty + } + + const request: RequestUrlParam = { + url: requestUrl.toString(), + headers: requestHeaders, + method: 'POST', + body: JSON.stringify(requestBody), + throw: false } - throw error + try { + // borrowed from: https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams + const stream = await obsidianRequest(request) + .then(async (response) => { + // eslint-disable-next-line @typescript-eslint/return-await + return await response?.json?.() + }) + .then((response) => { + const reader = response.body.getReader() + return new ReadableStream({ + start(controller) { + return pump() + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + function pump() { + // @ts-expect-error + return reader.read().then(({ done, value }) => { + // When no more data needs to be consumed, close the stream + if (done === true) { + controller.close() + return + } + // Enqueue the next data chunk into our target stream + controller.enqueue(value) + return pump() + }) + } + } + }) + }) + // Create a new response out of the stream + .then((stream) => new Response(stream)) + + // @ts-expect-error + return stream + } catch (error) { + logger.error(error) + + throw error + } } } diff --git a/src/services/openai/models/gpt-3.5-turbo-16k.ts b/src/services/openai/models/gpt-3.5-turbo-16k.ts new file mode 100644 index 0000000..1014336 --- /dev/null +++ b/src/services/openai/models/gpt-3.5-turbo-16k.ts @@ -0,0 +1,13 @@ +import OpenAIModelChatAdapter from '../adapters/chat' + +import type { ModelDefinition } from '../types' + +const GPT35Turbo16K: ModelDefinition = { + name: 'GPT-3.5 Turbo 16K', + adapter: OpenAIModelChatAdapter, + model: 'gpt-3.5-turbo-16k', + maxTokens: 16385, + tokenType: 'gpt4' +} + +export default GPT35Turbo16K diff --git a/src/services/openai/models/gpt-3.5-turbo-instruct.ts b/src/services/openai/models/gpt-3.5-turbo-instruct.ts new file mode 100644 index 0000000..4d9d109 --- /dev/null +++ b/src/services/openai/models/gpt-3.5-turbo-instruct.ts @@ -0,0 +1,13 @@ +import OpenAIModelCompletionAdapter from '../adapters/completion' + +import type { ModelDefinition } from '../types' + +const GPT35TurboInstruct: ModelDefinition = { + name: 'GPT-3.5 Turbo Instruct', + adapter: OpenAIModelCompletionAdapter, + model: 'gpt-3.5-turbo-instruct', + maxTokens: 8192, + tokenType: 'gpt4' +} + +export default GPT35TurboInstruct diff --git a/src/services/openai/models/gpt-3.5-turbo.ts b/src/services/openai/models/gpt-3.5-turbo.ts index e4162e6..730bcbd 100644 --- a/src/services/openai/models/gpt-3.5-turbo.ts +++ b/src/services/openai/models/gpt-3.5-turbo.ts @@ -1,18 +1,12 @@ -import type { ChatAdapter } from '../../../types' +import OpenAIModelChatAdapter from '../adapters/chat' import type { ModelDefinition } from '../types' -const GPT35TurboAdapter: ChatAdapter = { - name: 'openai', - engine: 'chat', - endpoint: '/v1/chat/completions' -} - const GPT35Turbo: ModelDefinition = { - name: 'GPT-3.5', - adapter: GPT35TurboAdapter, + name: 'GPT-3.5 Turbo', + adapter: OpenAIModelChatAdapter, model: 'gpt-3.5-turbo', - maxTokens: 4000, + maxTokens: 4097, tokenType: 'gpt4' } diff --git a/src/services/openai/models/gpt-4-32k.ts b/src/services/openai/models/gpt-4-32k.ts new file mode 100644 index 0000000..7b70810 --- /dev/null +++ b/src/services/openai/models/gpt-4-32k.ts @@ -0,0 +1,13 @@ +import OpenAIModelChatAdapter from '../adapters/chat' + +import type { ModelDefinition } from '../types' + +const GPT432K: ModelDefinition = { + name: 'GPT-4 32K', + adapter: OpenAIModelChatAdapter, + model: 'gpt-4-32k', + maxTokens: 32768, + tokenType: 'gpt4' +} + +export default GPT432K diff --git a/src/services/openai/models/gpt-4.ts b/src/services/openai/models/gpt-4.ts index 6457417..1fd8170 100644 --- a/src/services/openai/models/gpt-4.ts +++ b/src/services/openai/models/gpt-4.ts @@ -1,18 +1,12 @@ -import type { ChatAdapter } from '../../../types' +import OpenAIModelChatAdapter from '../adapters/chat' import type { ModelDefinition } from '../types' -const GPT4Adapter: ChatAdapter = { - name: 'openai', - engine: 'chat', - endpoint: '/v1/chat/completions' -} - const GPT4: ModelDefinition = { name: 'GPT-4', - adapter: GPT4Adapter, + adapter: OpenAIModelChatAdapter, model: 'gpt-4', - maxTokens: 8000, + maxTokens: 8192, tokenType: 'gpt4' } diff --git a/src/services/openai/models/index.ts b/src/services/openai/models/index.ts index 05c2dad..2f8fb62 100644 --- a/src/services/openai/models/index.ts +++ b/src/services/openai/models/index.ts @@ -1,9 +1,15 @@ import GPT35Turbo from './gpt-3.5-turbo' +import GPT35TurboInstruct from './gpt-3.5-turbo-instruct' +import GPT35Turbo16K from './gpt-3.5-turbo-16k' import GPT4 from './gpt-4' +import GPT432K from './gpt-4-32k' const models = { 'gpt-4': GPT4, - 'gpt-3.5-turbo': GPT35Turbo + 'gpt-3.5-turbo': GPT35Turbo, + 'gpt-3.5-turbo-instruct': GPT35TurboInstruct, + 'gpt-3.5-turbo-16k': GPT35Turbo16K, + 'gpt-4-32k': GPT432K } export default models diff --git a/src/services/openai/types.ts b/src/services/openai/types.ts index 081b6cd..e8992b6 100644 --- a/src/services/openai/types.ts +++ b/src/services/openai/types.ts @@ -5,10 +5,15 @@ import type { ChatAdapter } from '../../types' import type { TokenCounterType } from '../../utils/tokenCounter' // TODO: add other models -export type OpenAIModel = 'gpt-3.5-turbo' | 'gpt-4' -// | 'code-davinci-002' +export type OpenAIModel = + | 'gpt-3.5-turbo' + | 'gpt-3.5-turbo-16k' + | 'gpt-3.5-turbo-instruct' + | 'gpt-4' + | 'gpt-4-32k' // TODO: find out what other valid object values are +// tODO: It think this was deprecated with the older completion models export type OpenAICompletionObject = 'text_completion' export interface ModelDefinition { diff --git a/src/types.ts b/src/types.ts index e75c96c..34e0f19 100644 --- a/src/types.ts +++ b/src/types.ts @@ -9,7 +9,7 @@ import type { // TODO: update this union type with other valid adapters as they are added export type ChatAdapterName = 'openai' -export type ChatAdapterEngine = 'chat' | 'code' | 'prompt' +export type ChatAdapterEngine = 'chat' | 'code' | 'prompt' | 'completion' export type MemoryState = | 'default'