merge with master

This commit is contained in:
shatfield4 2024-10-15 14:59:44 -07:00
commit 3678365fca
45 changed files with 1067 additions and 393 deletions

View File

@ -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",

View File

@ -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 ##########
###########################################

View File

@ -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} />}

View 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>
);
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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"

View File

@ -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

View File

@ -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;
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -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>
);
}

View File

@ -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

View File

@ -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">

View File

@ -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>
);
}

View File

@ -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",

View File

@ -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 = {

View File

@ -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",

View File

@ -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.

View File

@ -80,9 +80,6 @@ export default {
return `/fine-tuning`;
},
settings: {
system: () => {
return `/settings/system-preferences`;
},
users: () => {
return `/settings/users`;
},

View File

@ -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 ##########
###########################################

View File

@ -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([]),

View File

@ -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",
}
}
}

View File

@ -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(

View File

@ -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();

View File

@ -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);

View File

@ -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 [];

View File

@ -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,
};
},

View File

@ -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 };

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "dailyMessageLimit" INTEGER;

View File

@ -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[]

View File

@ -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" },
];

View File

@ -2,3 +2,4 @@ Xenova
downloaded/*
!downloaded/.placeholder
openrouter
apipie

View File

@ -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"
}
}
}

View 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,
};

View File

@ -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(

View File

@ -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.`);
}

View 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;

View File

@ -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,
};

View 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;

View File

@ -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";
}

View File

@ -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",

View File

@ -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({

View File

@ -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;
}

View File

@ -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.`;
}