Files
anything-llm/frontend/src/components/EmbeddingSelection/LemonadeOptions/index.jsx
Marcello Fitton 0bfd27c6df feat: add optional API key support for Lemonade provider (#5281)
* add API key param to Lemonade LLM Provider and Embedding Provider

* add LEMONADE_LLM_API_KEY to .env.example

* add api key to aibitat provider

* fix api key from being sent to frontend

* fix tooltip id

* add null fallback for `apiKey`

* remove console log

* add missing api keys

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>
2026-03-30 14:44:12 -07:00

253 lines
8.9 KiB
JavaScript

import React, { useEffect, useState } from "react";
import System from "@/models/system";
import { LEMONADE_COMMON_URLS } from "@/utils/constants";
import { CaretDown, CaretUp, Info, CircleNotch } from "@phosphor-icons/react";
import { Tooltip } from "react-tooltip";
import useProviderEndpointAutoDiscovery from "@/hooks/useProviderEndpointAutoDiscovery";
import { cleanBasePath } from "@/components/LLMSelection/LemonadeOptions";
export default function LemonadeEmbeddingOptions({ settings }) {
const {
autoDetecting: loading,
basePath,
basePathValue,
showAdvancedControls,
setShowAdvancedControls,
handleAutoDetectClick,
} = useProviderEndpointAutoDiscovery({
provider: "lemonade",
initialBasePath: settings?.EmbeddingBasePath,
ENDPOINTS: LEMONADE_COMMON_URLS,
});
const [maxChunkLength, setMaxChunkLength] = useState(
settings?.EmbeddingModelMaxChunkLength || 8192
);
const handleMaxChunkLengthChange = (e) => {
setMaxChunkLength(Number(e.target.value));
};
return (
<div className="w-full flex flex-col gap-y-7">
<div className="w-full flex items-start gap-[36px] mt-1.5">
<LemonadeModelSelection settings={settings} basePath={basePath.value} />
<div className="flex flex-col w-60">
<div
data-tooltip-place="top"
data-tooltip-id="max-embedding-chunk-length-tooltip"
className="flex gap-x-1 items-center mb-3"
>
<label className="text-white text-sm font-semibold block">
Max embedding chunk length
</label>
<Info
size={16}
className="text-theme-text-secondary cursor-pointer"
/>
<Tooltip id="max-embedding-chunk-length-tooltip">
Maximum length of text chunks, in characters, for embedding.
</Tooltip>
</div>
<input
type="number"
name="EmbeddingModelMaxChunkLength"
className="border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
placeholder="8192"
min={1}
value={maxChunkLength}
onChange={handleMaxChunkLengthChange}
onScroll={(e) => e.target.blur()}
required={true}
autoComplete="off"
/>
</div>
<div className="flex flex-col w-60">
<div
data-tooltip-place="top"
data-tooltip-id="lemonade-embedding-api-key"
className="flex gap-x-1 items-center mb-3"
>
<label className="text-white text-sm font-semibold block">
API Key (optional)
</label>
<Info
size={16}
className="text-theme-text-secondary cursor-pointer"
/>
<Tooltip id="lemonade-embedding-api-key">
The API key for your Lemonade instance
</Tooltip>
</div>
<input
type="password"
name="LemonadeLLMApiKey"
defaultValue={settings?.LemonadeLLMApiKey ? "*".repeat(20) : ""}
className="border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
autoComplete="off"
/>
</div>
</div>
<div className="flex justify-start mt-4">
<button
onClick={(e) => {
e.preventDefault();
setShowAdvancedControls(!showAdvancedControls);
}}
className="border-none text-theme-text-primary hover:text-theme-text-secondary flex items-center text-sm"
>
{showAdvancedControls ? "Hide" : "Show"} Manual Endpoint Input
{showAdvancedControls ? (
<CaretUp size={14} className="ml-1" />
) : (
<CaretDown size={14} className="ml-1" />
)}
</button>
</div>
<div hidden={!showAdvancedControls}>
<div className="w-full flex items-start gap-4">
<div className="flex flex-col w-[300px]">
<div className="flex justify-between items-center mb-2">
<div className="flex items-center gap-1">
<label className="text-white text-sm font-semibold">
Lemonade Base URL
</label>
<Info
size={18}
className="text-theme-text-secondary cursor-pointer"
data-tooltip-id="lemonade-base-url"
data-tooltip-content="Enter the URL where Lemonade is running."
/>
<Tooltip
id="lemonade-base-url"
place="top"
delayShow={300}
className="tooltip !text-xs !opacity-100"
style={{
maxWidth: "250px",
whiteSpace: "normal",
wordWrap: "break-word",
}}
/>
</div>
{loading ? (
<CircleNotch
size={16}
className="text-theme-text-secondary animate-spin"
/>
) : (
<>
{!basePathValue.value && (
<button
onClick={handleAutoDetectClick}
className="border-none bg-primary-button text-xs font-medium px-2 py-1 rounded-lg hover:bg-secondary hover:text-white shadow-[0_4px_14px_rgba(0,0,0,0.25)]"
>
Auto-Detect
</button>
)}
</>
)}
</div>
<input
type="url"
name="EmbeddingBasePath"
className="border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
placeholder="http://localhost:8000/live"
value={cleanBasePath(basePathValue.value)}
required={true}
autoComplete="off"
spellCheck={false}
onChange={basePath.onChange}
onBlur={basePath.onBlur}
/>
</div>
</div>
</div>
</div>
);
}
function LemonadeModelSelection({ settings, basePath = null }) {
const [customModels, setCustomModels] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function findCustomModels() {
if (!basePath) {
setCustomModels([]);
setLoading(false);
return;
}
setLoading(true);
try {
const { models } = await System.customModels(
"lemonade-embedder",
null,
basePath
);
setCustomModels(models || []);
} catch (error) {
console.error("Failed to fetch custom models:", error);
setCustomModels([]);
}
setLoading(false);
}
findCustomModels();
}, [basePath]);
if (loading || customModels.length == 0) {
return (
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-2">
Lemonade Embedding Model
</label>
<select
name="EmbeddingModelPref"
disabled={true}
className="border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
>
<option disabled={true} selected={true}>
{!!basePath
? "--loading available models--"
: "Enter Lemonade URL first"}
</option>
</select>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60 mt-2">
Select the Lemonade model for embeddings. Models will load after
entering a valid Lemonade URL.
</p>
</div>
);
}
return (
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-3">
Lemonade Embedding Model
</label>
<select
name="EmbeddingModelPref"
required={true}
className="border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
>
{customModels.length > 0 && (
<optgroup label="Your loaded models">
{customModels.map((model) => {
return (
<option
key={model.id}
value={model.id}
selected={settings.EmbeddingModelPref === model.id}
>
{model.id}
</option>
);
})}
</optgroup>
)}
</select>
</div>
);
}