@@ -144,6 +147,162 @@ export default function LocalAiOptions({ settings, showAlert = false }) {
);
}
+const EMPTY_CONNECTION = {
+ name: "",
+ base_url: "http://localhost:8080/v1",
+ api_key: "",
+ model: "",
+ token_limit: 4096,
+};
+
+function LocalAiConnectionManager() {
+ const [connections, setConnections] = useState([]);
+ const [selectedId, setSelectedId] = useState("");
+ const [draft, setDraft] = useState(EMPTY_CONNECTION);
+ const [saving, setSaving] = useState(false);
+
+ async function refresh() {
+ setConnections(await LocalAiConnection.all());
+ }
+
+ useEffect(() => {
+ refresh();
+ }, []);
+
+ function updateDraft(field, value) {
+ setDraft((current) => ({ ...current, [field]: value }));
+ }
+
+ function selectConnection(id) {
+ setSelectedId(id);
+ const connection = connections.find((item) => String(item.id) === id);
+ setDraft(
+ connection ? { ...connection, api_key: "" } : { ...EMPTY_CONNECTION }
+ );
+ }
+
+ async function saveConnection() {
+ setSaving(true);
+ const data = {
+ name: draft.name,
+ base_url: draft.base_url,
+ model: draft.model,
+ token_limit: Number(draft.token_limit),
+ ...(draft.api_key ? { api_key: draft.api_key } : {}),
+ };
+ const result = selectedId
+ ? await LocalAiConnection.update(selectedId, data)
+ : await LocalAiConnection.create({ ...data, api_key: draft.api_key });
+ setSaving(false);
+ if (result.error) return showToast(result.error, "error");
+
+ await refresh();
+ setSelectedId(String(result.connection.id));
+ setDraft({ ...result.connection, api_key: "" });
+ showToast("LocalAI connection saved.", "success");
+ }
+
+ async function deleteConnection() {
+ if (!selectedId || !window.confirm("Delete this LocalAI connection?"))
+ return;
+ const result = await LocalAiConnection.delete(selectedId);
+ if (!result.success) return showToast(result.error, "error");
+ await refresh();
+ setSelectedId("");
+ setDraft({ ...EMPTY_CONNECTION });
+ showToast("LocalAI connection deleted.", "success");
+ }
+
+ return (
+
+
+
Saved connections
+
+ Workspaces and model routers can use these LocalAI endpoints
+ concurrently.
+
+
+
+
+ updateDraft("name", value)}
+ />
+ updateDraft("base_url", value)}
+ />
+ updateDraft("model", value)}
+ />
+ updateDraft("token_limit", value)}
+ />
+ updateDraft("api_key", value)}
+ />
+
+
+
+ {selectedId && (
+
+ )}
+
+
+ );
+}
+
+function ConnectionInput({ label, type = "text", value, onChange }) {
+ return (
+
+ );
+}
+
function LocalAIModelSelection({ settings, basePath = null, apiKey = null }) {
const [customModels, setCustomModels] = useState([]);
const [loading, setLoading] = useState(true);
diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/LLMSelector/ChatModelSelection/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/LLMSelector/ChatModelSelection/index.jsx
index 839011de4dc..e898e938e83 100644
--- a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/LLMSelector/ChatModelSelection/index.jsx
+++ b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/LLMSelector/ChatModelSelection/index.jsx
@@ -4,12 +4,13 @@ import useGetProviderModels, {
export default function ChatModelSelection({
provider,
+ connectionId,
setHasChanges,
selectedLLMModel,
setSelectedLLMModel,
}) {
const { defaultModels, customModels, loading, downloadedModels } =
- useGetProviderModels(provider);
+ useGetProviderModels(provider, connectionId);
if (DISABLED_PROVIDERS.includes(provider)) return null;
if (loading) {
diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/LLMSelector/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/LLMSelector/index.jsx
index 44b6527497a..3dc8bf44193 100644
--- a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/LLMSelector/index.jsx
+++ b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/LLMSelector/index.jsx
@@ -16,6 +16,7 @@ import { NoSetupWarning } from "./SetupProvider";
import showToast from "@/utils/toast";
import Workspace from "@/models/workspace";
import System from "@/models/system";
+import LocalAiConnectionSelector from "@/components/LLMSelection/LocalAiConnectionSelector";
export default function LLMSelectorModal({
workspaceSlug = null,
@@ -28,6 +29,7 @@ export default function LLMSelectorModal({
const [settings, setSettings] = useState(null);
const [selectedLLMProvider, setSelectedLLMProvider] = useState(null);
const [selectedLLMModel, setSelectedLLMModel] = useState("");
+ const [selectedConnectionId, setSelectedConnectionId] = useState("");
const [selectedRouterId, setSelectedRouterId] = useState(null);
const [availableProviders, setAvailableProviders] = useState(
WORKSPACE_LLM_PROVIDERS
@@ -50,6 +52,7 @@ export default function LLMSelectorModal({
setSelectedLLMProvider(providerToSelect);
autoScrollToSelectedLLMProvider(providerToSelect);
setSelectedLLMModel(savedModel);
+ setSelectedConnectionId(workspace.chatConnectionId || "");
setSelectedRouterId(
workspace.router_id || systemSettings?.ModelRouterId || null
);
@@ -78,6 +81,7 @@ export default function LLMSelectorModal({
autoScrollToSelectedLLMProvider(provider, 50);
document.getElementById("llm-search-input").value = "";
setHasChanges(true);
+ if (provider !== "localai") setSelectedConnectionId("");
setMissingCredentials(hasMissingCredentials(settings, provider));
}
@@ -95,6 +99,10 @@ export default function LLMSelectorModal({
: {
chatProvider: selectedLLMProvider,
chatModel: validatedModelSelection(selectedLLMModel),
+ chatConnectionId:
+ selectedLLMProvider === "localai"
+ ? selectedConnectionId || null
+ : null,
};
if (!isRouter && !updateData.chatModel)
@@ -160,12 +168,28 @@ export default function LLMSelectorModal({
setHasChanges={setHasChanges}
/>
) : (
-
+
+ {selectedLLMProvider === "localai" && (
+ {
+ setSelectedConnectionId(value);
+ setHasChanges(true);
+ }}
+ onConnectionChange={(connection) => {
+ if (connection) setSelectedLLMModel(connection.model);
+ }}
+ className="bg-zinc-900 light:bg-white text-white light:text-slate-900 text-sm rounded-lg h-8 w-full px-2.5 outline-none border border-zinc-900 light:border-slate-400 cursor-pointer"
+ />
+ )}
+
+
))}