mirror of
https://github.com/Mintplex-Labs/anything-llm.git
synced 2024-11-19 12:40:09 +01:00
merge with master
This commit is contained in:
commit
3678365fca
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@ -5,6 +5,7 @@
|
|||||||
"AIbitat",
|
"AIbitat",
|
||||||
"allm",
|
"allm",
|
||||||
"anythingllm",
|
"anythingllm",
|
||||||
|
"Apipie",
|
||||||
"Astra",
|
"Astra",
|
||||||
"Chartable",
|
"Chartable",
|
||||||
"cleancss",
|
"cleancss",
|
||||||
@ -18,6 +19,7 @@
|
|||||||
"elevenlabs",
|
"elevenlabs",
|
||||||
"Embeddable",
|
"Embeddable",
|
||||||
"epub",
|
"epub",
|
||||||
|
"fireworksai",
|
||||||
"GROQ",
|
"GROQ",
|
||||||
"hljs",
|
"hljs",
|
||||||
"huggingface",
|
"huggingface",
|
||||||
@ -40,14 +42,13 @@
|
|||||||
"pagerender",
|
"pagerender",
|
||||||
"Qdrant",
|
"Qdrant",
|
||||||
"royalblue",
|
"royalblue",
|
||||||
"searxng",
|
|
||||||
"SearchApi",
|
"SearchApi",
|
||||||
|
"searxng",
|
||||||
"Serper",
|
"Serper",
|
||||||
"Serply",
|
"Serply",
|
||||||
"streamable",
|
"streamable",
|
||||||
"textgenwebui",
|
"textgenwebui",
|
||||||
"togetherai",
|
"togetherai",
|
||||||
"fireworksai",
|
|
||||||
"Unembed",
|
"Unembed",
|
||||||
"vectordbs",
|
"vectordbs",
|
||||||
"Weaviate",
|
"Weaviate",
|
||||||
|
@ -105,6 +105,10 @@ GID='1000'
|
|||||||
# FIREWORKS_AI_LLM_API_KEY='my-fireworks-ai-key'
|
# FIREWORKS_AI_LLM_API_KEY='my-fireworks-ai-key'
|
||||||
# FIREWORKS_AI_LLM_MODEL_PREF='accounts/fireworks/models/llama-v3p1-8b-instruct'
|
# FIREWORKS_AI_LLM_MODEL_PREF='accounts/fireworks/models/llama-v3p1-8b-instruct'
|
||||||
|
|
||||||
|
# LLM_PROVIDER='apipie'
|
||||||
|
# APIPIE_LLM_API_KEY='sk-123abc'
|
||||||
|
# APIPIE_LLM_MODEL_PREF='openrouter/llama-3.1-8b-instruct'
|
||||||
|
|
||||||
###########################################
|
###########################################
|
||||||
######## Embedding API SElECTION ##########
|
######## Embedding API SElECTION ##########
|
||||||
###########################################
|
###########################################
|
||||||
|
@ -23,7 +23,6 @@ const WorkspaceChat = lazy(() => import("@/pages/WorkspaceChat"));
|
|||||||
const AdminUsers = lazy(() => import("@/pages/Admin/Users"));
|
const AdminUsers = lazy(() => import("@/pages/Admin/Users"));
|
||||||
const AdminInvites = lazy(() => import("@/pages/Admin/Invitations"));
|
const AdminInvites = lazy(() => import("@/pages/Admin/Invitations"));
|
||||||
const AdminWorkspaces = lazy(() => import("@/pages/Admin/Workspaces"));
|
const AdminWorkspaces = lazy(() => import("@/pages/Admin/Workspaces"));
|
||||||
const AdminSystem = lazy(() => import("@/pages/Admin/System"));
|
|
||||||
const AdminLogs = lazy(() => import("@/pages/Admin/Logging"));
|
const AdminLogs = lazy(() => import("@/pages/Admin/Logging"));
|
||||||
const AdminAgents = lazy(() => import("@/pages/Admin/Agents"));
|
const AdminAgents = lazy(() => import("@/pages/Admin/Agents"));
|
||||||
const GeneralChats = lazy(() => import("@/pages/GeneralSettings/Chats"));
|
const GeneralChats = lazy(() => import("@/pages/GeneralSettings/Chats"));
|
||||||
@ -172,10 +171,6 @@ export default function App() {
|
|||||||
path="/settings/workspace-chats"
|
path="/settings/workspace-chats"
|
||||||
element={<ManagerRoute Component={GeneralChats} />}
|
element={<ManagerRoute Component={GeneralChats} />}
|
||||||
/>
|
/>
|
||||||
<Route
|
|
||||||
path="/settings/system-preferences"
|
|
||||||
element={<ManagerRoute Component={AdminSystem} />}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
path="/settings/invites"
|
path="/settings/invites"
|
||||||
element={<ManagerRoute Component={AdminInvites} />}
|
element={<ManagerRoute Component={AdminInvites} />}
|
||||||
|
101
frontend/src/components/LLMSelection/ApiPieOptions/index.jsx
Normal file
101
frontend/src/components/LLMSelection/ApiPieOptions/index.jsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import System from "@/models/system";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
export default function ApiPieLLMOptions({ settings }) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-y-4 mt-1.5">
|
||||||
|
<div className="flex gap-[36px]">
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-3">
|
||||||
|
APIpie API Key
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="ApipieLLMApiKey"
|
||||||
|
className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||||
|
placeholder="APIpie API Key"
|
||||||
|
defaultValue={settings?.ApipieLLMApiKey ? "*".repeat(20) : ""}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{!settings?.credentialsOnly && (
|
||||||
|
<APIPieModelSelection settings={settings} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function APIPieModelSelection({ settings }) {
|
||||||
|
const [groupedModels, setGroupedModels] = useState({});
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function findCustomModels() {
|
||||||
|
setLoading(true);
|
||||||
|
const { models } = await System.customModels("apipie");
|
||||||
|
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-3">
|
||||||
|
Chat Model Selection
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
name="ApipieLLMModelPref"
|
||||||
|
disabled={true}
|
||||||
|
className="bg-zinc-900 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-3">
|
||||||
|
Chat Model Selection
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
name="ApipieLLMModelPref"
|
||||||
|
required={true}
|
||||||
|
className="bg-zinc-900 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?.ApipieLLMModelPref === model.id}
|
||||||
|
>
|
||||||
|
{model.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</optgroup>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -31,7 +31,7 @@ export default function FileRow({ item, selected, toggleSelection }) {
|
|||||||
className="shrink-0 text-base font-bold w-4 h-4 mr-[3px]"
|
className="shrink-0 text-base font-bold w-4 h-4 mr-[3px]"
|
||||||
weight="fill"
|
weight="fill"
|
||||||
/>
|
/>
|
||||||
<p className="whitespace-nowrap overflow-hidden text-ellipsis">
|
<p className="whitespace-nowrap overflow-hidden text-ellipsis max-w-[400px]">
|
||||||
{middleTruncate(item.title, 55)}
|
{middleTruncate(item.title, 55)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -51,7 +51,7 @@ export default function FolderRow({
|
|||||||
className="shrink-0 text-base font-bold w-4 h-4 mr-[3px]"
|
className="shrink-0 text-base font-bold w-4 h-4 mr-[3px]"
|
||||||
weight="fill"
|
weight="fill"
|
||||||
/>
|
/>
|
||||||
<p className="whitespace-nowrap overflow-show">
|
<p className="whitespace-nowrap overflow-show max-w-[400px]">
|
||||||
{middleTruncate(item.name, 35)}
|
{middleTruncate(item.name, 35)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -83,7 +83,7 @@ export default function WorkspaceFileRow({
|
|||||||
className="shrink-0 text-base font-bold w-4 h-4 mr-[3px] ml-1"
|
className="shrink-0 text-base font-bold w-4 h-4 mr-[3px] ml-1"
|
||||||
weight="fill"
|
weight="fill"
|
||||||
/>
|
/>
|
||||||
<p className="whitespace-nowrap overflow-hidden text-ellipsis">
|
<p className="whitespace-nowrap overflow-hidden text-ellipsis max-w-[400px]">
|
||||||
{middleTruncate(item.title, 50)}
|
{middleTruncate(item.title, 50)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -29,10 +29,8 @@ export default function SettingsButton() {
|
|||||||
return (
|
return (
|
||||||
<ToolTipWrapper id="open-settings">
|
<ToolTipWrapper id="open-settings">
|
||||||
<Link
|
<Link
|
||||||
to={
|
to={paths.settings.appearance()}
|
||||||
!!user?.role ? paths.settings.system() : paths.settings.appearance()
|
className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
|
||||||
}
|
|
||||||
className="transition-all duration-300 p-2 rounded-full text-white bg-theme-sidebar-footer-icon hover:bg-theme-sidebar-footer-icon-hover"
|
|
||||||
aria-label="Settings"
|
aria-label="Settings"
|
||||||
data-tooltip-id="open-settings"
|
data-tooltip-id="open-settings"
|
||||||
data-tooltip-content="Open settings"
|
data-tooltip-content="Open settings"
|
||||||
|
@ -277,11 +277,6 @@ const SidebarOptions = ({ user = null, t }) => (
|
|||||||
href: paths.settings.invites(),
|
href: paths.settings.invites(),
|
||||||
roles: ["admin", "manager"],
|
roles: ["admin", "manager"],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
btnText: t("settings.system"),
|
|
||||||
href: paths.settings.system(),
|
|
||||||
roles: ["admin", "manager"],
|
|
||||||
},
|
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Option
|
<Option
|
||||||
|
@ -122,9 +122,22 @@ export default function PromptInput({
|
|||||||
|
|
||||||
const pasteText = e.clipboardData.getData("text/plain");
|
const pasteText = e.clipboardData.getData("text/plain");
|
||||||
if (pasteText) {
|
if (pasteText) {
|
||||||
const newPromptInput = promptInput + pasteText.trim();
|
const textarea = textareaRef.current;
|
||||||
|
const start = textarea.selectionStart;
|
||||||
|
const end = textarea.selectionEnd;
|
||||||
|
const newPromptInput =
|
||||||
|
promptInput.substring(0, start) +
|
||||||
|
pasteText +
|
||||||
|
promptInput.substring(end);
|
||||||
setPromptInput(newPromptInput);
|
setPromptInput(newPromptInput);
|
||||||
onChange({ target: { value: newPromptInput } });
|
onChange({ target: { value: newPromptInput } });
|
||||||
|
|
||||||
|
// Set the cursor position after the pasted text
|
||||||
|
// we need to use setTimeout to prevent the cursor from being set to the end of the text
|
||||||
|
setTimeout(() => {
|
||||||
|
textarea.selectionStart = textarea.selectionEnd =
|
||||||
|
start + pasteText.length;
|
||||||
|
}, 0);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
BIN
frontend/src/media/llmprovider/apipie.png
Normal file
BIN
frontend/src/media/llmprovider/apipie.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
@ -1,128 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import Sidebar from "@/components/SettingsSidebar";
|
|
||||||
import { isMobile } from "react-device-detect";
|
|
||||||
import Admin from "@/models/admin";
|
|
||||||
import showToast from "@/utils/toast";
|
|
||||||
import CTAButton from "@/components/lib/CTAButton";
|
|
||||||
|
|
||||||
export default function AdminSystem() {
|
|
||||||
const [saving, setSaving] = useState(false);
|
|
||||||
const [hasChanges, setHasChanges] = useState(false);
|
|
||||||
const [messageLimit, setMessageLimit] = useState({
|
|
||||||
enabled: false,
|
|
||||||
limit: 10,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setSaving(true);
|
|
||||||
await Admin.updateSystemPreferences({
|
|
||||||
limit_user_messages: messageLimit.enabled,
|
|
||||||
message_limit: messageLimit.limit,
|
|
||||||
});
|
|
||||||
setSaving(false);
|
|
||||||
setHasChanges(false);
|
|
||||||
showToast("System preferences updated successfully.", "success");
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function fetchSettings() {
|
|
||||||
const settings = (await Admin.systemPreferences())?.settings;
|
|
||||||
if (!settings) return;
|
|
||||||
setMessageLimit({
|
|
||||||
enabled: settings.limit_user_messages,
|
|
||||||
limit: settings.message_limit,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
fetchSettings();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-screen h-screen overflow-hidden bg-theme-bg-container flex">
|
|
||||||
<Sidebar />
|
|
||||||
<div
|
|
||||||
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
|
||||||
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0"
|
|
||||||
>
|
|
||||||
<form
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
onChange={() => setHasChanges(true)}
|
|
||||||
className="flex flex-col w-full px-1 md:pl-6 md:pr-[50px] md:py-6 py-16"
|
|
||||||
>
|
|
||||||
<div className="w-full flex flex-col gap-y-1 pb-6 border-white/10 border-b-2">
|
|
||||||
<div className="items-center">
|
|
||||||
<p className="text-lg leading-6 font-bold text-white">
|
|
||||||
System Preferences
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
|
|
||||||
These are the overall settings and configurations of your
|
|
||||||
instance.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{hasChanges && (
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<CTAButton onClick={handleSubmit} className="mt-3 mr-0">
|
|
||||||
{saving ? "Saving..." : "Save changes"}
|
|
||||||
</CTAButton>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="mt-4 mb-8">
|
|
||||||
<div className="flex flex-col gap-y-1">
|
|
||||||
<h2 className="text-base leading-6 font-bold text-white">
|
|
||||||
Limit messages per user per day
|
|
||||||
</h2>
|
|
||||||
<p className="text-xs leading-[18px] font-base text-white/60">
|
|
||||||
Restrict non-admin users to a number of successful queries or
|
|
||||||
chats within a 24 hour window. Enable this to prevent users from
|
|
||||||
running up OpenAI costs.
|
|
||||||
</p>
|
|
||||||
<div className="mt-2">
|
|
||||||
<label className="relative inline-flex cursor-pointer items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
name="limit_user_messages"
|
|
||||||
value="yes"
|
|
||||||
checked={messageLimit.enabled}
|
|
||||||
onChange={(e) => {
|
|
||||||
setMessageLimit({
|
|
||||||
...messageLimit,
|
|
||||||
enabled: e.target.checked,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
className="peer sr-only"
|
|
||||||
/>
|
|
||||||
<div className="pointer-events-none peer h-6 w-11 rounded-full bg-stone-400 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border after:border-gray-600 after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-lime-300 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800"></div>
|
|
||||||
<span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"></span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{messageLimit.enabled && (
|
|
||||||
<div className="mt-4">
|
|
||||||
<label className="text-white text-sm font-semibold block mb-4">
|
|
||||||
Message limit per day
|
|
||||||
</label>
|
|
||||||
<div className="relative mt-2">
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
name="message_limit"
|
|
||||||
onScroll={(e) => e.target.blur()}
|
|
||||||
onChange={(e) => {
|
|
||||||
setMessageLimit({
|
|
||||||
enabled: true,
|
|
||||||
limit: Number(e?.target?.value || 0),
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
value={messageLimit.limit}
|
|
||||||
min={1}
|
|
||||||
className="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-60 p-2.5"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -2,11 +2,15 @@ import React, { useState } from "react";
|
|||||||
import { X } from "@phosphor-icons/react";
|
import { X } from "@phosphor-icons/react";
|
||||||
import Admin from "@/models/admin";
|
import Admin from "@/models/admin";
|
||||||
import { userFromStorage } from "@/utils/request";
|
import { userFromStorage } from "@/utils/request";
|
||||||
import { RoleHintDisplay } from "..";
|
import { MessageLimitInput, RoleHintDisplay } from "..";
|
||||||
|
|
||||||
export default function NewUserModal({ closeModal }) {
|
export default function NewUserModal({ closeModal }) {
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [role, setRole] = useState("default");
|
const [role, setRole] = useState("default");
|
||||||
|
const [messageLimit, setMessageLimit] = useState({
|
||||||
|
enabled: false,
|
||||||
|
limit: 10,
|
||||||
|
});
|
||||||
|
|
||||||
const handleCreate = async (e) => {
|
const handleCreate = async (e) => {
|
||||||
setError(null);
|
setError(null);
|
||||||
@ -14,6 +18,8 @@ export default function NewUserModal({ closeModal }) {
|
|||||||
const data = {};
|
const data = {};
|
||||||
const form = new FormData(e.target);
|
const form = new FormData(e.target);
|
||||||
for (var [key, value] of form.entries()) data[key] = value;
|
for (var [key, value] of form.entries()) data[key] = value;
|
||||||
|
data.dailyMessageLimit = messageLimit.enabled ? messageLimit.limit : null;
|
||||||
|
|
||||||
const { user, error } = await Admin.newUser(data);
|
const { user, error } = await Admin.newUser(data);
|
||||||
if (!!user) window.location.reload();
|
if (!!user) window.location.reload();
|
||||||
setError(error);
|
setError(error);
|
||||||
@ -59,13 +65,13 @@ export default function NewUserModal({ closeModal }) {
|
|||||||
pattern="^[a-z0-9_-]+$"
|
pattern="^[a-z0-9_-]+$"
|
||||||
onInvalid={(e) =>
|
onInvalid={(e) =>
|
||||||
e.target.setCustomValidity(
|
e.target.setCustomValidity(
|
||||||
"Username must be only contain lowercase letters, numbers, underscores, and hyphens with no spaces"
|
"Username must only contain lowercase letters, numbers, underscores, and hyphens with no spaces"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
onChange={(e) => e.target.setCustomValidity("")}
|
onChange={(e) => e.target.setCustomValidity("")}
|
||||||
/>
|
/>
|
||||||
<p className="mt-2 text-xs text-white/60">
|
<p className="mt-2 text-xs text-white/60">
|
||||||
Username must be only contain lowercase letters, numbers,
|
Username must only contain lowercase letters, numbers,
|
||||||
underscores, and hyphens with no spaces
|
underscores, and hyphens with no spaces
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -111,6 +117,12 @@ export default function NewUserModal({ closeModal }) {
|
|||||||
</select>
|
</select>
|
||||||
<RoleHintDisplay role={role} />
|
<RoleHintDisplay role={role} />
|
||||||
</div>
|
</div>
|
||||||
|
<MessageLimitInput
|
||||||
|
role={role}
|
||||||
|
enabled={messageLimit.enabled}
|
||||||
|
limit={messageLimit.limit}
|
||||||
|
updateState={setMessageLimit}
|
||||||
|
/>
|
||||||
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
|
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
|
||||||
<p className="text-white text-xs md:text-sm">
|
<p className="text-white text-xs md:text-sm">
|
||||||
After creating a user they will need to login with their initial
|
After creating a user they will need to login with their initial
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { X } from "@phosphor-icons/react";
|
import { X } from "@phosphor-icons/react";
|
||||||
import Admin from "@/models/admin";
|
import Admin from "@/models/admin";
|
||||||
import { RoleHintDisplay } from "../..";
|
import { MessageLimitInput, RoleHintDisplay } from "../..";
|
||||||
|
|
||||||
export default function EditUserModal({ currentUser, user, closeModal }) {
|
export default function EditUserModal({ currentUser, user, closeModal }) {
|
||||||
const [role, setRole] = useState(user.role);
|
const [role, setRole] = useState(user.role);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
const [messageLimit, setMessageLimit] = useState({
|
||||||
|
enabled: user.dailyMessageLimit !== null,
|
||||||
|
limit: user.dailyMessageLimit || 10,
|
||||||
|
});
|
||||||
|
|
||||||
const handleUpdate = async (e) => {
|
const handleUpdate = async (e) => {
|
||||||
setError(null);
|
setError(null);
|
||||||
@ -16,6 +20,12 @@ export default function EditUserModal({ currentUser, user, closeModal }) {
|
|||||||
if (!value || value === null) continue;
|
if (!value || value === null) continue;
|
||||||
data[key] = value;
|
data[key] = value;
|
||||||
}
|
}
|
||||||
|
if (messageLimit.enabled) {
|
||||||
|
data.dailyMessageLimit = messageLimit.limit;
|
||||||
|
} else {
|
||||||
|
data.dailyMessageLimit = null;
|
||||||
|
}
|
||||||
|
|
||||||
const { success, error } = await Admin.updateUser(user.id, data);
|
const { success, error } = await Admin.updateUser(user.id, data);
|
||||||
if (success) window.location.reload();
|
if (success) window.location.reload();
|
||||||
setError(error);
|
setError(error);
|
||||||
@ -59,7 +69,7 @@ export default function EditUserModal({ currentUser, user, closeModal }) {
|
|||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
<p className="mt-2 text-xs text-white/60">
|
<p className="mt-2 text-xs text-white/60">
|
||||||
Username must be only contain lowercase letters, numbers,
|
Username must only contain lowercase letters, numbers,
|
||||||
underscores, and hyphens with no spaces
|
underscores, and hyphens with no spaces
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -104,6 +114,12 @@ export default function EditUserModal({ currentUser, user, closeModal }) {
|
|||||||
</select>
|
</select>
|
||||||
<RoleHintDisplay role={role} />
|
<RoleHintDisplay role={role} />
|
||||||
</div>
|
</div>
|
||||||
|
<MessageLimitInput
|
||||||
|
role={role}
|
||||||
|
enabled={messageLimit.enabled}
|
||||||
|
limit={messageLimit.limit}
|
||||||
|
updateState={setMessageLimit}
|
||||||
|
/>
|
||||||
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
|
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center mt-6 pt-6 border-t border-theme-modal-border">
|
<div className="flex justify-between items-center mt-6 pt-6 border-t border-theme-modal-border">
|
||||||
|
@ -142,3 +142,58 @@ export function RoleHintDisplay({ role }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function MessageLimitInput({ enabled, limit, updateState, role }) {
|
||||||
|
if (role === "admin") return null;
|
||||||
|
return (
|
||||||
|
<div className="mt-4 mb-8">
|
||||||
|
<div className="flex flex-col gap-y-1">
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<h2 className="text-base leading-6 font-bold text-white">
|
||||||
|
Limit messages per day
|
||||||
|
</h2>
|
||||||
|
<label className="relative inline-flex cursor-pointer items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={enabled}
|
||||||
|
onChange={(e) => {
|
||||||
|
updateState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
enabled: e.target.checked,
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
className="peer sr-only"
|
||||||
|
/>
|
||||||
|
<div className="pointer-events-none peer h-6 w-11 rounded-full bg-stone-400 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border after:border-gray-600 after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-lime-300 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800"></div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs leading-[18px] font-base text-white/60">
|
||||||
|
Restrict this user to a number of successful queries or chats within a
|
||||||
|
24 hour window.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{enabled && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
Message limit per day
|
||||||
|
</label>
|
||||||
|
<div className="relative mt-2">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
onScroll={(e) => e.target.blur()}
|
||||||
|
onChange={(e) => {
|
||||||
|
updateState({
|
||||||
|
enabled: true,
|
||||||
|
limit: Number(e?.target?.value || 0),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
value={limit}
|
||||||
|
min={1}
|
||||||
|
className="bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-60 p-2.5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -26,6 +26,7 @@ import CohereLogo from "@/media/llmprovider/cohere.png";
|
|||||||
import LiteLLMLogo from "@/media/llmprovider/litellm.png";
|
import LiteLLMLogo from "@/media/llmprovider/litellm.png";
|
||||||
import AWSBedrockLogo from "@/media/llmprovider/bedrock.png";
|
import AWSBedrockLogo from "@/media/llmprovider/bedrock.png";
|
||||||
import DeepSeekLogo from "@/media/llmprovider/deepseek.png";
|
import DeepSeekLogo from "@/media/llmprovider/deepseek.png";
|
||||||
|
import APIPieLogo from "@/media/llmprovider/apipie.png";
|
||||||
|
|
||||||
import PreLoader from "@/components/Preloader";
|
import PreLoader from "@/components/Preloader";
|
||||||
import OpenAiOptions from "@/components/LLMSelection/OpenAiOptions";
|
import OpenAiOptions from "@/components/LLMSelection/OpenAiOptions";
|
||||||
@ -50,6 +51,7 @@ import TextGenWebUIOptions from "@/components/LLMSelection/TextGenWebUIOptions";
|
|||||||
import LiteLLMOptions from "@/components/LLMSelection/LiteLLMOptions";
|
import LiteLLMOptions from "@/components/LLMSelection/LiteLLMOptions";
|
||||||
import AWSBedrockLLMOptions from "@/components/LLMSelection/AwsBedrockLLMOptions";
|
import AWSBedrockLLMOptions from "@/components/LLMSelection/AwsBedrockLLMOptions";
|
||||||
import DeepSeekOptions from "@/components/LLMSelection/DeepSeekOptions";
|
import DeepSeekOptions from "@/components/LLMSelection/DeepSeekOptions";
|
||||||
|
import ApiPieLLMOptions from "@/components/LLMSelection/ApiPieOptions";
|
||||||
|
|
||||||
import LLMItem from "@/components/LLMSelection/LLMItem";
|
import LLMItem from "@/components/LLMSelection/LLMItem";
|
||||||
import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react";
|
import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react";
|
||||||
@ -221,6 +223,27 @@ export const AVAILABLE_LLM_PROVIDERS = [
|
|||||||
description: "Run DeepSeek's powerful LLMs.",
|
description: "Run DeepSeek's powerful LLMs.",
|
||||||
requiredConfig: ["DeepSeekApiKey"],
|
requiredConfig: ["DeepSeekApiKey"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "AWS Bedrock",
|
||||||
|
value: "bedrock",
|
||||||
|
logo: AWSBedrockLogo,
|
||||||
|
options: (settings) => <AWSBedrockLLMOptions settings={settings} />,
|
||||||
|
description: "Run powerful foundation models privately with AWS Bedrock.",
|
||||||
|
requiredConfig: [
|
||||||
|
"AwsBedrockLLMAccessKeyId",
|
||||||
|
"AwsBedrockLLMAccessKey",
|
||||||
|
"AwsBedrockLLMRegion",
|
||||||
|
"AwsBedrockLLMModel",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "APIpie",
|
||||||
|
value: "apipie",
|
||||||
|
logo: APIPieLogo,
|
||||||
|
options: (settings) => <ApiPieLLMOptions settings={settings} />,
|
||||||
|
description: "A unified API of AI services from leading providers",
|
||||||
|
requiredConfig: ["ApipieLLMApiKey", "ApipieLLMModelPref"],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Generic OpenAI",
|
name: "Generic OpenAI",
|
||||||
value: "generic-openai",
|
value: "generic-openai",
|
||||||
@ -235,19 +258,6 @@ export const AVAILABLE_LLM_PROVIDERS = [
|
|||||||
"GenericOpenAiKey",
|
"GenericOpenAiKey",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "AWS Bedrock",
|
|
||||||
value: "bedrock",
|
|
||||||
logo: AWSBedrockLogo,
|
|
||||||
options: (settings) => <AWSBedrockLLMOptions settings={settings} />,
|
|
||||||
description: "Run powerful foundation models privately with AWS Bedrock.",
|
|
||||||
requiredConfig: [
|
|
||||||
"AwsBedrockLLMAccessKeyId",
|
|
||||||
"AwsBedrockLLMAccessKey",
|
|
||||||
"AwsBedrockLLMRegion",
|
|
||||||
"AwsBedrockLLMModel",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "Native",
|
name: "Native",
|
||||||
value: "native",
|
value: "native",
|
||||||
|
@ -21,6 +21,7 @@ import TextGenWebUILogo from "@/media/llmprovider/text-generation-webui.png";
|
|||||||
import LiteLLMLogo from "@/media/llmprovider/litellm.png";
|
import LiteLLMLogo from "@/media/llmprovider/litellm.png";
|
||||||
import AWSBedrockLogo from "@/media/llmprovider/bedrock.png";
|
import AWSBedrockLogo from "@/media/llmprovider/bedrock.png";
|
||||||
import DeepSeekLogo from "@/media/llmprovider/deepseek.png";
|
import DeepSeekLogo from "@/media/llmprovider/deepseek.png";
|
||||||
|
import APIPieLogo from "@/media/llmprovider/apipie.png";
|
||||||
|
|
||||||
import CohereLogo from "@/media/llmprovider/cohere.png";
|
import CohereLogo from "@/media/llmprovider/cohere.png";
|
||||||
import ZillizLogo from "@/media/vectordbs/zilliz.png";
|
import ZillizLogo from "@/media/vectordbs/zilliz.png";
|
||||||
@ -202,6 +203,13 @@ export const LLM_SELECTION_PRIVACY = {
|
|||||||
description: ["Your model and chat contents are visible to DeepSeek"],
|
description: ["Your model and chat contents are visible to DeepSeek"],
|
||||||
logo: DeepSeekLogo,
|
logo: DeepSeekLogo,
|
||||||
},
|
},
|
||||||
|
apipie: {
|
||||||
|
name: "APIpie.AI",
|
||||||
|
description: [
|
||||||
|
"Your model and chat contents are visible to APIpie in accordance with their terms of service.",
|
||||||
|
],
|
||||||
|
logo: APIPieLogo,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VECTOR_DB_PRIVACY = {
|
export const VECTOR_DB_PRIVACY = {
|
||||||
|
@ -21,6 +21,7 @@ import TextGenWebUILogo from "@/media/llmprovider/text-generation-webui.png";
|
|||||||
import LiteLLMLogo from "@/media/llmprovider/litellm.png";
|
import LiteLLMLogo from "@/media/llmprovider/litellm.png";
|
||||||
import AWSBedrockLogo from "@/media/llmprovider/bedrock.png";
|
import AWSBedrockLogo from "@/media/llmprovider/bedrock.png";
|
||||||
import DeepSeekLogo from "@/media/llmprovider/deepseek.png";
|
import DeepSeekLogo from "@/media/llmprovider/deepseek.png";
|
||||||
|
import APIPieLogo from "@/media/llmprovider/apipie.png";
|
||||||
|
|
||||||
import CohereLogo from "@/media/llmprovider/cohere.png";
|
import CohereLogo from "@/media/llmprovider/cohere.png";
|
||||||
import OpenAiOptions from "@/components/LLMSelection/OpenAiOptions";
|
import OpenAiOptions from "@/components/LLMSelection/OpenAiOptions";
|
||||||
@ -45,6 +46,7 @@ import TextGenWebUIOptions from "@/components/LLMSelection/TextGenWebUIOptions";
|
|||||||
import LiteLLMOptions from "@/components/LLMSelection/LiteLLMOptions";
|
import LiteLLMOptions from "@/components/LLMSelection/LiteLLMOptions";
|
||||||
import AWSBedrockLLMOptions from "@/components/LLMSelection/AwsBedrockLLMOptions";
|
import AWSBedrockLLMOptions from "@/components/LLMSelection/AwsBedrockLLMOptions";
|
||||||
import DeepSeekOptions from "@/components/LLMSelection/DeepSeekOptions";
|
import DeepSeekOptions from "@/components/LLMSelection/DeepSeekOptions";
|
||||||
|
import ApiPieLLMOptions from "@/components/LLMSelection/ApiPieOptions";
|
||||||
|
|
||||||
import LLMItem from "@/components/LLMSelection/LLMItem";
|
import LLMItem from "@/components/LLMSelection/LLMItem";
|
||||||
import System from "@/models/system";
|
import System from "@/models/system";
|
||||||
@ -195,6 +197,13 @@ const LLMS = [
|
|||||||
options: (settings) => <DeepSeekOptions settings={settings} />,
|
options: (settings) => <DeepSeekOptions settings={settings} />,
|
||||||
description: "Run DeepSeek's powerful LLMs.",
|
description: "Run DeepSeek's powerful LLMs.",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "APIpie",
|
||||||
|
value: "apipie",
|
||||||
|
logo: APIPieLogo,
|
||||||
|
options: (settings) => <ApiPieLLMOptions settings={settings} />,
|
||||||
|
description: "A unified API of AI services from leading providers",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Generic OpenAI",
|
name: "Generic OpenAI",
|
||||||
value: "generic-openai",
|
value: "generic-openai",
|
||||||
|
@ -24,6 +24,8 @@ const ENABLED_PROVIDERS = [
|
|||||||
"bedrock",
|
"bedrock",
|
||||||
"fireworksai",
|
"fireworksai",
|
||||||
"deepseek",
|
"deepseek",
|
||||||
|
"litellm",
|
||||||
|
"apipie",
|
||||||
// TODO: More agent support.
|
// TODO: More agent support.
|
||||||
// "cohere", // Has tool calling and will need to build explicit support
|
// "cohere", // Has tool calling and will need to build explicit support
|
||||||
// "huggingface" // Can be done but already has issues with no-chat templated. Needs to be tested.
|
// "huggingface" // Can be done but already has issues with no-chat templated. Needs to be tested.
|
||||||
|
@ -80,9 +80,6 @@ export default {
|
|||||||
return `/fine-tuning`;
|
return `/fine-tuning`;
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
system: () => {
|
|
||||||
return `/settings/system-preferences`;
|
|
||||||
},
|
|
||||||
users: () => {
|
users: () => {
|
||||||
return `/settings/users`;
|
return `/settings/users`;
|
||||||
},
|
},
|
||||||
|
@ -95,6 +95,10 @@ SIG_SALT='salt' # Please generate random string at least 32 chars long.
|
|||||||
# COHERE_API_KEY=
|
# COHERE_API_KEY=
|
||||||
# COHERE_MODEL_PREF='command-r'
|
# COHERE_MODEL_PREF='command-r'
|
||||||
|
|
||||||
|
# LLM_PROVIDER='apipie'
|
||||||
|
# APIPIE_LLM_API_KEY='sk-123abc'
|
||||||
|
# APIPIE_LLM_MODEL_PREF='openrouter/llama-3.1-8b-instruct'
|
||||||
|
|
||||||
###########################################
|
###########################################
|
||||||
######## Embedding API SElECTION ##########
|
######## Embedding API SElECTION ##########
|
||||||
###########################################
|
###########################################
|
||||||
|
@ -347,14 +347,6 @@ function adminEndpoints(app) {
|
|||||||
: await SystemSettings.get({ label });
|
: await SystemSettings.get({ label });
|
||||||
|
|
||||||
switch (label) {
|
switch (label) {
|
||||||
case "limit_user_messages":
|
|
||||||
requestedSettings[label] = setting?.value === "true";
|
|
||||||
break;
|
|
||||||
case "message_limit":
|
|
||||||
requestedSettings[label] = setting?.value
|
|
||||||
? Number(setting.value)
|
|
||||||
: 10;
|
|
||||||
break;
|
|
||||||
case "footer_data":
|
case "footer_data":
|
||||||
requestedSettings[label] = setting?.value ?? JSON.stringify([]);
|
requestedSettings[label] = setting?.value ?? JSON.stringify([]);
|
||||||
break;
|
break;
|
||||||
@ -422,13 +414,6 @@ function adminEndpoints(app) {
|
|||||||
try {
|
try {
|
||||||
const embedder = getEmbeddingEngineSelection();
|
const embedder = getEmbeddingEngineSelection();
|
||||||
const settings = {
|
const settings = {
|
||||||
limit_user_messages:
|
|
||||||
(await SystemSettings.get({ label: "limit_user_messages" }))
|
|
||||||
?.value === "true",
|
|
||||||
message_limit:
|
|
||||||
Number(
|
|
||||||
(await SystemSettings.get({ label: "message_limit" }))?.value
|
|
||||||
) || 10,
|
|
||||||
footer_data:
|
footer_data:
|
||||||
(await SystemSettings.get({ label: "footer_data" }))?.value ||
|
(await SystemSettings.get({ label: "footer_data" }))?.value ||
|
||||||
JSON.stringify([]),
|
JSON.stringify([]),
|
||||||
|
@ -595,56 +595,6 @@ function apiAdminEndpoints(app) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
app.get("/v1/admin/preferences", [validApiKey], async (request, response) => {
|
|
||||||
/*
|
|
||||||
#swagger.tags = ['Admin']
|
|
||||||
#swagger.description = 'Show all multi-user preferences for instance. Methods are disabled until multi user mode is enabled via the UI.'
|
|
||||||
#swagger.responses[200] = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: {
|
|
||||||
type: 'object',
|
|
||||||
example: {
|
|
||||||
settings: {
|
|
||||||
limit_user_messages: false,
|
|
||||||
message_limit: 10,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#swagger.responses[403] = {
|
|
||||||
schema: {
|
|
||||||
"$ref": "#/definitions/InvalidAPIKey"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#swagger.responses[401] = {
|
|
||||||
description: "Instance is not in Multi-User mode. Method denied",
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
try {
|
|
||||||
if (!multiUserMode(response)) {
|
|
||||||
response.sendStatus(401).end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const settings = {
|
|
||||||
limit_user_messages:
|
|
||||||
(await SystemSettings.get({ label: "limit_user_messages" }))
|
|
||||||
?.value === "true",
|
|
||||||
message_limit:
|
|
||||||
Number(
|
|
||||||
(await SystemSettings.get({ label: "message_limit" }))?.value
|
|
||||||
) || 10,
|
|
||||||
};
|
|
||||||
response.status(200).json({ settings });
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
response.sendStatus(500).end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post(
|
app.post(
|
||||||
"/v1/admin/preferences",
|
"/v1/admin/preferences",
|
||||||
[validApiKey],
|
[validApiKey],
|
||||||
@ -658,8 +608,7 @@ function apiAdminEndpoints(app) {
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
example: {
|
example: {
|
||||||
limit_user_messages: true,
|
support_email: "support@example.com",
|
||||||
message_limit: 5,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
const { v4: uuidv4 } = require("uuid");
|
const { v4: uuidv4 } = require("uuid");
|
||||||
const { reqBody, userFromSession, multiUserMode } = require("../utils/http");
|
const { reqBody, userFromSession, multiUserMode } = require("../utils/http");
|
||||||
const { validatedRequest } = require("../utils/middleware/validatedRequest");
|
const { validatedRequest } = require("../utils/middleware/validatedRequest");
|
||||||
const { WorkspaceChats } = require("../models/workspaceChats");
|
|
||||||
const { SystemSettings } = require("../models/systemSettings");
|
|
||||||
const { Telemetry } = require("../models/telemetry");
|
const { Telemetry } = require("../models/telemetry");
|
||||||
const { streamChatWithWorkspace } = require("../utils/chats/stream");
|
const { streamChatWithWorkspace } = require("../utils/chats/stream");
|
||||||
const {
|
const {
|
||||||
@ -16,6 +14,7 @@ const {
|
|||||||
} = require("../utils/middleware/validWorkspace");
|
} = require("../utils/middleware/validWorkspace");
|
||||||
const { writeResponseChunk } = require("../utils/helpers/chat/responses");
|
const { writeResponseChunk } = require("../utils/helpers/chat/responses");
|
||||||
const { WorkspaceThread } = require("../models/workspaceThread");
|
const { WorkspaceThread } = require("../models/workspaceThread");
|
||||||
|
const { User } = require("../models/user");
|
||||||
const truncate = require("truncate");
|
const truncate = require("truncate");
|
||||||
|
|
||||||
function chatEndpoints(app) {
|
function chatEndpoints(app) {
|
||||||
@ -48,39 +47,16 @@ function chatEndpoints(app) {
|
|||||||
response.setHeader("Connection", "keep-alive");
|
response.setHeader("Connection", "keep-alive");
|
||||||
response.flushHeaders();
|
response.flushHeaders();
|
||||||
|
|
||||||
if (multiUserMode(response) && user.role !== ROLES.admin) {
|
if (multiUserMode(response) && !(await User.canSendChat(user))) {
|
||||||
const limitMessagesSetting = await SystemSettings.get({
|
writeResponseChunk(response, {
|
||||||
label: "limit_user_messages",
|
id: uuidv4(),
|
||||||
|
type: "abort",
|
||||||
|
textResponse: null,
|
||||||
|
sources: [],
|
||||||
|
close: true,
|
||||||
|
error: `You have met your maximum 24 hour chat quota of ${user.dailyMessageLimit} chats. Try again later.`,
|
||||||
});
|
});
|
||||||
const limitMessages = limitMessagesSetting?.value === "true";
|
return;
|
||||||
|
|
||||||
if (limitMessages) {
|
|
||||||
const messageLimitSetting = await SystemSettings.get({
|
|
||||||
label: "message_limit",
|
|
||||||
});
|
|
||||||
const systemLimit = Number(messageLimitSetting?.value);
|
|
||||||
|
|
||||||
if (!!systemLimit) {
|
|
||||||
const currentChatCount = await WorkspaceChats.count({
|
|
||||||
user_id: user.id,
|
|
||||||
createdAt: {
|
|
||||||
gte: new Date(new Date() - 24 * 60 * 60 * 1000),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (currentChatCount >= systemLimit) {
|
|
||||||
writeResponseChunk(response, {
|
|
||||||
id: uuidv4(),
|
|
||||||
type: "abort",
|
|
||||||
textResponse: null,
|
|
||||||
sources: [],
|
|
||||||
close: true,
|
|
||||||
error: `You have met your maximum 24 hour chat quota of ${systemLimit} chats set by the instance administrators. Try again later.`,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await streamChatWithWorkspace(
|
await streamChatWithWorkspace(
|
||||||
@ -157,41 +133,16 @@ function chatEndpoints(app) {
|
|||||||
response.setHeader("Connection", "keep-alive");
|
response.setHeader("Connection", "keep-alive");
|
||||||
response.flushHeaders();
|
response.flushHeaders();
|
||||||
|
|
||||||
if (multiUserMode(response) && user.role !== ROLES.admin) {
|
if (multiUserMode(response) && !(await User.canSendChat(user))) {
|
||||||
const limitMessagesSetting = await SystemSettings.get({
|
writeResponseChunk(response, {
|
||||||
label: "limit_user_messages",
|
id: uuidv4(),
|
||||||
|
type: "abort",
|
||||||
|
textResponse: null,
|
||||||
|
sources: [],
|
||||||
|
close: true,
|
||||||
|
error: `You have met your maximum 24 hour chat quota of ${user.dailyMessageLimit} chats. Try again later.`,
|
||||||
});
|
});
|
||||||
const limitMessages = limitMessagesSetting?.value === "true";
|
return;
|
||||||
|
|
||||||
if (limitMessages) {
|
|
||||||
const messageLimitSetting = await SystemSettings.get({
|
|
||||||
label: "message_limit",
|
|
||||||
});
|
|
||||||
const systemLimit = Number(messageLimitSetting?.value);
|
|
||||||
|
|
||||||
if (!!systemLimit) {
|
|
||||||
// Chat qty includes all threads because any user can freely
|
|
||||||
// create threads and would bypass this rule.
|
|
||||||
const currentChatCount = await WorkspaceChats.count({
|
|
||||||
user_id: user.id,
|
|
||||||
createdAt: {
|
|
||||||
gte: new Date(new Date() - 24 * 60 * 60 * 1000),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (currentChatCount >= systemLimit) {
|
|
||||||
writeResponseChunk(response, {
|
|
||||||
id: uuidv4(),
|
|
||||||
type: "abort",
|
|
||||||
textResponse: null,
|
|
||||||
sources: [],
|
|
||||||
close: true,
|
|
||||||
error: `You have met your maximum 24 hour chat quota of ${systemLimit} chats set by the instance administrators. Try again later.`,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await streamChatWithWorkspace(
|
await streamChatWithWorkspace(
|
||||||
|
@ -56,6 +56,7 @@ function embeddedEndpoints(app) {
|
|||||||
writeResponseChunk(response, {
|
writeResponseChunk(response, {
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
type: "abort",
|
type: "abort",
|
||||||
|
sources: [],
|
||||||
textResponse: null,
|
textResponse: null,
|
||||||
close: true,
|
close: true,
|
||||||
error: e.message,
|
error: e.message,
|
||||||
@ -72,11 +73,15 @@ function embeddedEndpoints(app) {
|
|||||||
try {
|
try {
|
||||||
const { sessionId } = request.params;
|
const { sessionId } = request.params;
|
||||||
const embed = response.locals.embedConfig;
|
const embed = response.locals.embedConfig;
|
||||||
|
const history = await EmbedChats.forEmbedByUser(
|
||||||
|
embed.id,
|
||||||
|
sessionId,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
const history = await EmbedChats.forEmbedByUser(embed.id, sessionId);
|
response.status(200).json({ history: convertToChatHistory(history) });
|
||||||
response.status(200).json({
|
|
||||||
history: convertToChatHistory(history),
|
|
||||||
});
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e.message, e);
|
console.error(e.message, e);
|
||||||
response.sendStatus(500).end();
|
response.sendStatus(500).end();
|
||||||
|
@ -490,8 +490,6 @@ function systemEndpoints(app) {
|
|||||||
|
|
||||||
await SystemSettings._updateSettings({
|
await SystemSettings._updateSettings({
|
||||||
multi_user_mode: true,
|
multi_user_mode: true,
|
||||||
limit_user_messages: false,
|
|
||||||
message_limit: 25,
|
|
||||||
});
|
});
|
||||||
await BrowserExtensionApiKey.migrateApiKeysToMultiUser(user.id);
|
await BrowserExtensionApiKey.migrateApiKeysToMultiUser(user.id);
|
||||||
|
|
||||||
|
@ -1,5 +1,17 @@
|
|||||||
|
const { safeJsonParse } = require("../utils/http");
|
||||||
const prisma = require("../utils/prisma");
|
const prisma = require("../utils/prisma");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} EmbedChat
|
||||||
|
* @property {number} id
|
||||||
|
* @property {number} embed_id
|
||||||
|
* @property {string} prompt
|
||||||
|
* @property {string} response
|
||||||
|
* @property {string} connection_information
|
||||||
|
* @property {string} session_id
|
||||||
|
* @property {boolean} include
|
||||||
|
*/
|
||||||
|
|
||||||
const EmbedChats = {
|
const EmbedChats = {
|
||||||
new: async function ({
|
new: async function ({
|
||||||
embedId,
|
embedId,
|
||||||
@ -25,11 +37,36 @@ const EmbedChats = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loops through each chat and filters out the sources from the response object.
|
||||||
|
* We do this when returning /history of an embed to the frontend to prevent inadvertent leaking
|
||||||
|
* of private sources the user may not have intended to share with users.
|
||||||
|
* @param {EmbedChat[]} chats
|
||||||
|
* @returns {EmbedChat[]} Returns a new array of chats with the sources filtered out of responses
|
||||||
|
*/
|
||||||
|
filterSources: function (chats) {
|
||||||
|
return chats.map((chat) => {
|
||||||
|
const { response, ...rest } = chat;
|
||||||
|
const { sources, ...responseRest } = safeJsonParse(response);
|
||||||
|
return { ...rest, response: JSON.stringify(responseRest) };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches chats for a given embed and session id.
|
||||||
|
* @param {number} embedId the id of the embed to fetch chats for
|
||||||
|
* @param {string} sessionId the id of the session to fetch chats for
|
||||||
|
* @param {number|null} limit the maximum number of chats to fetch
|
||||||
|
* @param {string|null} orderBy the order to fetch chats in
|
||||||
|
* @param {boolean} filterSources whether to filter out the sources from the response (default: false)
|
||||||
|
* @returns {Promise<EmbedChat[]>} Returns an array of chats for the given embed and session
|
||||||
|
*/
|
||||||
forEmbedByUser: async function (
|
forEmbedByUser: async function (
|
||||||
embedId = null,
|
embedId = null,
|
||||||
sessionId = null,
|
sessionId = null,
|
||||||
limit = null,
|
limit = null,
|
||||||
orderBy = null
|
orderBy = null,
|
||||||
|
filterSources = false
|
||||||
) {
|
) {
|
||||||
if (!embedId || !sessionId) return [];
|
if (!embedId || !sessionId) return [];
|
||||||
|
|
||||||
@ -43,7 +80,7 @@ const EmbedChats = {
|
|||||||
...(limit !== null ? { take: limit } : {}),
|
...(limit !== null ? { take: limit } : {}),
|
||||||
...(orderBy !== null ? { orderBy } : { orderBy: { id: "asc" } }),
|
...(orderBy !== null ? { orderBy } : { orderBy: { id: "asc" } }),
|
||||||
});
|
});
|
||||||
return chats;
|
return filterSources ? this.filterSources(chats) : chats;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error.message);
|
console.error(error.message);
|
||||||
return [];
|
return [];
|
||||||
|
@ -16,8 +16,6 @@ function isNullOrNaN(value) {
|
|||||||
const SystemSettings = {
|
const SystemSettings = {
|
||||||
protectedFields: ["multi_user_mode"],
|
protectedFields: ["multi_user_mode"],
|
||||||
publicFields: [
|
publicFields: [
|
||||||
"limit_user_messages",
|
|
||||||
"message_limit",
|
|
||||||
"footer_data",
|
"footer_data",
|
||||||
"support_email",
|
"support_email",
|
||||||
"text_splitter_chunk_size",
|
"text_splitter_chunk_size",
|
||||||
@ -33,8 +31,6 @@ const SystemSettings = {
|
|||||||
"meta_page_favicon",
|
"meta_page_favicon",
|
||||||
],
|
],
|
||||||
supportedFields: [
|
supportedFields: [
|
||||||
"limit_user_messages",
|
|
||||||
"message_limit",
|
|
||||||
"logo_filename",
|
"logo_filename",
|
||||||
"telemetry_id",
|
"telemetry_id",
|
||||||
"footer_data",
|
"footer_data",
|
||||||
@ -512,6 +508,10 @@ const SystemSettings = {
|
|||||||
// DeepSeek API Keys
|
// DeepSeek API Keys
|
||||||
DeepSeekApiKey: !!process.env.DEEPSEEK_API_KEY,
|
DeepSeekApiKey: !!process.env.DEEPSEEK_API_KEY,
|
||||||
DeepSeekModelPref: process.env.DEEPSEEK_MODEL_PREF,
|
DeepSeekModelPref: process.env.DEEPSEEK_MODEL_PREF,
|
||||||
|
|
||||||
|
// APIPie LLM API Keys
|
||||||
|
ApipieLLMApiKey: !!process.env.APIPIE_LLM_API_KEY,
|
||||||
|
ApipieLLMModelPref: process.env.APIPIE_LLM_MODEL_PREF,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1,6 +1,17 @@
|
|||||||
const prisma = require("../utils/prisma");
|
const prisma = require("../utils/prisma");
|
||||||
const { EventLogs } = require("./eventLogs");
|
const { EventLogs } = require("./eventLogs");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} User
|
||||||
|
* @property {number} id
|
||||||
|
* @property {string} username
|
||||||
|
* @property {string} password
|
||||||
|
* @property {string} pfpFilename
|
||||||
|
* @property {string} role
|
||||||
|
* @property {boolean} suspended
|
||||||
|
* @property {number|null} dailyMessageLimit
|
||||||
|
*/
|
||||||
|
|
||||||
const User = {
|
const User = {
|
||||||
usernameRegex: new RegExp(/^[a-z0-9_-]+$/),
|
usernameRegex: new RegExp(/^[a-z0-9_-]+$/),
|
||||||
writable: [
|
writable: [
|
||||||
@ -10,6 +21,7 @@ const User = {
|
|||||||
"pfpFilename",
|
"pfpFilename",
|
||||||
"role",
|
"role",
|
||||||
"suspended",
|
"suspended",
|
||||||
|
"dailyMessageLimit",
|
||||||
],
|
],
|
||||||
validations: {
|
validations: {
|
||||||
username: (newValue = "") => {
|
username: (newValue = "") => {
|
||||||
@ -32,12 +44,24 @@ const User = {
|
|||||||
}
|
}
|
||||||
return String(role);
|
return String(role);
|
||||||
},
|
},
|
||||||
|
dailyMessageLimit: (dailyMessageLimit = null) => {
|
||||||
|
if (dailyMessageLimit === null) return null;
|
||||||
|
const limit = Number(dailyMessageLimit);
|
||||||
|
if (isNaN(limit) || limit < 1) {
|
||||||
|
throw new Error(
|
||||||
|
"Daily message limit must be null or a number greater than or equal to 1"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return limit;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
// validations for the above writable fields.
|
// validations for the above writable fields.
|
||||||
castColumnValue: function (key, value) {
|
castColumnValue: function (key, value) {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case "suspended":
|
case "suspended":
|
||||||
return Number(Boolean(value));
|
return Number(Boolean(value));
|
||||||
|
case "dailyMessageLimit":
|
||||||
|
return value === null ? null : Number(value);
|
||||||
default:
|
default:
|
||||||
return String(value);
|
return String(value);
|
||||||
}
|
}
|
||||||
@ -48,7 +72,12 @@ const User = {
|
|||||||
return { ...rest };
|
return { ...rest };
|
||||||
},
|
},
|
||||||
|
|
||||||
create: async function ({ username, password, role = "default" }) {
|
create: async function ({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
role = "default",
|
||||||
|
dailyMessageLimit = null,
|
||||||
|
}) {
|
||||||
const passwordCheck = this.checkPasswordComplexity(password);
|
const passwordCheck = this.checkPasswordComplexity(password);
|
||||||
if (!passwordCheck.checkedOK) {
|
if (!passwordCheck.checkedOK) {
|
||||||
return { user: null, error: passwordCheck.error };
|
return { user: null, error: passwordCheck.error };
|
||||||
@ -58,7 +87,7 @@ const User = {
|
|||||||
// Do not allow new users to bypass validation
|
// Do not allow new users to bypass validation
|
||||||
if (!this.usernameRegex.test(username))
|
if (!this.usernameRegex.test(username))
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Username must be only contain lowercase letters, numbers, underscores, and hyphens with no spaces"
|
"Username must only contain lowercase letters, numbers, underscores, and hyphens with no spaces"
|
||||||
);
|
);
|
||||||
|
|
||||||
const bcrypt = require("bcrypt");
|
const bcrypt = require("bcrypt");
|
||||||
@ -68,6 +97,8 @@ const User = {
|
|||||||
username: this.validations.username(username),
|
username: this.validations.username(username),
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
role: this.validations.role(role),
|
role: this.validations.role(role),
|
||||||
|
dailyMessageLimit:
|
||||||
|
this.validations.dailyMessageLimit(dailyMessageLimit),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return { user: this.filterFields(user), error: null };
|
return { user: this.filterFields(user), error: null };
|
||||||
@ -135,7 +166,7 @@ const User = {
|
|||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error:
|
error:
|
||||||
"Username must be only contain lowercase letters, numbers, underscores, and hyphens with no spaces",
|
"Username must only contain lowercase letters, numbers, underscores, and hyphens with no spaces",
|
||||||
};
|
};
|
||||||
|
|
||||||
const user = await prisma.users.update({
|
const user = await prisma.users.update({
|
||||||
@ -260,6 +291,29 @@ const User = {
|
|||||||
|
|
||||||
return { checkedOK: true, error: "No error." };
|
return { checkedOK: true, error: "No error." };
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a user can send a chat based on their daily message limit.
|
||||||
|
* This limit is system wide and not per workspace and only applies to
|
||||||
|
* multi-user mode AND non-admin users.
|
||||||
|
* @param {User} user The user object record.
|
||||||
|
* @returns {Promise<boolean>} True if the user can send a chat, false otherwise.
|
||||||
|
*/
|
||||||
|
canSendChat: async function (user) {
|
||||||
|
const { ROLES } = require("../utils/middleware/multiUserProtected");
|
||||||
|
if (!user || user.dailyMessageLimit === null || user.role === ROLES.admin)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
const { WorkspaceChats } = require("./workspaceChats");
|
||||||
|
const currentChatCount = await WorkspaceChats.count({
|
||||||
|
user_id: user.id,
|
||||||
|
createdAt: {
|
||||||
|
gte: new Date(new Date() - 24 * 60 * 60 * 1000), // 24 hours
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return currentChatCount < user.dailyMessageLimit;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = { User };
|
module.exports = { User };
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "users" ADD COLUMN "dailyMessageLimit" INTEGER;
|
@ -67,6 +67,7 @@ model users {
|
|||||||
seen_recovery_codes Boolean? @default(false)
|
seen_recovery_codes Boolean? @default(false)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
lastUpdatedAt DateTime @default(now())
|
lastUpdatedAt DateTime @default(now())
|
||||||
|
dailyMessageLimit Int?
|
||||||
workspace_chats workspace_chats[]
|
workspace_chats workspace_chats[]
|
||||||
workspace_users workspace_users[]
|
workspace_users workspace_users[]
|
||||||
embed_configs embed_configs[]
|
embed_configs embed_configs[]
|
||||||
@ -309,4 +310,4 @@ model browser_extension_api_keys {
|
|||||||
user users? @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
user users? @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@index([user_id])
|
@@index([user_id])
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,6 @@ const prisma = new PrismaClient();
|
|||||||
async function main() {
|
async function main() {
|
||||||
const settings = [
|
const settings = [
|
||||||
{ label: "multi_user_mode", value: "false" },
|
{ label: "multi_user_mode", value: "false" },
|
||||||
{ label: "limit_user_messages", value: "false" },
|
|
||||||
{ label: "message_limit", value: "25" },
|
|
||||||
{ label: "logo_filename", value: "anything-llm.png" },
|
{ label: "logo_filename", value: "anything-llm.png" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
3
server/storage/models/.gitignore
vendored
3
server/storage/models/.gitignore
vendored
@ -1,4 +1,5 @@
|
|||||||
Xenova
|
Xenova
|
||||||
downloaded/*
|
downloaded/*
|
||||||
!downloaded/.placeholder
|
!downloaded/.placeholder
|
||||||
openrouter
|
openrouter
|
||||||
|
apipie
|
@ -693,52 +693,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/v1/admin/preferences": {
|
"/v1/admin/preferences": {
|
||||||
"get": {
|
|
||||||
"tags": [
|
|
||||||
"Admin"
|
|
||||||
],
|
|
||||||
"description": "Show all multi-user preferences for instance. Methods are disabled until multi user mode is enabled via the UI.",
|
|
||||||
"parameters": [],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"example": {
|
|
||||||
"settings": {
|
|
||||||
"limit_user_messages": false,
|
|
||||||
"message_limit": 10
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Instance is not in Multi-User mode. Method denied"
|
|
||||||
},
|
|
||||||
"403": {
|
|
||||||
"description": "Forbidden",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/InvalidAPIKey"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"application/xml": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/InvalidAPIKey"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": [
|
||||||
"Admin"
|
"Admin"
|
||||||
@ -788,8 +742,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"example": {
|
"example": {
|
||||||
"limit_user_messages": true,
|
"support_email": "support@example.com"
|
||||||
"message_limit": 5
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
336
server/utils/AiProviders/apipie/index.js
Normal file
336
server/utils/AiProviders/apipie/index.js
Normal file
@ -0,0 +1,336 @@
|
|||||||
|
const { NativeEmbedder } = require("../../EmbeddingEngines/native");
|
||||||
|
const {
|
||||||
|
handleDefaultStreamResponseV2,
|
||||||
|
} = require("../../helpers/chat/responses");
|
||||||
|
|
||||||
|
const { v4: uuidv4 } = require("uuid");
|
||||||
|
const {
|
||||||
|
writeResponseChunk,
|
||||||
|
clientAbortedHandler,
|
||||||
|
} = require("../../helpers/chat/responses");
|
||||||
|
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const { safeJsonParse } = require("../../http");
|
||||||
|
const cacheFolder = path.resolve(
|
||||||
|
process.env.STORAGE_DIR
|
||||||
|
? path.resolve(process.env.STORAGE_DIR, "models", "apipie")
|
||||||
|
: path.resolve(__dirname, `../../../storage/models/apipie`)
|
||||||
|
);
|
||||||
|
|
||||||
|
class ApiPieLLM {
|
||||||
|
constructor(embedder = null, modelPreference = null) {
|
||||||
|
if (!process.env.APIPIE_LLM_API_KEY)
|
||||||
|
throw new Error("No ApiPie LLM API key was set.");
|
||||||
|
|
||||||
|
const { OpenAI: OpenAIApi } = require("openai");
|
||||||
|
this.basePath = "https://apipie.ai/v1";
|
||||||
|
this.openai = new OpenAIApi({
|
||||||
|
baseURL: this.basePath,
|
||||||
|
apiKey: process.env.APIPIE_LLM_API_KEY ?? null,
|
||||||
|
});
|
||||||
|
this.model =
|
||||||
|
modelPreference ||
|
||||||
|
process.env.APIPIE_LLM_MODEL_PREF ||
|
||||||
|
"openrouter/mistral-7b-instruct";
|
||||||
|
this.limits = {
|
||||||
|
history: this.promptWindowLimit() * 0.15,
|
||||||
|
system: this.promptWindowLimit() * 0.15,
|
||||||
|
user: this.promptWindowLimit() * 0.7,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.embedder = embedder ?? new NativeEmbedder();
|
||||||
|
this.defaultTemp = 0.7;
|
||||||
|
|
||||||
|
if (!fs.existsSync(cacheFolder))
|
||||||
|
fs.mkdirSync(cacheFolder, { recursive: true });
|
||||||
|
this.cacheModelPath = path.resolve(cacheFolder, "models.json");
|
||||||
|
this.cacheAtPath = path.resolve(cacheFolder, ".cached_at");
|
||||||
|
}
|
||||||
|
|
||||||
|
log(text, ...args) {
|
||||||
|
console.log(`\x1b[36m[${this.constructor.name}]\x1b[0m ${text}`, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This checks if the .cached_at file has a timestamp that is more than 1Week (in millis)
|
||||||
|
// from the current date. If it is, then we will refetch the API so that all the models are up
|
||||||
|
// to date.
|
||||||
|
#cacheIsStale() {
|
||||||
|
const MAX_STALE = 6.048e8; // 1 Week in MS
|
||||||
|
if (!fs.existsSync(this.cacheAtPath)) return true;
|
||||||
|
const now = Number(new Date());
|
||||||
|
const timestampMs = Number(fs.readFileSync(this.cacheAtPath));
|
||||||
|
return now - timestampMs > MAX_STALE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function fetches the models from the ApiPie API and caches them locally.
|
||||||
|
// We do this because the ApiPie API has a lot of models, and we need to get the proper token context window
|
||||||
|
// for each model and this is a constructor property - so we can really only get it if this cache exists.
|
||||||
|
// We used to have this as a chore, but given there is an API to get the info - this makes little sense.
|
||||||
|
// This might slow down the first request, but we need the proper token context window
|
||||||
|
// for each model and this is a constructor property - so we can really only get it if this cache exists.
|
||||||
|
async #syncModels() {
|
||||||
|
if (fs.existsSync(this.cacheModelPath) && !this.#cacheIsStale())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
this.log("Model cache is not present or stale. Fetching from ApiPie API.");
|
||||||
|
await fetchApiPieModels();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#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("")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
models() {
|
||||||
|
if (!fs.existsSync(this.cacheModelPath)) return {};
|
||||||
|
return safeJsonParse(
|
||||||
|
fs.readFileSync(this.cacheModelPath, { encoding: "utf-8" }),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
streamingEnabled() {
|
||||||
|
return "streamGetChatCompletion" in this;
|
||||||
|
}
|
||||||
|
|
||||||
|
static promptWindowLimit(modelName) {
|
||||||
|
const cacheModelPath = path.resolve(cacheFolder, "models.json");
|
||||||
|
const availableModels = fs.existsSync(cacheModelPath)
|
||||||
|
? safeJsonParse(
|
||||||
|
fs.readFileSync(cacheModelPath, { encoding: "utf-8" }),
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
: {};
|
||||||
|
return availableModels[modelName]?.maxLength || 4096;
|
||||||
|
}
|
||||||
|
|
||||||
|
promptWindowLimit() {
|
||||||
|
const availableModels = this.models();
|
||||||
|
return availableModels[this.model]?.maxLength || 4096;
|
||||||
|
}
|
||||||
|
|
||||||
|
async isValidChatCompletionModel(model = "") {
|
||||||
|
await this.#syncModels();
|
||||||
|
const availableModels = this.models();
|
||||||
|
return availableModels.hasOwnProperty(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates appropriate content array for a message + attachments.
|
||||||
|
* @param {{userPrompt:string, attachments: import("../../helpers").Attachment[]}}
|
||||||
|
* @returns {string|object[]}
|
||||||
|
*/
|
||||||
|
#generateContent({ userPrompt, attachments = [] }) {
|
||||||
|
if (!attachments.length) {
|
||||||
|
return userPrompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = [{ type: "text", text: userPrompt }];
|
||||||
|
for (let attachment of attachments) {
|
||||||
|
content.push({
|
||||||
|
type: "image_url",
|
||||||
|
image_url: {
|
||||||
|
url: attachment.contentString,
|
||||||
|
detail: "auto",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return content.flat();
|
||||||
|
}
|
||||||
|
|
||||||
|
constructPrompt({
|
||||||
|
systemPrompt = "",
|
||||||
|
contextTexts = [],
|
||||||
|
chatHistory = [],
|
||||||
|
userPrompt = "",
|
||||||
|
attachments = [],
|
||||||
|
}) {
|
||||||
|
const prompt = {
|
||||||
|
role: "system",
|
||||||
|
content: `${systemPrompt}${this.#appendContext(contextTexts)}`,
|
||||||
|
};
|
||||||
|
return [
|
||||||
|
prompt,
|
||||||
|
...chatHistory,
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: this.#generateContent({ userPrompt, attachments }),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async getChatCompletion(messages = null, { temperature = 0.7 }) {
|
||||||
|
if (!(await this.isValidChatCompletionModel(this.model)))
|
||||||
|
throw new Error(
|
||||||
|
`ApiPie chat: ${this.model} is not valid for chat completion!`
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await this.openai.chat.completions
|
||||||
|
.create({
|
||||||
|
model: this.model,
|
||||||
|
messages,
|
||||||
|
temperature,
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
throw new Error(e.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.hasOwnProperty("choices") || result.choices.length === 0)
|
||||||
|
return null;
|
||||||
|
return result.choices[0].message.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// APIPie says it supports streaming, but it does not work across all models and providers.
|
||||||
|
// Notably, it is not working for OpenRouter models at all.
|
||||||
|
// async streamGetChatCompletion(messages = null, { temperature = 0.7 }) {
|
||||||
|
// if (!(await this.isValidChatCompletionModel(this.model)))
|
||||||
|
// throw new Error(
|
||||||
|
// `ApiPie chat: ${this.model} is not valid for chat completion!`
|
||||||
|
// );
|
||||||
|
|
||||||
|
// const streamRequest = await this.openai.chat.completions.create({
|
||||||
|
// model: this.model,
|
||||||
|
// stream: true,
|
||||||
|
// messages,
|
||||||
|
// temperature,
|
||||||
|
// });
|
||||||
|
// return streamRequest;
|
||||||
|
// }
|
||||||
|
|
||||||
|
handleStream(response, stream, responseProps) {
|
||||||
|
const { uuid = uuidv4(), sources = [] } = responseProps;
|
||||||
|
|
||||||
|
return new Promise(async (resolve) => {
|
||||||
|
let fullText = "";
|
||||||
|
|
||||||
|
// Establish listener to early-abort a streaming response
|
||||||
|
// in case things go sideways or the user does not like the response.
|
||||||
|
// We preserve the generated text but continue as if chat was completed
|
||||||
|
// to preserve previously generated content.
|
||||||
|
const handleAbort = () => clientAbortedHandler(resolve, fullText);
|
||||||
|
response.on("close", handleAbort);
|
||||||
|
|
||||||
|
try {
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
const message = chunk?.choices?.[0];
|
||||||
|
const token = message?.delta?.content;
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
fullText += token;
|
||||||
|
writeResponseChunk(response, {
|
||||||
|
uuid,
|
||||||
|
sources: [],
|
||||||
|
type: "textResponseChunk",
|
||||||
|
textResponse: token,
|
||||||
|
close: false,
|
||||||
|
error: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message === undefined || message.finish_reason !== null) {
|
||||||
|
writeResponseChunk(response, {
|
||||||
|
uuid,
|
||||||
|
sources,
|
||||||
|
type: "textResponseChunk",
|
||||||
|
textResponse: "",
|
||||||
|
close: true,
|
||||||
|
error: false,
|
||||||
|
});
|
||||||
|
response.removeListener("close", handleAbort);
|
||||||
|
resolve(fullText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
writeResponseChunk(response, {
|
||||||
|
uuid,
|
||||||
|
sources,
|
||||||
|
type: "abort",
|
||||||
|
textResponse: null,
|
||||||
|
close: true,
|
||||||
|
error: e.message,
|
||||||
|
});
|
||||||
|
response.removeListener("close", handleAbort);
|
||||||
|
resolve(fullText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleStream(response, stream, responseProps) {
|
||||||
|
// return handleDefaultStreamResponseV2(response, stream, responseProps);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchApiPieModels(providedApiKey = null) {
|
||||||
|
const apiKey = providedApiKey || process.env.APIPIE_LLM_API_KEY || null;
|
||||||
|
return await fetch(`https://apipie.ai/v1/models`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then(({ data = [] }) => {
|
||||||
|
const models = {};
|
||||||
|
data.forEach((model) => {
|
||||||
|
models[`${model.provider}/${model.model}`] = {
|
||||||
|
id: `${model.provider}/${model.model}`,
|
||||||
|
name: `${model.provider}/${model.model}`,
|
||||||
|
organization: model.provider,
|
||||||
|
maxLength: model.max_tokens,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cache all response information
|
||||||
|
if (!fs.existsSync(cacheFolder))
|
||||||
|
fs.mkdirSync(cacheFolder, { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.resolve(cacheFolder, "models.json"),
|
||||||
|
JSON.stringify(models),
|
||||||
|
{
|
||||||
|
encoding: "utf-8",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.resolve(cacheFolder, ".cached_at"),
|
||||||
|
String(Number(new Date())),
|
||||||
|
{
|
||||||
|
encoding: "utf-8",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return models;
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
ApiPieLLM,
|
||||||
|
fetchApiPieModels,
|
||||||
|
};
|
@ -785,6 +785,10 @@ ${this.getHistory({ to: route.to })
|
|||||||
return new Providers.FireworksAIProvider({ model: config.model });
|
return new Providers.FireworksAIProvider({ model: config.model });
|
||||||
case "deepseek":
|
case "deepseek":
|
||||||
return new Providers.DeepSeekProvider({ model: config.model });
|
return new Providers.DeepSeekProvider({ model: config.model });
|
||||||
|
case "litellm":
|
||||||
|
return new Providers.LiteLLMProvider({ model: config.model });
|
||||||
|
case "apipie":
|
||||||
|
return new Providers.ApiPieProvider({ model: config.model });
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
@ -130,6 +130,22 @@ class Provider {
|
|||||||
apiKey: process.env.FIREWORKS_AI_LLM_API_KEY,
|
apiKey: process.env.FIREWORKS_AI_LLM_API_KEY,
|
||||||
...config,
|
...config,
|
||||||
});
|
});
|
||||||
|
case "apipie":
|
||||||
|
return new ChatOpenAI({
|
||||||
|
configuration: {
|
||||||
|
baseURL: "https://apipie.ai/v1",
|
||||||
|
},
|
||||||
|
apiKey: process.env.APIPIE_LLM_API_KEY ?? null,
|
||||||
|
...config,
|
||||||
|
});
|
||||||
|
case "deepseek":
|
||||||
|
return new ChatOpenAI({
|
||||||
|
configuration: {
|
||||||
|
baseURL: "https://api.deepseek.com/v1",
|
||||||
|
},
|
||||||
|
apiKey: process.env.DEEPSEEK_API_KEY ?? null,
|
||||||
|
...config,
|
||||||
|
});
|
||||||
|
|
||||||
// OSS Model Runners
|
// OSS Model Runners
|
||||||
// case "anythingllm_ollama":
|
// case "anythingllm_ollama":
|
||||||
@ -174,14 +190,15 @@ class Provider {
|
|||||||
apiKey: process.env.TEXT_GEN_WEB_UI_API_KEY ?? "not-used",
|
apiKey: process.env.TEXT_GEN_WEB_UI_API_KEY ?? "not-used",
|
||||||
...config,
|
...config,
|
||||||
});
|
});
|
||||||
case "deepseek":
|
case "litellm":
|
||||||
return new ChatOpenAI({
|
return new ChatOpenAI({
|
||||||
configuration: {
|
configuration: {
|
||||||
baseURL: "https://api.deepseek.com/v1",
|
baseURL: process.env.LITE_LLM_BASE_PATH,
|
||||||
},
|
},
|
||||||
apiKey: process.env.DEEPSEEK_API_KEY ?? null,
|
apiKey: process.env.LITE_LLM_API_KEY ?? null,
|
||||||
...config,
|
...config,
|
||||||
});
|
});
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported provider ${provider} for this task.`);
|
throw new Error(`Unsupported provider ${provider} for this task.`);
|
||||||
}
|
}
|
||||||
|
116
server/utils/agents/aibitat/providers/apipie.js
Normal file
116
server/utils/agents/aibitat/providers/apipie.js
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
const OpenAI = require("openai");
|
||||||
|
const Provider = require("./ai-provider.js");
|
||||||
|
const InheritMultiple = require("./helpers/classes.js");
|
||||||
|
const UnTooled = require("./helpers/untooled.js");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The agent provider for the OpenRouter provider.
|
||||||
|
*/
|
||||||
|
class ApiPieProvider extends InheritMultiple([Provider, UnTooled]) {
|
||||||
|
model;
|
||||||
|
|
||||||
|
constructor(config = {}) {
|
||||||
|
const { model = "openrouter/llama-3.1-8b-instruct" } = config;
|
||||||
|
super();
|
||||||
|
const client = new OpenAI({
|
||||||
|
baseURL: "https://apipie.ai/v1",
|
||||||
|
apiKey: process.env.APIPIE_LLM_API_KEY,
|
||||||
|
maxRetries: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
this._client = client;
|
||||||
|
this.model = model;
|
||||||
|
this.verbose = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
get client() {
|
||||||
|
return this._client;
|
||||||
|
}
|
||||||
|
|
||||||
|
async #handleFunctionCallChat({ messages = [] }) {
|
||||||
|
return await this.client.chat.completions
|
||||||
|
.create({
|
||||||
|
model: this.model,
|
||||||
|
temperature: 0,
|
||||||
|
messages,
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
if (!result.hasOwnProperty("choices"))
|
||||||
|
throw new Error("ApiPie chat: No results!");
|
||||||
|
if (result.choices.length === 0)
|
||||||
|
throw new Error("ApiPie chat: No results length!");
|
||||||
|
return result.choices[0].message.content;
|
||||||
|
})
|
||||||
|
.catch((_) => {
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a completion based on the received messages.
|
||||||
|
*
|
||||||
|
* @param messages A list of messages to send to the API.
|
||||||
|
* @param functions
|
||||||
|
* @returns The completion.
|
||||||
|
*/
|
||||||
|
async complete(messages, functions = null) {
|
||||||
|
try {
|
||||||
|
let completion;
|
||||||
|
if (functions.length > 0) {
|
||||||
|
const { toolCall, text } = await this.functionCall(
|
||||||
|
messages,
|
||||||
|
functions,
|
||||||
|
this.#handleFunctionCallChat.bind(this)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (toolCall !== null) {
|
||||||
|
this.providerLog(`Valid tool call found - running ${toolCall.name}.`);
|
||||||
|
this.deduplicator.trackRun(toolCall.name, toolCall.arguments);
|
||||||
|
return {
|
||||||
|
result: null,
|
||||||
|
functionCall: {
|
||||||
|
name: toolCall.name,
|
||||||
|
arguments: toolCall.arguments,
|
||||||
|
},
|
||||||
|
cost: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
completion = { content: text };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!completion?.content) {
|
||||||
|
this.providerLog(
|
||||||
|
"Will assume chat completion without tool call inputs."
|
||||||
|
);
|
||||||
|
const response = await this.client.chat.completions.create({
|
||||||
|
model: this.model,
|
||||||
|
messages: this.cleanMsgs(messages),
|
||||||
|
});
|
||||||
|
completion = response.choices[0].message;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The UnTooled class inherited Deduplicator is mostly useful to prevent the agent
|
||||||
|
// from calling the exact same function over and over in a loop within a single chat exchange
|
||||||
|
// _but_ we should enable it to call previously used tools in a new chat interaction.
|
||||||
|
this.deduplicator.reset("runs");
|
||||||
|
return {
|
||||||
|
result: completion.content,
|
||||||
|
cost: 0,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the cost of the completion.
|
||||||
|
*
|
||||||
|
* @param _usage The completion to get the cost for.
|
||||||
|
* @returns The cost of the completion.
|
||||||
|
*/
|
||||||
|
getCost(_usage) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ApiPieProvider;
|
@ -15,6 +15,8 @@ const TextWebGenUiProvider = require("./textgenwebui.js");
|
|||||||
const AWSBedrockProvider = require("./bedrock.js");
|
const AWSBedrockProvider = require("./bedrock.js");
|
||||||
const FireworksAIProvider = require("./fireworksai.js");
|
const FireworksAIProvider = require("./fireworksai.js");
|
||||||
const DeepSeekProvider = require("./deepseek.js");
|
const DeepSeekProvider = require("./deepseek.js");
|
||||||
|
const LiteLLMProvider = require("./litellm.js");
|
||||||
|
const ApiPieProvider = require("./apipie.js");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
OpenAIProvider,
|
OpenAIProvider,
|
||||||
@ -34,4 +36,6 @@ module.exports = {
|
|||||||
TextWebGenUiProvider,
|
TextWebGenUiProvider,
|
||||||
AWSBedrockProvider,
|
AWSBedrockProvider,
|
||||||
FireworksAIProvider,
|
FireworksAIProvider,
|
||||||
|
LiteLLMProvider,
|
||||||
|
ApiPieProvider,
|
||||||
};
|
};
|
||||||
|
110
server/utils/agents/aibitat/providers/litellm.js
Normal file
110
server/utils/agents/aibitat/providers/litellm.js
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
const OpenAI = require("openai");
|
||||||
|
const Provider = require("./ai-provider.js");
|
||||||
|
const InheritMultiple = require("./helpers/classes.js");
|
||||||
|
const UnTooled = require("./helpers/untooled.js");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The agent provider for LiteLLM.
|
||||||
|
*/
|
||||||
|
class LiteLLMProvider extends InheritMultiple([Provider, UnTooled]) {
|
||||||
|
model;
|
||||||
|
|
||||||
|
constructor(config = {}) {
|
||||||
|
super();
|
||||||
|
const { model = null } = config;
|
||||||
|
const client = new OpenAI({
|
||||||
|
baseURL: process.env.LITE_LLM_BASE_PATH,
|
||||||
|
apiKey: process.env.LITE_LLM_API_KEY ?? null,
|
||||||
|
maxRetries: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
this._client = client;
|
||||||
|
this.model = model || process.env.LITE_LLM_MODEL_PREF;
|
||||||
|
this.verbose = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
get client() {
|
||||||
|
return this._client;
|
||||||
|
}
|
||||||
|
|
||||||
|
async #handleFunctionCallChat({ messages = [] }) {
|
||||||
|
return await this.client.chat.completions
|
||||||
|
.create({
|
||||||
|
model: this.model,
|
||||||
|
temperature: 0,
|
||||||
|
messages,
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
if (!result.hasOwnProperty("choices"))
|
||||||
|
throw new Error("LiteLLM chat: No results!");
|
||||||
|
if (result.choices.length === 0)
|
||||||
|
throw new Error("LiteLLM chat: No results length!");
|
||||||
|
return result.choices[0].message.content;
|
||||||
|
})
|
||||||
|
.catch((_) => {
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a completion based on the received messages.
|
||||||
|
*
|
||||||
|
* @param messages A list of messages to send to the API.
|
||||||
|
* @param functions
|
||||||
|
* @returns The completion.
|
||||||
|
*/
|
||||||
|
async complete(messages, functions = null) {
|
||||||
|
try {
|
||||||
|
let completion;
|
||||||
|
if (functions.length > 0) {
|
||||||
|
const { toolCall, text } = await this.functionCall(
|
||||||
|
messages,
|
||||||
|
functions,
|
||||||
|
this.#handleFunctionCallChat.bind(this)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (toolCall !== null) {
|
||||||
|
this.providerLog(`Valid tool call found - running ${toolCall.name}.`);
|
||||||
|
this.deduplicator.trackRun(toolCall.name, toolCall.arguments);
|
||||||
|
return {
|
||||||
|
result: null,
|
||||||
|
functionCall: {
|
||||||
|
name: toolCall.name,
|
||||||
|
arguments: toolCall.arguments,
|
||||||
|
},
|
||||||
|
cost: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
completion = { content: text };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!completion?.content) {
|
||||||
|
this.providerLog(
|
||||||
|
"Will assume chat completion without tool call inputs."
|
||||||
|
);
|
||||||
|
const response = await this.client.chat.completions.create({
|
||||||
|
model: this.model,
|
||||||
|
messages: this.cleanMsgs(messages),
|
||||||
|
});
|
||||||
|
completion = response.choices[0].message;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The UnTooled class inherited Deduplicator is mostly useful to prevent the agent
|
||||||
|
// from calling the exact same function over and over in a loop within a single chat exchange
|
||||||
|
// _but_ we should enable it to call previously used tools in a new chat interaction.
|
||||||
|
this.deduplicator.reset("runs");
|
||||||
|
return {
|
||||||
|
result: completion.content,
|
||||||
|
cost: 0,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getCost(_usage) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = LiteLLMProvider;
|
@ -166,6 +166,16 @@ class AgentHandler {
|
|||||||
if (!process.env.DEEPSEEK_API_KEY)
|
if (!process.env.DEEPSEEK_API_KEY)
|
||||||
throw new Error("DeepSeek API Key must be provided to use agents.");
|
throw new Error("DeepSeek API Key must be provided to use agents.");
|
||||||
break;
|
break;
|
||||||
|
case "litellm":
|
||||||
|
if (!process.env.LITE_LLM_BASE_PATH)
|
||||||
|
throw new Error(
|
||||||
|
"LiteLLM API base path and key must be provided to use agents."
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "apipie":
|
||||||
|
if (!process.env.APIPIE_LLM_API_KEY)
|
||||||
|
throw new Error("ApiPie API Key must be provided to use agents.");
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@ -212,6 +222,10 @@ class AgentHandler {
|
|||||||
return null;
|
return null;
|
||||||
case "deepseek":
|
case "deepseek":
|
||||||
return "deepseek-chat";
|
return "deepseek-chat";
|
||||||
|
case "litellm":
|
||||||
|
return null;
|
||||||
|
case "apipie":
|
||||||
|
return null;
|
||||||
default:
|
default:
|
||||||
return "unknown";
|
return "unknown";
|
||||||
}
|
}
|
||||||
|
@ -60,8 +60,7 @@ async function streamChatWithForEmbed(
|
|||||||
const { rawHistory, chatHistory } = await recentEmbedChatHistory(
|
const { rawHistory, chatHistory } = await recentEmbedChatHistory(
|
||||||
sessionId,
|
sessionId,
|
||||||
embed,
|
embed,
|
||||||
messageLimit,
|
messageLimit
|
||||||
chatMode
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// See stream.js comment for more information on this implementation.
|
// See stream.js comment for more information on this implementation.
|
||||||
@ -113,16 +112,27 @@ async function streamChatWithForEmbed(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
contextTexts = [...contextTexts, ...vectorSearchResults.contextTexts];
|
const { fillSourceWindow } = require("../helpers/chat");
|
||||||
|
const filledSources = fillSourceWindow({
|
||||||
|
nDocs: embed.workspace?.topN || 4,
|
||||||
|
searchResults: vectorSearchResults.sources,
|
||||||
|
history: rawHistory,
|
||||||
|
filterIdentifiers: pinnedDocIdentifiers,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Why does contextTexts get all the info, but sources only get current search?
|
||||||
|
// This is to give the ability of the LLM to "comprehend" a contextual response without
|
||||||
|
// populating the Citations under a response with documents the user "thinks" are irrelevant
|
||||||
|
// due to how we manage backfilling of the context to keep chats with the LLM more correct in responses.
|
||||||
|
// If a past citation was used to answer the question - that is visible in the history so it logically makes sense
|
||||||
|
// and does not appear to the user that a new response used information that is otherwise irrelevant for a given prompt.
|
||||||
|
// TLDR; reduces GitHub issues for "LLM citing document that has no answer in it" while keep answers highly accurate.
|
||||||
|
contextTexts = [...contextTexts, ...filledSources.contextTexts];
|
||||||
sources = [...sources, ...vectorSearchResults.sources];
|
sources = [...sources, ...vectorSearchResults.sources];
|
||||||
|
|
||||||
// If in query mode and no sources are found, do not
|
// If in query mode and no sources are found in current search or backfilled from history, do not
|
||||||
// let the LLM try to hallucinate a response or use general knowledge
|
// let the LLM try to hallucinate a response or use general knowledge
|
||||||
if (
|
if (chatMode === "query" && contextTexts.length === 0) {
|
||||||
chatMode === "query" &&
|
|
||||||
sources.length === 0 &&
|
|
||||||
pinnedDocIdentifiers.length === 0
|
|
||||||
) {
|
|
||||||
writeResponseChunk(response, {
|
writeResponseChunk(response, {
|
||||||
id: uuid,
|
id: uuid,
|
||||||
type: "textResponse",
|
type: "textResponse",
|
||||||
@ -178,7 +188,7 @@ async function streamChatWithForEmbed(
|
|||||||
await EmbedChats.new({
|
await EmbedChats.new({
|
||||||
embedId: embed.id,
|
embedId: embed.id,
|
||||||
prompt: message,
|
prompt: message,
|
||||||
response: { text: completeText, type: chatMode },
|
response: { text: completeText, type: chatMode, sources },
|
||||||
connection_information: response.locals.connection
|
connection_information: response.locals.connection
|
||||||
? {
|
? {
|
||||||
...response.locals.connection,
|
...response.locals.connection,
|
||||||
@ -190,15 +200,13 @@ async function streamChatWithForEmbed(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// On query we don't return message history. All other chat modes and when chatting
|
/**
|
||||||
// with no embeddings we return history.
|
* @param {string} sessionId the session id of the user from embed widget
|
||||||
async function recentEmbedChatHistory(
|
* @param {Object} embed the embed config object
|
||||||
sessionId,
|
* @param {Number} messageLimit the number of messages to return
|
||||||
embed,
|
* @returns {Promise<{rawHistory: import("@prisma/client").embed_chats[], chatHistory: {role: string, content: string}[]}>
|
||||||
messageLimit = 20,
|
*/
|
||||||
chatMode = null
|
async function recentEmbedChatHistory(sessionId, embed, messageLimit = 20) {
|
||||||
) {
|
|
||||||
if (chatMode === "query") return { rawHistory: [], chatHistory: [] };
|
|
||||||
const rawHistory = (
|
const rawHistory = (
|
||||||
await EmbedChats.forEmbedByUser(embed.id, sessionId, messageLimit, {
|
await EmbedChats.forEmbedByUser(embed.id, sessionId, messageLimit, {
|
||||||
id: "desc",
|
id: "desc",
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
const { fetchOpenRouterModels } = require("../AiProviders/openRouter");
|
const { fetchOpenRouterModels } = require("../AiProviders/openRouter");
|
||||||
|
const { fetchApiPieModels } = require("../AiProviders/apipie");
|
||||||
const { perplexityModels } = require("../AiProviders/perplexity");
|
const { perplexityModels } = require("../AiProviders/perplexity");
|
||||||
const { togetherAiModels } = require("../AiProviders/togetherAi");
|
const { togetherAiModels } = require("../AiProviders/togetherAi");
|
||||||
const { fireworksAiModels } = require("../AiProviders/fireworksAi");
|
const { fireworksAiModels } = require("../AiProviders/fireworksAi");
|
||||||
@ -19,6 +20,7 @@ const SUPPORT_CUSTOM_MODELS = [
|
|||||||
"elevenlabs-tts",
|
"elevenlabs-tts",
|
||||||
"groq",
|
"groq",
|
||||||
"deepseek",
|
"deepseek",
|
||||||
|
"apipie",
|
||||||
];
|
];
|
||||||
|
|
||||||
async function getCustomModels(provider = "", apiKey = null, basePath = null) {
|
async function getCustomModels(provider = "", apiKey = null, basePath = null) {
|
||||||
@ -56,6 +58,8 @@ async function getCustomModels(provider = "", apiKey = null, basePath = null) {
|
|||||||
return await getGroqAiModels(apiKey);
|
return await getGroqAiModels(apiKey);
|
||||||
case "deepseek":
|
case "deepseek":
|
||||||
return await getDeepSeekModels(apiKey);
|
return await getDeepSeekModels(apiKey);
|
||||||
|
case "apipie":
|
||||||
|
return await getAPIPieModels(apiKey);
|
||||||
default:
|
default:
|
||||||
return { models: [], error: "Invalid provider for custom models" };
|
return { models: [], error: "Invalid provider for custom models" };
|
||||||
}
|
}
|
||||||
@ -355,6 +359,21 @@ async function getOpenRouterModels() {
|
|||||||
return { models, error: null };
|
return { models, error: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getAPIPieModels(apiKey = null) {
|
||||||
|
const knownModels = await fetchApiPieModels(apiKey);
|
||||||
|
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) {
|
async function getMistralModels(apiKey = null) {
|
||||||
const { OpenAI: OpenAIApi } = require("openai");
|
const { OpenAI: OpenAIApi } = require("openai");
|
||||||
const openai = new OpenAIApi({
|
const openai = new OpenAIApi({
|
||||||
|
@ -162,6 +162,9 @@ function getLLMProvider({ provider = null, model = null } = {}) {
|
|||||||
case "deepseek":
|
case "deepseek":
|
||||||
const { DeepSeekLLM } = require("../AiProviders/deepseek");
|
const { DeepSeekLLM } = require("../AiProviders/deepseek");
|
||||||
return new DeepSeekLLM(embedder, model);
|
return new DeepSeekLLM(embedder, model);
|
||||||
|
case "apipie":
|
||||||
|
const { ApiPieLLM } = require("../AiProviders/apipie");
|
||||||
|
return new ApiPieLLM(embedder, model);
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`ENV: No valid LLM_PROVIDER value found in environment! Using ${process.env.LLM_PROVIDER}`
|
`ENV: No valid LLM_PROVIDER value found in environment! Using ${process.env.LLM_PROVIDER}`
|
||||||
@ -285,6 +288,12 @@ function getLLMProviderClass({ provider = null } = {}) {
|
|||||||
case "bedrock":
|
case "bedrock":
|
||||||
const { AWSBedrockLLM } = require("../AiProviders/bedrock");
|
const { AWSBedrockLLM } = require("../AiProviders/bedrock");
|
||||||
return AWSBedrockLLM;
|
return AWSBedrockLLM;
|
||||||
|
case "deepseek":
|
||||||
|
const { DeepSeekLLM } = require("../AiProviders/deepseek");
|
||||||
|
return DeepSeekLLM;
|
||||||
|
case "apipie":
|
||||||
|
const { ApiPieLLM } = require("../AiProviders/apipie");
|
||||||
|
return ApiPieLLM;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -515,6 +515,16 @@ const KEY_MAPPING = {
|
|||||||
envKey: "DEEPSEEK_MODEL_PREF",
|
envKey: "DEEPSEEK_MODEL_PREF",
|
||||||
checks: [isNotEmpty],
|
checks: [isNotEmpty],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// APIPie Options
|
||||||
|
ApipieLLMApiKey: {
|
||||||
|
envKey: "APIPIE_LLM_API_KEY",
|
||||||
|
checks: [isNotEmpty],
|
||||||
|
},
|
||||||
|
ApipieLLMModelPref: {
|
||||||
|
envKey: "APIPIE_LLM_MODEL_PREF",
|
||||||
|
checks: [isNotEmpty],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function isNotEmpty(input = "") {
|
function isNotEmpty(input = "") {
|
||||||
@ -617,6 +627,7 @@ function supportedLLM(input = "") {
|
|||||||
"generic-openai",
|
"generic-openai",
|
||||||
"bedrock",
|
"bedrock",
|
||||||
"deepseek",
|
"deepseek",
|
||||||
|
"apipie",
|
||||||
].includes(input);
|
].includes(input);
|
||||||
return validSelection ? null : `${input} is not a valid LLM provider.`;
|
return validSelection ? null : `${input} is not a valid LLM provider.`;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user