From df17fbda36ed020f3b23f0b569a654a0b224e7de Mon Sep 17 00:00:00 2001 From: Timothy Carambat <rambat1010@gmail.com> Date: Tue, 23 Apr 2024 13:06:07 -0700 Subject: [PATCH] Add generic OpenAI endpoint support (#1178) * Add generic OpenAI endpoint support * allow any input for model in case provider does not support models endpoint --- docker/.env.example | 6 + .../GenericOpenAiOptions/index.jsx | 70 ++++++ .../src/media/llmprovider/generic-openai.png | Bin 0 -> 29556 bytes .../GeneralSettings/LLMPreference/index.jsx | 10 + .../Steps/DataHandling/index.jsx | 8 + .../Steps/LLMPreference/index.jsx | 224 +++++++++--------- server/.env.example | 6 + server/models/systemSettings.js | 6 + .../utils/AiProviders/genericOpenAi/index.js | 193 +++++++++++++++ server/utils/helpers/index.js | 7 +- server/utils/helpers/updateENV.js | 19 ++ 11 files changed, 441 insertions(+), 108 deletions(-) create mode 100644 frontend/src/components/LLMSelection/GenericOpenAiOptions/index.jsx create mode 100644 frontend/src/media/llmprovider/generic-openai.png create mode 100644 server/utils/AiProviders/genericOpenAi/index.js diff --git a/docker/.env.example b/docker/.env.example index aabc139f8..5130ddb74 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -66,6 +66,12 @@ GID='1000' # GROQ_API_KEY=gsk_abcxyz # GROQ_MODEL_PREF=llama2-70b-4096 +# LLM_PROVIDER='generic-openai' +# GENERIC_OPEN_AI_BASE_PATH='http://proxy.url.openai.com/v1' +# GENERIC_OPEN_AI_MODEL_PREF='gpt-3.5-turbo' +# GENERIC_OPEN_AI_MODEL_TOKEN_LIMIT=4096 +# GENERIC_OPEN_AI_API_KEY=sk-123abc + ########################################### ######## Embedding API SElECTION ########## ########################################### diff --git a/frontend/src/components/LLMSelection/GenericOpenAiOptions/index.jsx b/frontend/src/components/LLMSelection/GenericOpenAiOptions/index.jsx new file mode 100644 index 000000000..456b50427 --- /dev/null +++ b/frontend/src/components/LLMSelection/GenericOpenAiOptions/index.jsx @@ -0,0 +1,70 @@ +export default function GenericOpenAiOptions({ settings }) { + return ( + <div className="flex gap-4 flex-wrap"> + <div className="flex flex-col w-60"> + <label className="text-white text-sm font-semibold block mb-4"> + Base URL + </label> + <input + type="url" + name="GenericOpenAiBasePath" + className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5" + placeholder="eg: https://proxy.openai.com" + defaultValue={settings?.GenericOpenAiBasePath} + required={true} + autoComplete="off" + spellCheck={false} + /> + </div> + <div className="flex flex-col w-60"> + <label className="text-white text-sm font-semibold block mb-4"> + API Key + </label> + <input + type="password" + name="GenericOpenAiKey" + className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5" + placeholder="Generic service API Key" + defaultValue={settings?.GenericOpenAiKey ? "*".repeat(20) : ""} + required={false} + autoComplete="off" + spellCheck={false} + /> + </div> + {!settings?.credentialsOnly && ( + <> + <div className="flex flex-col w-60"> + <label className="text-white text-sm font-semibold block mb-4"> + Chat Model Name + </label> + <input + type="text" + name="GenericOpenAiModelPref" + className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5" + placeholder="Model id used for chat requests" + defaultValue={settings?.GenericOpenAiModelPref} + required={true} + autoComplete="off" + /> + </div> + <div className="flex flex-col w-60"> + <label className="text-white text-sm font-semibold block mb-4"> + Token context window + </label> + <input + type="number" + name="GenericOpenAiTokenLimit" + className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5" + placeholder="Content window limit (eg: 4096)" + min={1} + onScroll={(e) => e.target.blur()} + defaultValue={settings?.GenericOpenAiTokenLimit} + required={true} + autoComplete="off" + /> + </div> + </> + )} + </div> + ); +} diff --git a/frontend/src/media/llmprovider/generic-openai.png b/frontend/src/media/llmprovider/generic-openai.png new file mode 100644 index 0000000000000000000000000000000000000000..302f5dbee0aebe15a3fc4cd6073d58314c418487 GIT binary patch literal 29556 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 Nvd<MIt0atr{sXCDy=MRb literal 0 HcmV?d00001 diff --git a/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx b/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx index 5fbc826ca..0575f34c0 100644 --- a/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx +++ b/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx @@ -5,6 +5,7 @@ import System from "@/models/system"; import showToast from "@/utils/toast"; import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png"; import OpenAiLogo from "@/media/llmprovider/openai.png"; +import GenericOpenAiLogo from "@/media/llmprovider/generic-openai.png"; import AzureOpenAiLogo from "@/media/llmprovider/azure.png"; import AnthropicLogo from "@/media/llmprovider/anthropic.png"; import GeminiLogo from "@/media/llmprovider/gemini.png"; @@ -19,6 +20,7 @@ import OpenRouterLogo from "@/media/llmprovider/openrouter.jpeg"; import GroqLogo from "@/media/llmprovider/groq.png"; import PreLoader from "@/components/Preloader"; import OpenAiOptions from "@/components/LLMSelection/OpenAiOptions"; +import GenericOpenAiOptions from "@/components/LLMSelection/GenericOpenAiOptions"; import AzureAiOptions from "@/components/LLMSelection/AzureAiOptions"; import AnthropicAiOptions from "@/components/LLMSelection/AnthropicAiOptions"; import LMStudioOptions from "@/components/LLMSelection/LMStudioOptions"; @@ -150,6 +152,14 @@ export const AVAILABLE_LLM_PROVIDERS = [ "The fastest LLM inferencing available for real-time AI applications.", requiredConfig: ["GroqApiKey"], }, + { + name: "Generic OpenAI", + value: "generic-openai", + logo: GenericOpenAiLogo, + options: (settings) => <GenericOpenAiOptions settings={settings} />, + description: + "Connect to any OpenAi-compatible service via a custom configuration", + }, { name: "Native", value: "native", diff --git a/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx index b30dd45a9..548272fe0 100644 --- a/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx +++ b/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx @@ -2,6 +2,7 @@ import PreLoader from "@/components/Preloader"; import System from "@/models/system"; import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png"; import OpenAiLogo from "@/media/llmprovider/openai.png"; +import GenericOpenAiLogo from "@/media/llmprovider/generic-openai.png"; import AzureOpenAiLogo from "@/media/llmprovider/azure.png"; import AnthropicLogo from "@/media/llmprovider/anthropic.png"; import GeminiLogo from "@/media/llmprovider/gemini.png"; @@ -136,6 +137,13 @@ export const LLM_SELECTION_PRIVACY = { ], logo: GroqLogo, }, + "generic-openai": { + name: "Generic OpenAI compatible service", + description: [ + "Data is shared according to the terms of service applicable with your generic endpoint provider.", + ], + logo: GenericOpenAiLogo, + }, }; export const VECTOR_DB_PRIVACY = { diff --git a/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx index 29b4e8456..b9e0f5bb1 100644 --- a/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx +++ b/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx @@ -1,6 +1,7 @@ import { MagnifyingGlass } from "@phosphor-icons/react"; import { useEffect, useState, useRef } from "react"; import OpenAiLogo from "@/media/llmprovider/openai.png"; +import GenericOpenAiLogo from "@/media/llmprovider/generic-openai.png"; import AzureOpenAiLogo from "@/media/llmprovider/azure.png"; import AnthropicLogo from "@/media/llmprovider/anthropic.png"; import GeminiLogo from "@/media/llmprovider/gemini.png"; @@ -15,6 +16,7 @@ import PerplexityLogo from "@/media/llmprovider/perplexity.png"; import OpenRouterLogo from "@/media/llmprovider/openrouter.jpeg"; import GroqLogo from "@/media/llmprovider/groq.png"; import OpenAiOptions from "@/components/LLMSelection/OpenAiOptions"; +import GenericOpenAiOptions from "@/components/LLMSelection/GenericOpenAiOptions"; import AzureAiOptions from "@/components/LLMSelection/AzureAiOptions"; import AnthropicAiOptions from "@/components/LLMSelection/AnthropicAiOptions"; import LMStudioOptions from "@/components/LLMSelection/LMStudioOptions"; @@ -38,6 +40,120 @@ const TITLE = "LLM Preference"; const DESCRIPTION = "AnythingLLM can work with many LLM providers. This will be the service which handles chatting."; +const LLMS = [ + { + name: "OpenAI", + value: "openai", + logo: OpenAiLogo, + options: (settings) => <OpenAiOptions settings={settings} />, + description: "The standard option for most non-commercial use.", + }, + { + name: "Azure OpenAI", + value: "azure", + logo: AzureOpenAiLogo, + options: (settings) => <AzureAiOptions settings={settings} />, + description: "The enterprise option of OpenAI hosted on Azure services.", + }, + { + name: "Anthropic", + value: "anthropic", + logo: AnthropicLogo, + options: (settings) => <AnthropicAiOptions settings={settings} />, + description: "A friendly AI Assistant hosted by Anthropic.", + }, + { + name: "Gemini", + value: "gemini", + logo: GeminiLogo, + options: (settings) => <GeminiLLMOptions settings={settings} />, + description: "Google's largest and most capable AI model", + }, + { + name: "HuggingFace", + value: "huggingface", + logo: HuggingFaceLogo, + options: (settings) => <HuggingFaceOptions settings={settings} />, + description: + "Access 150,000+ open-source LLMs and the world's AI community", + }, + { + name: "Ollama", + value: "ollama", + logo: OllamaLogo, + options: (settings) => <OllamaLLMOptions settings={settings} />, + description: "Run LLMs locally on your own machine.", + }, + { + name: "LM Studio", + value: "lmstudio", + logo: LMStudioLogo, + options: (settings) => <LMStudioOptions settings={settings} />, + description: + "Discover, download, and run thousands of cutting edge LLMs in a few clicks.", + }, + { + name: "Local AI", + value: "localai", + logo: LocalAiLogo, + options: (settings) => <LocalAiOptions settings={settings} />, + description: "Run LLMs locally on your own machine.", + }, + { + name: "Together AI", + value: "togetherai", + logo: TogetherAILogo, + options: (settings) => <TogetherAiOptions settings={settings} />, + description: "Run open source models from Together AI.", + }, + { + name: "Mistral", + value: "mistral", + logo: MistralLogo, + options: (settings) => <MistralOptions settings={settings} />, + description: "Run open source models from Mistral AI.", + }, + { + name: "Perplexity AI", + value: "perplexity", + logo: PerplexityLogo, + options: (settings) => <PerplexityOptions settings={settings} />, + description: + "Run powerful and internet-connected models hosted by Perplexity AI.", + }, + { + name: "OpenRouter", + value: "openrouter", + logo: OpenRouterLogo, + options: (settings) => <OpenRouterOptions settings={settings} />, + description: "A unified interface for LLMs.", + }, + { + name: "Groq", + value: "groq", + logo: GroqLogo, + options: (settings) => <GroqAiOptions settings={settings} />, + description: + "The fastest LLM inferencing available for real-time AI applications.", + }, + { + name: "Generic OpenAI", + value: "generic-openai", + logo: GenericOpenAiLogo, + options: (settings) => <GenericOpenAiOptions settings={settings} />, + description: + "Connect to any OpenAi-compatible service via a custom configuration", + }, + { + name: "Native", + value: "native", + logo: AnythingLLMIcon, + options: (settings) => <NativeLLMOptions settings={settings} />, + description: + "Use a downloaded custom Llama model for chatting on this AnythingLLM instance.", + }, +]; + export default function LLMPreference({ setHeader, setForwardBtn, @@ -61,112 +177,6 @@ export default function LLMPreference({ fetchKeys(); }, []); - const LLMS = [ - { - name: "OpenAI", - value: "openai", - logo: OpenAiLogo, - options: <OpenAiOptions settings={settings} />, - description: "The standard option for most non-commercial use.", - }, - { - name: "Azure OpenAI", - value: "azure", - logo: AzureOpenAiLogo, - options: <AzureAiOptions settings={settings} />, - description: "The enterprise option of OpenAI hosted on Azure services.", - }, - { - name: "Anthropic", - value: "anthropic", - logo: AnthropicLogo, - options: <AnthropicAiOptions settings={settings} />, - description: "A friendly AI Assistant hosted by Anthropic.", - }, - { - name: "Gemini", - value: "gemini", - logo: GeminiLogo, - options: <GeminiLLMOptions settings={settings} />, - description: "Google's largest and most capable AI model", - }, - { - name: "HuggingFace", - value: "huggingface", - logo: HuggingFaceLogo, - options: <HuggingFaceOptions settings={settings} />, - description: - "Access 150,000+ open-source LLMs and the world's AI community", - }, - { - name: "Ollama", - value: "ollama", - logo: OllamaLogo, - options: <OllamaLLMOptions settings={settings} />, - description: "Run LLMs locally on your own machine.", - }, - { - name: "LM Studio", - value: "lmstudio", - logo: LMStudioLogo, - options: <LMStudioOptions settings={settings} />, - description: - "Discover, download, and run thousands of cutting edge LLMs in a few clicks.", - }, - { - name: "Local AI", - value: "localai", - logo: LocalAiLogo, - options: <LocalAiOptions settings={settings} />, - description: "Run LLMs locally on your own machine.", - }, - { - name: "Together AI", - value: "togetherai", - logo: TogetherAILogo, - options: <TogetherAiOptions settings={settings} />, - description: "Run open source models from Together AI.", - }, - { - name: "Mistral", - value: "mistral", - logo: MistralLogo, - options: <MistralOptions settings={settings} />, - description: "Run open source models from Mistral AI.", - }, - { - name: "Perplexity AI", - value: "perplexity", - logo: PerplexityLogo, - options: <PerplexityOptions settings={settings} />, - description: - "Run powerful and internet-connected models hosted by Perplexity AI.", - }, - { - name: "OpenRouter", - value: "openrouter", - logo: OpenRouterLogo, - options: <OpenRouterOptions settings={settings} />, - description: "A unified interface for LLMs.", - }, - { - name: "Groq", - value: "groq", - logo: GroqLogo, - options: <GroqAiOptions settings={settings} />, - description: - "The fastest LLM inferencing available for real-time AI applications.", - }, - { - name: "Native", - value: "native", - logo: AnythingLLMIcon, - options: <NativeLLMOptions settings={settings} />, - description: - "Use a downloaded custom Llama model for chatting on this AnythingLLM instance.", - }, - ]; - function handleForward() { if (hiddenSubmitButtonRef.current) { hiddenSubmitButtonRef.current.click(); @@ -251,7 +261,7 @@ export default function LLMPreference({ </div> <div className="mt-4 flex flex-col gap-y-1"> {selectedLLM && - LLMS.find((llm) => llm.value === selectedLLM)?.options} + LLMS.find((llm) => llm.value === selectedLLM)?.options(settings)} </div> <button type="submit" diff --git a/server/.env.example b/server/.env.example index 131dcf895..c333e4ec0 100644 --- a/server/.env.example +++ b/server/.env.example @@ -63,6 +63,12 @@ JWT_SECRET="my-random-string-for-seeding" # Please generate random string at lea # GROQ_API_KEY=gsk_abcxyz # GROQ_MODEL_PREF=llama2-70b-4096 +# LLM_PROVIDER='generic-openai' +# GENERIC_OPEN_AI_BASE_PATH='http://proxy.url.openai.com/v1' +# GENERIC_OPEN_AI_MODEL_PREF='gpt-3.5-turbo' +# GENERIC_OPEN_AI_MODEL_TOKEN_LIMIT=4096 +# GENERIC_OPEN_AI_API_KEY=sk-123abc + ########################################### ######## Embedding API SElECTION ########## ########################################### diff --git a/server/models/systemSettings.js b/server/models/systemSettings.js index 20c161cd5..bdec2af3d 100644 --- a/server/models/systemSettings.js +++ b/server/models/systemSettings.js @@ -358,6 +358,12 @@ const SystemSettings = { HuggingFaceLLMEndpoint: process.env.HUGGING_FACE_LLM_ENDPOINT, HuggingFaceLLMAccessToken: !!process.env.HUGGING_FACE_LLM_API_KEY, HuggingFaceLLMTokenLimit: process.env.HUGGING_FACE_LLM_TOKEN_LIMIT, + + // Generic OpenAI Keys + GenericOpenAiBasePath: process.env.GENERIC_OPEN_AI_BASE_PATH, + GenericOpenAiModelPref: process.env.GENERIC_OPEN_AI_MODEL_PREF, + GenericOpenAiTokenLimit: process.env.GENERIC_OPEN_AI_MODEL_TOKEN_LIMIT, + GenericOpenAiKey: !!process.env.GENERIC_OPEN_AI_API_KEY, }; }, }; diff --git a/server/utils/AiProviders/genericOpenAi/index.js b/server/utils/AiProviders/genericOpenAi/index.js new file mode 100644 index 000000000..61e7bccf0 --- /dev/null +++ b/server/utils/AiProviders/genericOpenAi/index.js @@ -0,0 +1,193 @@ +const { NativeEmbedder } = require("../../EmbeddingEngines/native"); +const { chatPrompt } = require("../../chats"); +const { handleDefaultStreamResponse } = require("../../helpers/chat/responses"); + +class GenericOpenAiLLM { + constructor(embedder = null, modelPreference = null) { + const { Configuration, OpenAIApi } = require("openai"); + if (!process.env.GENERIC_OPEN_AI_BASE_PATH) + throw new Error( + "GenericOpenAI must have a valid base path to use for the api." + ); + + this.basePath = process.env.GENERIC_OPEN_AI_BASE_PATH; + const config = new Configuration({ + basePath: this.basePath, + apiKey: process.env.GENERIC_OPEN_AI_API_KEY ?? null, + }); + this.openai = new OpenAIApi(config); + this.model = + modelPreference ?? process.env.GENERIC_OPEN_AI_MODEL_PREF ?? null; + if (!this.model) + throw new Error("GenericOpenAI must have a valid model set."); + this.limits = { + history: this.promptWindowLimit() * 0.15, + system: this.promptWindowLimit() * 0.15, + user: this.promptWindowLimit() * 0.7, + }; + + if (!embedder) + console.warn( + "No embedding provider defined for GenericOpenAiLLM - falling back to NativeEmbedder for embedding!" + ); + this.embedder = !embedder ? new NativeEmbedder() : embedder; + this.defaultTemp = 0.7; + this.log(`Inference API: ${this.basePath} Model: ${this.model}`); + } + + log(text, ...args) { + console.log(`\x1b[36m[${this.constructor.name}]\x1b[0m ${text}`, ...args); + } + + #appendContext(contextTexts = []) { + if (!contextTexts || !contextTexts.length) return ""; + return ( + "\nContext:\n" + + contextTexts + .map((text, i) => { + return `[CONTEXT ${i}]:\n${text}\n[END CONTEXT ${i}]\n\n`; + }) + .join("") + ); + } + + streamingEnabled() { + return "streamChat" in this && "streamGetChatCompletion" in this; + } + + // Ensure the user set a value for the token limit + // and if undefined - assume 4096 window. + promptWindowLimit() { + const limit = process.env.GENERIC_OPEN_AI_MODEL_TOKEN_LIMIT || 4096; + if (!limit || isNaN(Number(limit))) + throw new Error("No token context limit was set."); + return Number(limit); + } + + // Short circuit since we have no idea if the model is valid or not + // in pre-flight for generic endpoints + isValidChatCompletionModel(_modelName = "") { + return true; + } + + constructPrompt({ + systemPrompt = "", + contextTexts = [], + chatHistory = [], + userPrompt = "", + }) { + const prompt = { + role: "system", + content: `${systemPrompt}${this.#appendContext(contextTexts)}`, + }; + return [prompt, ...chatHistory, { role: "user", content: userPrompt }]; + } + + async isSafe(_input = "") { + // Not implemented so must be stubbed + return { safe: true, reasons: [] }; + } + + async sendChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { + const textResponse = await this.openai + .createChatCompletion({ + model: this.model, + temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), + n: 1, + messages: await this.compressMessages( + { + systemPrompt: chatPrompt(workspace), + userPrompt: prompt, + chatHistory, + }, + rawHistory + ), + }) + .then((json) => { + const res = json.data; + if (!res.hasOwnProperty("choices")) + throw new Error("GenericOpenAI chat: No results!"); + if (res.choices.length === 0) + throw new Error("GenericOpenAI chat: No results length!"); + return res.choices[0].message.content; + }) + .catch((error) => { + throw new Error( + `GenericOpenAI::createChatCompletion failed with: ${error.message}` + ); + }); + + return textResponse; + } + + async streamChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) { + const streamRequest = await this.openai.createChatCompletion( + { + model: this.model, + stream: true, + temperature: Number(workspace?.openAiTemp ?? this.defaultTemp), + n: 1, + messages: await this.compressMessages( + { + systemPrompt: chatPrompt(workspace), + userPrompt: prompt, + chatHistory, + }, + rawHistory + ), + }, + { responseType: "stream" } + ); + return streamRequest; + } + + async getChatCompletion(messages = null, { temperature = 0.7 }) { + const { data } = await this.openai + .createChatCompletion({ + model: this.model, + messages, + temperature, + }) + .catch((e) => { + throw new Error(e.response.data.error.message); + }); + + if (!data.hasOwnProperty("choices")) return null; + return data.choices[0].message.content; + } + + async streamGetChatCompletion(messages = null, { temperature = 0.7 }) { + const streamRequest = await this.openai.createChatCompletion( + { + model: this.model, + stream: true, + messages, + temperature, + }, + { responseType: "stream" } + ); + return streamRequest; + } + + handleStream(response, stream, responseProps) { + return handleDefaultStreamResponse(response, stream, responseProps); + } + + // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations + async embedTextInput(textInput) { + return await this.embedder.embedTextInput(textInput); + } + async embedChunks(textChunks = []) { + return await this.embedder.embedChunks(textChunks); + } + + async compressMessages(promptArgs = {}, rawHistory = []) { + const { messageArrayCompressor } = require("../../helpers/chat"); + const messageArray = this.constructPrompt(promptArgs); + return await messageArrayCompressor(this, messageArray, rawHistory); + } +} + +module.exports = { + GenericOpenAiLLM, +}; diff --git a/server/utils/helpers/index.js b/server/utils/helpers/index.js index 837a28f60..c8cdd870f 100644 --- a/server/utils/helpers/index.js +++ b/server/utils/helpers/index.js @@ -77,8 +77,13 @@ function getLLMProvider({ provider = null, model = null } = {}) { case "groq": const { GroqLLM } = require("../AiProviders/groq"); return new GroqLLM(embedder, model); + case "generic-openai": + const { GenericOpenAiLLM } = require("../AiProviders/genericOpenAi"); + return new GenericOpenAiLLM(embedder, model); default: - throw new Error("ENV: No LLM_PROVIDER value found in environment!"); + throw new Error( + `ENV: No valid LLM_PROVIDER value found in environment! Using ${process.env.LLM_PROVIDER}` + ); } } diff --git a/server/utils/helpers/updateENV.js b/server/utils/helpers/updateENV.js index ee9d4effa..5e629baf8 100644 --- a/server/utils/helpers/updateENV.js +++ b/server/utils/helpers/updateENV.js @@ -132,6 +132,24 @@ const KEY_MAPPING = { checks: [nonZero], }, + // Generic OpenAI InferenceSettings + GenericOpenAiBasePath: { + envKey: "GENERIC_OPEN_AI_BASE_PATH", + checks: [isValidURL], + }, + GenericOpenAiModelPref: { + envKey: "GENERIC_OPEN_AI_MODEL_PREF", + checks: [isNotEmpty], + }, + GenericOpenAiTokenLimit: { + envKey: "GENERIC_OPEN_AI_MODEL_TOKEN_LIMIT", + checks: [nonZero], + }, + GenericOpenAiKey: { + envKey: "GENERIC_OPEN_AI_API_KEY", + checks: [], + }, + EmbeddingEngine: { envKey: "EMBEDDING_ENGINE", checks: [supportedEmbeddingModel], @@ -375,6 +393,7 @@ function supportedLLM(input = "") { "perplexity", "openrouter", "groq", + "generic-openai", ].includes(input); return validSelection ? null : `${input} is not a valid LLM provider.`; } -- GitLab