From 24227e48a73a63deadef4214e7a1ef85d3c3f40e Mon Sep 17 00:00:00 2001 From: Timothy Carambat 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 9ed7cc60..44e0557f 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 8bbdd1dd..cc9fa06f 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 00000000..4d09e043 --- /dev/null +++ b/frontend/src/components/LLMSelection/GeminiLLMOptions/index.jsx @@ -0,0 +1,43 @@ +export default function GeminiLLMOptions({ settings }) { + return ( +
+
+
+ + +
+ +
+ + +
+
+
+ ); +} 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 1abf3a4b..d6224906 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() {
Embedding Providers
-
+
-
+
{embeddingChoice === "native" && } {embeddingChoice === "openai" && ( diff --git a/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx b/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx index 1c18d1ff..a0169fe1 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() {
LLM Providers
-
+
+ )} + {llmChoice === "gemini" && ( + + )} {llmChoice === "lmstudio" && ( )} diff --git a/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx b/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx index 1635fef8..2ddf1d5a 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 98a1671c..cd63d74d 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 1f44c463..98e1262a 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 bb87486b..f877e31d 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} /> + )} + {llmChoice === "gemini" && } {llmChoice === "lmstudio" && ( )} diff --git a/server/.env.example b/server/.env.example index a4bc9fe5..f73e0e08 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 068359bb..b5dfeb70 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 1100adbc..4f84327a 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 00000000..d0a76c55 --- /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 4eb9cf02..5bdb7a1f 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 3b7f4ccc..115df400 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 3a8ea55d..fe4f4f5c 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 caffe137..f9a621f6 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"