From 27a5f4eb74f6366181c6792e3efbf615b0af79bf Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Fri, 1 Sep 2023 16:19:59 +0200 Subject: [PATCH] :zap: (openai) Add custom provider and custom models Closes #532 --- apps/builder/package.json | 1 + .../components/inputs/AutocompleteInput.tsx | 9 +- apps/builder/src/components/inputs/Select.tsx | 15 +- .../integrations/openai/api/listModels.ts | 134 +++++++ .../blocks/integrations/openai/api/router.ts | 6 + .../openai/components/OpenAISettings.tsx | 98 +++++- .../createChatCompletion/ModelsDropdown.tsx | 42 +++ .../OpenAIChatCompletionSettings.tsx | 140 ++++---- .../blocks/integrations/openai/openai.spec.ts | 5 +- .../nodes/block/SettingsPopoverContent.tsx | 10 +- .../helpers/server/routers/v1/trpcRouter.ts | 2 + apps/docs/openapi/builder/_spec_.json | 327 ++++++++++++------ apps/docs/openapi/chat/_spec_.json | 36 +- apps/viewer/package.json | 3 +- .../openai/createChatCompletionOpenAI.ts | 2 + .../executeChatCompletionOpenAIRequest.ts | 69 ++-- .../openai/getChatCompletionStream.ts | 12 + .../openai/parseChatCompletionMessages.ts | 2 +- .../pages/api/integrations/openai/streamer.ts | 2 +- .../features/blocks/integrations/openai.ts | 24 +- pnpm-lock.yaml | 23 +- 21 files changed, 684 insertions(+), 278 deletions(-) create mode 100644 apps/builder/src/features/blocks/integrations/openai/api/listModels.ts create mode 100644 apps/builder/src/features/blocks/integrations/openai/api/router.ts create mode 100644 apps/builder/src/features/blocks/integrations/openai/components/createChatCompletion/ModelsDropdown.tsx diff --git a/apps/builder/package.json b/apps/builder/package.json index d05b3147155..1353cd504eb 100644 --- a/apps/builder/package.json +++ b/apps/builder/package.json @@ -75,6 +75,7 @@ "nextjs-cors": "^2.1.2", "nodemailer": "6.9.3", "nprogress": "0.2.0", + "openai-edge": "1.2.2", "papaparse": "5.4.1", "posthog-js": "^1.77.1", "posthog-node": "3.1.1", diff --git a/apps/builder/src/components/inputs/AutocompleteInput.tsx b/apps/builder/src/components/inputs/AutocompleteInput.tsx index 228350a64d5..b7391586e4b 100644 --- a/apps/builder/src/components/inputs/AutocompleteInput.tsx +++ b/apps/builder/src/components/inputs/AutocompleteInput.tsx @@ -24,7 +24,7 @@ import { MoreInfoTooltip } from '../MoreInfoTooltip' import { env } from '@typebot.io/env' type Props = { - items: string[] + items: string[] | undefined value?: string defaultValue?: string debounceTimeout?: number @@ -77,9 +77,9 @@ export const AutocompleteInput = ({ const filteredItems = ( inputValue === '' - ? items + ? items ?? [] : [ - ...items.filter( + ...(items ?? []).filter( (item) => item.toLowerCase().startsWith((inputValue ?? '').toLowerCase()) && item.toLowerCase() !== inputValue.toLowerCase() @@ -186,7 +186,8 @@ export const AutocompleteInput = ({ onFocus={onOpen} onBlur={updateCarretPosition} onKeyDown={updateFocusedDropdownItem} - placeholder={placeholder} + placeholder={!items ? 'Loading...' : placeholder} + isDisabled={!items} /> {filteredItems.length > 0 && ( diff --git a/apps/builder/src/components/inputs/Select.tsx b/apps/builder/src/components/inputs/Select.tsx index 79c915e0d2d..077b8b979a3 100644 --- a/apps/builder/src/components/inputs/Select.tsx +++ b/apps/builder/src/components/inputs/Select.tsx @@ -35,7 +35,7 @@ type Item = type Props = { isPopoverMatchingInputWidth?: boolean selectedItem?: string - items: readonly T[] + items: readonly T[] | undefined placeholder?: string onSelect?: (value: string | undefined, item?: T) => void } @@ -53,7 +53,7 @@ export const Select = ({ const { onOpen, onClose, isOpen } = useDisclosure() const [inputValue, setInputValue] = useState( getItemLabel( - items.find((item) => + items?.find((item) => typeof item === 'string' ? selectedItem === item : selectedItem === item.value @@ -72,13 +72,13 @@ export const Select = ({ const filteredItems = ( isTouched ? [ - ...items.filter((item) => + ...(items ?? []).filter((item) => getItemLabel(item) .toLowerCase() .includes((inputValue ?? '').toLowerCase()) ), ] - : items + : items ?? [] ).slice(0, 50) const closeDropdown = () => { @@ -181,12 +181,17 @@ export const Select = ({ className="select-input" value={isTouched ? inputValue : ''} placeholder={ - !isTouched && inputValue !== '' ? undefined : placeholder + !items + ? 'Loading...' + : !isTouched && inputValue !== '' + ? undefined + : placeholder } onChange={updateInputValue} onFocus={onOpen} onKeyDown={updateFocusedDropdownItem} pr={selectedItem ? 16 : undefined} + isDisabled={!items} /> { + const workspace = await prisma.workspace.findFirst({ + where: { id: workspaceId }, + select: { + members: { + select: { + userId: true, + }, + }, + typebots: { + where: { + id: typebotId, + }, + select: { + groups: true, + }, + }, + credentials: { + where: { + id: credentialsId, + }, + select: { + id: true, + data: true, + iv: true, + }, + }, + }, + }) + + if (!workspace || isReadWorkspaceFobidden(workspace, user)) + throw new TRPCError({ + code: 'NOT_FOUND', + message: 'No workspace found', + }) + + const credentials = workspace.credentials.at(0) + + if (!credentials) + throw new TRPCError({ + code: 'NOT_FOUND', + message: 'No credentials found', + }) + + const typebot = workspace.typebots.at(0) + + if (!typebot) + throw new TRPCError({ + code: 'NOT_FOUND', + message: 'Typebot not found', + }) + + const block = typebotSchema._def.schema.shape.groups + .parse(workspace.typebots.at(0)?.groups) + .flatMap((group) => group.blocks) + .find((block) => block.id === blockId) + + if (!block || block.type !== IntegrationBlockType.OPEN_AI) + throw new TRPCError({ + code: 'NOT_FOUND', + message: 'OpenAI block not found', + }) + + const data = (await decrypt( + credentials.data, + credentials.iv + )) as OpenAICredentials['data'] + + const config = new Configuration({ + apiKey: data.apiKey, + basePath: block.options.baseUrl, + baseOptions: { + headers: { + 'api-key': data.apiKey, + }, + }, + defaultQueryParams: isNotEmpty(block.options.apiVersion) + ? new URLSearchParams({ + 'api-version': block.options.apiVersion, + }) + : undefined, + }) + + const openai = new OpenAIApi(config) + + const response = await openai.listModels() + + const modelsData = (await response.json()) as ResponseTypes['listModels'] + + return { + models: modelsData.data + .sort((a, b) => b.created - a.created) + .map((model) => model.id), + } + } + ) diff --git a/apps/builder/src/features/blocks/integrations/openai/api/router.ts b/apps/builder/src/features/blocks/integrations/openai/api/router.ts new file mode 100644 index 00000000000..134a5145258 --- /dev/null +++ b/apps/builder/src/features/blocks/integrations/openai/api/router.ts @@ -0,0 +1,6 @@ +import { router } from '@/helpers/server/trpc' +import { listModels } from './listModels' + +export const openAIRouter = router({ + listModels, +}) diff --git a/apps/builder/src/features/blocks/integrations/openai/components/OpenAISettings.tsx b/apps/builder/src/features/blocks/integrations/openai/components/OpenAISettings.tsx index 8352d1cfe16..d5b3320ba90 100644 --- a/apps/builder/src/features/blocks/integrations/openai/components/OpenAISettings.tsx +++ b/apps/builder/src/features/blocks/integrations/openai/components/OpenAISettings.tsx @@ -1,9 +1,19 @@ -import { Stack, useDisclosure } from '@chakra-ui/react' +import { + Accordion, + AccordionButton, + AccordionIcon, + AccordionItem, + AccordionPanel, + Stack, + useDisclosure, + Text, +} from '@chakra-ui/react' import React from 'react' import { CredentialsDropdown } from '@/features/credentials/components/CredentialsDropdown' import { ChatCompletionOpenAIOptions, CreateImageOpenAIOptions, + defaultBaseUrl, defaultChatCompletionOptions, OpenAIBlock, openAITasks, @@ -13,15 +23,19 @@ import { useWorkspace } from '@/features/workspace/WorkspaceProvider' import { DropdownList } from '@/components/DropdownList' import { OpenAIChatCompletionSettings } from './createChatCompletion/OpenAIChatCompletionSettings' import { createId } from '@paralleldrive/cuid2' +import { TextInput } from '@/components/inputs' type OpenAITask = (typeof openAITasks)[number] type Props = { - options: OpenAIBlock['options'] + block: OpenAIBlock onOptionsChange: (options: OpenAIBlock['options']) => void } -export const OpenAISettings = ({ options, onOptionsChange }: Props) => { +export const OpenAISettings = ({ + block: { options, id }, + onOptionsChange, +}: Props) => { const { workspace } = useWorkspace() const { isOpen, onOpen, onClose } = useDisclosure() @@ -44,6 +58,20 @@ export const OpenAISettings = ({ options, onOptionsChange }: Props) => { } } + const updateBaseUrl = (baseUrl: string) => { + onOptionsChange({ + ...options, + baseUrl, + }) + } + + const updateApiVersion = (apiVersion: string) => { + onOptionsChange({ + ...options, + apiVersion, + }) + } + return ( {workspace && ( @@ -56,22 +84,51 @@ export const OpenAISettings = ({ options, onOptionsChange }: Props) => { credentialsName="OpenAI account" /> )} - - - {options.task && ( - + {options.credentialsId && ( + <> + + + + + Customize provider + + + + + + {options.baseUrl !== defaultBaseUrl && ( + + )} + + + + + + {options.task && ( + + )} + )} ) @@ -80,14 +137,17 @@ export const OpenAISettings = ({ options, onOptionsChange }: Props) => { const OpenAITaskSettings = ({ options, onOptionsChange, + blockId, }: { options: ChatCompletionOpenAIOptions | CreateImageOpenAIOptions + blockId: string onOptionsChange: (options: OpenAIBlock['options']) => void }) => { switch (options.task) { case 'Create chat completion': { return ( diff --git a/apps/builder/src/features/blocks/integrations/openai/components/createChatCompletion/ModelsDropdown.tsx b/apps/builder/src/features/blocks/integrations/openai/components/createChatCompletion/ModelsDropdown.tsx new file mode 100644 index 00000000000..cc4e97d2003 --- /dev/null +++ b/apps/builder/src/features/blocks/integrations/openai/components/createChatCompletion/ModelsDropdown.tsx @@ -0,0 +1,42 @@ +import { Select } from '@/components/inputs/Select' +import { useTypebot } from '@/features/editor/providers/TypebotProvider' +import { useWorkspace } from '@/features/workspace/WorkspaceProvider' +import { trpc } from '@/lib/trpc' + +type Props = { + credentialsId: string + blockId: string + defaultValue: string + onChange: (model: string | undefined) => void +} + +export const ModelsDropdown = ({ + defaultValue, + onChange, + credentialsId, + blockId, +}: Props) => { + const { typebot } = useTypebot() + const { workspace } = useWorkspace() + + const { data } = trpc.openAI.listModels.useQuery( + { + credentialsId, + blockId, + typebotId: typebot?.id as string, + workspaceId: workspace?.id as string, + }, + { + enabled: !!typebot && !!workspace, + } + ) + + return ( + deprecatedCompletionModels.indexOf(model) === -1 - )} - onSelect={updateModel} - /> - - - - - Messages - - - + {options.credentialsId && ( + <> + + + + + + Messages + + + - - - - - - - - Advanced settings - - - - - - - - - - - Save answer - - - - - - - - + + + + + + + + Advanced settings + + + + + + + + + + + Save answer + + + + + + + + + + )} ) } diff --git a/apps/builder/src/features/blocks/integrations/openai/openai.spec.ts b/apps/builder/src/features/blocks/integrations/openai/openai.spec.ts index 0757a98da33..8ea415dbeef 100644 --- a/apps/builder/src/features/blocks/integrations/openai/openai.spec.ts +++ b/apps/builder/src/features/blocks/integrations/openai/openai.spec.ts @@ -3,6 +3,7 @@ import { createTypebots } from '@typebot.io/lib/playwright/databaseActions' import { createId } from '@paralleldrive/cuid2' import { IntegrationBlockType } from '@typebot.io/schemas' import { parseDefaultGroupWithBlock } from '@typebot.io/lib/playwright/databaseHelpers' +import { defaultBaseUrl } from '@typebot.io/schemas/features/blocks/integrations/openai' const typebotId = createId() @@ -12,7 +13,9 @@ test('should be configurable', async ({ page }) => { id: typebotId, ...parseDefaultGroupWithBlock({ type: IntegrationBlockType.OPEN_AI, - options: {}, + options: { + baseUrl: defaultBaseUrl, + }, }), }, ]) diff --git a/apps/builder/src/features/graph/components/nodes/block/SettingsPopoverContent.tsx b/apps/builder/src/features/graph/components/nodes/block/SettingsPopoverContent.tsx index 36d31a4ef6d..8c72101af27 100644 --- a/apps/builder/src/features/graph/components/nodes/block/SettingsPopoverContent.tsx +++ b/apps/builder/src/features/graph/components/nodes/block/SettingsPopoverContent.tsx @@ -69,8 +69,7 @@ export const SettingsPopoverContent = ({ onExpandClick, ...props }: Props) => { - ) + return } case IntegrationBlockType.PIXEL: { return ( diff --git a/apps/builder/src/helpers/server/routers/v1/trpcRouter.ts b/apps/builder/src/helpers/server/routers/v1/trpcRouter.ts index aaf1da4c9ed..d96561d5c68 100644 --- a/apps/builder/src/helpers/server/routers/v1/trpcRouter.ts +++ b/apps/builder/src/helpers/server/routers/v1/trpcRouter.ts @@ -14,6 +14,7 @@ import { analyticsRouter } from '@/features/analytics/api/router' import { collaboratorsRouter } from '@/features/collaboration/api/router' import { customDomainsRouter } from '@/features/customDomains/api/router' import { whatsAppRouter } from '@/features/whatsapp/router' +import { openAIRouter } from '@/features/blocks/integrations/openai/api/router' export const trpcRouter = router({ getAppVersionProcedure, @@ -31,6 +32,7 @@ export const trpcRouter = router({ collaborators: collaboratorsRouter, customDomains: customDomainsRouter, whatsApp: whatsAppRouter, + openAI: openAIRouter, }) export type AppRouter = typeof trpcRouter diff --git a/apps/docs/openapi/builder/_spec_.json b/apps/docs/openapi/builder/_spec_.json index c0521034757..abc22f28629 100644 --- a/apps/docs/openapi/builder/_spec_.json +++ b/apps/docs/openapi/builder/_spec_.json @@ -2923,6 +2923,13 @@ }, "credentialsId": { "type": "string" + }, + "baseUrl": { + "type": "string", + "default": "https://api.openai.com/v1" + }, + "apiVersion": { + "type": "string" } }, "additionalProperties": false @@ -2937,20 +2944,7 @@ ] }, "model": { - "type": "string", - "enum": [ - "gpt-3.5-turbo", - "gpt-3.5-turbo-0613", - "gpt-3.5-turbo-16k", - "gpt-3.5-turbo-16k-0613", - "gpt-3.5-turbo-0301", - "gpt-4", - "gpt-4-0613", - "gpt-4-32k", - "gpt-4-32k-0613", - "gpt-4-32k-0314", - "gpt-4-0314" - ] + "type": "string" }, "messages": { "type": "array", @@ -3057,6 +3051,13 @@ }, "credentialsId": { "type": "string" + }, + "baseUrl": { + "type": "string", + "default": "https://api.openai.com/v1" + }, + "apiVersion": { + "type": "string" } }, "required": [ @@ -3120,6 +3121,13 @@ }, "credentialsId": { "type": "string" + }, + "baseUrl": { + "type": "string", + "default": "https://api.openai.com/v1" + }, + "apiVersion": { + "type": "string" } }, "required": [ @@ -7208,6 +7216,13 @@ }, "credentialsId": { "type": "string" + }, + "baseUrl": { + "type": "string", + "default": "https://api.openai.com/v1" + }, + "apiVersion": { + "type": "string" } }, "additionalProperties": false @@ -7222,20 +7237,7 @@ ] }, "model": { - "type": "string", - "enum": [ - "gpt-3.5-turbo", - "gpt-3.5-turbo-0613", - "gpt-3.5-turbo-16k", - "gpt-3.5-turbo-16k-0613", - "gpt-3.5-turbo-0301", - "gpt-4", - "gpt-4-0613", - "gpt-4-32k", - "gpt-4-32k-0613", - "gpt-4-32k-0314", - "gpt-4-0314" - ] + "type": "string" }, "messages": { "type": "array", @@ -7342,6 +7344,13 @@ }, "credentialsId": { "type": "string" + }, + "baseUrl": { + "type": "string", + "default": "https://api.openai.com/v1" + }, + "apiVersion": { + "type": "string" } }, "required": [ @@ -7405,6 +7414,13 @@ }, "credentialsId": { "type": "string" + }, + "baseUrl": { + "type": "string", + "default": "https://api.openai.com/v1" + }, + "apiVersion": { + "type": "string" } }, "required": [ @@ -11128,6 +11144,13 @@ }, "credentialsId": { "type": "string" + }, + "baseUrl": { + "type": "string", + "default": "https://api.openai.com/v1" + }, + "apiVersion": { + "type": "string" } }, "additionalProperties": false @@ -11142,20 +11165,7 @@ ] }, "model": { - "type": "string", - "enum": [ - "gpt-3.5-turbo", - "gpt-3.5-turbo-0613", - "gpt-3.5-turbo-16k", - "gpt-3.5-turbo-16k-0613", - "gpt-3.5-turbo-0301", - "gpt-4", - "gpt-4-0613", - "gpt-4-32k", - "gpt-4-32k-0613", - "gpt-4-32k-0314", - "gpt-4-0314" - ] + "type": "string" }, "messages": { "type": "array", @@ -11262,6 +11272,13 @@ }, "credentialsId": { "type": "string" + }, + "baseUrl": { + "type": "string", + "default": "https://api.openai.com/v1" + }, + "apiVersion": { + "type": "string" } }, "required": [ @@ -11325,6 +11342,13 @@ }, "credentialsId": { "type": "string" + }, + "baseUrl": { + "type": "string", + "default": "https://api.openai.com/v1" + }, + "apiVersion": { + "type": "string" } }, "required": [ @@ -15188,6 +15212,13 @@ }, "credentialsId": { "type": "string" + }, + "baseUrl": { + "type": "string", + "default": "https://api.openai.com/v1" + }, + "apiVersion": { + "type": "string" } }, "additionalProperties": false @@ -15202,20 +15233,7 @@ ] }, "model": { - "type": "string", - "enum": [ - "gpt-3.5-turbo", - "gpt-3.5-turbo-0613", - "gpt-3.5-turbo-16k", - "gpt-3.5-turbo-16k-0613", - "gpt-3.5-turbo-0301", - "gpt-4", - "gpt-4-0613", - "gpt-4-32k", - "gpt-4-32k-0613", - "gpt-4-32k-0314", - "gpt-4-0314" - ] + "type": "string" }, "messages": { "type": "array", @@ -15322,6 +15340,13 @@ }, "credentialsId": { "type": "string" + }, + "baseUrl": { + "type": "string", + "default": "https://api.openai.com/v1" + }, + "apiVersion": { + "type": "string" } }, "required": [ @@ -15385,6 +15410,13 @@ }, "credentialsId": { "type": "string" + }, + "baseUrl": { + "type": "string", + "default": "https://api.openai.com/v1" + }, + "apiVersion": { + "type": "string" } }, "required": [ @@ -19128,6 +19160,13 @@ }, "credentialsId": { "type": "string" + }, + "baseUrl": { + "type": "string", + "default": "https://api.openai.com/v1" + }, + "apiVersion": { + "type": "string" } }, "additionalProperties": false @@ -19142,20 +19181,7 @@ ] }, "model": { - "type": "string", - "enum": [ - "gpt-3.5-turbo", - "gpt-3.5-turbo-0613", - "gpt-3.5-turbo-16k", - "gpt-3.5-turbo-16k-0613", - "gpt-3.5-turbo-0301", - "gpt-4", - "gpt-4-0613", - "gpt-4-32k", - "gpt-4-32k-0613", - "gpt-4-32k-0314", - "gpt-4-0314" - ] + "type": "string" }, "messages": { "type": "array", @@ -19262,6 +19288,13 @@ }, "credentialsId": { "type": "string" + }, + "baseUrl": { + "type": "string", + "default": "https://api.openai.com/v1" + }, + "apiVersion": { + "type": "string" } }, "required": [ @@ -19325,6 +19358,13 @@ }, "credentialsId": { "type": "string" + }, + "baseUrl": { + "type": "string", + "default": "https://api.openai.com/v1" + }, + "apiVersion": { + "type": "string" } }, "required": [ @@ -23123,6 +23163,13 @@ }, "credentialsId": { "type": "string" + }, + "baseUrl": { + "type": "string", + "default": "https://api.openai.com/v1" + }, + "apiVersion": { + "type": "string" } }, "additionalProperties": false @@ -23137,20 +23184,7 @@ ] }, "model": { - "type": "string", - "enum": [ - "gpt-3.5-turbo", - "gpt-3.5-turbo-0613", - "gpt-3.5-turbo-16k", - "gpt-3.5-turbo-16k-0613", - "gpt-3.5-turbo-0301", - "gpt-4", - "gpt-4-0613", - "gpt-4-32k", - "gpt-4-32k-0613", - "gpt-4-32k-0314", - "gpt-4-0314" - ] + "type": "string" }, "messages": { "type": "array", @@ -23257,6 +23291,13 @@ }, "credentialsId": { "type": "string" + }, + "baseUrl": { + "type": "string", + "default": "https://api.openai.com/v1" + }, + "apiVersion": { + "type": "string" } }, "required": [ @@ -23320,6 +23361,13 @@ }, "credentialsId": { "type": "string" + }, + "baseUrl": { + "type": "string", + "default": "https://api.openai.com/v1" + }, + "apiVersion": { + "type": "string" } }, "required": [ @@ -27181,6 +27229,13 @@ }, "credentialsId": { "type": "string" + }, + "baseUrl": { + "type": "string", + "default": "https://api.openai.com/v1" + }, + "apiVersion": { + "type": "string" } }, "additionalProperties": false @@ -27195,20 +27250,7 @@ ] }, "model": { - "type": "string", - "enum": [ - "gpt-3.5-turbo", - "gpt-3.5-turbo-0613", - "gpt-3.5-turbo-16k", - "gpt-3.5-turbo-16k-0613", - "gpt-3.5-turbo-0301", - "gpt-4", - "gpt-4-0613", - "gpt-4-32k", - "gpt-4-32k-0613", - "gpt-4-32k-0314", - "gpt-4-0314" - ] + "type": "string" }, "messages": { "type": "array", @@ -27315,6 +27357,13 @@ }, "credentialsId": { "type": "string" + }, + "baseUrl": { + "type": "string", + "default": "https://api.openai.com/v1" + }, + "apiVersion": { + "type": "string" } }, "required": [ @@ -27378,6 +27427,13 @@ }, "credentialsId": { "type": "string" + }, + "baseUrl": { + "type": "string", + "default": "https://api.openai.com/v1" + }, + "apiVersion": { + "type": "string" } }, "required": [ @@ -31999,6 +32055,81 @@ } } } + }, + "/typebots/{typebotId}/blocks/{blockId}/openai/models": { + "get": { + "operationId": "openAI-listModels", + "summary": "List OpenAI models", + "tags": [ + "OpenAI" + ], + "security": [ + { + "Authorization": [] + } + ], + "parameters": [ + { + "name": "typebotId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "blockId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "credentialsId", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "workspaceId", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "models": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "models" + ], + "additionalProperties": false + } + } + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } } }, "components": { diff --git a/apps/docs/openapi/chat/_spec_.json b/apps/docs/openapi/chat/_spec_.json index 6c769bf733d..7d0dd2453fa 100644 --- a/apps/docs/openapi/chat/_spec_.json +++ b/apps/docs/openapi/chat/_spec_.json @@ -2506,6 +2506,13 @@ }, "credentialsId": { "type": "string" + }, + "baseUrl": { + "type": "string", + "default": "https://api.openai.com/v1" + }, + "apiVersion": { + "type": "string" } }, "additionalProperties": false @@ -2520,20 +2527,7 @@ ] }, "model": { - "type": "string", - "enum": [ - "gpt-3.5-turbo", - "gpt-3.5-turbo-0613", - "gpt-3.5-turbo-16k", - "gpt-3.5-turbo-16k-0613", - "gpt-3.5-turbo-0301", - "gpt-4", - "gpt-4-0613", - "gpt-4-32k", - "gpt-4-32k-0613", - "gpt-4-32k-0314", - "gpt-4-0314" - ] + "type": "string" }, "messages": { "type": "array", @@ -2640,6 +2634,13 @@ }, "credentialsId": { "type": "string" + }, + "baseUrl": { + "type": "string", + "default": "https://api.openai.com/v1" + }, + "apiVersion": { + "type": "string" } }, "required": [ @@ -2703,6 +2704,13 @@ }, "credentialsId": { "type": "string" + }, + "baseUrl": { + "type": "string", + "default": "https://api.openai.com/v1" + }, + "apiVersion": { + "type": "string" } }, "required": [ diff --git a/apps/viewer/package.json b/apps/viewer/package.json index 2a10f75baf2..49a6c766ebc 100644 --- a/apps/viewer/package.json +++ b/apps/viewer/package.json @@ -29,8 +29,7 @@ "nextjs-cors": "2.1.2", "node-html-parser": "^6.1.5", "nodemailer": "6.9.3", - "openai": "3.3.0", - "openai-edge": "^1.2.0", + "openai-edge": "1.2.2", "qs": "6.11.2", "react": "18.2.0", "react-dom": "18.2.0", diff --git a/apps/viewer/src/features/blocks/integrations/openai/createChatCompletionOpenAI.ts b/apps/viewer/src/features/blocks/integrations/openai/createChatCompletionOpenAI.ts index ed885ff880e..c00f6cdf9a0 100644 --- a/apps/viewer/src/features/blocks/integrations/openai/createChatCompletionOpenAI.ts +++ b/apps/viewer/src/features/blocks/integrations/openai/createChatCompletionOpenAI.ts @@ -107,6 +107,8 @@ export const createChatCompletionOpenAI = async ( messages, model: options.model, temperature, + baseUrl: options.baseUrl, + apiVersion: options.apiVersion, }) if (!response) return { diff --git a/apps/viewer/src/features/blocks/integrations/openai/executeChatCompletionOpenAIRequest.ts b/apps/viewer/src/features/blocks/integrations/openai/executeChatCompletionOpenAIRequest.ts index 3f796e31b51..d1bbadf5db5 100644 --- a/apps/viewer/src/features/blocks/integrations/openai/executeChatCompletionOpenAIRequest.ts +++ b/apps/viewer/src/features/blocks/integrations/openai/executeChatCompletionOpenAIRequest.ts @@ -1,24 +1,30 @@ +import { isNotEmpty } from '@typebot.io/lib/utils' import { ChatReply } from '@typebot.io/schemas' -import got, { HTTPError } from 'got' -import type { - CreateChatCompletionRequest, - CreateChatCompletionResponse, -} from 'openai' - -const createChatEndpoint = 'https://api.openai.com/v1/chat/completions' +import { OpenAIBlock } from '@typebot.io/schemas/features/blocks/integrations/openai' +import { HTTPError } from 'got' +import { + Configuration, + OpenAIApi, + type CreateChatCompletionRequest, + type CreateChatCompletionResponse, + ResponseTypes, +} from 'openai-edge' type Props = Pick & { apiKey: string temperature: number | undefined currentLogs?: ChatReply['logs'] isRetrying?: boolean -} +} & Pick export const executeChatCompletionOpenAIRequest = async ({ apiKey, model, messages, temperature, + baseUrl, + apiVersion, + isRetrying, currentLogs = [], }: Props): Promise<{ response?: CreateChatCompletionResponse @@ -27,22 +33,40 @@ export const executeChatCompletionOpenAIRequest = async ({ const logs: ChatReply['logs'] = currentLogs if (messages.length === 0) return { logs } try { - const response = await got - .post(createChatEndpoint, { + const config = new Configuration({ + apiKey, + basePath: baseUrl, + baseOptions: { headers: { - Authorization: `Bearer ${apiKey}`, + 'api-key': apiKey, }, - json: { - model, - messages, - temperature, - } satisfies CreateChatCompletionRequest, - }) - .json() - return { response, logs } + }, + defaultQueryParams: isNotEmpty(apiVersion) + ? new URLSearchParams({ + 'api-version': apiVersion, + }) + : undefined, + }) + + const openai = new OpenAIApi(config) + + const response = await openai.createChatCompletion({ + model, + messages, + temperature, + }) + + const completion = + (await response.json()) as ResponseTypes['createChatCompletion'] + return { response: completion, logs } } catch (error) { if (error instanceof HTTPError) { - if (error.response.statusCode === 503) { + if ( + (error.response.statusCode === 503 || + error.response.statusCode === 500 || + error.response.statusCode === 403) && + !isRetrying + ) { console.log('OpenAI API error - 503, retrying in 3 seconds') await new Promise((resolve) => setTimeout(resolve, 3000)) return executeChatCompletionOpenAIRequest({ @@ -51,6 +75,9 @@ export const executeChatCompletionOpenAIRequest = async ({ messages, temperature, currentLogs: logs, + baseUrl, + apiVersion, + isRetrying: true, }) } if (error.response.statusCode === 400) { @@ -67,6 +94,8 @@ export const executeChatCompletionOpenAIRequest = async ({ messages: messages.slice(1), temperature, currentLogs: logs, + baseUrl, + apiVersion, }) } logs.push({ diff --git a/apps/viewer/src/features/blocks/integrations/openai/getChatCompletionStream.ts b/apps/viewer/src/features/blocks/integrations/openai/getChatCompletionStream.ts index 55bccc5958c..b64f039b9e0 100644 --- a/apps/viewer/src/features/blocks/integrations/openai/getChatCompletionStream.ts +++ b/apps/viewer/src/features/blocks/integrations/openai/getChatCompletionStream.ts @@ -1,6 +1,7 @@ import { parseVariableNumber } from '@/features/variables/parseVariableNumber' import { Connection } from '@planetscale/database' import { decrypt } from '@typebot.io/lib/api/encryption' +import { isNotEmpty } from '@typebot.io/lib/utils' import { ChatCompletionOpenAIOptions, OpenAICredentials, @@ -42,6 +43,17 @@ export const getChatCompletionStream = const config = new Configuration({ apiKey, + basePath: options.baseUrl, + baseOptions: { + headers: { + 'api-key': apiKey, + }, + }, + defaultQueryParams: isNotEmpty(options.apiVersion) + ? new URLSearchParams({ + 'api-version': options.apiVersion, + }) + : undefined, }) const openai = new OpenAIApi(config) diff --git a/apps/viewer/src/features/blocks/integrations/openai/parseChatCompletionMessages.ts b/apps/viewer/src/features/blocks/integrations/openai/parseChatCompletionMessages.ts index 82a1e121b7c..d427d233eb6 100644 --- a/apps/viewer/src/features/blocks/integrations/openai/parseChatCompletionMessages.ts +++ b/apps/viewer/src/features/blocks/integrations/openai/parseChatCompletionMessages.ts @@ -3,7 +3,7 @@ import { transformStringVariablesToList } from '@/features/variables/transformVa import { byId, isNotEmpty } from '@typebot.io/lib' import { Variable, VariableWithValue } from '@typebot.io/schemas' import { ChatCompletionOpenAIOptions } from '@typebot.io/schemas/features/blocks/integrations/openai' -import type { ChatCompletionRequestMessage } from 'openai' +import type { ChatCompletionRequestMessage } from 'openai-edge' export const parseChatCompletionMessages = (variables: Variable[]) => diff --git a/apps/viewer/src/pages/api/integrations/openai/streamer.ts b/apps/viewer/src/pages/api/integrations/openai/streamer.ts index ddc4b8a68d4..41bd46fd576 100644 --- a/apps/viewer/src/pages/api/integrations/openai/streamer.ts +++ b/apps/viewer/src/pages/api/integrations/openai/streamer.ts @@ -3,7 +3,7 @@ import { connect } from '@planetscale/database' import { env } from '@typebot.io/env' import { IntegrationBlockType, SessionState } from '@typebot.io/schemas' import { StreamingTextResponse } from 'ai' -import { ChatCompletionRequestMessage } from 'openai' +import { ChatCompletionRequestMessage } from 'openai-edge' export const config = { runtime: 'edge', diff --git a/packages/schemas/features/blocks/integrations/openai.ts b/packages/schemas/features/blocks/integrations/openai.ts index 02df5dcc8d5..5a7bda5fff6 100644 --- a/packages/schemas/features/blocks/integrations/openai.ts +++ b/packages/schemas/features/blocks/integrations/openai.ts @@ -5,23 +5,6 @@ import { IntegrationBlockType } from './enums' export const openAITasks = ['Create chat completion', 'Create image'] as const -export const chatCompletionModels = [ - 'gpt-3.5-turbo', - 'gpt-3.5-turbo-0613', - 'gpt-3.5-turbo-16k', - 'gpt-3.5-turbo-16k-0613', - 'gpt-3.5-turbo-0301', - 'gpt-4', - 'gpt-4-0613', - 'gpt-4-32k', - 'gpt-4-32k-0613', - 'gpt-4-32k-0314', - 'gpt-4-0314', -] as const - -export const deprecatedCompletionModels: (typeof chatCompletionModels)[number][] = - ['gpt-3.5-turbo-0301', 'gpt-4-32k-0314', 'gpt-4-0314'] - export const chatCompletionMessageRoles = [ 'system', 'user', @@ -37,8 +20,12 @@ export const chatCompletionResponseValues = [ 'Total tokens', ] as const +export const defaultBaseUrl = 'https://api.openai.com/v1' + const openAIBaseOptionsSchema = z.object({ credentialsId: z.string().optional(), + baseUrl: z.string().default(defaultBaseUrl), + apiVersion: z.string().optional(), }) const initialOptionsSchema = z @@ -68,7 +55,7 @@ const chatCompletionCustomMessageSchema = z.object({ const chatCompletionOptionsSchema = z .object({ task: z.literal(openAITasks[0]), - model: z.enum(chatCompletionModels), + model: z.string(), messages: z.array( z.union([chatCompletionMessageSchema, chatCompletionCustomMessageSchema]) ), @@ -130,6 +117,7 @@ export const openAICredentialsSchema = z export const defaultChatCompletionOptions = ( createId: () => string ): ChatCompletionOpenAIOptions => ({ + baseUrl: defaultBaseUrl, task: 'Create chat completion', messages: [ { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 366f0f72bc2..94de26962c1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -218,6 +218,9 @@ importers: nprogress: specifier: 0.2.0 version: 0.2.0 + openai-edge: + specifier: 1.2.2 + version: 1.2.2 papaparse: specifier: 5.4.1 version: 5.4.1 @@ -569,12 +572,9 @@ importers: nodemailer: specifier: 6.9.3 version: 6.9.3 - openai: - specifier: 3.3.0 - version: 3.3.0 openai-edge: - specifier: ^1.2.0 - version: 1.2.0 + specifier: 1.2.2 + version: 1.2.2 qs: specifier: 6.11.2 version: 6.11.2 @@ -17593,20 +17593,11 @@ packages: is-wsl: 2.2.0 dev: false - /openai-edge@1.2.0: - resolution: {integrity: sha512-eaQs+O/1k6OZMUibNlBzWPXdHFxpUNLMy4BwhtXCFDub5iz7ve/PxOJTL8GBG3/1S1j6LIL93xjdlzCPQpbdgQ==} + /openai-edge@1.2.2: + resolution: {integrity: sha512-C3/Ao9Hkx5uBPv9YFBpX/x59XMPgPUU4dyGg/0J2sOJ7O9D98kD+lfdOc7v/60oYo5xzMGct80uFkYLH+X2qgw==} engines: {node: '>=18'} dev: false - /openai@3.3.0: - resolution: {integrity: sha512-uqxI/Au+aPRnsaQRe8CojU0eCR7I0mBiKjD3sNMzY6DaC1ZVrc85u98mtJW6voDug8fgGN+DIZmTDxTthxb7dQ==} - dependencies: - axios: 0.26.1 - form-data: 4.0.0 - transitivePeerDependencies: - - debug - dev: false - /openapi-to-postmanv2@1.2.7: resolution: {integrity: sha512-oG3PZfAAljy5ebot8DZGLFDNNmDZ/qWqI/dboWlgg5hRj6dSSrXeiyXL6VQpcGDalxVX4jSChufOq2eDsFXp4w==} engines: {node: '>=4'}