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