From 24227e48a73a63deadef4214e7a1ef85d3c3f40e Mon Sep 17 00:00:00 2001
From: Timothy Carambat <rambat1010@gmail.com>
Date: Wed, 27 Dec 2023 17:08:03 -0800
Subject: [PATCH] Add LLM support for Google Gemini-Pro (#492)

resolves #489
---
 README.md                                     |   1 +
 docker/.env.example                           |   4 +
 .../LLMSelection/GeminiLLMOptions/index.jsx   |  43 ++++
 frontend/src/media/llmprovider/gemini.png     | Bin 0 -> 26348 bytes
 .../EmbeddingPreference/index.jsx             |   8 +-
 .../GeneralSettings/LLMPreference/index.jsx   |  22 +-
 .../GeneralSettings/VectorDatabase/index.jsx  |   4 +-
 .../Steps/DataHandling/index.jsx              |   9 +
 .../Steps/EmbeddingSelection/index.jsx        |   4 +-
 .../Steps/LLMSelection/index.jsx              |  16 +-
 server/.env.example                           |   4 +
 server/models/systemSettings.js               |  14 ++
 server/package.json                           |   3 +-
 server/utils/AiProviders/gemini/index.js      | 200 ++++++++++++++++++
 server/utils/chats/stream.js                  |  29 +++
 server/utils/helpers/index.js                 |   3 +
 server/utils/helpers/updateENV.js             |  17 ++
 server/yarn.lock                              |   5 +
 18 files changed, 371 insertions(+), 15 deletions(-)
 create mode 100644 frontend/src/components/LLMSelection/GeminiLLMOptions/index.jsx
 create mode 100644 frontend/src/media/llmprovider/gemini.png
 create mode 100644 server/utils/AiProviders/gemini/index.js

diff --git a/README.md b/README.md
index 9ed7cc609..44e0557fa 100644
--- a/README.md
+++ b/README.md
@@ -58,6 +58,7 @@ Some cool features of AnythingLLM
 - [OpenAI](https://openai.com)
 - [Azure OpenAI](https://azure.microsoft.com/en-us/products/ai-services/openai-service)
 - [Anthropic ClaudeV2](https://www.anthropic.com/)
+- [Google Gemini Pro](https://ai.google.dev/)
 - [LM Studio (all models)](https://lmstudio.ai)
 - [LocalAi (all models)](https://localai.io/)
 
diff --git a/docker/.env.example b/docker/.env.example
index 8bbdd1dd6..cc9fa06fc 100644
--- a/docker/.env.example
+++ b/docker/.env.example
@@ -11,6 +11,10 @@ GID='1000'
 # OPEN_AI_KEY=
 # OPEN_MODEL_PREF='gpt-3.5-turbo'
 
+# LLM_PROVIDER='gemini'
+# GEMINI_API_KEY=
+# GEMINI_LLM_MODEL_PREF='gemini-pro'
+
 # LLM_PROVIDER='azure'
 # AZURE_OPENAI_ENDPOINT=
 # AZURE_OPENAI_KEY=
diff --git a/frontend/src/components/LLMSelection/GeminiLLMOptions/index.jsx b/frontend/src/components/LLMSelection/GeminiLLMOptions/index.jsx
new file mode 100644
index 000000000..4d09e0432
--- /dev/null
+++ b/frontend/src/components/LLMSelection/GeminiLLMOptions/index.jsx
@@ -0,0 +1,43 @@
+export default function GeminiLLMOptions({ settings }) {
+  return (
+    <div className="w-full flex flex-col">
+      <div className="w-full flex items-center gap-4">
+        <div className="flex flex-col w-60">
+          <label className="text-white text-sm font-semibold block mb-4">
+            Google AI API Key
+          </label>
+          <input
+            type="password"
+            name="GeminiLLMApiKey"
+            className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
+            placeholder="Google Gemini API Key"
+            defaultValue={settings?.GeminiLLMApiKey ? "*".repeat(20) : ""}
+            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">
+            Chat Model Selection
+          </label>
+          <select
+            name="GeminiLLMModelPref"
+            defaultValue={settings?.GeminiLLMModelPref || "gemini-pro"}
+            required={true}
+            className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
+          >
+            {["gemini-pro"].map((model) => {
+              return (
+                <option key={model} value={model}>
+                  {model}
+                </option>
+              );
+            })}
+          </select>
+        </div>
+      </div>
+    </div>
+  );
+}
diff --git a/frontend/src/media/llmprovider/gemini.png b/frontend/src/media/llmprovider/gemini.png
new file mode 100644
index 0000000000000000000000000000000000000000..aa81cfd86f9766dee0ba7530deecd74fcf740f0d
GIT binary patch
literal 26348
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
Z@F;oLc21yG$^U*NBQNtsx&~kz^gnxmi_HK4

literal 0
HcmV?d00001

diff --git a/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx b/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx
index 1abf3a4b4..d6224906c 100644
--- a/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx
+++ b/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx
@@ -46,10 +46,10 @@ export default function GeneralEmbeddingPreference() {
 
     const { error } = await System.updateSystem(settingsData);
     if (error) {
-      showToast(`Failed to save LLM settings: ${error}`, "error");
+      showToast(`Failed to save embedding settings: ${error}`, "error");
       setHasChanges(true);
     } else {
-      showToast("LLM preferences saved successfully.", "success");
+      showToast("Embedding preferences saved successfully.", "success");
       setHasChanges(false);
     }
     setSaving(false);
@@ -132,7 +132,7 @@ export default function GeneralEmbeddingPreference() {
                 <div className="text-white text-sm font-medium py-4">
                   Embedding Providers
                 </div>
-                <div className="w-full flex md:flex-wrap overflow-x-scroll gap-4 max-w-[900px]">
+                <div className="w-full flex md:flex-wrap overflow-x-scroll gap-4">
                   <input
                     hidden={true}
                     name="EmbeddingEngine"
@@ -174,7 +174,7 @@ export default function GeneralEmbeddingPreference() {
                     onClick={updateChoice}
                   />
                 </div>
-                <div className="mt-10 flex flex-wrap gap-4 max-w-[800px]">
+                <div className="mt-10 flex flex-wrap gap-4">
                   {embeddingChoice === "native" && <NativeEmbeddingOptions />}
                   {embeddingChoice === "openai" && (
                     <OpenAiOptions settings={settings} />
diff --git a/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx b/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx
index 1c18d1ff1..a0169fe15 100644
--- a/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx
+++ b/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx
@@ -7,6 +7,7 @@ import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png";
 import OpenAiLogo from "@/media/llmprovider/openai.png";
 import AzureOpenAiLogo from "@/media/llmprovider/azure.png";
 import AnthropicLogo from "@/media/llmprovider/anthropic.png";
+import GeminiLogo from "@/media/llmprovider/gemini.png";
 import LMStudioLogo from "@/media/llmprovider/lmstudio.png";
 import LocalAiLogo from "@/media/llmprovider/localai.png";
 import PreLoader from "@/components/Preloader";
@@ -17,6 +18,7 @@ import AnthropicAiOptions from "@/components/LLMSelection/AnthropicAiOptions";
 import LMStudioOptions from "@/components/LLMSelection/LMStudioOptions";
 import LocalAiOptions from "@/components/LLMSelection/LocalAiOptions";
 import NativeLLMOptions from "@/components/LLMSelection/NativeLLMOptions";
+import GeminiLLMOptions from "@/components/LLMSelection/GeminiLLMOptions";
 
 export default function GeneralLLMPreference() {
   const [saving, setSaving] = useState(false);
@@ -105,13 +107,13 @@ export default function GeneralLLMPreference() {
               <div className="text-white text-sm font-medium py-4">
                 LLM Providers
               </div>
-              <div className="w-full flex md:flex-wrap overflow-x-scroll gap-4 max-w-[900px]">
+              <div className="w-full flex md:flex-wrap overflow-x-scroll gap-4">
                 <input hidden={true} name="LLMProvider" value={llmChoice} />
                 <LLMProviderOption
                   name="OpenAI"
                   value="openai"
                   link="openai.com"
-                  description="The standard option for most non-commercial use. Provides both chat and embedding."
+                  description="The standard option for most non-commercial use."
                   checked={llmChoice === "openai"}
                   image={OpenAiLogo}
                   onClick={updateLLMChoice}
@@ -120,7 +122,7 @@ export default function GeneralLLMPreference() {
                   name="Azure OpenAI"
                   value="azure"
                   link="azure.microsoft.com"
-                  description="The enterprise option of OpenAI hosted on Azure services. Provides both chat and embedding."
+                  description="The enterprise option of OpenAI hosted on Azure services."
                   checked={llmChoice === "azure"}
                   image={AzureOpenAiLogo}
                   onClick={updateLLMChoice}
@@ -129,11 +131,20 @@ export default function GeneralLLMPreference() {
                   name="Anthropic Claude 2"
                   value="anthropic"
                   link="anthropic.com"
-                  description="A friendly AI Assistant hosted by Anthropic. Provides chat services only!"
+                  description="A friendly AI Assistant hosted by Anthropic."
                   checked={llmChoice === "anthropic"}
                   image={AnthropicLogo}
                   onClick={updateLLMChoice}
                 />
+                <LLMProviderOption
+                  name="Google Gemini"
+                  value="gemini"
+                  link="ai.google.dev"
+                  description="Google's largest and most capable AI model"
+                  checked={llmChoice === "gemini"}
+                  image={GeminiLogo}
+                  onClick={updateLLMChoice}
+                />
                 <LLMProviderOption
                   name="LM Studio"
                   value="lmstudio"
@@ -173,6 +184,9 @@ export default function GeneralLLMPreference() {
                 {llmChoice === "anthropic" && (
                   <AnthropicAiOptions settings={settings} showAlert={true} />
                 )}
+                {llmChoice === "gemini" && (
+                  <GeminiLLMOptions settings={settings} />
+                )}
                 {llmChoice === "lmstudio" && (
                   <LMStudioOptions settings={settings} showAlert={true} />
                 )}
diff --git a/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx b/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx
index 1635fef8b..2ddf1d5a7 100644
--- a/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx
+++ b/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx
@@ -55,10 +55,10 @@ export default function GeneralVectorDatabase() {
 
     const { error } = await System.updateSystem(settingsData);
     if (error) {
-      showToast(`Failed to save LLM settings: ${error}`, "error");
+      showToast(`Failed to save vector database settings: ${error}`, "error");
       setHasChanges(true);
     } else {
-      showToast("LLM preferences saved successfully.", "success");
+      showToast("Vector database preferences saved successfully.", "success");
       setHasChanges(false);
     }
     setSaving(false);
diff --git a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/DataHandling/index.jsx b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/DataHandling/index.jsx
index 98a1671cb..cd63d74d8 100644
--- a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/DataHandling/index.jsx
+++ b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/DataHandling/index.jsx
@@ -4,6 +4,7 @@ import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png";
 import OpenAiLogo from "@/media/llmprovider/openai.png";
 import AzureOpenAiLogo from "@/media/llmprovider/azure.png";
 import AnthropicLogo from "@/media/llmprovider/anthropic.png";
+import GeminiLogo from "@/media/llmprovider/gemini.png";
 import LMStudioLogo from "@/media/llmprovider/lmstudio.png";
 import LocalAiLogo from "@/media/llmprovider/localai.png";
 import ChromaLogo from "@/media/vectordbs/chroma.png";
@@ -38,6 +39,14 @@ const LLM_SELECTION_PRIVACY = {
     ],
     logo: AnthropicLogo,
   },
+  gemini: {
+    name: "Google Gemini",
+    description: [
+      "Your chats are de-identified and used in training",
+      "Your prompts and document text are visible in responses to Google",
+    ],
+    logo: GeminiLogo,
+  },
   lmstudio: {
     name: "LMStudio",
     description: [
diff --git a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/EmbeddingSelection/index.jsx b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/EmbeddingSelection/index.jsx
index 1f44c463b..98e1262a0 100644
--- a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/EmbeddingSelection/index.jsx
+++ b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/EmbeddingSelection/index.jsx
@@ -76,7 +76,7 @@ function EmbeddingSelection({ nextStep, prevStep, currentStep }) {
               name="OpenAI"
               value="openai"
               link="openai.com"
-              description="The standard option for most non-commercial use. Provides both chat and embedding."
+              description="The standard option for most non-commercial use."
               checked={embeddingChoice === "openai"}
               image={OpenAiLogo}
               onClick={updateChoice}
@@ -85,7 +85,7 @@ function EmbeddingSelection({ nextStep, prevStep, currentStep }) {
               name="Azure OpenAI"
               value="azure"
               link="azure.microsoft.com"
-              description="The enterprise option of OpenAI hosted on Azure services. Provides both chat and embedding."
+              description="The enterprise option of OpenAI hosted on Azure services."
               checked={embeddingChoice === "azure"}
               image={AzureOpenAiLogo}
               onClick={updateChoice}
diff --git a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/LLMSelection/index.jsx b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/LLMSelection/index.jsx
index bb87486ba..f877e31db 100644
--- a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/LLMSelection/index.jsx
+++ b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/LLMSelection/index.jsx
@@ -3,6 +3,7 @@ import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png";
 import OpenAiLogo from "@/media/llmprovider/openai.png";
 import AzureOpenAiLogo from "@/media/llmprovider/azure.png";
 import AnthropicLogo from "@/media/llmprovider/anthropic.png";
+import GeminiLogo from "@/media/llmprovider/gemini.png";
 import LMStudioLogo from "@/media/llmprovider/lmstudio.png";
 import LocalAiLogo from "@/media/llmprovider/localai.png";
 import System from "@/models/system";
@@ -14,6 +15,7 @@ import AnthropicAiOptions from "@/components/LLMSelection/AnthropicAiOptions";
 import LMStudioOptions from "@/components/LLMSelection/LMStudioOptions";
 import LocalAiOptions from "@/components/LLMSelection/LocalAiOptions";
 import NativeLLMOptions from "@/components/LLMSelection/NativeLLMOptions";
+import GeminiLLMOptions from "@/components/LLMSelection/GeminiLLMOptions";
 
 function LLMSelection({ nextStep, prevStep, currentStep }) {
   const [llmChoice, setLLMChoice] = useState("openai");
@@ -71,7 +73,7 @@ function LLMSelection({ nextStep, prevStep, currentStep }) {
               name="OpenAI"
               value="openai"
               link="openai.com"
-              description="The standard option for most non-commercial use. Provides both chat and embedding."
+              description="The standard option for most non-commercial use."
               checked={llmChoice === "openai"}
               image={OpenAiLogo}
               onClick={updateLLMChoice}
@@ -80,7 +82,7 @@ function LLMSelection({ nextStep, prevStep, currentStep }) {
               name="Azure OpenAI"
               value="azure"
               link="azure.microsoft.com"
-              description="The enterprise option of OpenAI hosted on Azure services. Provides both chat and embedding."
+              description="The enterprise option of OpenAI hosted on Azure services."
               checked={llmChoice === "azure"}
               image={AzureOpenAiLogo}
               onClick={updateLLMChoice}
@@ -94,6 +96,15 @@ function LLMSelection({ nextStep, prevStep, currentStep }) {
               image={AnthropicLogo}
               onClick={updateLLMChoice}
             />
+            <LLMProviderOption
+              name="Google Gemini"
+              value="gemini"
+              link="ai.google.dev"
+              description="Google's largest and most capable AI model"
+              checked={llmChoice === "gemini"}
+              image={GeminiLogo}
+              onClick={updateLLMChoice}
+            />
             <LLMProviderOption
               name="LM Studio"
               value="lmstudio"
@@ -127,6 +138,7 @@ function LLMSelection({ nextStep, prevStep, currentStep }) {
             {llmChoice === "anthropic" && (
               <AnthropicAiOptions settings={settings} />
             )}
+            {llmChoice === "gemini" && <GeminiLLMOptions settings={settings} />}
             {llmChoice === "lmstudio" && (
               <LMStudioOptions settings={settings} />
             )}
diff --git a/server/.env.example b/server/.env.example
index a4bc9fe5b..f73e0e083 100644
--- a/server/.env.example
+++ b/server/.env.example
@@ -8,6 +8,10 @@ JWT_SECRET="my-random-string-for-seeding" # Please generate random string at lea
 # OPEN_AI_KEY=
 # OPEN_MODEL_PREF='gpt-3.5-turbo'
 
+# LLM_PROVIDER='gemini'
+# GEMINI_API_KEY=
+# GEMINI_LLM_MODEL_PREF='gemini-pro'
+
 # LLM_PROVIDER='azure'
 # AZURE_OPENAI_ENDPOINT=
 # AZURE_OPENAI_KEY=
diff --git a/server/models/systemSettings.js b/server/models/systemSettings.js
index 068359bb0..b5dfeb700 100644
--- a/server/models/systemSettings.js
+++ b/server/models/systemSettings.js
@@ -87,6 +87,20 @@ const SystemSettings = {
           }
         : {}),
 
+      ...(llmProvider === "gemini"
+        ? {
+            GeminiLLMApiKey: !!process.env.GEMINI_API_KEY,
+            GeminiLLMModelPref:
+              process.env.GEMINI_LLM_MODEL_PREF || "gemini-pro",
+
+            // For embedding credentials when Gemini is selected.
+            OpenAiKey: !!process.env.OPEN_AI_KEY,
+            AzureOpenAiEndpoint: process.env.AZURE_OPENAI_ENDPOINT,
+            AzureOpenAiKey: !!process.env.AZURE_OPENAI_KEY,
+            AzureOpenAiEmbeddingModelPref: process.env.EMBEDDING_MODEL_PREF,
+          }
+        : {}),
+
       ...(llmProvider === "lmstudio"
         ? {
             LMStudioBasePath: process.env.LMSTUDIO_BASE_PATH,
diff --git a/server/package.json b/server/package.json
index 1100adbc3..4f84327a3 100644
--- a/server/package.json
+++ b/server/package.json
@@ -22,6 +22,7 @@
   "dependencies": {
     "@anthropic-ai/sdk": "^0.8.1",
     "@azure/openai": "^1.0.0-beta.3",
+    "@google/generative-ai": "^0.1.3",
     "@googleapis/youtube": "^9.0.0",
     "@pinecone-database/pinecone": "^0.1.6",
     "@prisma/client": "5.3.0",
@@ -65,4 +66,4 @@
     "nodemon": "^2.0.22",
     "prettier": "^2.4.1"
   }
-}
\ No newline at end of file
+}
diff --git a/server/utils/AiProviders/gemini/index.js b/server/utils/AiProviders/gemini/index.js
new file mode 100644
index 000000000..d0a76c550
--- /dev/null
+++ b/server/utils/AiProviders/gemini/index.js
@@ -0,0 +1,200 @@
+const { v4 } = require("uuid");
+const { chatPrompt } = require("../../chats");
+
+class GeminiLLM {
+  constructor(embedder = null) {
+    if (!process.env.GEMINI_API_KEY)
+      throw new Error("No Gemini API key was set.");
+
+    // Docs: https://ai.google.dev/tutorials/node_quickstart
+    const { GoogleGenerativeAI } = require("@google/generative-ai");
+    const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);
+    this.model = process.env.GEMINI_LLM_MODEL_PREF || "gemini-pro";
+    this.gemini = genAI.getGenerativeModel({ model: this.model });
+    this.limits = {
+      history: this.promptWindowLimit() * 0.15,
+      system: this.promptWindowLimit() * 0.15,
+      user: this.promptWindowLimit() * 0.7,
+    };
+
+    if (!embedder)
+      throw new Error(
+        "INVALID GEMINI LLM SETUP. No embedding engine has been set. Go to instance settings and set up an embedding interface to use Gemini as your LLM."
+      );
+    this.embedder = embedder;
+    this.answerKey = v4().split("-")[0];
+  }
+
+  streamingEnabled() {
+    return "streamChat" in this && "streamGetChatCompletion" in this;
+  }
+
+  promptWindowLimit() {
+    switch (this.model) {
+      case "gemini-pro":
+        return 30_720;
+      default:
+        return 30_720; // assume a gemini-pro model
+    }
+  }
+
+  isValidChatCompletionModel(modelName = "") {
+    const validModels = ["gemini-pro"];
+    return validModels.includes(modelName);
+  }
+
+  // Moderation cannot be done with Gemini.
+  // Not implemented so must be stubbed
+  async isSafe(_input = "") {
+    return { safe: true, reasons: [] };
+  }
+
+  constructPrompt({
+    systemPrompt = "",
+    contextTexts = [],
+    chatHistory = [],
+    userPrompt = "",
+  }) {
+    const prompt = {
+      role: "system",
+      content: `${systemPrompt}
+Context:
+    ${contextTexts
+      .map((text, i) => {
+        return `[CONTEXT ${i}]:\n${text}\n[END CONTEXT ${i}]\n\n`;
+      })
+      .join("")}`,
+    };
+    return [
+      prompt,
+      { role: "assistant", content: "Okay." },
+      ...chatHistory,
+      { role: "USER_PROMPT", content: userPrompt },
+    ];
+  }
+
+  // This will take an OpenAi format message array and only pluck valid roles from it.
+  formatMessages(messages = []) {
+    // Gemini roles are either user || model.
+    // and all "content" is relabeled to "parts"
+    return messages
+      .map((message) => {
+        if (message.role === "system")
+          return { role: "user", parts: message.content };
+        if (message.role === "user")
+          return { role: "user", parts: message.content };
+        if (message.role === "assistant")
+          return { role: "model", parts: message.content };
+        return null;
+      })
+      .filter((msg) => !!msg);
+  }
+
+  async sendChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) {
+    if (!this.isValidChatCompletionModel(this.model))
+      throw new Error(
+        `Gemini chat: ${this.model} is not valid for chat completion!`
+      );
+
+    const compressedHistory = await this.compressMessages(
+      {
+        systemPrompt: chatPrompt(workspace),
+        chatHistory,
+      },
+      rawHistory
+    );
+
+    const chatThread = this.gemini.startChat({
+      history: this.formatMessages(compressedHistory),
+    });
+    const result = await chatThread.sendMessage(prompt);
+    const response = result.response;
+    const responseText = response.text();
+
+    if (!responseText) throw new Error("Gemini: No response could be parsed.");
+
+    return responseText;
+  }
+
+  async getChatCompletion(messages = [], _opts = {}) {
+    if (!this.isValidChatCompletionModel(this.model))
+      throw new Error(
+        `Gemini chat: ${this.model} is not valid for chat completion!`
+      );
+
+    const prompt = messages.find(
+      (chat) => chat.role === "USER_PROMPT"
+    )?.content;
+    const chatThread = this.gemini.startChat({
+      history: this.formatMessages(messages),
+    });
+    const result = await chatThread.sendMessage(prompt);
+    const response = result.response;
+    const responseText = response.text();
+
+    if (!responseText) throw new Error("Gemini: No response could be parsed.");
+
+    return responseText;
+  }
+
+  async streamChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) {
+    if (!this.isValidChatCompletionModel(this.model))
+      throw new Error(
+        `Gemini chat: ${this.model} is not valid for chat completion!`
+      );
+
+    const compressedHistory = await this.compressMessages(
+      {
+        systemPrompt: chatPrompt(workspace),
+        chatHistory,
+      },
+      rawHistory
+    );
+
+    const chatThread = this.gemini.startChat({
+      history: this.formatMessages(compressedHistory),
+    });
+    const responseStream = await chatThread.sendMessageStream(prompt);
+    if (!responseStream.stream)
+      throw new Error("Could not stream response stream from Gemini.");
+
+    return { type: "geminiStream", ...responseStream };
+  }
+
+  async streamGetChatCompletion(messages = [], _opts = {}) {
+    if (!this.isValidChatCompletionModel(this.model))
+      throw new Error(
+        `Gemini chat: ${this.model} is not valid for chat completion!`
+      );
+
+    const prompt = messages.find(
+      (chat) => chat.role === "USER_PROMPT"
+    )?.content;
+    const chatThread = this.gemini.startChat({
+      history: this.formatMessages(messages),
+    });
+    const responseStream = await chatThread.sendMessageStream(prompt);
+    if (!responseStream.stream)
+      throw new Error("Could not stream response stream from Gemini.");
+
+    return { type: "geminiStream", ...responseStream };
+  }
+
+  async compressMessages(promptArgs = {}, rawHistory = []) {
+    const { messageArrayCompressor } = require("../../helpers/chat");
+    const messageArray = this.constructPrompt(promptArgs);
+    return await messageArrayCompressor(this, messageArray, rawHistory);
+  }
+
+  // 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);
+  }
+}
+
+module.exports = {
+  GeminiLLM,
+};
diff --git a/server/utils/chats/stream.js b/server/utils/chats/stream.js
index 4eb9cf022..5bdb7a1f0 100644
--- a/server/utils/chats/stream.js
+++ b/server/utils/chats/stream.js
@@ -202,6 +202,35 @@ async function streamEmptyEmbeddingChat({
 function handleStreamResponses(response, stream, responseProps) {
   const { uuid = uuidv4(), sources = [] } = responseProps;
 
+  // Gemini likes to return a stream asyncIterator which will
+  // be a totally different object than other models.
+  if (stream?.type === "geminiStream") {
+    return new Promise(async (resolve) => {
+      let fullText = "";
+      for await (const chunk of stream.stream) {
+        fullText += chunk.text();
+        writeResponseChunk(response, {
+          uuid,
+          sources: [],
+          type: "textResponseChunk",
+          textResponse: chunk.text(),
+          close: false,
+          error: false,
+        });
+      }
+
+      writeResponseChunk(response, {
+        uuid,
+        sources,
+        type: "textResponseChunk",
+        textResponse: "",
+        close: true,
+        error: false,
+      });
+      resolve(fullText);
+    });
+  }
+
   // If stream is not a regular OpenAI Stream (like if using native model)
   // we can just iterate the stream content instead.
   if (!stream.hasOwnProperty("data")) {
diff --git a/server/utils/helpers/index.js b/server/utils/helpers/index.js
index 3b7f4ccc2..115df4003 100644
--- a/server/utils/helpers/index.js
+++ b/server/utils/helpers/index.js
@@ -34,6 +34,9 @@ function getLLMProvider() {
     case "anthropic":
       const { AnthropicLLM } = require("../AiProviders/anthropic");
       return new AnthropicLLM(embedder);
+    case "gemini":
+      const { GeminiLLM } = require("../AiProviders/gemini");
+      return new GeminiLLM(embedder);
     case "lmstudio":
       const { LMStudioLLM } = require("../AiProviders/lmStudio");
       return new LMStudioLLM(embedder);
diff --git a/server/utils/helpers/updateENV.js b/server/utils/helpers/updateENV.js
index 3a8ea55dd..fe4f4f5c9 100644
--- a/server/utils/helpers/updateENV.js
+++ b/server/utils/helpers/updateENV.js
@@ -44,6 +44,15 @@ const KEY_MAPPING = {
     checks: [isNotEmpty, validAnthropicModel],
   },
 
+  GeminiLLMApiKey: {
+    envKey: "GEMINI_API_KEY",
+    checks: [isNotEmpty],
+  },
+  GeminiLLMModelPref: {
+    envKey: "GEMINI_LLM_MODEL_PREF",
+    checks: [isNotEmpty, validGeminiModel],
+  },
+
   // LMStudio Settings
   LMStudioBasePath: {
     envKey: "LMSTUDIO_BASE_PATH",
@@ -204,12 +213,20 @@ function supportedLLM(input = "") {
     "openai",
     "azure",
     "anthropic",
+    "gemini",
     "lmstudio",
     "localai",
     "native",
   ].includes(input);
 }
 
+function validGeminiModel(input = "") {
+  const validModels = ["gemini-pro"];
+  return validModels.includes(input)
+    ? null
+    : `Invalid Model type. Must be one of ${validModels.join(", ")}.`;
+}
+
 function validAnthropicModel(input = "") {
   const validModels = ["claude-2", "claude-instant-1"];
   return validModels.includes(input)
diff --git a/server/yarn.lock b/server/yarn.lock
index caffe137a..f9a621f69 100644
--- a/server/yarn.lock
+++ b/server/yarn.lock
@@ -140,6 +140,11 @@
   resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6"
   integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==
 
+"@google/generative-ai@^0.1.3":
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/@google/generative-ai/-/generative-ai-0.1.3.tgz#8e529d4d86c85b64d297b4abf1a653d613a09a9f"
+  integrity sha512-Cm4uJX1sKarpm1mje/MiOIinM7zdUUrQp/5/qGPAgznbdd/B9zup5ehT6c1qGqycFcSopTA1J1HpqHS5kJR8hQ==
+
 "@googleapis/youtube@^9.0.0":
   version "9.0.0"
   resolved "https://registry.yarnpkg.com/@googleapis/youtube/-/youtube-9.0.0.tgz#e45f6f5f7eac198c6391782b94b3ca54bacf0b63"