From fa528e0cf3e614bd90b7e463bb593ff5c33f8965 Mon Sep 17 00:00:00 2001 From: Sean Hatfield Date: Tue, 15 Oct 2024 19:42:13 -0700 Subject: [PATCH 1/3] OpenAI o1 model support (#2427) * support openai o1 models * Prevent O1 use for agents getter for isO1Model; --------- Co-authored-by: timothycarambat --- .../AgentConfig/AgentModelSelection/index.jsx | 13 ++++++++++--- server/utils/AiProviders/modelMap.js | 4 ++++ server/utils/AiProviders/openAi/index.js | 18 +++++++++++++++--- server/utils/helpers/customModels.js | 2 +- 4 files changed, 30 insertions(+), 7 deletions(-) diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentModelSelection/index.jsx b/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentModelSelection/index.jsx index 4e0a9592c..a16e1689c 100644 --- a/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentModelSelection/index.jsx +++ b/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentModelSelection/index.jsx @@ -6,12 +6,19 @@ import { useTranslation } from "react-i18next"; import { Link, useParams } from "react-router-dom"; // These models do NOT support function calling +// and therefore are not supported for agents. function supportedModel(provider, model = "") { if (provider !== "openai") return true; return ( - ["gpt-3.5-turbo-0301", "gpt-4-turbo-2024-04-09", "gpt-4-turbo"].includes( - model - ) === false + [ + "gpt-3.5-turbo-0301", + "gpt-4-turbo-2024-04-09", + "gpt-4-turbo", + "o1-preview", + "o1-preview-2024-09-12", + "o1-mini", + "o1-mini-2024-09-12", + ].includes(model) === false ); } diff --git a/server/utils/AiProviders/modelMap.js b/server/utils/AiProviders/modelMap.js index 99d78dc14..84e480b31 100644 --- a/server/utils/AiProviders/modelMap.js +++ b/server/utils/AiProviders/modelMap.js @@ -52,6 +52,10 @@ const MODEL_MAP = { "gpt-4-turbo-preview": 128_000, "gpt-4": 8_192, "gpt-4-32k": 32_000, + "o1-preview": 128_000, + "o1-preview-2024-09-12": 128_000, + "o1-mini": 128_000, + "o1-mini-2024-09-12": 128_000, }, deepseek: { "deepseek-chat": 128_000, diff --git a/server/utils/AiProviders/openAi/index.js b/server/utils/AiProviders/openAi/index.js index b0e52dc2b..4f6bc2219 100644 --- a/server/utils/AiProviders/openAi/index.js +++ b/server/utils/AiProviders/openAi/index.js @@ -23,6 +23,14 @@ class OpenAiLLM { this.defaultTemp = 0.7; } + /** + * Check if the model is an o1 model. + * @returns {boolean} + */ + get isO1Model() { + return this.model.startsWith("o1"); + } + #appendContext(contextTexts = []) { if (!contextTexts || !contextTexts.length) return ""; return ( @@ -36,6 +44,7 @@ class OpenAiLLM { } streamingEnabled() { + if (this.isO1Model) return false; return "streamGetChatCompletion" in this; } @@ -98,8 +107,11 @@ class OpenAiLLM { userPrompt = "", attachments = [], // This is the specific attachment for only this prompt }) { + // o1 Models do not support the "system" role + // in order to combat this, we can use the "user" role as a replacement for now + // https://community.openai.com/t/o1-models-do-not-support-system-role-in-chat-completion/953880 const prompt = { - role: "system", + role: this.isO1Model ? "user" : "system", content: `${systemPrompt}${this.#appendContext(contextTexts)}`, }; return [ @@ -122,7 +134,7 @@ class OpenAiLLM { .create({ model: this.model, messages, - temperature, + temperature: this.isO1Model ? 1 : temperature, // o1 models only accept temperature 1 }) .catch((e) => { throw new Error(e.message); @@ -143,7 +155,7 @@ class OpenAiLLM { model: this.model, stream: true, messages, - temperature, + temperature: this.isO1Model ? 1 : temperature, // o1 models only accept temperature 1 }); return streamRequest; } diff --git a/server/utils/helpers/customModels.js b/server/utils/helpers/customModels.js index f3430cecc..086144bfe 100644 --- a/server/utils/helpers/customModels.js +++ b/server/utils/helpers/customModels.js @@ -128,7 +128,7 @@ async function openAiModels(apiKey = null) { }); const gpts = allModels - .filter((model) => model.id.startsWith("gpt")) + .filter((model) => model.id.startsWith("gpt") || model.id.startsWith("o1")) .filter( (model) => !model.id.includes("vision") && !model.id.includes("instruct") ) From 3dc0f3f490b9bdfa12c23026ffa1d7cd8716af5e Mon Sep 17 00:00:00 2001 From: Timothy Carambat Date: Tue, 15 Oct 2024 21:39:31 -0700 Subject: [PATCH 2/3] Tts open ai compatible endpoints (#2487) * Update OpenAI TTS config to allow a custom BaseURL * uncheck config file * break openai generic TTS into its own provider * add space * hide TTS on user msg --------- Co-authored-by: Adam --- docker/.env.example | 5 ++ .../OpenAiGenericOptions/index.jsx | 69 ++++++++++++++++++ .../Actions/TTSButton/index.jsx | 1 + .../ChatHistory/HistoricalMessage/index.jsx | 12 +-- .../src/media/ttsproviders/generic-openai.png | Bin 0 -> 29556 bytes .../GeneralSettings/AudioPreference/tts.jsx | 11 +++ server/.env.example | 5 ++ server/models/systemSettings.js | 6 ++ server/utils/TextToSpeech/index.js | 3 + .../utils/TextToSpeech/openAiGeneric/index.js | 50 +++++++++++++ server/utils/helpers/updateENV.js | 15 ++++ 11 files changed, 172 insertions(+), 5 deletions(-) create mode 100644 frontend/src/components/TextToSpeech/OpenAiGenericOptions/index.jsx create mode 100644 frontend/src/media/ttsproviders/generic-openai.png create mode 100644 server/utils/TextToSpeech/openAiGeneric/index.js diff --git a/docker/.env.example b/docker/.env.example index 55f3b2627..2f9e23288 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -219,6 +219,11 @@ GID='1000' # TTS_OPEN_AI_KEY=sk-example # TTS_OPEN_AI_VOICE_MODEL=nova +# TTS_PROVIDER="generic-openai" +# TTS_OPEN_AI_COMPATIBLE_KEY=sk-example +# TTS_OPEN_AI_COMPATIBLE_VOICE_MODEL=nova +# TTS_OPEN_AI_COMPATIBLE_ENDPOINT="https://api.openai.com/v1" + # TTS_PROVIDER="elevenlabs" # TTS_ELEVEN_LABS_KEY= # TTS_ELEVEN_LABS_VOICE_MODEL=21m00Tcm4TlvDq8ikWAM # Rachel diff --git a/frontend/src/components/TextToSpeech/OpenAiGenericOptions/index.jsx b/frontend/src/components/TextToSpeech/OpenAiGenericOptions/index.jsx new file mode 100644 index 000000000..2247544cd --- /dev/null +++ b/frontend/src/components/TextToSpeech/OpenAiGenericOptions/index.jsx @@ -0,0 +1,69 @@ +import React from "react"; + +export default function OpenAiGenericTextToSpeechOptions({ settings }) { + return ( +
+
+
+
+ +
+ +

+ This should be the base URL of the OpenAI compatible TTS service you + will generate TTS responses from. +

+
+ +
+ + +

+ Some TTS services require an API key to generate TTS responses - + this is optional if your service does not require one. +

+
+
+ + +

+ Most TTS services will have several voice models available, this is + the identifier for the voice model you want to use. +

+
+
+
+ ); +} diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/TTSButton/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/TTSButton/index.jsx index 88d063387..31ac70670 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/TTSButton/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/TTSButton/index.jsx @@ -23,6 +23,7 @@ export default function TTSMessage({ slug, chatId, message }) { switch (provider) { case "openai": + case "generic-openai": case "elevenlabs": return ; case "piper_local": diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx index f1d7bbe1d..b7da93750 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx @@ -81,11 +81,13 @@ const HistoricalMessage = ({
- + {role === "assistant" && ( + + )}
{isEditing ? ( diff --git a/frontend/src/media/ttsproviders/generic-openai.png b/frontend/src/media/ttsproviders/generic-openai.png new file mode 100644 index 0000000000000000000000000000000000000000..302f5dbee0aebe15a3fc4cd6073d58314c418487 GIT binary patch literal 29556 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 , description: "Run TTS models locally in your browser privately.", }, + { + name: "OpenAI Compatible", + value: "generic-openai", + logo: GenericOpenAiLogo, + options: (settings) => , + description: + "Connect to an OpenAI compatible TTS service running locally or remotely.", + }, ]; export default function TextToSpeechProvider({ settings }) { diff --git a/server/.env.example b/server/.env.example index e6a3871d6..3f60b0e5b 100644 --- a/server/.env.example +++ b/server/.env.example @@ -213,6 +213,11 @@ TTS_PROVIDER="native" # TTS_ELEVEN_LABS_KEY= # TTS_ELEVEN_LABS_VOICE_MODEL=21m00Tcm4TlvDq8ikWAM # Rachel +# TTS_PROVIDER="generic-openai" +# TTS_OPEN_AI_COMPATIBLE_KEY=sk-example +# TTS_OPEN_AI_COMPATIBLE_VOICE_MODEL=nova +# TTS_OPEN_AI_COMPATIBLE_ENDPOINT="https://api.openai.com/v1" + # CLOUD DEPLOYMENT VARIRABLES ONLY # AUTH_TOKEN="hunter2" # This is the password to your application if remote hosting. # STORAGE_DIR= # absolute filesystem path with no trailing slash diff --git a/server/models/systemSettings.js b/server/models/systemSettings.js index 0c67bf2f4..c69794b48 100644 --- a/server/models/systemSettings.js +++ b/server/models/systemSettings.js @@ -221,12 +221,18 @@ const SystemSettings = { TextToSpeechProvider: process.env.TTS_PROVIDER || "native", TTSOpenAIKey: !!process.env.TTS_OPEN_AI_KEY, TTSOpenAIVoiceModel: process.env.TTS_OPEN_AI_VOICE_MODEL, + // Eleven Labs TTS TTSElevenLabsKey: !!process.env.TTS_ELEVEN_LABS_KEY, TTSElevenLabsVoiceModel: process.env.TTS_ELEVEN_LABS_VOICE_MODEL, // Piper TTS TTSPiperTTSVoiceModel: process.env.TTS_PIPER_VOICE_MODEL ?? "en_US-hfc_female-medium", + // OpenAI Generic TTS + TTSOpenAICompatibleKey: !!process.env.TTS_OPEN_AI_COMPATIBLE_KEY, + TTSOpenAICompatibleVoiceModel: + process.env.TTS_OPEN_AI_COMPATIBLE_VOICE_MODEL, + TTSOpenAICompatibleEndpoint: process.env.TTS_OPEN_AI_COMPATIBLE_ENDPOINT, // -------------------------------------------------------- // Agent Settings & Configs diff --git a/server/utils/TextToSpeech/index.js b/server/utils/TextToSpeech/index.js index 155fc9540..5ed5684de 100644 --- a/server/utils/TextToSpeech/index.js +++ b/server/utils/TextToSpeech/index.js @@ -7,6 +7,9 @@ function getTTSProvider() { case "elevenlabs": const { ElevenLabsTTS } = require("./elevenLabs"); return new ElevenLabsTTS(); + case "generic-openai": + const { GenericOpenAiTTS } = require("./openAiGeneric"); + return new GenericOpenAiTTS(); default: throw new Error("ENV: No TTS_PROVIDER value found in environment!"); } diff --git a/server/utils/TextToSpeech/openAiGeneric/index.js b/server/utils/TextToSpeech/openAiGeneric/index.js new file mode 100644 index 000000000..df39e6348 --- /dev/null +++ b/server/utils/TextToSpeech/openAiGeneric/index.js @@ -0,0 +1,50 @@ +class GenericOpenAiTTS { + constructor() { + if (!process.env.TTS_OPEN_AI_COMPATIBLE_KEY) + this.#log( + "No OpenAI compatible API key was set. You might need to set this to use your OpenAI compatible TTS service." + ); + if (!process.env.TTS_OPEN_AI_COMPATIBLE_VOICE_MODEL) + this.#log( + "No OpenAI compatible voice model was set. We will use the default voice model 'alloy'. This may not exist for your selected endpoint." + ); + if (!process.env.TTS_OPEN_AI_COMPATIBLE_ENDPOINT) + throw new Error( + "No OpenAI compatible endpoint was set. Please set this to use your OpenAI compatible TTS service." + ); + + const { OpenAI: OpenAIApi } = require("openai"); + this.openai = new OpenAIApi({ + apiKey: process.env.TTS_OPEN_AI_COMPATIBLE_KEY || null, + baseURL: process.env.TTS_OPEN_AI_COMPATIBLE_ENDPOINT, + }); + this.voice = process.env.TTS_OPEN_AI_COMPATIBLE_VOICE_MODEL ?? "alloy"; + } + + #log(text, ...args) { + console.log(`\x1b[32m[OpenAiGenericTTS]\x1b[0m ${text}`, ...args); + } + + /** + * Generates a buffer from the given text input using the OpenAI compatible TTS service. + * @param {string} textInput - The text to be converted to audio. + * @returns {Promise} A buffer containing the audio data. + */ + async ttsBuffer(textInput) { + try { + const result = await this.openai.audio.speech.create({ + model: "tts-1", + voice: this.voice, + input: textInput, + }); + return Buffer.from(await result.arrayBuffer()); + } catch (e) { + console.error(e); + } + return null; + } +} + +module.exports = { + GenericOpenAiTTS, +}; diff --git a/server/utils/helpers/updateENV.js b/server/utils/helpers/updateENV.js index 160e85d44..294214a0b 100644 --- a/server/utils/helpers/updateENV.js +++ b/server/utils/helpers/updateENV.js @@ -506,6 +506,20 @@ const KEY_MAPPING = { checks: [], }, + // OpenAI Generic TTS + TTSOpenAICompatibleKey: { + envKey: "TTS_OPEN_AI_COMPATIBLE_KEY", + checks: [], + }, + TTSOpenAICompatibleVoiceModel: { + envKey: "TTS_OPEN_AI_COMPATIBLE_VOICE_MODEL", + checks: [isNotEmpty], + }, + TTSOpenAICompatibleEndpoint: { + envKey: "TTS_OPEN_AI_COMPATIBLE_ENDPOINT", + checks: [isValidURL], + }, + // DeepSeek Options DeepSeekApiKey: { envKey: "DEEPSEEK_API_KEY", @@ -589,6 +603,7 @@ function supportedTTSProvider(input = "") { "openai", "elevenlabs", "piper_local", + "generic-openai", ].includes(input); return validSelection ? null : `${input} is not a valid TTS provider.`; } From 93d7ce6d3471caeacbe8d3dadcdf06edb0c5fa93 Mon Sep 17 00:00:00 2001 From: Timothy Carambat Date: Wed, 16 Oct 2024 12:31:04 -0700 Subject: [PATCH 3/3] Handle Bedrock models that cannot use `system` prompts (#2489) --- .../AgentConfig/AgentModelSelection/index.jsx | 37 ++++++++++++------- server/utils/AiProviders/bedrock/index.js | 34 +++++++++++++++++ 2 files changed, 57 insertions(+), 14 deletions(-) diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentModelSelection/index.jsx b/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentModelSelection/index.jsx index a16e1689c..aeb9db067 100644 --- a/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentModelSelection/index.jsx +++ b/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentModelSelection/index.jsx @@ -5,21 +5,30 @@ import paths from "@/utils/paths"; import { useTranslation } from "react-i18next"; import { Link, useParams } from "react-router-dom"; -// These models do NOT support function calling -// and therefore are not supported for agents. +/** + * These models do NOT support function calling + * or do not support system prompts + * and therefore are not supported for agents. + * @param {string} provider - The AI provider. + * @param {string} model - The model name. + * @returns {boolean} Whether the model is supported for agents. + */ function supportedModel(provider, model = "") { - if (provider !== "openai") return true; - return ( - [ - "gpt-3.5-turbo-0301", - "gpt-4-turbo-2024-04-09", - "gpt-4-turbo", - "o1-preview", - "o1-preview-2024-09-12", - "o1-mini", - "o1-mini-2024-09-12", - ].includes(model) === false - ); + if (provider === "openai") { + return ( + [ + "gpt-3.5-turbo-0301", + "gpt-4-turbo-2024-04-09", + "gpt-4-turbo", + "o1-preview", + "o1-preview-2024-09-12", + "o1-mini", + "o1-mini-2024-09-12", + ].includes(model) === false + ); + } + + return true; } export default function AgentModelSelection({ diff --git a/server/utils/AiProviders/bedrock/index.js b/server/utils/AiProviders/bedrock/index.js index 28d0c2ce3..c271f7297 100644 --- a/server/utils/AiProviders/bedrock/index.js +++ b/server/utils/AiProviders/bedrock/index.js @@ -7,6 +7,20 @@ const { NativeEmbedder } = require("../../EmbeddingEngines/native"); // Docs: https://js.langchain.com/v0.2/docs/integrations/chat/bedrock_converse class AWSBedrockLLM { + /** + * These models do not support system prompts + * It is not explicitly stated but it is observed that they do not use the system prompt + * in their responses and will crash when a system prompt is provided. + * We can add more models to this list as we discover them or new models are added. + * We may want to extend this list or make a user-config if using custom bedrock models. + */ + noSystemPromptModels = [ + "amazon.titan-text-express-v1", + "amazon.titan-text-lite-v1", + "cohere.command-text-v14", + "cohere.command-light-text-v14", + ]; + constructor(embedder = null, modelPreference = null) { if (!process.env.AWS_BEDROCK_LLM_ACCESS_KEY_ID) throw new Error("No AWS Bedrock LLM profile id was set."); @@ -59,6 +73,22 @@ class AWSBedrockLLM { for (const chat of chats) { if (!roleToMessageMap.hasOwnProperty(chat.role)) continue; + + // When a model does not support system prompts, we need to handle it. + // We will add a new message that simulates the system prompt via a user message and AI response. + // This will allow the model to respond without crashing but we can still inject context. + if ( + this.noSystemPromptModels.includes(this.model) && + chat.role === "system" + ) { + this.#log( + `Model does not support system prompts! Simulating system prompt via Human/AI message pairs.` + ); + langchainChats.push(new HumanMessage({ content: chat.content })); + langchainChats.push(new AIMessage({ content: "Okay." })); + continue; + } + const MessageClass = roleToMessageMap[chat.role]; langchainChats.push(new MessageClass({ content: chat.content })); } @@ -78,6 +108,10 @@ class AWSBedrockLLM { ); } + #log(text, ...args) { + console.log(`\x1b[32m[AWSBedrock]\x1b[0m ${text}`, ...args); + } + streamingEnabled() { return "streamGetChatCompletion" in this; }