From f11ef10e5294e7be2a1ebf543234f7fe03574749 Mon Sep 17 00:00:00 2001 From: D1m7asis Date: Mon, 14 Jul 2025 23:37:17 +0200 Subject: [PATCH 1/6] feat: Add AI/ML API (aimlapi) as new LLM provider Introduces support for the AI/ML API (aimlapi) as a new LLM provider across the frontend and backend. Adds configuration options, provider selection UI, privacy info, and model selection for aimlapi. Implements backend integration for chat completions, agent support, and custom model listing. Updates environment examples and helper utilities to support the new provider. --- docker/.env.example | 10 +- .../AimlApiOptions/index.jsx | 110 ++++++ .../LLMSelection/AimlApiOptions/index.jsx | 111 ++++++ frontend/src/hooks/useGetProvidersModels.js | 1 + frontend/src/media/llmprovider/aimlapi.png | Bin 0 -> 11954 bytes .../EmbeddingPreference/index.jsx | 9 + .../GeneralSettings/LLMPreference/index.jsx | 10 + .../Steps/DataHandling/index.jsx | 9 + .../Steps/LLMPreference/index.jsx | 9 + .../AgentConfig/AgentLLMSelection/index.jsx | 1 + server/.env.example | 10 +- server/models/systemSettings.js | 4 + server/utils/AiProviders/aimlapi/index.js | 330 ++++++++++++++++++ .../utils/EmbeddingEngines/aimlapi/index.js | 122 +++++++ server/utils/agents/aibitat/index.js | 2 + .../utils/agents/aibitat/providers/aimlapi.js | 89 +++++ server/utils/agents/index.js | 6 + server/utils/helpers/customModels.js | 48 +++ server/utils/helpers/index.js | 11 + server/utils/helpers/updateENV.js | 2 + 20 files changed, 892 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/EmbeddingSelection/AimlApiOptions/index.jsx create mode 100644 frontend/src/components/LLMSelection/AimlApiOptions/index.jsx create mode 100644 frontend/src/media/llmprovider/aimlapi.png create mode 100644 server/utils/AiProviders/aimlapi/index.js create mode 100644 server/utils/EmbeddingEngines/aimlapi/index.js create mode 100644 server/utils/agents/aibitat/providers/aimlapi.js diff --git a/docker/.env.example b/docker/.env.example index 9c65405c235..387fc16aa92 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -129,6 +129,10 @@ GID='1000' # DEEPSEEK_API_KEY='your-deepseek-api-key-here' # DEEPSEEK_MODEL_PREF='deepseek-chat' +# LLM_PROVIDER='aimlapi' +# AIML_API_KEY='your-aimlapi-key' +# AIML_MODEL_PREF='gpt-3.5-turbo' + # LLM_PROVIDER='ppio' # PPIO_API_KEY='your-ppio-api-key-here' # PPIO_MODEL_PREF=deepseek/deepseek-v3/community @@ -182,6 +186,10 @@ GID='1000' # GENERIC_OPEN_AI_EMBEDDING_API_KEY='sk-123abc' # GENERIC_OPEN_AI_EMBEDDING_MAX_CONCURRENT_CHUNKS=500 +# EMBEDDING_ENGINE='aimlapi' +# AIML_API_KEY='your-aimlapi-key' +# EMBEDDING_MODEL_PREF='text-embedding-ada-002' + # EMBEDDING_ENGINE='gemini' # GEMINI_EMBEDDING_API_KEY= # EMBEDDING_MODEL_PREF='text-embedding-004' @@ -338,4 +346,4 @@ GID='1000' # Specify the target languages for when using OCR to parse images and PDFs. # This is a comma separated list of language codes as a string. Unsupported languages will be ignored. # Default is English. See https://tesseract-ocr.github.io/tessdoc/Data-Files-in-different-versions.html for a list of valid language codes. -# TARGET_OCR_LANG=eng,deu,ita,spa,fra,por,rus,nld,tur,hun,pol,ita,spa,fra,por,rus,nld,tur,hun,pol \ No newline at end of file +# TARGET_OCR_LANG=eng,deu,ita,spa,fra,por,rus,nld,tur,hun,pol,ita,spa,fra,por,rus,nld,tur,hun,pol diff --git a/frontend/src/components/EmbeddingSelection/AimlApiOptions/index.jsx b/frontend/src/components/EmbeddingSelection/AimlApiOptions/index.jsx new file mode 100644 index 00000000000..47916c5fde4 --- /dev/null +++ b/frontend/src/components/EmbeddingSelection/AimlApiOptions/index.jsx @@ -0,0 +1,110 @@ +import { useState, useEffect } from "react"; +import System from "@/models/system"; + +export default function AimlApiOptions({ settings }) { + const [inputValue, setInputValue] = useState(settings?.AimlApiKey); + const [apiKey, setApiKey] = useState(settings?.AimlApiKey); + + return ( +
+
+
+ + setInputValue(e.target.value)} + onBlur={() => setApiKey(inputValue)} + /> +
+ +
+
+ ); +} + +function AimlApiEmbeddingModelSelection({ apiKey, settings }) { + const [groupedModels, setGroupedModels] = useState({}); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function findModels() { + if (!apiKey) { + setGroupedModels({}); + setLoading(true); + return; + } + setLoading(true); + const { models } = await System.customModels( + "aimlapi-embed", + typeof apiKey === "boolean" ? null : apiKey + ); + if (models?.length > 0) { + const byDev = models.reduce((acc, model) => { + acc[model.organization] = acc[model.organization] || []; + acc[model.organization].push(model); + return acc; + }, {}); + setGroupedModels(byDev); + } + setLoading(false); + } + findModels(); + }, [apiKey]); + + if (loading || Object.keys(groupedModels).length === 0) { + return ( +
+ + +
+ ); + } + + return ( +
+ + +
+ ); +} diff --git a/frontend/src/components/LLMSelection/AimlApiOptions/index.jsx b/frontend/src/components/LLMSelection/AimlApiOptions/index.jsx new file mode 100644 index 00000000000..75d18b6cc5e --- /dev/null +++ b/frontend/src/components/LLMSelection/AimlApiOptions/index.jsx @@ -0,0 +1,111 @@ +import { useState, useEffect } from "react"; +import System from "@/models/system"; + +export default function AimlApiOptions({ settings }) { + const [inputValue, setInputValue] = useState(settings?.AimlApiKey); + const [apiKey, setApiKey] = useState(settings?.AimlApiKey); + + return ( +
+
+ + setInputValue(e.target.value)} + onBlur={() => setApiKey(inputValue)} + /> +
+ {!settings?.credentialsOnly && ( + + )} +
+ ); +} + +function AimlApiModelSelection({ apiKey, settings }) { + const [groupedModels, setGroupedModels] = useState({}); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function findCustomModels() { + if (!apiKey) { + setGroupedModels({}); + setLoading(true); + return; + } + + setLoading(true); + const { models } = await System.customModels( + "aimlapi", + typeof apiKey === "boolean" ? null : apiKey + ); + if (models?.length > 0) { + const byDev = models.reduce((acc, model) => { + acc[model.organization] = acc[model.organization] || []; + acc[model.organization].push(model); + return acc; + }, {}); + setGroupedModels(byDev); + } + setLoading(false); + } + findCustomModels(); + }, [apiKey]); + + if (loading || Object.keys(groupedModels).length === 0) { + return ( +
+ + +
+ ); + } + + return ( +
+ + +
+ ); +} diff --git a/frontend/src/hooks/useGetProvidersModels.js b/frontend/src/hooks/useGetProvidersModels.js index 82ef427cfd5..9cb964f1e7d 100644 --- a/frontend/src/hooks/useGetProvidersModels.js +++ b/frontend/src/hooks/useGetProvidersModels.js @@ -52,6 +52,7 @@ const groupedProviders = [ "novita", "openrouter", "ppio", + "aimlapi", ]; export default function useGetProviderModels(provider = null) { const [defaultModels, setDefaultModels] = useState([]); diff --git a/frontend/src/media/llmprovider/aimlapi.png b/frontend/src/media/llmprovider/aimlapi.png new file mode 100644 index 0000000000000000000000000000000000000000..86376b8e766df74c3673be4acabad81c7abc481e GIT binary patch literal 11954 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 literal 0 HcmV?d00001 diff --git a/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx b/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx index de27acb80a2..33a9ccc1dc2 100644 --- a/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx +++ b/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx @@ -37,6 +37,8 @@ import ModalWrapper from "@/components/ModalWrapper"; import CTAButton from "@/components/lib/CTAButton"; import { useTranslation } from "react-i18next"; import MistralAiOptions from "@/components/EmbeddingSelection/MistralAiOptions"; +import AimlApiLogo from "@/media/llmprovider/aimlapi.png"; +import AimlApiOptions from "@/components/EmbeddingSelection/AimlApiOptions"; const EMBEDDERS = [ { @@ -118,6 +120,13 @@ const EMBEDDERS = [ options: (settings) => , description: "Run powerful embedding models from Mistral AI.", }, + { + name: "AI/ML API", + value: "aimlapi", + logo: AimlApiLogo, + options: (settings) => , + description: "Use embedding models hosted on AI/ML API.", + }, { name: "Generic OpenAI", value: "generic-openai", diff --git a/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx b/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx index f9aceec189f..b10c0f4a077 100644 --- a/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx +++ b/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx @@ -32,6 +32,7 @@ import XAILogo from "@/media/llmprovider/xai.png"; import NvidiaNimLogo from "@/media/llmprovider/nvidia-nim.png"; import PPIOLogo from "@/media/llmprovider/ppio.png"; import DellProAiStudioLogo from "@/media/llmprovider/dpais.png"; +import AimlApiLogo from "@/media/llmprovider/aimlapi.png"; import PreLoader from "@/components/Preloader"; import OpenAiOptions from "@/components/LLMSelection/OpenAiOptions"; @@ -61,6 +62,7 @@ import XAILLMOptions from "@/components/LLMSelection/XAiLLMOptions"; import NvidiaNimOptions from "@/components/LLMSelection/NvidiaNimOptions"; import PPIOLLMOptions from "@/components/LLMSelection/PPIOLLMOptions"; import DellProAiStudioOptions from "@/components/LLMSelection/DPAISOptions"; +import AimlApiOptions from "@/components/LLMSelection/AimlApiOptions"; import LLMItem from "@/components/LLMSelection/LLMItem"; import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react"; @@ -263,6 +265,14 @@ export const AVAILABLE_LLM_PROVIDERS = [ description: "Run DeepSeek's powerful LLMs.", requiredConfig: ["DeepSeekApiKey"], }, + { + name: "AI/ML API", + value: "aimlapi", + logo: AimlApiLogo, + options: (settings) => , + description: "Access 300+ AI models with enterprise uptime.", + requiredConfig: ["AimlApiKey"], + }, { name: "PPIO", value: "ppio", diff --git a/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx index bc48209da9c..dca20825751 100644 --- a/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx +++ b/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx @@ -38,6 +38,7 @@ import VoyageAiLogo from "@/media/embeddingprovider/voyageai.png"; import PPIOLogo from "@/media/llmprovider/ppio.png"; import PGVectorLogo from "@/media/vectordbs/pgvector.png"; import DPAISLogo from "@/media/llmprovider/dpais.png"; +import AimlApiLogo from "@/media/llmprovider/aimlapi.png"; import React, { useState, useEffect } from "react"; import paths from "@/utils/paths"; import { useNavigate } from "react-router-dom"; @@ -235,6 +236,14 @@ export const LLM_SELECTION_PRIVACY = { ], logo: PPIOLogo, }, + aimlapi: { + name: "AI/ML API", + description: [ + "Your chats will not be used for training", + "Your prompts and document text used in response creation are visible to AI/ML API", + ], + logo: AimlApiLogo, + }, dpais: { name: "Dell Pro AI Studio", description: [ diff --git a/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx index 02d97893a79..4b8546d4f21 100644 --- a/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx +++ b/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx @@ -27,6 +27,7 @@ import NvidiaNimLogo from "@/media/llmprovider/nvidia-nim.png"; import CohereLogo from "@/media/llmprovider/cohere.png"; import PPIOLogo from "@/media/llmprovider/ppio.png"; import DellProAiStudioLogo from "@/media/llmprovider/dpais.png"; +import AimlApiLogo from "@/media/llmprovider/aimlapi.png"; import OpenAiOptions from "@/components/LLMSelection/OpenAiOptions"; import GenericOpenAiOptions from "@/components/LLMSelection/GenericOpenAiOptions"; @@ -55,6 +56,7 @@ import XAILLMOptions from "@/components/LLMSelection/XAiLLMOptions"; import NvidiaNimOptions from "@/components/LLMSelection/NvidiaNimOptions"; import PPIOLLMOptions from "@/components/LLMSelection/PPIOLLMOptions"; import DellProAiStudioOptions from "@/components/LLMSelection/DPAISOptions"; +import AimlApiOptions from "@/components/LLMSelection/AimlApiOptions"; import LLMItem from "@/components/LLMSelection/LLMItem"; import System from "@/models/system"; @@ -226,6 +228,13 @@ const LLMS = [ options: (settings) => , description: "Run DeepSeek's powerful LLMs.", }, + { + name: "AI/ML API", + value: "aimlapi", + logo: AimlApiLogo, + options: (settings) => , + description: "Access 300+ AI models with enterprise uptime.", + }, { name: "PPIO", value: "ppio", diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentLLMSelection/index.jsx b/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentLLMSelection/index.jsx index 6baae1ddee5..fabf8449df3 100644 --- a/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentLLMSelection/index.jsx +++ b/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentLLMSelection/index.jsx @@ -31,6 +31,7 @@ const ENABLED_PROVIDERS = [ "xai", "nvidia-nim", "gemini", + "aimlapi", // 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. diff --git a/server/.env.example b/server/.env.example index 809e36ed8fd..ecf87d5f3bb 100644 --- a/server/.env.example +++ b/server/.env.example @@ -57,6 +57,10 @@ SIG_SALT='salt' # Please generate random string at least 32 chars long. # DEEPSEEK_API_KEY=YOUR_API_KEY # DEEPSEEK_MODEL_PREF='deepseek-chat' +# LLM_PROVIDER='aimlapi' +# AIML_API_KEY='your-aimlapi-key' +# AIML_MODEL_PREF='gpt-3.5-turbo' + # LLM_PROVIDER='openrouter' # OPENROUTER_API_KEY='my-openrouter-key' # OPENROUTER_MODEL_PREF='openrouter/auto' @@ -180,6 +184,10 @@ SIG_SALT='salt' # Please generate random string at least 32 chars long. # GENERIC_OPEN_AI_EMBEDDING_API_KEY='sk-123abc' # GENERIC_OPEN_AI_EMBEDDING_MAX_CONCURRENT_CHUNKS=500 +# EMBEDDING_ENGINE='aimlapi' +# AIML_API_KEY='your-aimlapi-key' +# EMBEDDING_MODEL_PREF='text-embedding-ada-002' + # EMBEDDING_ENGINE='gemini' # GEMINI_EMBEDDING_API_KEY= # EMBEDDING_MODEL_PREF='text-embedding-004' @@ -335,4 +343,4 @@ TTS_PROVIDER="native" # Specify the target languages for when using OCR to parse images and PDFs. # This is a comma separated list of language codes as a string. Unsupported languages will be ignored. # Default is English. See https://tesseract-ocr.github.io/tessdoc/Data-Files-in-different-versions.html for a list of valid language codes. -# TARGET_OCR_LANG=eng,deu,ita,spa,fra,por,rus,nld,tur,hun,pol,ita,spa,fra,por,rus,nld,tur,hun,pol \ No newline at end of file +# TARGET_OCR_LANG=eng,deu,ita,spa,fra,por,rus,nld,tur,hun,pol,ita,spa,fra,por,rus,nld,tur,hun,pol diff --git a/server/models/systemSettings.js b/server/models/systemSettings.js index bb7311fb857..bd796024e3f 100644 --- a/server/models/systemSettings.js +++ b/server/models/systemSettings.js @@ -583,6 +583,10 @@ const SystemSettings = { PPIOApiKey: !!process.env.PPIO_API_KEY, PPIOModelPref: process.env.PPIO_MODEL_PREF, + // AI/ML API Keys + AimlApiKey: !!process.env.AIML_API_KEY, + AimlModelPref: process.env.AIML_MODEL_PREF, + // Dell Pro AI Studio Keys DellProAiStudioBasePath: process.env.DPAIS_LLM_BASE_PATH, DellProAiStudioModelPref: process.env.DPAIS_LLM_MODEL_PREF, diff --git a/server/utils/AiProviders/aimlapi/index.js b/server/utils/AiProviders/aimlapi/index.js new file mode 100644 index 00000000000..d02ac302ba8 --- /dev/null +++ b/server/utils/AiProviders/aimlapi/index.js @@ -0,0 +1,330 @@ +const { NativeEmbedder } = require("../../EmbeddingEngines/native"); +const { + LLMPerformanceMonitor, +} = require("../../helpers/chat/LLMPerformanceMonitor"); +const { + handleDefaultStreamResponseV2, + formatChatHistory, +} = require("../../helpers/chat/responses"); +const fs = require("fs"); +const path = require("path"); +const { safeJsonParse } = require("../../http"); + +const AIMLAPI_BASE_URL = "https://api.aimlapi.com/v1"; +const AIMLAPI_HEADERS = { + "HTTP-Referer": "https://anythingllm.com/", + "X-Title": "anything", +}; + +const cacheFolder = path.resolve( + process.env.STORAGE_DIR + ? path.resolve(process.env.STORAGE_DIR, "models", "aimlapi") + : path.resolve(__dirname, `../../../storage/models/aimlapi`) +); +const embedCacheFolder = path.resolve(cacheFolder, "embeddings"); + +class AimlApiLLM { + constructor(embedder = null, modelPreference = null) { + if (!process.env.AIML_API_KEY) throw new Error("No AI/ML API key was set."); + const { OpenAI: OpenAIApi } = require("openai"); + this.openai = new OpenAIApi({ + apiKey: process.env.AIML_API_KEY, + baseURL: AIMLAPI_BASE_URL, + defaultHeaders: AIMLAPI_HEADERS, + }); + this.model = + modelPreference || process.env.AIML_MODEL_PREF || "gpt-3.5-turbo"; + this.limits = { + history: this.promptWindowLimit() * 0.15, + system: this.promptWindowLimit() * 0.15, + user: this.promptWindowLimit() * 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"); + + this.embedder = embedder ?? new NativeEmbedder(); + this.defaultTemp = 0.7; + this.log( + `Initialized ${this.model} with context window ${this.promptWindowLimit()}` + ); + } + + log(text, ...args) { + console.log(`\x1b[36m[${this.constructor.name}]\x1b[0m ${text}`, ...args); + } + + #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("") + ); + } + + async #syncModels() { + if (fs.existsSync(this.cacheModelPath) && !this.#cacheIsStale()) + return false; + this.log("Model cache is not present or stale. Fetching from AimlApi API."); + await fetchAimlApiModels(); + return; + } + + #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; + } + + 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(modelName = "") { + await this.#syncModels(); + const availableModels = this.models(); + return Object.prototype.hasOwnProperty.call(availableModels, modelName); + } + + #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: "high" }, + }); + } + return content.flat(); + } + + constructPrompt({ + systemPrompt = "", + contextTexts = [], + chatHistory = [], + userPrompt = "", + attachments = [], + }) { + const prompt = { + role: "system", + content: `${systemPrompt}${this.#appendContext(contextTexts)}`, + }; + return [ + prompt, + ...formatChatHistory(chatHistory, this.#generateContent), + { + role: "user", + content: this.#generateContent({ userPrompt, attachments }), + }, + ]; + } + + async getChatCompletion(messages = null, { temperature = 0.7 }) { + if (!(await this.isValidChatCompletionModel(this.model))) + throw new Error( + `AI/ML API chat: ${this.model} is not valid for chat completion!` + ); + + const result = await LLMPerformanceMonitor.measureAsyncFunction( + this.openai.chat.completions.create({ + model: this.model, + messages, + temperature, + }) + ); + + if ( + !result.output.hasOwnProperty("choices") || + result.output.choices.length === 0 + ) + return null; + + return { + textResponse: result.output.choices[0].message.content, + metrics: { + prompt_tokens: result.output.usage.prompt_tokens || 0, + completion_tokens: result.output.usage.completion_tokens || 0, + total_tokens: result.output.usage.total_tokens || 0, + outputTps: result.output.usage.completion_tokens / result.duration, + duration: result.duration, + }, + }; + } + + async streamGetChatCompletion(messages = null, { temperature = 0.7 }) { + if (!(await this.isValidChatCompletionModel(this.model))) + throw new Error( + `AI/ML API chat: ${this.model} is not valid for chat completion!` + ); + + const measuredStreamRequest = await LLMPerformanceMonitor.measureStream( + this.openai.chat.completions.create({ + model: this.model, + stream: true, + messages, + temperature, + }), + messages, + false + ); + return measuredStreamRequest; + } + + handleStream(response, stream, responseProps) { + return handleDefaultStreamResponseV2(response, stream, responseProps); + } + + 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 fetchAimlApiModels(providedApiKey = null) { + const apiKey = providedApiKey || process.env.AIML_API_KEY || null; + return await fetch(`${AIMLAPI_BASE_URL}/models`, { + method: "GET", + headers: { + "Content-Type": "application/json", + ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}), + ...AIMLAPI_HEADERS, + }, + }) + .then((res) => res.json()) + .then(({ data = [] }) => { + const models = {}; + data + .filter((m) => m.type === "chat-completion") + .forEach((model) => { + const developer = + model.info?.developer || + model.provider || + model.id?.split("/")[0] || + "AimlApi"; + models[model.id] = { + id: model.id, + name: model.name || model.id, + developer: developer.charAt(0).toUpperCase() + developer.slice(1), + maxLength: model.context_length || model.max_tokens || 4096, + }; + }); + + 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 {}; + }); +} + +async function fetchAimlApiEmbeddingModels(providedApiKey = null) { + const apiKey = providedApiKey || process.env.AIML_API_KEY || null; + return await fetch(`${AIMLAPI_BASE_URL}/models`, { + method: "GET", + headers: { + "Content-Type": "application/json", + ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}), + ...AIMLAPI_HEADERS, + }, + }) + .then((res) => res.json()) + .then(({ data = [] }) => { + const models = {}; + data + .filter((m) => m.type === "embedding") + .forEach((model) => { + const developer = + model.info?.developer || + model.provider || + model.id?.split("/")[0] || + "AimlApi"; + models[model.id] = { + id: model.id, + name: model.name || model.id, + developer: developer.charAt(0).toUpperCase() + developer.slice(1), + maxLength: model.context_length || model.max_tokens || 4096, + }; + }); + + if (!fs.existsSync(embedCacheFolder)) + fs.mkdirSync(embedCacheFolder, { recursive: true }); + fs.writeFileSync( + path.resolve(embedCacheFolder, "models.json"), + JSON.stringify(models), + { encoding: "utf-8" } + ); + fs.writeFileSync( + path.resolve(embedCacheFolder, ".cached_at"), + String(Number(new Date())), + { encoding: "utf-8" } + ); + + return models; + }) + .catch((e) => { + console.error(e); + return {}; + }); +} + +module.exports = { + AimlApiLLM, + fetchAimlApiModels, + fetchAimlApiEmbeddingModels, + AIMLAPI_HEADERS, + AIMLAPI_BASE_URL, +}; diff --git a/server/utils/EmbeddingEngines/aimlapi/index.js b/server/utils/EmbeddingEngines/aimlapi/index.js new file mode 100644 index 00000000000..55c4fd79101 --- /dev/null +++ b/server/utils/EmbeddingEngines/aimlapi/index.js @@ -0,0 +1,122 @@ +const { toChunks, maximumChunkLength } = require("../../helpers"); +const { + AIMLAPI_HEADERS, + AIMLAPI_BASE_URL, + fetchAimlApiEmbeddingModels, +} = require("../../AiProviders/aimlapi"); +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", "aimlapi", "embeddings") + : path.resolve(__dirname, `../../../storage/models/aimlapi/embeddings`) +); + +class AimlApiEmbedder { + constructor() { + if (!process.env.AIML_API_KEY) throw new Error("No AI/ML API key was set."); + const { OpenAI: OpenAIApi } = require("openai"); + this.openai = new OpenAIApi({ + apiKey: process.env.AIML_API_KEY, + baseURL: AIMLAPI_BASE_URL, + defaultHeaders: AIMLAPI_HEADERS, + }); + this.model = process.env.EMBEDDING_MODEL_PREF || "text-embedding-ada-002"; + if (!fs.existsSync(cacheFolder)) + fs.mkdirSync(cacheFolder, { recursive: true }); + this.cacheModelPath = path.resolve(cacheFolder, "models.json"); + this.cacheAtPath = path.resolve(cacheFolder, ".cached_at"); + this.maxConcurrentChunks = 500; + this.embeddingMaxChunkLength = maximumChunkLength(); + this.log(`Initialized ${this.model}`); + this.#syncModels().catch((e) => + this.log(`Failed to sync models: ${e.message}`) + ); + } + + log(text, ...args) { + console.log(`\x1b[36m[AimlApiEmbedder]\x1b[0m ${text}`, ...args); + } + + #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; + } + + async #syncModels() { + if (fs.existsSync(this.cacheModelPath) && !this.#cacheIsStale()) + return false; + this.log("Model cache is not present or stale. Fetching from AimlApi API."); + await fetchAimlApiEmbeddingModels(); + return; + } + + models() { + if (!fs.existsSync(this.cacheModelPath)) return {}; + return safeJsonParse( + fs.readFileSync(this.cacheModelPath, { encoding: "utf-8" }), + {} + ); + } + + async isValidEmbeddingModel(modelName = "") { + await this.#syncModels(); + const availableModels = this.models(); + return Object.prototype.hasOwnProperty.call(availableModels, modelName); + } + + async embedTextInput(textInput) { + const result = await this.embedChunks( + Array.isArray(textInput) ? textInput : [textInput] + ); + return result?.[0] || []; + } + + async embedChunks(textChunks = []) { + this.log(`Embedding ${textChunks.length} chunks...`); + const embeddingRequests = []; + for (const chunk of toChunks(textChunks, this.maxConcurrentChunks)) { + embeddingRequests.push( + new Promise((resolve) => { + this.openai.embeddings + .create({ model: this.model, input: chunk }) + .then((result) => resolve({ data: result?.data, error: null })) + .catch((e) => { + e.type = + e?.response?.data?.error?.code || + e?.response?.status || + "failed_to_embed"; + e.message = e?.response?.data?.error?.message || e.message; + resolve({ data: [], error: e }); + }); + }) + ); + } + + const { data = [], error = null } = await Promise.all( + embeddingRequests + ).then((results) => { + const errors = results + .filter((res) => !!res.error) + .map((res) => res.error); + if (errors.length > 0) { + const unique = new Set(); + errors.forEach((err) => unique.add(`[${err.type}]: ${err.message}`)); + return { data: [], error: Array.from(unique).join(", ") }; + } + return { data: results.map((r) => r.data || []).flat(), error: null }; + }); + + if (error) throw new Error(`AimlApi Failed to embed: ${error}`); + return data.length > 0 && data.every((d) => d.hasOwnProperty("embedding")) + ? data.map((d) => d.embedding) + : null; + } +} + +module.exports = { AimlApiEmbedder }; diff --git a/server/utils/agents/aibitat/index.js b/server/utils/agents/aibitat/index.js index 6e069defd3a..50e873743e9 100644 --- a/server/utils/agents/aibitat/index.js +++ b/server/utils/agents/aibitat/index.js @@ -826,6 +826,8 @@ ${this.getHistory({ to: route.to }) return new Providers.PPIOProvider({ model: config.model }); case "gemini": return new Providers.GeminiProvider({ model: config.model }); + case "aimlapi": + return new Providers.AimlApiProvider({ model: config.model }); case "dpais": return new Providers.DellProAiStudioProvider({ model: config.model }); default: diff --git a/server/utils/agents/aibitat/providers/aimlapi.js b/server/utils/agents/aibitat/providers/aimlapi.js new file mode 100644 index 00000000000..ce15b819070 --- /dev/null +++ b/server/utils/agents/aibitat/providers/aimlapi.js @@ -0,0 +1,89 @@ +const OpenAI = require("openai"); +const { + AIMLAPI_HEADERS, + AIMLAPI_BASE_URL, +} = require("../../AiProviders/aimlapi"); +const Provider = require("./ai-provider.js"); +const InheritMultiple = require("./helpers/classes.js"); +const UnTooled = require("./helpers/untooled.js"); + +class AimlApiProvider extends InheritMultiple([Provider, UnTooled]) { + model; + + constructor(config = {}) { + super(); + const { model = "gpt-3.5-turbo" } = config; + const client = new OpenAI({ + baseURL: AIMLAPI_BASE_URL, + apiKey: process.env.AIML_API_KEY ?? null, + maxRetries: 3, + defaultHeaders: AIMLAPI_HEADERS, + }); + + 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("AimlApi chat: No results!"); + if (result.choices.length === 0) + throw new Error("AimlApi chat: No results length!"); + return result.choices[0].message.content; + }) + .catch(() => { + return null; + }); + } + + async complete(messages, functions = []) { + 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 }; + } else { + completion = await this.client.chat.completions.create({ + model: this.model, + messages, + }); + completion = completion.choices[0]; + } + return { result: completion.content, cost: 0 }; + } catch (e) { + this.providerLog(e.message); + return { result: null, cost: 0 }; + } + } +} + +module.exports = AimlApiProvider; diff --git a/server/utils/agents/index.js b/server/utils/agents/index.js index 915e5a59bed..913bf9cf304 100644 --- a/server/utils/agents/index.js +++ b/server/utils/agents/index.js @@ -185,6 +185,10 @@ class AgentHandler { if (!process.env.PPIO_API_KEY) throw new Error("PPIO API Key must be provided to use agents."); break; + case "aimlapi": + if (!process.env.AIML_API_KEY) + throw new Error("AI/ML API key must be provided to use agents."); + break; case "gemini": if (!process.env.GEMINI_API_KEY) throw new Error("Gemini API key must be provided to use agents."); @@ -266,6 +270,8 @@ class AgentHandler { return process.env.PPIO_MODEL_PREF ?? "qwen/qwen2.5-32b-instruct"; case "gemini": return process.env.GEMINI_LLM_MODEL_PREF ?? "gemini-2.0-flash-lite"; + case "aimlapi": + return process.env.AIML_MODEL_PREF ?? "gpt-3.5-turbo"; case "dpais": return process.env.DPAIS_LLM_MODEL_PREF; default: diff --git a/server/utils/helpers/customModels.js b/server/utils/helpers/customModels.js index cff97ff888f..cabfffba8a1 100644 --- a/server/utils/helpers/customModels.js +++ b/server/utils/helpers/customModels.js @@ -9,6 +9,10 @@ const { parseLMStudioBasePath } = require("../AiProviders/lmStudio"); const { parseNvidiaNimBasePath } = require("../AiProviders/nvidiaNim"); const { fetchPPIOModels } = require("../AiProviders/ppio"); const { GeminiLLM } = require("../AiProviders/gemini"); +const { + fetchAimlApiModels, + fetchAimlApiEmbeddingModels, +} = require("../AiProviders/aimlapi"); const SUPPORT_CUSTOM_MODELS = [ "openai", @@ -33,6 +37,8 @@ const SUPPORT_CUSTOM_MODELS = [ "gemini", "ppio", "dpais", + "aimlapi", + "aimlapi-embed", ]; async function getCustomModels(provider = "", apiKey = null, basePath = null) { @@ -84,6 +90,10 @@ async function getCustomModels(provider = "", apiKey = null, basePath = null) { return await getPPIOModels(apiKey); case "dpais": return await getDellProAiStudioModels(basePath); + case "aimlapi": + return await getAimlApiModels(apiKey); + case "aimlapi-embed": + return await getAimlApiEmbeddingModels(apiKey); default: return { models: [], error: "Invalid provider for custom models" }; } @@ -675,6 +685,44 @@ async function getDellProAiStudioModels(basePath = null) { } } +async function getAimlApiModels(apiKey = null) { + const knownModels = await fetchAimlApiModels(apiKey); + if (!Object.keys(knownModels).length === 0) + return { models: [], error: null }; + + if (Object.keys(knownModels).length > 0 && !!apiKey) + process.env.AIML_API_KEY = apiKey; + + const models = Object.values(knownModels).map((model) => { + return { + id: model.id, + organization: model.developer, + name: model.name, + }; + }); + return { models, error: null }; +} + +async function getAimlApiEmbeddingModels(apiKey = null) { + const knownModels = await fetchAimlApiEmbeddingModels(apiKey); + if (!Object.keys(knownModels).length === 0) + return { models: [], error: null }; + + if (Object.keys(knownModels).length > 0 && !!apiKey) + process.env.AIML_API_KEY = apiKey; + + const models = Object.values(knownModels).map((model) => { + return { + id: model.id, + organization: model.developer, + name: model.name, + }; + }); + return { models, error: null }; +} + module.exports = { getCustomModels, + getAimlApiModels, + getAimlApiEmbeddingModels, }; diff --git a/server/utils/helpers/index.js b/server/utils/helpers/index.js index 2017c618fac..18daba32bc6 100644 --- a/server/utils/helpers/index.js +++ b/server/utils/helpers/index.js @@ -203,6 +203,9 @@ function getLLMProvider({ provider = null, model = null } = {}) { case "ppio": const { PPIOLLM } = require("../AiProviders/ppio"); return new PPIOLLM(embedder, model); + case "aimlapi": + const { AimlApiLLM } = require("../AiProviders/aimlapi"); + return new AimlApiLLM(embedder, model); case "dpais": const { DellProAiStudioLLM } = require("../AiProviders/dellProAiStudio"); return new DellProAiStudioLLM(embedder, model); @@ -260,6 +263,9 @@ function getEmbeddingEngineSelection() { case "gemini": const { GeminiEmbedder } = require("../EmbeddingEngines/gemini"); return new GeminiEmbedder(); + case "aimlapi": + const { AimlApiEmbedder } = require("../EmbeddingEngines/aimlapi"); + return new AimlApiEmbedder(); default: return new NativeEmbedder(); } @@ -350,6 +356,9 @@ function getLLMProviderClass({ provider = null } = {}) { case "ppio": const { PPIOLLM } = require("../AiProviders/ppio"); return PPIOLLM; + case "aimlapi": + const { AimlApiLLM } = require("../AiProviders/aimlapi"); + return AimlApiLLM; case "dpais": const { DellProAiStudioLLM } = require("../AiProviders/dellProAiStudio"); return DellProAiStudioLLM; @@ -417,6 +426,8 @@ function getBaseLLMProviderModel({ provider = null } = {}) { return process.env.NVIDIA_NIM_LLM_MODEL_PREF; case "ppio": return process.env.PPIO_API_KEY; + case "aimlapi": + return process.env.AIML_MODEL_PREF; case "dpais": return process.env.DPAIS_LLM_MODEL_PREF; default: diff --git a/server/utils/helpers/updateENV.js b/server/utils/helpers/updateENV.js index f64a042ce9a..8660ece7829 100644 --- a/server/utils/helpers/updateENV.js +++ b/server/utils/helpers/updateENV.js @@ -780,6 +780,7 @@ function supportedLLM(input = "") { "nvidia-nim", "ppio", "dpais", + "aimlapi", ].includes(input); return validSelection ? null : `${input} is not a valid LLM provider.`; } @@ -817,6 +818,7 @@ function supportedEmbeddingModel(input = "") { "litellm", "generic-openai", "mistral", + "aimlapi", ]; return supported.includes(input) ? null From d6969ea7bec933fca31268aff338eb87c385b605 Mon Sep 17 00:00:00 2001 From: shatfield4 Date: Thu, 17 Jul 2025 18:06:03 -0700 Subject: [PATCH 2/6] patch model not saving --- server/utils/helpers/updateENV.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/server/utils/helpers/updateENV.js b/server/utils/helpers/updateENV.js index ab05e054736..39f3d22a732 100644 --- a/server/utils/helpers/updateENV.js +++ b/server/utils/helpers/updateENV.js @@ -203,6 +203,16 @@ const KEY_MAPPING = { checks: [], }, + // AI/ML API Options + AimlApiKey: { + envKey: "AIML_API_KEY", + checks: [isNotEmpty], + }, + AimlModelPref: { + envKey: "AIML_MODEL_PREF", + checks: [isNotEmpty], + }, + // Generic OpenAI InferenceSettings GenericOpenAiBasePath: { envKey: "GENERIC_OPEN_AI_BASE_PATH", From f009b0af721c6698d410a2e8b744e45cfdaeada0 Mon Sep 17 00:00:00 2001 From: shatfield4 Date: Thu, 17 Jul 2025 18:21:35 -0700 Subject: [PATCH 3/6] update readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a8199403b28..411e46369a8 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,7 @@ AnythingLLM divides your documents into objects called `workspaces`. A Workspace - [LocalAI (all models)](https://localai.io/) - [Together AI (chat models)](https://www.together.ai/) - [Fireworks AI (chat models)](https://fireworks.ai/) +- [AI/ML API (chat models)](https://aimlapi.com/) - [Perplexity (chat models)](https://www.perplexity.ai/) - [OpenRouter (chat models)](https://openrouter.ai/) - [DeepSeek (chat models)](https://deepseek.com/) From bd390b6e76e48139e432a5d73b0cc07e8be4c994 Mon Sep 17 00:00:00 2001 From: shatfield4 Date: Thu, 17 Jul 2025 18:24:47 -0700 Subject: [PATCH 4/6] update gitignore --- server/storage/models/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/server/storage/models/.gitignore b/server/storage/models/.gitignore index 5e83df7bcc4..b3c9a7a50dd 100644 --- a/server/storage/models/.gitignore +++ b/server/storage/models/.gitignore @@ -9,4 +9,5 @@ gemini togetherAi tesseract ppio +aimlapi context-windows/* \ No newline at end of file From f6f9231bdea832bda789a6fb421add766fbd5400 Mon Sep 17 00:00:00 2001 From: D1m7asis Date: Mon, 21 Jul 2025 17:30:18 +0200 Subject: [PATCH 5/6] fix: Refactor AI/ML API key handling for LLM and embedding Standardizes environment variable names for AI/ML API keys, separating LLM and embedding keys as AIML_LLM_API_KEY and AIML_EMBEDDER_API_KEY. Updates frontend, backend, and documentation to use the new variable names, improves provider integration, and adds privacy info for AI/ML API embedding. Also updates the AimlApiProvider implementation and related helper logic. --- README.md | 2 +- docker/.env.example | 4 +- .../AimlApiOptions/index.jsx | 8 +- .../LLMSelection/AimlApiOptions/index.jsx | 8 +- frontend/src/media/llmprovider/aimlapi.png | Bin 11954 -> 9070 bytes .../GeneralSettings/LLMPreference/index.jsx | 2 +- .../Steps/DataHandling/index.jsx | 8 ++ server/.env.example | 4 +- server/models/systemSettings.js | 3 +- server/utils/AiProviders/aimlapi/index.js | 34 +++-- .../utils/EmbeddingEngines/aimlapi/index.js | 12 +- .../utils/agents/aibitat/providers/aimlapi.js | 125 +++++++++--------- .../utils/agents/aibitat/providers/index.js | 2 + server/utils/agents/index.js | 2 +- server/utils/helpers/customModels.js | 4 +- server/utils/helpers/updateENV.js | 9 +- 16 files changed, 121 insertions(+), 106 deletions(-) diff --git a/README.md b/README.md index 411e46369a8..6187d570aa0 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ AnythingLLM divides your documents into objects called `workspaces`. A Workspace - [LocalAI (all models)](https://localai.io/) - [Together AI (chat models)](https://www.together.ai/) - [Fireworks AI (chat models)](https://fireworks.ai/) -- [AI/ML API (chat models)](https://aimlapi.com/) +- [AI/ML API (chat models)](https://aimlapi.com/models/?utm_source=anythingllm&utm_medium=github&utm_campaign=integration) - [Perplexity (chat models)](https://www.perplexity.ai/) - [OpenRouter (chat models)](https://openrouter.ai/) - [DeepSeek (chat models)](https://deepseek.com/) diff --git a/docker/.env.example b/docker/.env.example index b4abe88a361..b8e6676959d 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -130,7 +130,7 @@ GID='1000' # DEEPSEEK_MODEL_PREF='deepseek-chat' # LLM_PROVIDER='aimlapi' -# AIML_API_KEY='your-aimlapi-key' +# AIML_LLM_API_KEY='your-aimlapi-key' # AIML_MODEL_PREF='gpt-3.5-turbo' # LLM_PROVIDER='ppio' @@ -187,7 +187,7 @@ GID='1000' # GENERIC_OPEN_AI_EMBEDDING_MAX_CONCURRENT_CHUNKS=500 # EMBEDDING_ENGINE='aimlapi' -# AIML_API_KEY='your-aimlapi-key' +# AIML_EMBEDDER_API_KEY='your-aimlapi-key' # EMBEDDING_MODEL_PREF='text-embedding-ada-002' # EMBEDDING_ENGINE='gemini' diff --git a/frontend/src/components/EmbeddingSelection/AimlApiOptions/index.jsx b/frontend/src/components/EmbeddingSelection/AimlApiOptions/index.jsx index 47916c5fde4..18ed62be566 100644 --- a/frontend/src/components/EmbeddingSelection/AimlApiOptions/index.jsx +++ b/frontend/src/components/EmbeddingSelection/AimlApiOptions/index.jsx @@ -2,8 +2,8 @@ import { useState, useEffect } from "react"; import System from "@/models/system"; export default function AimlApiOptions({ settings }) { - const [inputValue, setInputValue] = useState(settings?.AimlApiKey); - const [apiKey, setApiKey] = useState(settings?.AimlApiKey); + const [inputValue, setInputValue] = useState(settings?.AimlEmbedderApiKey); + const [apiKey, setApiKey] = useState(settings?.AimlEmbedderApiKey); return (
@@ -14,10 +14,10 @@ export default function AimlApiOptions({ settings }) { @@ -13,10 +13,10 @@ export default function AimlApiOptions({ settings }) { |HJ!(>l&x*v(LTPeXo13b)sKtt5Z@iQV9QU>E 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 Y{OZs?i=L-T;2)xAs@f{0%9f%31Cec1vH$=8 literal 11954 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 diff --git a/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx b/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx index b10c0f4a077..7e0013f89fd 100644 --- a/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx +++ b/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx @@ -271,7 +271,7 @@ export const AVAILABLE_LLM_PROVIDERS = [ logo: AimlApiLogo, options: (settings) => , description: "Access 300+ AI models with enterprise uptime.", - requiredConfig: ["AimlApiKey"], + requiredConfig: ["AimlLlmApiKey"], }, { name: "PPIO", diff --git a/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx index dca20825751..4b6e819dd81 100644 --- a/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx +++ b/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx @@ -388,6 +388,14 @@ export const EMBEDDING_ENGINE_PRIVACY = { ], logo: MistralLogo, }, + aimlapi: { + name: "AI/ML API", + description: [ + "Your document text is visible to AI/ML API", + "Your documents are not used for training", + ], + logo: AimlApiLogo, + }, litellm: { name: "LiteLLM", description: [ diff --git a/server/.env.example b/server/.env.example index 674b58bef6a..19d4756076f 100644 --- a/server/.env.example +++ b/server/.env.example @@ -58,7 +58,7 @@ SIG_SALT='salt' # Please generate random string at least 32 chars long. # DEEPSEEK_MODEL_PREF='deepseek-chat' # LLM_PROVIDER='aimlapi' -# AIML_API_KEY='your-aimlapi-key' +# AIML_LLM_API_KEY='your-aimlapi-key' # AIML_MODEL_PREF='gpt-3.5-turbo' # LLM_PROVIDER='openrouter' @@ -185,7 +185,7 @@ SIG_SALT='salt' # Please generate random string at least 32 chars long. # GENERIC_OPEN_AI_EMBEDDING_MAX_CONCURRENT_CHUNKS=500 # EMBEDDING_ENGINE='aimlapi' -# AIML_API_KEY='your-aimlapi-key' +# AIML_EMBEDDER_API_KEY='your-aimlapi-key' # EMBEDDING_MODEL_PREF='text-embedding-ada-002' # EMBEDDING_ENGINE='gemini' diff --git a/server/models/systemSettings.js b/server/models/systemSettings.js index 3307c30c127..73ac6674d82 100644 --- a/server/models/systemSettings.js +++ b/server/models/systemSettings.js @@ -221,6 +221,7 @@ const SystemSettings = { GenericOpenAiEmbeddingMaxConcurrentChunks: process.env.GENERIC_OPEN_AI_EMBEDDING_MAX_CONCURRENT_CHUNKS || 500, GeminiEmbeddingApiKey: !!process.env.GEMINI_EMBEDDING_API_KEY, + AimlEmbedderApiKey: !!process.env.AIML_EMBEDDER_API_KEY, // -------------------------------------------------------- // VectorDB Provider Selection Settings & Configs @@ -589,7 +590,7 @@ const SystemSettings = { PPIOModelPref: process.env.PPIO_MODEL_PREF, // AI/ML API Keys - AimlApiKey: !!process.env.AIML_API_KEY, + AimlLlmApiKey: !!process.env.AIML_LLM_API_KEY, AimlModelPref: process.env.AIML_MODEL_PREF, // Dell Pro AI Studio Keys diff --git a/server/utils/AiProviders/aimlapi/index.js b/server/utils/AiProviders/aimlapi/index.js index d02ac302ba8..29a4e98a40f 100644 --- a/server/utils/AiProviders/aimlapi/index.js +++ b/server/utils/AiProviders/aimlapi/index.js @@ -10,12 +10,6 @@ const fs = require("fs"); const path = require("path"); const { safeJsonParse } = require("../../http"); -const AIMLAPI_BASE_URL = "https://api.aimlapi.com/v1"; -const AIMLAPI_HEADERS = { - "HTTP-Referer": "https://anythingllm.com/", - "X-Title": "anything", -}; - const cacheFolder = path.resolve( process.env.STORAGE_DIR ? path.resolve(process.env.STORAGE_DIR, "models", "aimlapi") @@ -24,13 +18,19 @@ const cacheFolder = path.resolve( const embedCacheFolder = path.resolve(cacheFolder, "embeddings"); class AimlApiLLM { + static BASE_URL = "https://api.aimlapi.com/v1"; + static HEADERS = { + "HTTP-Referer": "https://anythingllm.com/", + "X-Title": "anything", + }; constructor(embedder = null, modelPreference = null) { - if (!process.env.AIML_API_KEY) throw new Error("No AI/ML API key was set."); + if (!process.env.AIML_LLM_API_KEY) + throw new Error("No AI/ML API key was set."); const { OpenAI: OpenAIApi } = require("openai"); this.openai = new OpenAIApi({ - apiKey: process.env.AIML_API_KEY, - baseURL: AIMLAPI_BASE_URL, - defaultHeaders: AIMLAPI_HEADERS, + apiKey: process.env.AIML_LLM_API_KEY, + baseURL: AimlApiLLM.BASE_URL, + defaultHeaders: AimlApiLLM.HEADERS, }); this.model = modelPreference || process.env.AIML_MODEL_PREF || "gpt-3.5-turbo"; @@ -222,13 +222,13 @@ class AimlApiLLM { } async function fetchAimlApiModels(providedApiKey = null) { - const apiKey = providedApiKey || process.env.AIML_API_KEY || null; - return await fetch(`${AIMLAPI_BASE_URL}/models`, { + const apiKey = providedApiKey || process.env.AIML_LLM_API_KEY || null; + return await fetch(`${AimlApiLLM.BASE_URL}/models`, { method: "GET", headers: { "Content-Type": "application/json", ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}), - ...AIMLAPI_HEADERS, + ...AimlApiLLM.HEADERS, }, }) .then((res) => res.json()) @@ -272,13 +272,13 @@ async function fetchAimlApiModels(providedApiKey = null) { } async function fetchAimlApiEmbeddingModels(providedApiKey = null) { - const apiKey = providedApiKey || process.env.AIML_API_KEY || null; - return await fetch(`${AIMLAPI_BASE_URL}/models`, { + const apiKey = providedApiKey || process.env.AIML_EMBEDDER_API_KEY || null; + return await fetch(`${AimlApiLLM.BASE_URL}/models`, { method: "GET", headers: { "Content-Type": "application/json", ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}), - ...AIMLAPI_HEADERS, + ...AimlApiLLM.HEADERS, }, }) .then((res) => res.json()) @@ -325,6 +325,4 @@ module.exports = { AimlApiLLM, fetchAimlApiModels, fetchAimlApiEmbeddingModels, - AIMLAPI_HEADERS, - AIMLAPI_BASE_URL, }; diff --git a/server/utils/EmbeddingEngines/aimlapi/index.js b/server/utils/EmbeddingEngines/aimlapi/index.js index 55c4fd79101..4fb3f73d477 100644 --- a/server/utils/EmbeddingEngines/aimlapi/index.js +++ b/server/utils/EmbeddingEngines/aimlapi/index.js @@ -1,7 +1,6 @@ const { toChunks, maximumChunkLength } = require("../../helpers"); const { - AIMLAPI_HEADERS, - AIMLAPI_BASE_URL, + AimlApiLLM, fetchAimlApiEmbeddingModels, } = require("../../AiProviders/aimlapi"); const fs = require("fs"); @@ -16,12 +15,13 @@ const cacheFolder = path.resolve( class AimlApiEmbedder { constructor() { - if (!process.env.AIML_API_KEY) throw new Error("No AI/ML API key was set."); + if (!process.env.AIML_EMBEDDER_API_KEY) + throw new Error("No AI/ML API key was set."); const { OpenAI: OpenAIApi } = require("openai"); this.openai = new OpenAIApi({ - apiKey: process.env.AIML_API_KEY, - baseURL: AIMLAPI_BASE_URL, - defaultHeaders: AIMLAPI_HEADERS, + apiKey: process.env.AIML_EMBEDDER_API_KEY, + baseURL: AimlApiLLM.BASE_URL, + defaultHeaders: AimlApiLLM.HEADERS, }); this.model = process.env.EMBEDDING_MODEL_PREF || "text-embedding-ada-002"; if (!fs.existsSync(cacheFolder)) diff --git a/server/utils/agents/aibitat/providers/aimlapi.js b/server/utils/agents/aibitat/providers/aimlapi.js index ce15b819070..8aeee5ca2ba 100644 --- a/server/utils/agents/aibitat/providers/aimlapi.js +++ b/server/utils/agents/aibitat/providers/aimlapi.js @@ -1,89 +1,90 @@ const OpenAI = require("openai"); -const { - AIMLAPI_HEADERS, - AIMLAPI_BASE_URL, -} = require("../../AiProviders/aimlapi"); +const { AimlApiLLM } = require("../../../AiProviders/aimlapi"); const Provider = require("./ai-provider.js"); -const InheritMultiple = require("./helpers/classes.js"); -const UnTooled = require("./helpers/untooled.js"); +const { RetryError } = require("../error.js"); -class AimlApiProvider extends InheritMultiple([Provider, UnTooled]) { + +class AimlApiProvider extends Provider { model; constructor(config = {}) { - super(); const { model = "gpt-3.5-turbo" } = config; const client = new OpenAI({ - baseURL: AIMLAPI_BASE_URL, - apiKey: process.env.AIML_API_KEY ?? null, + baseURL: AimlApiLLM.BASE_URL, + apiKey: process.env.AIML_LLM_API_KEY ?? null, maxRetries: 3, - defaultHeaders: AIMLAPI_HEADERS, + defaultHeaders: AimlApiLLM.HEADERS, }); + super(client); - this._client = client; this.model = model; this.verbose = true; } - get client() { - return this._client; - } - - async #handleFunctionCallChat({ messages = [] }) { - return await this.client.chat.completions - .create({ + async complete(messages, functions = []) { + try { + const response = await this.client.chat.completions.create({ model: this.model, - temperature: 0, messages, - }) - .then((result) => { - if (!result.hasOwnProperty("choices")) - throw new Error("AimlApi chat: No results!"); - if (result.choices.length === 0) - throw new Error("AimlApi chat: No results length!"); - return result.choices[0].message.content; - }) - .catch(() => { - return null; + ...(Array.isArray(functions) && functions.length > 0 + ? { functions } + : {}), }); - } - async complete(messages, functions = []) { - try { - let completion; - if (functions.length > 0) { - const { toolCall, text } = await this.functionCall( - messages, - functions, - this.#handleFunctionCallChat.bind(this) - ); + const completion = response.choices[0].message; + const cost = this.getCost(response.usage); - 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, - }; + if (completion.function_call) { + let functionArgs = {}; + try { + functionArgs = JSON.parse(completion.function_call.arguments); + } catch (error) { + return this.complete( + [ + ...messages, + { + role: "function", + name: completion.function_call.name, + function_call: completion.function_call, + content: error?.message, + }, + ], + functions + ); } - completion = { content: text }; - } else { - completion = await this.client.chat.completions.create({ - model: this.model, - messages, - }); - completion = completion.choices[0]; + + return { + result: null, + functionCall: { + name: completion.function_call.name, + arguments: functionArgs, + }, + cost, + }; + } + + return { + result: completion.content, + cost, + }; + } catch (error) { + if (error instanceof OpenAI.AuthenticationError) throw error; + + if ( + error instanceof OpenAI.RateLimitError || + error instanceof OpenAI.InternalServerError || + error instanceof OpenAI.APIError + ) { + throw new RetryError(error.message); } - return { result: completion.content, cost: 0 }; - } catch (e) { - this.providerLog(e.message); - return { result: null, cost: 0 }; + + throw error; } } + + getCost(_usage) { + return 0; + } } module.exports = AimlApiProvider; diff --git a/server/utils/agents/aibitat/providers/index.js b/server/utils/agents/aibitat/providers/index.js index d8c174862e4..8ea5814e9c0 100644 --- a/server/utils/agents/aibitat/providers/index.js +++ b/server/utils/agents/aibitat/providers/index.js @@ -23,6 +23,7 @@ const NvidiaNimProvider = require("./nvidiaNim.js"); const PPIOProvider = require("./ppio.js"); const GeminiProvider = require("./gemini.js"); const DellProAiStudioProvider = require("./dellProAiStudio.js"); +const AimlApiProvider = require("./aimlapi.js"); module.exports = { OpenAIProvider, @@ -50,4 +51,5 @@ module.exports = { PPIOProvider, GeminiProvider, DellProAiStudioProvider, + AimlApiProvider, }; diff --git a/server/utils/agents/index.js b/server/utils/agents/index.js index 913bf9cf304..6884a93ef5c 100644 --- a/server/utils/agents/index.js +++ b/server/utils/agents/index.js @@ -186,7 +186,7 @@ class AgentHandler { throw new Error("PPIO API Key must be provided to use agents."); break; case "aimlapi": - if (!process.env.AIML_API_KEY) + if (!process.env.AIML_LLM_API_KEY) throw new Error("AI/ML API key must be provided to use agents."); break; case "gemini": diff --git a/server/utils/helpers/customModels.js b/server/utils/helpers/customModels.js index cabfffba8a1..01b54b9e371 100644 --- a/server/utils/helpers/customModels.js +++ b/server/utils/helpers/customModels.js @@ -691,7 +691,7 @@ async function getAimlApiModels(apiKey = null) { return { models: [], error: null }; if (Object.keys(knownModels).length > 0 && !!apiKey) - process.env.AIML_API_KEY = apiKey; + process.env.AIML_LLM_API_KEY = apiKey; const models = Object.values(knownModels).map((model) => { return { @@ -709,7 +709,7 @@ async function getAimlApiEmbeddingModels(apiKey = null) { return { models: [], error: null }; if (Object.keys(knownModels).length > 0 && !!apiKey) - process.env.AIML_API_KEY = apiKey; + process.env.AIML_EMBEDDER_API_KEY = apiKey; const models = Object.values(knownModels).map((model) => { return { diff --git a/server/utils/helpers/updateENV.js b/server/utils/helpers/updateENV.js index 39f3d22a732..b34cb3f9c4d 100644 --- a/server/utils/helpers/updateENV.js +++ b/server/utils/helpers/updateENV.js @@ -204,8 +204,8 @@ const KEY_MAPPING = { }, // AI/ML API Options - AimlApiKey: { - envKey: "AIML_API_KEY", + AimlLlmApiKey: { + envKey: "AIML_LLM_API_KEY", checks: [isNotEmpty], }, AimlModelPref: { @@ -321,6 +321,11 @@ const KEY_MAPPING = { checks: [nonZero], }, + AimlEmbedderApiKey: { + envKey: "AIML_EMBEDDER_API_KEY", + checks: [isNotEmpty], + }, + // Vector Database Selection Settings VectorDB: { envKey: "VECTOR_DB", From 6a5669adffd565c633377b381906838611ce3f26 Mon Sep 17 00:00:00 2001 From: D1m7asis Date: Tue, 12 Aug 2025 13:33:58 +0200 Subject: [PATCH 6/6] Update customModels.js --- server/utils/helpers/customModels.js | 1 + 1 file changed, 1 insertion(+) diff --git a/server/utils/helpers/customModels.js b/server/utils/helpers/customModels.js index afb94c1b218..d95133c39c7 100644 --- a/server/utils/helpers/customModels.js +++ b/server/utils/helpers/customModels.js @@ -754,6 +754,7 @@ async function getMoonshotAiModels(_apiKey = null) { // Api Key was successful so lets save it for future uses if (models.length > 0) process.env.MOONSHOT_AI_API_KEY = apiKey; + return { models, error: null }; } module.exports = {