From 633f4252067a1b38a63f375c3fe8d4a6e78e9bc3 Mon Sep 17 00:00:00 2001
From: Sean Hatfield <seanhatfield5@gmail.com>
Date: Fri, 23 Feb 2024 17:18:58 -0800
Subject: [PATCH] [FEAT] OpenRouter integration (#784)

* WIP openrouter integration

* add OpenRouter options to onboarding flow and data handling

* add todo to fix headers for rankings

* OpenRouter LLM support complete

* Fix hanging response stream with OpenRouter
update tagline
update comment

* update timeout comment

* wait for first chunk to start timer

* sort OpenRouter models by organization

* uppercase first letter of organization

* sort grouped models by org

---------

Co-authored-by: timothycarambat <rambat1010@gmail.com>
---
 README.md                                     |   1 +
 docker/.env.example                           |   4 +
 .../LLMSelection/OpenRouterOptions/index.jsx  |  97 +++
 .../LLMSelection/TogetherAiOptions/index.jsx  |  28 +-
 .../src/media/llmprovider/openrouter.jpeg     | Bin 0 -> 6366 bytes
 .../GeneralSettings/LLMPreference/index.jsx   |   9 +
 .../Steps/DataHandling/index.jsx              |   9 +
 .../Steps/LLMPreference/index.jsx             |   9 +
 server/.env.example                           |   4 +
 server/models/systemSettings.js               |  12 +
 server/utils/AiProviders/openRouter/index.js  | 334 ++++++++++
 server/utils/AiProviders/openRouter/models.js | 622 ++++++++++++++++++
 .../AiProviders/openRouter/scripts/.gitignore |   1 +
 .../AiProviders/openRouter/scripts/parse.mjs  |  37 ++
 server/utils/helpers/customModels.js          |  19 +
 server/utils/helpers/index.js                 |   3 +
 server/utils/helpers/updateENV.js             |  11 +
 17 files changed, 1187 insertions(+), 13 deletions(-)
 create mode 100644 frontend/src/components/LLMSelection/OpenRouterOptions/index.jsx
 create mode 100644 frontend/src/media/llmprovider/openrouter.jpeg
 create mode 100644 server/utils/AiProviders/openRouter/index.js
 create mode 100644 server/utils/AiProviders/openRouter/models.js
 create mode 100644 server/utils/AiProviders/openRouter/scripts/.gitignore
 create mode 100644 server/utils/AiProviders/openRouter/scripts/parse.mjs

diff --git a/README.md b/README.md
index 200355707..f77cdf2b8 100644
--- a/README.md
+++ b/README.md
@@ -72,6 +72,7 @@ Some cool features of AnythingLLM
 - [LocalAi (all models)](https://localai.io/)
 - [Together AI (chat models)](https://www.together.ai/)
 - [Perplexity (chat models)](https://www.perplexity.ai/)
+- [OpenRouter (chat models)](https://openrouter.ai/)
 - [Mistral](https://mistral.ai/)
 
 **Supported Embedding models:**
diff --git a/docker/.env.example b/docker/.env.example
index eed505782..16413ad3c 100644
--- a/docker/.env.example
+++ b/docker/.env.example
@@ -52,6 +52,10 @@ GID='1000'
 # PERPLEXITY_API_KEY='my-perplexity-key'
 # PERPLEXITY_MODEL_PREF='codellama-34b-instruct'
 
+# LLM_PROVIDER='openrouter'
+# OPENROUTER_API_KEY='my-openrouter-key'
+# OPENROUTER_MODEL_PREF='openrouter/auto'
+
 # LLM_PROVIDER='huggingface'
 # HUGGING_FACE_LLM_ENDPOINT=https://uuid-here.us-east-1.aws.endpoints.huggingface.cloud
 # HUGGING_FACE_LLM_API_KEY=hf_xxxxxx
diff --git a/frontend/src/components/LLMSelection/OpenRouterOptions/index.jsx b/frontend/src/components/LLMSelection/OpenRouterOptions/index.jsx
new file mode 100644
index 000000000..aa4ccdb2e
--- /dev/null
+++ b/frontend/src/components/LLMSelection/OpenRouterOptions/index.jsx
@@ -0,0 +1,97 @@
+import System from "@/models/system";
+import { useState, useEffect } from "react";
+
+export default function OpenRouterOptions({ settings }) {
+  return (
+    <div className="flex gap-x-4">
+      <div className="flex flex-col w-60">
+        <label className="text-white text-sm font-semibold block mb-4">
+          OpenRouter API Key
+        </label>
+        <input
+          type="password"
+          name="OpenRouterApiKey"
+          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="OpenRouter API Key"
+          defaultValue={settings?.OpenRouterApiKey ? "*".repeat(20) : ""}
+          required={true}
+          autoComplete="off"
+          spellCheck={false}
+        />
+      </div>
+      <OpenRouterModelSelection settings={settings} />
+    </div>
+  );
+}
+
+function OpenRouterModelSelection({ settings }) {
+  const [groupedModels, setGroupedModels] = useState({});
+  const [loading, setLoading] = useState(true);
+
+  useEffect(() => {
+    async function findCustomModels() {
+      setLoading(true);
+      const { models } = await System.customModels("openrouter");
+      if (models?.length > 0) {
+        const modelsByOrganization = models.reduce((acc, model) => {
+          acc[model.organization] = acc[model.organization] || [];
+          acc[model.organization].push(model);
+          return acc;
+        }, {});
+
+        setGroupedModels(modelsByOrganization);
+      }
+
+      setLoading(false);
+    }
+    findCustomModels();
+  }, []);
+
+  if (loading || Object.keys(groupedModels).length === 0) {
+    return (
+      <div className="flex flex-col w-60">
+        <label className="text-white text-sm font-semibold block mb-4">
+          Chat Model Selection
+        </label>
+        <select
+          name="OpenRouterModelPref"
+          disabled={true}
+          className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
+        >
+          <option disabled={true} selected={true}>
+            -- loading available models --
+          </option>
+        </select>
+      </div>
+    );
+  }
+
+  return (
+    <div className="flex flex-col w-60">
+      <label className="text-white text-sm font-semibold block mb-4">
+        Chat Model Selection
+      </label>
+      <select
+        name="OpenRouterModelPref"
+        required={true}
+        className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
+      >
+        {Object.keys(groupedModels)
+          .sort()
+          .map((organization) => (
+            <optgroup key={organization} label={organization}>
+              {groupedModels[organization].map((model) => (
+                <option
+                  key={model.id}
+                  value={model.id}
+                  selected={settings.OpenRouterModelPref === model.id}
+                >
+                  {model.name}
+                </option>
+              ))}
+            </optgroup>
+          ))}
+      </select>
+    </div>
+  );
+}
diff --git a/frontend/src/components/LLMSelection/TogetherAiOptions/index.jsx b/frontend/src/components/LLMSelection/TogetherAiOptions/index.jsx
index e526b3afe..66ba1715e 100644
--- a/frontend/src/components/LLMSelection/TogetherAiOptions/index.jsx
+++ b/frontend/src/components/LLMSelection/TogetherAiOptions/index.jsx
@@ -76,19 +76,21 @@ function TogetherAiModelSelection({ settings }) {
         required={true}
         className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
       >
-        {Object.entries(groupedModels).map(([organization, models]) => (
-          <optgroup key={organization} label={organization}>
-            {models.map((model) => (
-              <option
-                key={model.id}
-                value={model.id}
-                selected={settings.TogetherAiModelPref === model.id}
-              >
-                {model.name}
-              </option>
-            ))}
-          </optgroup>
-        ))}
+        {Object.keys(groupedModels)
+          .sort()
+          .map((organization) => (
+            <optgroup key={organization} label={organization}>
+              {groupedModels[organization].map((model) => (
+                <option
+                  key={model.id}
+                  value={model.id}
+                  selected={settings.OpenRouterModelPref === model.id}
+                >
+                  {model.name}
+                </option>
+              ))}
+            </optgroup>
+          ))}
       </select>
     </div>
   );
diff --git a/frontend/src/media/llmprovider/openrouter.jpeg b/frontend/src/media/llmprovider/openrouter.jpeg
new file mode 100644
index 0000000000000000000000000000000000000000..ce272ac81e15a41ebc3b2724085275a103bb0837
GIT binary patch
literal 6366
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
GhyDw4i6SZh

literal 0
HcmV?d00001

diff --git a/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx b/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx
index 8c51e559e..579c3903b 100644
--- a/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx
+++ b/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx
@@ -15,6 +15,7 @@ import TogetherAILogo from "@/media/llmprovider/togetherai.png";
 import MistralLogo from "@/media/llmprovider/mistral.jpeg";
 import HuggingFaceLogo from "@/media/llmprovider/huggingface.png";
 import PerplexityLogo from "@/media/llmprovider/perplexity.png";
+import OpenRouterLogo from "@/media/llmprovider/openrouter.jpeg";
 import PreLoader from "@/components/Preloader";
 import OpenAiOptions from "@/components/LLMSelection/OpenAiOptions";
 import AzureAiOptions from "@/components/LLMSelection/AzureAiOptions";
@@ -31,6 +32,7 @@ import HuggingFaceOptions from "@/components/LLMSelection/HuggingFaceOptions";
 import LLMItem from "@/components/LLMSelection/LLMItem";
 import { MagnifyingGlass } from "@phosphor-icons/react";
 import PerplexityOptions from "@/components/LLMSelection/PerplexityOptions";
+import OpenRouterOptions from "@/components/LLMSelection/OpenRouterOptions";
 
 export default function GeneralLLMPreference() {
   const [saving, setSaving] = useState(false);
@@ -164,6 +166,13 @@ export default function GeneralLLMPreference() {
       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: "Native",
       value: "native",
diff --git a/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx
index f9c4c4169..51dc73004 100644
--- a/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx
+++ b/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx
@@ -12,6 +12,7 @@ import LocalAiLogo from "@/media/llmprovider/localai.png";
 import MistralLogo from "@/media/llmprovider/mistral.jpeg";
 import HuggingFaceLogo from "@/media/llmprovider/huggingface.png";
 import PerplexityLogo from "@/media/llmprovider/perplexity.png";
+import OpenRouterLogo from "@/media/llmprovider/openrouter.jpeg";
 import ZillizLogo from "@/media/vectordbs/zilliz.png";
 import AstraDBLogo from "@/media/vectordbs/astraDB.png";
 import ChromaLogo from "@/media/vectordbs/chroma.png";
@@ -118,6 +119,14 @@ const LLM_SELECTION_PRIVACY = {
     ],
     logo: PerplexityLogo,
   },
+  openrouter: {
+    name: "OpenRouter",
+    description: [
+      "Your chats will not be used for training",
+      "Your prompts and document text used in response creation are visible to OpenRouter",
+    ],
+    logo: OpenRouterLogo,
+  },
 };
 
 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 296a28d9e..df94652a8 100644
--- a/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx
+++ b/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx
@@ -12,6 +12,7 @@ import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png";
 import MistralLogo from "@/media/llmprovider/mistral.jpeg";
 import HuggingFaceLogo from "@/media/llmprovider/huggingface.png";
 import PerplexityLogo from "@/media/llmprovider/perplexity.png";
+import OpenRouterLogo from "@/media/llmprovider/openrouter.jpeg";
 import OpenAiOptions from "@/components/LLMSelection/OpenAiOptions";
 import AzureAiOptions from "@/components/LLMSelection/AzureAiOptions";
 import AnthropicAiOptions from "@/components/LLMSelection/AnthropicAiOptions";
@@ -29,6 +30,7 @@ import System from "@/models/system";
 import paths from "@/utils/paths";
 import showToast from "@/utils/toast";
 import { useNavigate } from "react-router-dom";
+import OpenRouterOptions from "@/components/LLMSelection/OpenRouterOptions";
 
 const TITLE = "LLM Preference";
 const DESCRIPTION =
@@ -138,6 +140,13 @@ export default function LLMPreference({
       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: "Native",
       value: "native",
diff --git a/server/.env.example b/server/.env.example
index 863486ad4..bed943925 100644
--- a/server/.env.example
+++ b/server/.env.example
@@ -45,6 +45,10 @@ JWT_SECRET="my-random-string-for-seeding" # Please generate random string at lea
 # PERPLEXITY_API_KEY='my-perplexity-key'
 # PERPLEXITY_MODEL_PREF='codellama-34b-instruct'
 
+# LLM_PROVIDER='openrouter'
+# OPENROUTER_API_KEY='my-openrouter-key'
+# OPENROUTER_MODEL_PREF='openrouter/auto'
+
 # LLM_PROVIDER='mistral'
 # MISTRAL_API_KEY='example-mistral-ai-api-key'
 # MISTRAL_MODEL_PREF='mistral-tiny'
diff --git a/server/models/systemSettings.js b/server/models/systemSettings.js
index 415448282..31d5c59a8 100644
--- a/server/models/systemSettings.js
+++ b/server/models/systemSettings.js
@@ -188,6 +188,18 @@ const SystemSettings = {
             PerplexityApiKey: !!process.env.PERPLEXITY_API_KEY,
             PerplexityModelPref: process.env.PERPLEXITY_MODEL_PREF,
 
+            // For embedding credentials when ollama 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 === "openrouter"
+        ? {
+            OpenRouterApiKey: !!process.env.OPENROUTER_API_KEY,
+            OpenRouterModelPref: process.env.OPENROUTER_MODEL_PREF,
+
             // For embedding credentials when ollama is selected.
             OpenAiKey: !!process.env.OPEN_AI_KEY,
             AzureOpenAiEndpoint: process.env.AZURE_OPENAI_ENDPOINT,
diff --git a/server/utils/AiProviders/openRouter/index.js b/server/utils/AiProviders/openRouter/index.js
new file mode 100644
index 000000000..38a6f9f09
--- /dev/null
+++ b/server/utils/AiProviders/openRouter/index.js
@@ -0,0 +1,334 @@
+const { NativeEmbedder } = require("../../EmbeddingEngines/native");
+const { chatPrompt } = require("../../chats");
+const { v4: uuidv4 } = require("uuid");
+const { writeResponseChunk } = require("../../helpers/chat/responses");
+
+function openRouterModels() {
+  const { MODELS } = require("./models.js");
+  return MODELS || {};
+}
+
+class OpenRouterLLM {
+  constructor(embedder = null, modelPreference = null) {
+    const { Configuration, OpenAIApi } = require("openai");
+    if (!process.env.OPENROUTER_API_KEY)
+      throw new Error("No OpenRouter API key was set.");
+
+    const config = new Configuration({
+      basePath: "https://openrouter.ai/api/v1",
+      apiKey: process.env.OPENROUTER_API_KEY,
+      baseOptions: {
+        headers: {
+          "HTTP-Referer": "https://useanything.com",
+          "X-Title": "AnythingLLM",
+        },
+      },
+    });
+    this.openai = new OpenAIApi(config);
+    this.model =
+      modelPreference || process.env.OPENROUTER_MODEL_PREF || "openrouter/auto";
+    this.limits = {
+      history: this.promptWindowLimit() * 0.15,
+      system: this.promptWindowLimit() * 0.15,
+      user: this.promptWindowLimit() * 0.7,
+    };
+
+    this.embedder = !embedder ? new NativeEmbedder() : embedder;
+    this.defaultTemp = 0.7;
+  }
+
+  #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("")
+    );
+  }
+
+  allModelInformation() {
+    return openRouterModels();
+  }
+
+  streamingEnabled() {
+    return "streamChat" in this && "streamGetChatCompletion" in this;
+  }
+
+  promptWindowLimit() {
+    const availableModels = this.allModelInformation();
+    return availableModels[this.model]?.maxLength || 4096;
+  }
+
+  async isValidChatCompletionModel(model = "") {
+    const availableModels = this.allModelInformation();
+    return availableModels.hasOwnProperty(model);
+  }
+
+  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 = []) {
+    if (!(await this.isValidChatCompletionModel(this.model)))
+      throw new Error(
+        `OpenRouter chat: ${this.model} is not valid for chat completion!`
+      );
+
+    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("OpenRouter chat: No results!");
+        if (res.choices.length === 0)
+          throw new Error("OpenRouter chat: No results length!");
+        return res.choices[0].message.content;
+      })
+      .catch((error) => {
+        throw new Error(
+          `OpenRouter::createChatCompletion failed with: ${error.message}`
+        );
+      });
+
+    return textResponse;
+  }
+
+  async streamChat(chatHistory = [], prompt, workspace = {}, rawHistory = []) {
+    if (!(await this.isValidChatCompletionModel(this.model)))
+      throw new Error(
+        `OpenRouter chat: ${this.model} is not valid for chat completion!`
+      );
+
+    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 }) {
+    if (!(await this.isValidChatCompletionModel(this.model)))
+      throw new Error(
+        `OpenRouter chat: ${this.model} is not valid for chat completion!`
+      );
+
+    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 }) {
+    if (!(await this.isValidChatCompletionModel(this.model)))
+      throw new Error(
+        `OpenRouter chat: ${this.model} is not valid for chat completion!`
+      );
+
+    const streamRequest = await this.openai.createChatCompletion(
+      {
+        model: this.model,
+        stream: true,
+        messages,
+        temperature,
+      },
+      { responseType: "stream" }
+    );
+    return streamRequest;
+  }
+
+  handleStream(response, stream, responseProps) {
+    const timeoutThresholdMs = 500;
+    const { uuid = uuidv4(), sources = [] } = responseProps;
+
+    return new Promise((resolve) => {
+      let fullText = "";
+      let chunk = "";
+      let lastChunkTime = null; // null when first token is still not received.
+
+      // NOTICE: Not all OpenRouter models will return a stop reason
+      // which keeps the connection open and so the model never finalizes the stream
+      // like the traditional OpenAI response schema does. So in the case the response stream
+      // never reaches a formal close state we maintain an interval timer that if we go >=timeoutThresholdMs with
+      // no new chunks then we kill the stream and assume it to be complete. OpenRouter is quite fast
+      // so this threshold should permit most responses, but we can adjust `timeoutThresholdMs` if
+      // we find it is too aggressive.
+      const timeoutCheck = setInterval(() => {
+        if (lastChunkTime === null) return;
+
+        const now = Number(new Date());
+        const diffMs = now - lastChunkTime;
+        if (diffMs >= timeoutThresholdMs) {
+          console.log(
+            `OpenRouter stream did not self-close and has been stale for >${timeoutThresholdMs}ms. Closing response stream.`
+          );
+          writeResponseChunk(response, {
+            uuid,
+            sources,
+            type: "textResponseChunk",
+            textResponse: "",
+            close: true,
+            error: false,
+          });
+          clearInterval(timeoutCheck);
+          resolve(fullText);
+        }
+      }, 500);
+
+      stream.data.on("data", (data) => {
+        const lines = data
+          ?.toString()
+          ?.split("\n")
+          .filter((line) => line.trim() !== "");
+
+        for (const line of lines) {
+          let validJSON = false;
+          const message = chunk + line.replace(/^data: /, "");
+
+          // JSON chunk is incomplete and has not ended yet
+          // so we need to stitch it together. You would think JSON
+          // chunks would only come complete - but they don't!
+          try {
+            JSON.parse(message);
+            validJSON = true;
+          } catch {}
+
+          if (!validJSON) {
+            // It can be possible that the chunk decoding is running away
+            // and the message chunk fails to append due to string length.
+            // In this case abort the chunk and reset so we can continue.
+            // ref: https://github.com/Mintplex-Labs/anything-llm/issues/416
+            try {
+              chunk += message;
+            } catch (e) {
+              console.error(`Chunk appending error`, e);
+              chunk = "";
+            }
+            continue;
+          } else {
+            chunk = "";
+          }
+
+          if (message == "[DONE]") {
+            lastChunkTime = Number(new Date());
+            writeResponseChunk(response, {
+              uuid,
+              sources,
+              type: "textResponseChunk",
+              textResponse: "",
+              close: true,
+              error: false,
+            });
+            clearInterval(timeoutCheck);
+            resolve(fullText);
+          } else {
+            let finishReason = null;
+            let token = "";
+            try {
+              const json = JSON.parse(message);
+              token = json?.choices?.[0]?.delta?.content;
+              finishReason = json?.choices?.[0]?.finish_reason || null;
+            } catch {
+              continue;
+            }
+
+            if (token) {
+              fullText += token;
+              lastChunkTime = Number(new Date());
+              writeResponseChunk(response, {
+                uuid,
+                sources: [],
+                type: "textResponseChunk",
+                textResponse: token,
+                close: false,
+                error: false,
+              });
+            }
+
+            if (finishReason !== null) {
+              lastChunkTime = Number(new Date());
+              writeResponseChunk(response, {
+                uuid,
+                sources,
+                type: "textResponseChunk",
+                textResponse: "",
+                close: true,
+                error: false,
+              });
+              clearInterval(timeoutCheck);
+              resolve(fullText);
+            }
+          }
+        }
+      });
+    });
+  }
+
+  // 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 = {
+  OpenRouterLLM,
+  openRouterModels,
+};
diff --git a/server/utils/AiProviders/openRouter/models.js b/server/utils/AiProviders/openRouter/models.js
new file mode 100644
index 000000000..c920b88a4
--- /dev/null
+++ b/server/utils/AiProviders/openRouter/models.js
@@ -0,0 +1,622 @@
+const MODELS = {
+  "nousresearch/nous-capybara-34b": {
+    id: "nousresearch/nous-capybara-34b",
+    name: "Nous: Capybara 34B",
+    organization: "Nousresearch",
+    maxLength: 32768,
+  },
+  "openrouter/auto": {
+    id: "openrouter/auto",
+    name: "Auto (best for prompt)",
+    organization: "Openrouter",
+    maxLength: 128000,
+  },
+  "nousresearch/nous-capybara-7b:free": {
+    id: "nousresearch/nous-capybara-7b:free",
+    name: "Nous: Capybara 7B (free)",
+    organization: "Nousresearch",
+    maxLength: 4096,
+  },
+  "mistralai/mistral-7b-instruct:free": {
+    id: "mistralai/mistral-7b-instruct:free",
+    name: "Mistral 7B Instruct (free)",
+    organization: "Mistralai",
+    maxLength: 8192,
+  },
+  "gryphe/mythomist-7b:free": {
+    id: "gryphe/mythomist-7b:free",
+    name: "MythoMist 7B (free)",
+    organization: "Gryphe",
+    maxLength: 32768,
+  },
+  "undi95/toppy-m-7b:free": {
+    id: "undi95/toppy-m-7b:free",
+    name: "Toppy M 7B (free)",
+    organization: "Undi95",
+    maxLength: 4096,
+  },
+  "openrouter/cinematika-7b:free": {
+    id: "openrouter/cinematika-7b:free",
+    name: "Cinematika 7B (alpha) (free)",
+    organization: "Openrouter",
+    maxLength: 8000,
+  },
+  "google/gemma-7b-it:free": {
+    id: "google/gemma-7b-it:free",
+    name: "Google: Gemma 7B (free)",
+    organization: "Google",
+    maxLength: 8000,
+  },
+  "jondurbin/bagel-34b": {
+    id: "jondurbin/bagel-34b",
+    name: "Bagel 34B v0.2",
+    organization: "Jondurbin",
+    maxLength: 8000,
+  },
+  "jebcarter/psyfighter-13b": {
+    id: "jebcarter/psyfighter-13b",
+    name: "Psyfighter 13B",
+    organization: "Jebcarter",
+    maxLength: 4096,
+  },
+  "koboldai/psyfighter-13b-2": {
+    id: "koboldai/psyfighter-13b-2",
+    name: "Psyfighter v2 13B",
+    organization: "Koboldai",
+    maxLength: 4096,
+  },
+  "neversleep/noromaid-mixtral-8x7b-instruct": {
+    id: "neversleep/noromaid-mixtral-8x7b-instruct",
+    name: "Noromaid Mixtral 8x7B Instruct",
+    organization: "Neversleep",
+    maxLength: 8000,
+  },
+  "nousresearch/nous-hermes-llama2-13b": {
+    id: "nousresearch/nous-hermes-llama2-13b",
+    name: "Nous: Hermes 13B",
+    organization: "Nousresearch",
+    maxLength: 4096,
+  },
+  "meta-llama/codellama-34b-instruct": {
+    id: "meta-llama/codellama-34b-instruct",
+    name: "Meta: CodeLlama 34B Instruct",
+    organization: "Meta-llama",
+    maxLength: 8192,
+  },
+  "phind/phind-codellama-34b": {
+    id: "phind/phind-codellama-34b",
+    name: "Phind: CodeLlama 34B v2",
+    organization: "Phind",
+    maxLength: 4096,
+  },
+  "intel/neural-chat-7b": {
+    id: "intel/neural-chat-7b",
+    name: "Neural Chat 7B v3.1",
+    organization: "Intel",
+    maxLength: 4096,
+  },
+  "mistralai/mixtral-8x7b-instruct": {
+    id: "mistralai/mixtral-8x7b-instruct",
+    name: "Mistral: Mixtral 8x7B Instruct",
+    organization: "Mistralai",
+    maxLength: 32768,
+  },
+  "nousresearch/nous-hermes-2-mixtral-8x7b-dpo": {
+    id: "nousresearch/nous-hermes-2-mixtral-8x7b-dpo",
+    name: "Nous: Hermes 2 Mixtral 8x7B DPO",
+    organization: "Nousresearch",
+    maxLength: 32000,
+  },
+  "nousresearch/nous-hermes-2-mixtral-8x7b-sft": {
+    id: "nousresearch/nous-hermes-2-mixtral-8x7b-sft",
+    name: "Nous: Hermes 2 Mixtral 8x7B SFT",
+    organization: "Nousresearch",
+    maxLength: 32000,
+  },
+  "haotian-liu/llava-13b": {
+    id: "haotian-liu/llava-13b",
+    name: "Llava 13B",
+    organization: "Haotian-liu",
+    maxLength: 2048,
+  },
+  "nousresearch/nous-hermes-2-vision-7b": {
+    id: "nousresearch/nous-hermes-2-vision-7b",
+    name: "Nous: Hermes 2 Vision 7B (alpha)",
+    organization: "Nousresearch",
+    maxLength: 4096,
+  },
+  "meta-llama/llama-2-13b-chat": {
+    id: "meta-llama/llama-2-13b-chat",
+    name: "Meta: Llama v2 13B Chat",
+    organization: "Meta-llama",
+    maxLength: 4096,
+  },
+  "migtissera/synthia-70b": {
+    id: "migtissera/synthia-70b",
+    name: "Synthia 70B",
+    organization: "Migtissera",
+    maxLength: 8192,
+  },
+  "pygmalionai/mythalion-13b": {
+    id: "pygmalionai/mythalion-13b",
+    name: "Pygmalion: Mythalion 13B",
+    organization: "Pygmalionai",
+    maxLength: 8192,
+  },
+  "undi95/remm-slerp-l2-13b-6k": {
+    id: "undi95/remm-slerp-l2-13b-6k",
+    name: "ReMM SLERP 13B 6k",
+    organization: "Undi95",
+    maxLength: 6144,
+  },
+  "gryphe/mythomax-l2-13b": {
+    id: "gryphe/mythomax-l2-13b",
+    name: "MythoMax 13B",
+    organization: "Gryphe",
+    maxLength: 4096,
+  },
+  "xwin-lm/xwin-lm-70b": {
+    id: "xwin-lm/xwin-lm-70b",
+    name: "Xwin 70B",
+    organization: "Xwin-lm",
+    maxLength: 8192,
+  },
+  "gryphe/mythomax-l2-13b-8k": {
+    id: "gryphe/mythomax-l2-13b-8k",
+    name: "MythoMax 13B 8k",
+    organization: "Gryphe",
+    maxLength: 8192,
+  },
+  "alpindale/goliath-120b": {
+    id: "alpindale/goliath-120b",
+    name: "Goliath 120B",
+    organization: "Alpindale",
+    maxLength: 6144,
+  },
+  "neversleep/noromaid-20b": {
+    id: "neversleep/noromaid-20b",
+    name: "Noromaid 20B",
+    organization: "Neversleep",
+    maxLength: 8192,
+  },
+  "gryphe/mythomist-7b": {
+    id: "gryphe/mythomist-7b",
+    name: "MythoMist 7B",
+    organization: "Gryphe",
+    maxLength: 32768,
+  },
+  "mancer/weaver": {
+    id: "mancer/weaver",
+    name: "Mancer: Weaver (alpha)",
+    organization: "Mancer",
+    maxLength: 8000,
+  },
+  "nousresearch/nous-hermes-llama2-70b": {
+    id: "nousresearch/nous-hermes-llama2-70b",
+    name: "Nous: Hermes 70B",
+    organization: "Nousresearch",
+    maxLength: 4096,
+  },
+  "nousresearch/nous-capybara-7b": {
+    id: "nousresearch/nous-capybara-7b",
+    name: "Nous: Capybara 7B",
+    organization: "Nousresearch",
+    maxLength: 4096,
+  },
+  "codellama/codellama-70b-instruct": {
+    id: "codellama/codellama-70b-instruct",
+    name: "Meta: CodeLlama 70B Instruct",
+    organization: "Codellama",
+    maxLength: 2048,
+  },
+  "teknium/openhermes-2-mistral-7b": {
+    id: "teknium/openhermes-2-mistral-7b",
+    name: "OpenHermes 2 Mistral 7B",
+    organization: "Teknium",
+    maxLength: 4096,
+  },
+  "teknium/openhermes-2.5-mistral-7b": {
+    id: "teknium/openhermes-2.5-mistral-7b",
+    name: "OpenHermes 2.5 Mistral 7B",
+    organization: "Teknium",
+    maxLength: 4096,
+  },
+  "undi95/remm-slerp-l2-13b": {
+    id: "undi95/remm-slerp-l2-13b",
+    name: "ReMM SLERP 13B",
+    organization: "Undi95",
+    maxLength: 4096,
+  },
+  "undi95/toppy-m-7b": {
+    id: "undi95/toppy-m-7b",
+    name: "Toppy M 7B",
+    organization: "Undi95",
+    maxLength: 4096,
+  },
+  "openrouter/cinematika-7b": {
+    id: "openrouter/cinematika-7b",
+    name: "Cinematika 7B (alpha)",
+    organization: "Openrouter",
+    maxLength: 8000,
+  },
+  "01-ai/yi-34b-chat": {
+    id: "01-ai/yi-34b-chat",
+    name: "Yi 34B Chat",
+    organization: "01-ai",
+    maxLength: 4096,
+  },
+  "01-ai/yi-34b": {
+    id: "01-ai/yi-34b",
+    name: "Yi 34B (base)",
+    organization: "01-ai",
+    maxLength: 4096,
+  },
+  "01-ai/yi-6b": {
+    id: "01-ai/yi-6b",
+    name: "Yi 6B (base)",
+    organization: "01-ai",
+    maxLength: 4096,
+  },
+  "togethercomputer/stripedhyena-nous-7b": {
+    id: "togethercomputer/stripedhyena-nous-7b",
+    name: "StripedHyena Nous 7B",
+    organization: "Togethercomputer",
+    maxLength: 32768,
+  },
+  "togethercomputer/stripedhyena-hessian-7b": {
+    id: "togethercomputer/stripedhyena-hessian-7b",
+    name: "StripedHyena Hessian 7B (base)",
+    organization: "Togethercomputer",
+    maxLength: 32768,
+  },
+  "mistralai/mixtral-8x7b": {
+    id: "mistralai/mixtral-8x7b",
+    name: "Mistral: Mixtral 8x7B (base)",
+    organization: "Mistralai",
+    maxLength: 32768,
+  },
+  "nousresearch/nous-hermes-yi-34b": {
+    id: "nousresearch/nous-hermes-yi-34b",
+    name: "Nous: Hermes 2 Yi 34B",
+    organization: "Nousresearch",
+    maxLength: 4096,
+  },
+  "nousresearch/nous-hermes-2-mistral-7b-dpo": {
+    id: "nousresearch/nous-hermes-2-mistral-7b-dpo",
+    name: "Nous: Hermes 2 Mistral 7B DPO",
+    organization: "Nousresearch",
+    maxLength: 8192,
+  },
+  "open-orca/mistral-7b-openorca": {
+    id: "open-orca/mistral-7b-openorca",
+    name: "Mistral OpenOrca 7B",
+    organization: "Open-orca",
+    maxLength: 8192,
+  },
+  "huggingfaceh4/zephyr-7b-beta": {
+    id: "huggingfaceh4/zephyr-7b-beta",
+    name: "Hugging Face: Zephyr 7B",
+    organization: "Huggingfaceh4",
+    maxLength: 4096,
+  },
+  "openai/gpt-3.5-turbo": {
+    id: "openai/gpt-3.5-turbo",
+    name: "OpenAI: GPT-3.5 Turbo",
+    organization: "Openai",
+    maxLength: 4095,
+  },
+  "openai/gpt-3.5-turbo-0125": {
+    id: "openai/gpt-3.5-turbo-0125",
+    name: "OpenAI: GPT-3.5 Turbo 16k",
+    organization: "Openai",
+    maxLength: 16385,
+  },
+  "openai/gpt-3.5-turbo-1106": {
+    id: "openai/gpt-3.5-turbo-1106",
+    name: "OpenAI: GPT-3.5 Turbo 16k (older v1106)",
+    organization: "Openai",
+    maxLength: 16385,
+  },
+  "openai/gpt-3.5-turbo-0613": {
+    id: "openai/gpt-3.5-turbo-0613",
+    name: "OpenAI: GPT-3.5 Turbo (older v0613)",
+    organization: "Openai",
+    maxLength: 4095,
+  },
+  "openai/gpt-3.5-turbo-0301": {
+    id: "openai/gpt-3.5-turbo-0301",
+    name: "OpenAI: GPT-3.5 Turbo (older v0301)",
+    organization: "Openai",
+    maxLength: 4095,
+  },
+  "openai/gpt-3.5-turbo-16k": {
+    id: "openai/gpt-3.5-turbo-16k",
+    name: "OpenAI: GPT-3.5 Turbo 16k",
+    organization: "Openai",
+    maxLength: 16385,
+  },
+  "openai/gpt-4-turbo-preview": {
+    id: "openai/gpt-4-turbo-preview",
+    name: "OpenAI: GPT-4 Turbo (preview)",
+    organization: "Openai",
+    maxLength: 128000,
+  },
+  "openai/gpt-4-1106-preview": {
+    id: "openai/gpt-4-1106-preview",
+    name: "OpenAI: GPT-4 Turbo (older v1106)",
+    organization: "Openai",
+    maxLength: 128000,
+  },
+  "openai/gpt-4": {
+    id: "openai/gpt-4",
+    name: "OpenAI: GPT-4",
+    organization: "Openai",
+    maxLength: 8191,
+  },
+  "openai/gpt-4-0314": {
+    id: "openai/gpt-4-0314",
+    name: "OpenAI: GPT-4 (older v0314)",
+    organization: "Openai",
+    maxLength: 8191,
+  },
+  "openai/gpt-4-32k": {
+    id: "openai/gpt-4-32k",
+    name: "OpenAI: GPT-4 32k",
+    organization: "Openai",
+    maxLength: 32767,
+  },
+  "openai/gpt-4-32k-0314": {
+    id: "openai/gpt-4-32k-0314",
+    name: "OpenAI: GPT-4 32k (older v0314)",
+    organization: "Openai",
+    maxLength: 32767,
+  },
+  "openai/gpt-4-vision-preview": {
+    id: "openai/gpt-4-vision-preview",
+    name: "OpenAI: GPT-4 Vision (preview)",
+    organization: "Openai",
+    maxLength: 128000,
+  },
+  "openai/gpt-3.5-turbo-instruct": {
+    id: "openai/gpt-3.5-turbo-instruct",
+    name: "OpenAI: GPT-3.5 Turbo Instruct",
+    organization: "Openai",
+    maxLength: 4095,
+  },
+  "google/palm-2-chat-bison": {
+    id: "google/palm-2-chat-bison",
+    name: "Google: PaLM 2 Chat",
+    organization: "Google",
+    maxLength: 36864,
+  },
+  "google/palm-2-codechat-bison": {
+    id: "google/palm-2-codechat-bison",
+    name: "Google: PaLM 2 Code Chat",
+    organization: "Google",
+    maxLength: 28672,
+  },
+  "google/palm-2-chat-bison-32k": {
+    id: "google/palm-2-chat-bison-32k",
+    name: "Google: PaLM 2 Chat 32k",
+    organization: "Google",
+    maxLength: 131072,
+  },
+  "google/palm-2-codechat-bison-32k": {
+    id: "google/palm-2-codechat-bison-32k",
+    name: "Google: PaLM 2 Code Chat 32k",
+    organization: "Google",
+    maxLength: 131072,
+  },
+  "google/gemini-pro": {
+    id: "google/gemini-pro",
+    name: "Google: Gemini Pro (preview)",
+    organization: "Google",
+    maxLength: 131040,
+  },
+  "google/gemini-pro-vision": {
+    id: "google/gemini-pro-vision",
+    name: "Google: Gemini Pro Vision (preview)",
+    organization: "Google",
+    maxLength: 65536,
+  },
+  "perplexity/pplx-70b-online": {
+    id: "perplexity/pplx-70b-online",
+    name: "Perplexity: PPLX 70B Online",
+    organization: "Perplexity",
+    maxLength: 4096,
+  },
+  "perplexity/pplx-7b-online": {
+    id: "perplexity/pplx-7b-online",
+    name: "Perplexity: PPLX 7B Online",
+    organization: "Perplexity",
+    maxLength: 4096,
+  },
+  "perplexity/pplx-7b-chat": {
+    id: "perplexity/pplx-7b-chat",
+    name: "Perplexity: PPLX 7B Chat",
+    organization: "Perplexity",
+    maxLength: 8192,
+  },
+  "perplexity/pplx-70b-chat": {
+    id: "perplexity/pplx-70b-chat",
+    name: "Perplexity: PPLX 70B Chat",
+    organization: "Perplexity",
+    maxLength: 4096,
+  },
+  "meta-llama/llama-2-70b-chat": {
+    id: "meta-llama/llama-2-70b-chat",
+    name: "Meta: Llama v2 70B Chat",
+    organization: "Meta-llama",
+    maxLength: 4096,
+  },
+  "jondurbin/airoboros-l2-70b": {
+    id: "jondurbin/airoboros-l2-70b",
+    name: "Airoboros 70B",
+    organization: "Jondurbin",
+    maxLength: 4096,
+  },
+  "austism/chronos-hermes-13b": {
+    id: "austism/chronos-hermes-13b",
+    name: "Chronos Hermes 13B v2",
+    organization: "Austism",
+    maxLength: 4096,
+  },
+  "mistralai/mistral-7b-instruct": {
+    id: "mistralai/mistral-7b-instruct",
+    name: "Mistral 7B Instruct",
+    organization: "Mistralai",
+    maxLength: 8192,
+  },
+  "openchat/openchat-7b": {
+    id: "openchat/openchat-7b",
+    name: "OpenChat 3.5",
+    organization: "Openchat",
+    maxLength: 8192,
+  },
+  "lizpreciatior/lzlv-70b-fp16-hf": {
+    id: "lizpreciatior/lzlv-70b-fp16-hf",
+    name: "lzlv 70B",
+    organization: "Lizpreciatior",
+    maxLength: 4096,
+  },
+  "cognitivecomputations/dolphin-mixtral-8x7b": {
+    id: "cognitivecomputations/dolphin-mixtral-8x7b",
+    name: "Dolphin 2.6 Mixtral 8x7B 🐬",
+    organization: "Cognitivecomputations",
+    maxLength: 32000,
+  },
+  "rwkv/rwkv-5-world-3b": {
+    id: "rwkv/rwkv-5-world-3b",
+    name: "RWKV v5 World 3B",
+    organization: "Rwkv",
+    maxLength: 10000,
+  },
+  "recursal/rwkv-5-3b-ai-town": {
+    id: "recursal/rwkv-5-3b-ai-town",
+    name: "RWKV v5 3B AI Town",
+    organization: "Recursal",
+    maxLength: 10000,
+  },
+  "recursal/eagle-7b": {
+    id: "recursal/eagle-7b",
+    name: "RWKV v5: Eagle 7B",
+    organization: "Recursal",
+    maxLength: 10000,
+  },
+  "google/gemma-7b-it": {
+    id: "google/gemma-7b-it",
+    name: "Google: Gemma 7B",
+    organization: "Google",
+    maxLength: 8000,
+  },
+  "anthropic/claude-2": {
+    id: "anthropic/claude-2",
+    name: "Anthropic: Claude v2",
+    organization: "Anthropic",
+    maxLength: 200000,
+  },
+  "anthropic/claude-2.1": {
+    id: "anthropic/claude-2.1",
+    name: "Anthropic: Claude v2.1",
+    organization: "Anthropic",
+    maxLength: 200000,
+  },
+  "anthropic/claude-2.0": {
+    id: "anthropic/claude-2.0",
+    name: "Anthropic: Claude v2.0",
+    organization: "Anthropic",
+    maxLength: 100000,
+  },
+  "anthropic/claude-instant-1": {
+    id: "anthropic/claude-instant-1",
+    name: "Anthropic: Claude Instant v1",
+    organization: "Anthropic",
+    maxLength: 100000,
+  },
+  "anthropic/claude-instant-1.2": {
+    id: "anthropic/claude-instant-1.2",
+    name: "Anthropic: Claude Instant v1.2",
+    organization: "Anthropic",
+    maxLength: 100000,
+  },
+  "anthropic/claude-1": {
+    id: "anthropic/claude-1",
+    name: "Anthropic: Claude v1",
+    organization: "Anthropic",
+    maxLength: 100000,
+  },
+  "anthropic/claude-1.2": {
+    id: "anthropic/claude-1.2",
+    name: "Anthropic: Claude (older v1)",
+    organization: "Anthropic",
+    maxLength: 100000,
+  },
+  "anthropic/claude-instant-1.0": {
+    id: "anthropic/claude-instant-1.0",
+    name: "Anthropic: Claude Instant (older v1)",
+    organization: "Anthropic",
+    maxLength: 100000,
+  },
+  "anthropic/claude-instant-1.1": {
+    id: "anthropic/claude-instant-1.1",
+    name: "Anthropic: Claude Instant (older v1.1)",
+    organization: "Anthropic",
+    maxLength: 100000,
+  },
+  "anthropic/claude-2:beta": {
+    id: "anthropic/claude-2:beta",
+    name: "Anthropic: Claude v2 (experimental)",
+    organization: "Anthropic",
+    maxLength: 200000,
+  },
+  "anthropic/claude-2.1:beta": {
+    id: "anthropic/claude-2.1:beta",
+    name: "Anthropic: Claude v2.1 (experimental)",
+    organization: "Anthropic",
+    maxLength: 200000,
+  },
+  "anthropic/claude-2.0:beta": {
+    id: "anthropic/claude-2.0:beta",
+    name: "Anthropic: Claude v2.0 (experimental)",
+    organization: "Anthropic",
+    maxLength: 100000,
+  },
+  "anthropic/claude-instant-1:beta": {
+    id: "anthropic/claude-instant-1:beta",
+    name: "Anthropic: Claude Instant v1 (experimental)",
+    organization: "Anthropic",
+    maxLength: 100000,
+  },
+  "huggingfaceh4/zephyr-7b-beta:free": {
+    id: "huggingfaceh4/zephyr-7b-beta:free",
+    name: "Hugging Face: Zephyr 7B (free)",
+    organization: "Huggingfaceh4",
+    maxLength: 4096,
+  },
+  "openchat/openchat-7b:free": {
+    id: "openchat/openchat-7b:free",
+    name: "OpenChat 3.5 (free)",
+    organization: "Openchat",
+    maxLength: 8192,
+  },
+  "mistralai/mistral-tiny": {
+    id: "mistralai/mistral-tiny",
+    name: "Mistral: Tiny",
+    organization: "Mistralai",
+    maxLength: 32000,
+  },
+  "mistralai/mistral-small": {
+    id: "mistralai/mistral-small",
+    name: "Mistral: Small",
+    organization: "Mistralai",
+    maxLength: 32000,
+  },
+  "mistralai/mistral-medium": {
+    id: "mistralai/mistral-medium",
+    name: "Mistral: Medium",
+    organization: "Mistralai",
+    maxLength: 32000,
+  },
+};
+
+module.exports.MODELS = MODELS;
diff --git a/server/utils/AiProviders/openRouter/scripts/.gitignore b/server/utils/AiProviders/openRouter/scripts/.gitignore
new file mode 100644
index 000000000..94a2dd146
--- /dev/null
+++ b/server/utils/AiProviders/openRouter/scripts/.gitignore
@@ -0,0 +1 @@
+*.json
\ No newline at end of file
diff --git a/server/utils/AiProviders/openRouter/scripts/parse.mjs b/server/utils/AiProviders/openRouter/scripts/parse.mjs
new file mode 100644
index 000000000..fb3b562b5
--- /dev/null
+++ b/server/utils/AiProviders/openRouter/scripts/parse.mjs
@@ -0,0 +1,37 @@
+// OpenRouter has lots of models we can use so we use this script
+// to cache all the models. We can see the list of all the models
+// here: https://openrouter.ai/docs#models
+
+// To run, cd into this directory and run `node parse.mjs`
+// copy outputs into the export in ../models.js
+
+// Update the date below if you run this again because OpenRouter added new models.
+// Last Collected: Feb 23, 2024
+
+import fs from "fs";
+
+async function parseChatModels() {
+  const models = {};
+  const response = await fetch("https://openrouter.ai/api/v1/models");
+  const data = await response.json();
+  data.data.forEach((model) => {
+    models[model.id] = {
+      id: model.id,
+      name: model.name,
+      // capitalize first letter
+      organization:
+        model.id.split("/")[0].charAt(0).toUpperCase() +
+        model.id.split("/")[0].slice(1),
+      maxLength: model.context_length,
+    };
+  });
+
+  fs.writeFileSync(
+    "chat_models.json",
+    JSON.stringify(models, null, 2),
+    "utf-8"
+  );
+  return models;
+}
+
+parseChatModels();
diff --git a/server/utils/helpers/customModels.js b/server/utils/helpers/customModels.js
index 8f8ca0657..f434ac078 100644
--- a/server/utils/helpers/customModels.js
+++ b/server/utils/helpers/customModels.js
@@ -1,3 +1,4 @@
+const { openRouterModels } = require("../AiProviders/openRouter");
 const { perplexityModels } = require("../AiProviders/perplexity");
 const { togetherAiModels } = require("../AiProviders/togetherAi");
 const SUPPORT_CUSTOM_MODELS = [
@@ -8,6 +9,7 @@ const SUPPORT_CUSTOM_MODELS = [
   "togetherai",
   "mistral",
   "perplexity",
+  "openrouter",
 ];
 
 async function getCustomModels(provider = "", apiKey = null, basePath = null) {
@@ -29,6 +31,8 @@ async function getCustomModels(provider = "", apiKey = null, basePath = null) {
       return nativeLLMModels();
     case "perplexity":
       return await getPerplexityModels();
+    case "openrouter":
+      return await getOpenRouterModels();
     default:
       return { models: [], error: "Invalid provider for custom models" };
   }
@@ -138,6 +142,21 @@ async function getPerplexityModels() {
   return { models, error: null };
 }
 
+async function getOpenRouterModels() {
+  const knownModels = await openRouterModels();
+  if (!Object.keys(knownModels).length === 0)
+    return { models: [], error: null };
+
+  const models = Object.values(knownModels).map((model) => {
+    return {
+      id: model.id,
+      organization: model.organization,
+      name: model.name,
+    };
+  });
+  return { models, error: null };
+}
+
 async function getMistralModels(apiKey = null) {
   const { Configuration, OpenAIApi } = require("openai");
   const config = new Configuration({
diff --git a/server/utils/helpers/index.js b/server/utils/helpers/index.js
index 818d92dbc..8bda716aa 100644
--- a/server/utils/helpers/index.js
+++ b/server/utils/helpers/index.js
@@ -61,6 +61,9 @@ function getLLMProvider(modelPreference = null) {
     case "perplexity":
       const { PerplexityLLM } = require("../AiProviders/perplexity");
       return new PerplexityLLM(embedder, modelPreference);
+    case "openrouter":
+      const { OpenRouterLLM } = require("../AiProviders/openRouter");
+      return new OpenRouterLLM(embedder, modelPreference);
     case "mistral":
       const { MistralLLM } = require("../AiProviders/mistral");
       return new MistralLLM(embedder, modelPreference);
diff --git a/server/utils/helpers/updateENV.js b/server/utils/helpers/updateENV.js
index 5a384740b..247e3ba48 100644
--- a/server/utils/helpers/updateENV.js
+++ b/server/utils/helpers/updateENV.js
@@ -249,6 +249,16 @@ const KEY_MAPPING = {
     checks: [isNotEmpty],
   },
 
+  // OpenRouter Options
+  OpenRouterApiKey: {
+    envKey: "OPENROUTER_API_KEY",
+    checks: [isNotEmpty],
+  },
+  OpenRouterModelPref: {
+    envKey: "OPENROUTER_MODEL_PREF",
+    checks: [isNotEmpty],
+  },
+
   // System Settings
   AuthToken: {
     envKey: "AUTH_TOKEN",
@@ -325,6 +335,7 @@ function supportedLLM(input = "") {
     "mistral",
     "huggingface",
     "perplexity",
+    "openrouter",
   ].includes(input);
   return validSelection ? null : `${input} is not a valid LLM provider.`;
 }