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",
|
||||
"allm",
|
||||
"anythingllm",
|
||||
"Apipie",
|
||||
"Astra",
|
||||
"Chartable",
|
||||
"cleancss",
|
||||
@ -18,6 +19,7 @@
|
||||
"elevenlabs",
|
||||
"Embeddable",
|
||||
"epub",
|
||||
"fireworksai",
|
||||
"GROQ",
|
||||
"hljs",
|
||||
"huggingface",
|
||||
@ -40,14 +42,13 @@
|
||||
"pagerender",
|
||||
"Qdrant",
|
||||
"royalblue",
|
||||
"searxng",
|
||||
"SearchApi",
|
||||
"searxng",
|
||||
"Serper",
|
||||
"Serply",
|
||||
"streamable",
|
||||
"textgenwebui",
|
||||
"togetherai",
|
||||
"fireworksai",
|
||||
"Unembed",
|
||||
"vectordbs",
|
||||
"Weaviate",
|
||||
|
@ -105,6 +105,10 @@ GID='1000'
|
||||
# FIREWORKS_AI_LLM_API_KEY='my-fireworks-ai-key'
|
||||
# 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 ##########
|
||||
###########################################
|
||||
|
@ -23,7 +23,6 @@ const WorkspaceChat = lazy(() => import("@/pages/WorkspaceChat"));
|
||||
const AdminUsers = lazy(() => import("@/pages/Admin/Users"));
|
||||
const AdminInvites = lazy(() => import("@/pages/Admin/Invitations"));
|
||||
const AdminWorkspaces = lazy(() => import("@/pages/Admin/Workspaces"));
|
||||
const AdminSystem = lazy(() => import("@/pages/Admin/System"));
|
||||
const AdminLogs = lazy(() => import("@/pages/Admin/Logging"));
|
||||
const AdminAgents = lazy(() => import("@/pages/Admin/Agents"));
|
||||
const GeneralChats = lazy(() => import("@/pages/GeneralSettings/Chats"));
|
||||
@ -172,10 +171,6 @@ export default function App() {
|
||||
path="/settings/workspace-chats"
|
||||
element={<ManagerRoute Component={GeneralChats} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/system-preferences"
|
||||
element={<ManagerRoute Component={AdminSystem} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/invites"
|
||||
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]"
|
||||
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)}
|
||||
</p>
|
||||
</div>
|
||||
|
@ -51,7 +51,7 @@ export default function FolderRow({
|
||||
className="shrink-0 text-base font-bold w-4 h-4 mr-[3px]"
|
||||
weight="fill"
|
||||
/>
|
||||
<p className="whitespace-nowrap overflow-show">
|
||||
<p className="whitespace-nowrap overflow-show max-w-[400px]">
|
||||
{middleTruncate(item.name, 35)}
|
||||
</p>
|
||||
</div>
|
||||
|
@ -83,7 +83,7 @@ export default function WorkspaceFileRow({
|
||||
className="shrink-0 text-base font-bold w-4 h-4 mr-[3px] ml-1"
|
||||
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)}
|
||||
</p>
|
||||
</div>
|
||||
|
@ -29,10 +29,8 @@ export default function SettingsButton() {
|
||||
return (
|
||||
<ToolTipWrapper id="open-settings">
|
||||
<Link
|
||||
to={
|
||||
!!user?.role ? paths.settings.system() : paths.settings.appearance()
|
||||
}
|
||||
className="transition-all duration-300 p-2 rounded-full text-white bg-theme-sidebar-footer-icon hover:bg-theme-sidebar-footer-icon-hover"
|
||||
to={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"
|
||||
aria-label="Settings"
|
||||
data-tooltip-id="open-settings"
|
||||
data-tooltip-content="Open settings"
|
||||
|
@ -277,11 +277,6 @@ const SidebarOptions = ({ user = null, t }) => (
|
||||
href: paths.settings.invites(),
|
||||
roles: ["admin", "manager"],
|
||||
},
|
||||
{
|
||||
btnText: t("settings.system"),
|
||||
href: paths.settings.system(),
|
||||
roles: ["admin", "manager"],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Option
|
||||
|
@ -122,9 +122,22 @@ export default function PromptInput({
|
||||
|
||||
const pasteText = e.clipboardData.getData("text/plain");
|
||||
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);
|
||||
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;
|
||||
};
|
||||
|
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 Admin from "@/models/admin";
|
||||
import { userFromStorage } from "@/utils/request";
|
||||
import { RoleHintDisplay } from "..";
|
||||
import { MessageLimitInput, RoleHintDisplay } from "..";
|
||||
|
||||
export default function NewUserModal({ closeModal }) {
|
||||
const [error, setError] = useState(null);
|
||||
const [role, setRole] = useState("default");
|
||||
const [messageLimit, setMessageLimit] = useState({
|
||||
enabled: false,
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
const handleCreate = async (e) => {
|
||||
setError(null);
|
||||
@ -14,6 +18,8 @@ export default function NewUserModal({ closeModal }) {
|
||||
const data = {};
|
||||
const form = new FormData(e.target);
|
||||
for (var [key, value] of form.entries()) data[key] = value;
|
||||
data.dailyMessageLimit = messageLimit.enabled ? messageLimit.limit : null;
|
||||
|
||||
const { user, error } = await Admin.newUser(data);
|
||||
if (!!user) window.location.reload();
|
||||
setError(error);
|
||||
@ -59,13 +65,13 @@ export default function NewUserModal({ closeModal }) {
|
||||
pattern="^[a-z0-9_-]+$"
|
||||
onInvalid={(e) =>
|
||||
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("")}
|
||||
/>
|
||||
<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
|
||||
</p>
|
||||
</div>
|
||||
@ -111,6 +117,12 @@ export default function NewUserModal({ closeModal }) {
|
||||
</select>
|
||||
<RoleHintDisplay role={role} />
|
||||
</div>
|
||||
<MessageLimitInput
|
||||
role={role}
|
||||
enabled={messageLimit.enabled}
|
||||
limit={messageLimit.limit}
|
||||
updateState={setMessageLimit}
|
||||
/>
|
||||
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
|
||||
<p className="text-white text-xs md:text-sm">
|
||||
After creating a user they will need to login with their initial
|
||||
|
@ -1,11 +1,15 @@
|
||||
import React, { useState } from "react";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import Admin from "@/models/admin";
|
||||
import { RoleHintDisplay } from "../..";
|
||||
import { MessageLimitInput, RoleHintDisplay } from "../..";
|
||||
|
||||
export default function EditUserModal({ currentUser, user, closeModal }) {
|
||||
const [role, setRole] = useState(user.role);
|
||||
const [error, setError] = useState(null);
|
||||
const [messageLimit, setMessageLimit] = useState({
|
||||
enabled: user.dailyMessageLimit !== null,
|
||||
limit: user.dailyMessageLimit || 10,
|
||||
});
|
||||
|
||||
const handleUpdate = async (e) => {
|
||||
setError(null);
|
||||
@ -16,6 +20,12 @@ export default function EditUserModal({ currentUser, user, closeModal }) {
|
||||
if (!value || value === null) continue;
|
||||
data[key] = value;
|
||||
}
|
||||
if (messageLimit.enabled) {
|
||||
data.dailyMessageLimit = messageLimit.limit;
|
||||
} else {
|
||||
data.dailyMessageLimit = null;
|
||||
}
|
||||
|
||||
const { success, error } = await Admin.updateUser(user.id, data);
|
||||
if (success) window.location.reload();
|
||||
setError(error);
|
||||
@ -59,7 +69,7 @@ export default function EditUserModal({ currentUser, user, closeModal }) {
|
||||
autoComplete="off"
|
||||
/>
|
||||
<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
|
||||
</p>
|
||||
</div>
|
||||
@ -104,6 +114,12 @@ export default function EditUserModal({ currentUser, user, closeModal }) {
|
||||
</select>
|
||||
<RoleHintDisplay role={role} />
|
||||
</div>
|
||||
<MessageLimitInput
|
||||
role={role}
|
||||
enabled={messageLimit.enabled}
|
||||
limit={messageLimit.limit}
|
||||
updateState={setMessageLimit}
|
||||
/>
|
||||
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
|
||||
</div>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
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 AWSBedrockLogo from "@/media/llmprovider/bedrock.png";
|
||||
import DeepSeekLogo from "@/media/llmprovider/deepseek.png";
|
||||
import APIPieLogo from "@/media/llmprovider/apipie.png";
|
||||
|
||||
import PreLoader from "@/components/Preloader";
|
||||
import OpenAiOptions from "@/components/LLMSelection/OpenAiOptions";
|
||||
@ -50,6 +51,7 @@ import TextGenWebUIOptions from "@/components/LLMSelection/TextGenWebUIOptions";
|
||||
import LiteLLMOptions from "@/components/LLMSelection/LiteLLMOptions";
|
||||
import AWSBedrockLLMOptions from "@/components/LLMSelection/AwsBedrockLLMOptions";
|
||||
import DeepSeekOptions from "@/components/LLMSelection/DeepSeekOptions";
|
||||
import ApiPieLLMOptions from "@/components/LLMSelection/ApiPieOptions";
|
||||
|
||||
import LLMItem from "@/components/LLMSelection/LLMItem";
|
||||
import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react";
|
||||
@ -221,6 +223,27 @@ export const AVAILABLE_LLM_PROVIDERS = [
|
||||
description: "Run DeepSeek's powerful LLMs.",
|
||||
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",
|
||||
value: "generic-openai",
|
||||
@ -235,19 +258,6 @@ export const AVAILABLE_LLM_PROVIDERS = [
|
||||
"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",
|
||||
value: "native",
|
||||
|
@ -21,6 +21,7 @@ import TextGenWebUILogo from "@/media/llmprovider/text-generation-webui.png";
|
||||
import LiteLLMLogo from "@/media/llmprovider/litellm.png";
|
||||
import AWSBedrockLogo from "@/media/llmprovider/bedrock.png";
|
||||
import DeepSeekLogo from "@/media/llmprovider/deepseek.png";
|
||||
import APIPieLogo from "@/media/llmprovider/apipie.png";
|
||||
|
||||
import CohereLogo from "@/media/llmprovider/cohere.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"],
|
||||
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 = {
|
||||
|
@ -21,6 +21,7 @@ import TextGenWebUILogo from "@/media/llmprovider/text-generation-webui.png";
|
||||
import LiteLLMLogo from "@/media/llmprovider/litellm.png";
|
||||
import AWSBedrockLogo from "@/media/llmprovider/bedrock.png";
|
||||
import DeepSeekLogo from "@/media/llmprovider/deepseek.png";
|
||||
import APIPieLogo from "@/media/llmprovider/apipie.png";
|
||||
|
||||
import CohereLogo from "@/media/llmprovider/cohere.png";
|
||||
import OpenAiOptions from "@/components/LLMSelection/OpenAiOptions";
|
||||
@ -45,6 +46,7 @@ import TextGenWebUIOptions from "@/components/LLMSelection/TextGenWebUIOptions";
|
||||
import LiteLLMOptions from "@/components/LLMSelection/LiteLLMOptions";
|
||||
import AWSBedrockLLMOptions from "@/components/LLMSelection/AwsBedrockLLMOptions";
|
||||
import DeepSeekOptions from "@/components/LLMSelection/DeepSeekOptions";
|
||||
import ApiPieLLMOptions from "@/components/LLMSelection/ApiPieOptions";
|
||||
|
||||
import LLMItem from "@/components/LLMSelection/LLMItem";
|
||||
import System from "@/models/system";
|
||||
@ -195,6 +197,13 @@ const LLMS = [
|
||||
options: (settings) => <DeepSeekOptions settings={settings} />,
|
||||
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",
|
||||
value: "generic-openai",
|
||||
|
@ -24,6 +24,8 @@ const ENABLED_PROVIDERS = [
|
||||
"bedrock",
|
||||
"fireworksai",
|
||||
"deepseek",
|
||||
"litellm",
|
||||
"apipie",
|
||||
// TODO: More agent 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.
|
||||
|
@ -80,9 +80,6 @@ export default {
|
||||
return `/fine-tuning`;
|
||||
},
|
||||
settings: {
|
||||
system: () => {
|
||||
return `/settings/system-preferences`;
|
||||
},
|
||||
users: () => {
|
||||
return `/settings/users`;
|
||||
},
|
||||
|
@ -95,6 +95,10 @@ SIG_SALT='salt' # Please generate random string at least 32 chars long.
|
||||
# COHERE_API_KEY=
|
||||
# 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 ##########
|
||||
###########################################
|
||||
|
@ -347,14 +347,6 @@ function adminEndpoints(app) {
|
||||
: await SystemSettings.get({ 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":
|
||||
requestedSettings[label] = setting?.value ?? JSON.stringify([]);
|
||||
break;
|
||||
@ -422,13 +414,6 @@ function adminEndpoints(app) {
|
||||
try {
|
||||
const embedder = getEmbeddingEngineSelection();
|
||||
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:
|
||||
(await SystemSettings.get({ label: "footer_data" }))?.value ||
|
||||
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(
|
||||
"/v1/admin/preferences",
|
||||
[validApiKey],
|
||||
@ -658,8 +608,7 @@ function apiAdminEndpoints(app) {
|
||||
content: {
|
||||
"application/json": {
|
||||
example: {
|
||||
limit_user_messages: true,
|
||||
message_limit: 5,
|
||||
support_email: "support@example.com",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
const { v4: uuidv4 } = require("uuid");
|
||||
const { reqBody, userFromSession, multiUserMode } = require("../utils/http");
|
||||
const { validatedRequest } = require("../utils/middleware/validatedRequest");
|
||||
const { WorkspaceChats } = require("../models/workspaceChats");
|
||||
const { SystemSettings } = require("../models/systemSettings");
|
||||
const { Telemetry } = require("../models/telemetry");
|
||||
const { streamChatWithWorkspace } = require("../utils/chats/stream");
|
||||
const {
|
||||
@ -16,6 +14,7 @@ const {
|
||||
} = require("../utils/middleware/validWorkspace");
|
||||
const { writeResponseChunk } = require("../utils/helpers/chat/responses");
|
||||
const { WorkspaceThread } = require("../models/workspaceThread");
|
||||
const { User } = require("../models/user");
|
||||
const truncate = require("truncate");
|
||||
|
||||
function chatEndpoints(app) {
|
||||
@ -48,39 +47,16 @@ function chatEndpoints(app) {
|
||||
response.setHeader("Connection", "keep-alive");
|
||||
response.flushHeaders();
|
||||
|
||||
if (multiUserMode(response) && user.role !== ROLES.admin) {
|
||||
const limitMessagesSetting = await SystemSettings.get({
|
||||
label: "limit_user_messages",
|
||||
if (multiUserMode(response) && !(await User.canSendChat(user))) {
|
||||
writeResponseChunk(response, {
|
||||
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";
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await streamChatWithWorkspace(
|
||||
@ -157,41 +133,16 @@ function chatEndpoints(app) {
|
||||
response.setHeader("Connection", "keep-alive");
|
||||
response.flushHeaders();
|
||||
|
||||
if (multiUserMode(response) && user.role !== ROLES.admin) {
|
||||
const limitMessagesSetting = await SystemSettings.get({
|
||||
label: "limit_user_messages",
|
||||
if (multiUserMode(response) && !(await User.canSendChat(user))) {
|
||||
writeResponseChunk(response, {
|
||||
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";
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await streamChatWithWorkspace(
|
||||
|
@ -56,6 +56,7 @@ function embeddedEndpoints(app) {
|
||||
writeResponseChunk(response, {
|
||||
id: uuidv4(),
|
||||
type: "abort",
|
||||
sources: [],
|
||||
textResponse: null,
|
||||
close: true,
|
||||
error: e.message,
|
||||
@ -72,11 +73,15 @@ function embeddedEndpoints(app) {
|
||||
try {
|
||||
const { sessionId } = request.params;
|
||||
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) {
|
||||
console.error(e.message, e);
|
||||
response.sendStatus(500).end();
|
||||
|
@ -490,8 +490,6 @@ function systemEndpoints(app) {
|
||||
|
||||
await SystemSettings._updateSettings({
|
||||
multi_user_mode: true,
|
||||
limit_user_messages: false,
|
||||
message_limit: 25,
|
||||
});
|
||||
await BrowserExtensionApiKey.migrateApiKeysToMultiUser(user.id);
|
||||
|
||||
|
@ -1,5 +1,17 @@
|
||||
const { safeJsonParse } = require("../utils/http");
|
||||
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 = {
|
||||
new: async function ({
|
||||
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 (
|
||||
embedId = null,
|
||||
sessionId = null,
|
||||
limit = null,
|
||||
orderBy = null
|
||||
orderBy = null,
|
||||
filterSources = false
|
||||
) {
|
||||
if (!embedId || !sessionId) return [];
|
||||
|
||||
@ -43,7 +80,7 @@ const EmbedChats = {
|
||||
...(limit !== null ? { take: limit } : {}),
|
||||
...(orderBy !== null ? { orderBy } : { orderBy: { id: "asc" } }),
|
||||
});
|
||||
return chats;
|
||||
return filterSources ? this.filterSources(chats) : chats;
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
return [];
|
||||
|
@ -16,8 +16,6 @@ function isNullOrNaN(value) {
|
||||
const SystemSettings = {
|
||||
protectedFields: ["multi_user_mode"],
|
||||
publicFields: [
|
||||
"limit_user_messages",
|
||||
"message_limit",
|
||||
"footer_data",
|
||||
"support_email",
|
||||
"text_splitter_chunk_size",
|
||||
@ -33,8 +31,6 @@ const SystemSettings = {
|
||||
"meta_page_favicon",
|
||||
],
|
||||
supportedFields: [
|
||||
"limit_user_messages",
|
||||
"message_limit",
|
||||
"logo_filename",
|
||||
"telemetry_id",
|
||||
"footer_data",
|
||||
@ -512,6 +508,10 @@ const SystemSettings = {
|
||||
// DeepSeek API Keys
|
||||
DeepSeekApiKey: !!process.env.DEEPSEEK_API_KEY,
|
||||
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 { 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 = {
|
||||
usernameRegex: new RegExp(/^[a-z0-9_-]+$/),
|
||||
writable: [
|
||||
@ -10,6 +21,7 @@ const User = {
|
||||
"pfpFilename",
|
||||
"role",
|
||||
"suspended",
|
||||
"dailyMessageLimit",
|
||||
],
|
||||
validations: {
|
||||
username: (newValue = "") => {
|
||||
@ -32,12 +44,24 @@ const User = {
|
||||
}
|
||||
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.
|
||||
castColumnValue: function (key, value) {
|
||||
switch (key) {
|
||||
case "suspended":
|
||||
return Number(Boolean(value));
|
||||
case "dailyMessageLimit":
|
||||
return value === null ? null : Number(value);
|
||||
default:
|
||||
return String(value);
|
||||
}
|
||||
@ -48,7 +72,12 @@ const User = {
|
||||
return { ...rest };
|
||||
},
|
||||
|
||||
create: async function ({ username, password, role = "default" }) {
|
||||
create: async function ({
|
||||
username,
|
||||
password,
|
||||
role = "default",
|
||||
dailyMessageLimit = null,
|
||||
}) {
|
||||
const passwordCheck = this.checkPasswordComplexity(password);
|
||||
if (!passwordCheck.checkedOK) {
|
||||
return { user: null, error: passwordCheck.error };
|
||||
@ -58,7 +87,7 @@ const User = {
|
||||
// Do not allow new users to bypass validation
|
||||
if (!this.usernameRegex.test(username))
|
||||
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");
|
||||
@ -68,6 +97,8 @@ const User = {
|
||||
username: this.validations.username(username),
|
||||
password: hashedPassword,
|
||||
role: this.validations.role(role),
|
||||
dailyMessageLimit:
|
||||
this.validations.dailyMessageLimit(dailyMessageLimit),
|
||||
},
|
||||
});
|
||||
return { user: this.filterFields(user), error: null };
|
||||
@ -135,7 +166,7 @@ const User = {
|
||||
return {
|
||||
success: false,
|
||||
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({
|
||||
@ -260,6 +291,29 @@ const User = {
|
||||
|
||||
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 };
|
||||
|
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "users" ADD COLUMN "dailyMessageLimit" INTEGER;
|
@ -67,6 +67,7 @@ model users {
|
||||
seen_recovery_codes Boolean? @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
lastUpdatedAt DateTime @default(now())
|
||||
dailyMessageLimit Int?
|
||||
workspace_chats workspace_chats[]
|
||||
workspace_users workspace_users[]
|
||||
embed_configs embed_configs[]
|
||||
|
@ -4,8 +4,6 @@ const prisma = new PrismaClient();
|
||||
async function main() {
|
||||
const settings = [
|
||||
{ label: "multi_user_mode", value: "false" },
|
||||
{ label: "limit_user_messages", value: "false" },
|
||||
{ label: "message_limit", value: "25" },
|
||||
{ label: "logo_filename", value: "anything-llm.png" },
|
||||
];
|
||||
|
||||
|
1
server/storage/models/.gitignore
vendored
1
server/storage/models/.gitignore
vendored
@ -2,3 +2,4 @@ Xenova
|
||||
downloaded/*
|
||||
!downloaded/.placeholder
|
||||
openrouter
|
||||
apipie
|
@ -693,52 +693,6 @@
|
||||
}
|
||||
},
|
||||
"/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": {
|
||||
"tags": [
|
||||
"Admin"
|
||||
@ -788,8 +742,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"limit_user_messages": true,
|
||||
"message_limit": 5
|
||||
"support_email": "support@example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
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 });
|
||||
case "deepseek":
|
||||
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:
|
||||
throw new Error(
|
||||
|
@ -130,6 +130,22 @@ class Provider {
|
||||
apiKey: process.env.FIREWORKS_AI_LLM_API_KEY,
|
||||
...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
|
||||
// case "anythingllm_ollama":
|
||||
@ -174,14 +190,15 @@ class Provider {
|
||||
apiKey: process.env.TEXT_GEN_WEB_UI_API_KEY ?? "not-used",
|
||||
...config,
|
||||
});
|
||||
case "deepseek":
|
||||
case "litellm":
|
||||
return new ChatOpenAI({
|
||||
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,
|
||||
});
|
||||
|
||||
default:
|
||||
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 FireworksAIProvider = require("./fireworksai.js");
|
||||
const DeepSeekProvider = require("./deepseek.js");
|
||||
const LiteLLMProvider = require("./litellm.js");
|
||||
const ApiPieProvider = require("./apipie.js");
|
||||
|
||||
module.exports = {
|
||||
OpenAIProvider,
|
||||
@ -34,4 +36,6 @@ module.exports = {
|
||||
TextWebGenUiProvider,
|
||||
AWSBedrockProvider,
|
||||
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)
|
||||
throw new Error("DeepSeek API Key must be provided to use agents.");
|
||||
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:
|
||||
throw new Error(
|
||||
@ -212,6 +222,10 @@ class AgentHandler {
|
||||
return null;
|
||||
case "deepseek":
|
||||
return "deepseek-chat";
|
||||
case "litellm":
|
||||
return null;
|
||||
case "apipie":
|
||||
return null;
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
|
@ -60,8 +60,7 @@ async function streamChatWithForEmbed(
|
||||
const { rawHistory, chatHistory } = await recentEmbedChatHistory(
|
||||
sessionId,
|
||||
embed,
|
||||
messageLimit,
|
||||
chatMode
|
||||
messageLimit
|
||||
);
|
||||
|
||||
// See stream.js comment for more information on this implementation.
|
||||
@ -113,16 +112,27 @@ async function streamChatWithForEmbed(
|
||||
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];
|
||||
|
||||
// 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
|
||||
if (
|
||||
chatMode === "query" &&
|
||||
sources.length === 0 &&
|
||||
pinnedDocIdentifiers.length === 0
|
||||
) {
|
||||
if (chatMode === "query" && contextTexts.length === 0) {
|
||||
writeResponseChunk(response, {
|
||||
id: uuid,
|
||||
type: "textResponse",
|
||||
@ -178,7 +188,7 @@ async function streamChatWithForEmbed(
|
||||
await EmbedChats.new({
|
||||
embedId: embed.id,
|
||||
prompt: message,
|
||||
response: { text: completeText, type: chatMode },
|
||||
response: { text: completeText, type: chatMode, sources },
|
||||
connection_information: response.locals.connection
|
||||
? {
|
||||
...response.locals.connection,
|
||||
@ -190,15 +200,13 @@ async function streamChatWithForEmbed(
|
||||
return;
|
||||
}
|
||||
|
||||
// On query we don't return message history. All other chat modes and when chatting
|
||||
// with no embeddings we return history.
|
||||
async function recentEmbedChatHistory(
|
||||
sessionId,
|
||||
embed,
|
||||
messageLimit = 20,
|
||||
chatMode = null
|
||||
) {
|
||||
if (chatMode === "query") return { rawHistory: [], chatHistory: [] };
|
||||
/**
|
||||
* @param {string} sessionId the session id of the user from embed widget
|
||||
* @param {Object} embed the embed config object
|
||||
* @param {Number} messageLimit the number of messages to return
|
||||
* @returns {Promise<{rawHistory: import("@prisma/client").embed_chats[], chatHistory: {role: string, content: string}[]}>
|
||||
*/
|
||||
async function recentEmbedChatHistory(sessionId, embed, messageLimit = 20) {
|
||||
const rawHistory = (
|
||||
await EmbedChats.forEmbedByUser(embed.id, sessionId, messageLimit, {
|
||||
id: "desc",
|
||||
|
@ -1,4 +1,5 @@
|
||||
const { fetchOpenRouterModels } = require("../AiProviders/openRouter");
|
||||
const { fetchApiPieModels } = require("../AiProviders/apipie");
|
||||
const { perplexityModels } = require("../AiProviders/perplexity");
|
||||
const { togetherAiModels } = require("../AiProviders/togetherAi");
|
||||
const { fireworksAiModels } = require("../AiProviders/fireworksAi");
|
||||
@ -19,6 +20,7 @@ const SUPPORT_CUSTOM_MODELS = [
|
||||
"elevenlabs-tts",
|
||||
"groq",
|
||||
"deepseek",
|
||||
"apipie",
|
||||
];
|
||||
|
||||
async function getCustomModels(provider = "", apiKey = null, basePath = null) {
|
||||
@ -56,6 +58,8 @@ async function getCustomModels(provider = "", apiKey = null, basePath = null) {
|
||||
return await getGroqAiModels(apiKey);
|
||||
case "deepseek":
|
||||
return await getDeepSeekModels(apiKey);
|
||||
case "apipie":
|
||||
return await getAPIPieModels(apiKey);
|
||||
default:
|
||||
return { models: [], error: "Invalid provider for custom models" };
|
||||
}
|
||||
@ -355,6 +359,21 @@ async function getOpenRouterModels() {
|
||||
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) {
|
||||
const { OpenAI: OpenAIApi } = require("openai");
|
||||
const openai = new OpenAIApi({
|
||||
|
@ -162,6 +162,9 @@ function getLLMProvider({ provider = null, model = null } = {}) {
|
||||
case "deepseek":
|
||||
const { DeepSeekLLM } = require("../AiProviders/deepseek");
|
||||
return new DeepSeekLLM(embedder, model);
|
||||
case "apipie":
|
||||
const { ApiPieLLM } = require("../AiProviders/apipie");
|
||||
return new ApiPieLLM(embedder, model);
|
||||
default:
|
||||
throw new Error(
|
||||
`ENV: No valid LLM_PROVIDER value found in environment! Using ${process.env.LLM_PROVIDER}`
|
||||
@ -285,6 +288,12 @@ function getLLMProviderClass({ provider = null } = {}) {
|
||||
case "bedrock":
|
||||
const { AWSBedrockLLM } = require("../AiProviders/bedrock");
|
||||
return AWSBedrockLLM;
|
||||
case "deepseek":
|
||||
const { DeepSeekLLM } = require("../AiProviders/deepseek");
|
||||
return DeepSeekLLM;
|
||||
case "apipie":
|
||||
const { ApiPieLLM } = require("../AiProviders/apipie");
|
||||
return ApiPieLLM;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
@ -515,6 +515,16 @@ const KEY_MAPPING = {
|
||||
envKey: "DEEPSEEK_MODEL_PREF",
|
||||
checks: [isNotEmpty],
|
||||
},
|
||||
|
||||
// APIPie Options
|
||||
ApipieLLMApiKey: {
|
||||
envKey: "APIPIE_LLM_API_KEY",
|
||||
checks: [isNotEmpty],
|
||||
},
|
||||
ApipieLLMModelPref: {
|
||||
envKey: "APIPIE_LLM_MODEL_PREF",
|
||||
checks: [isNotEmpty],
|
||||
},
|
||||
};
|
||||
|
||||
function isNotEmpty(input = "") {
|
||||
@ -617,6 +627,7 @@ function supportedLLM(input = "") {
|
||||
"generic-openai",
|
||||
"bedrock",
|
||||
"deepseek",
|
||||
"apipie",
|
||||
].includes(input);
|
||||
return validSelection ? null : `${input} is not a valid LLM provider.`;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user