From e1862d5e11748ab28d0bb0ba2af7b3f36329eb8b Mon Sep 17 00:00:00 2001 From: TensorNull Date: Sat, 13 Sep 2025 17:49:52 +0800 Subject: [PATCH 1/2] feat: Implement CometAPI integration for chat completions and model management - Added CometApiLLM class for handling chat completions using CometAPI. - Implemented model synchronization and caching mechanisms. - Introduced streaming support for chat responses with timeout handling. - Created CometApiProvider class for agent interactions with CometAPI. - Enhanced error handling and logging throughout the integration. - Established a structure for managing function calls and completions. --- .vscode/settings.json | 3 +- README.md | 2 +- docker/.env.example | 5 + .../LLMSelection/CometApiLLMOptions/index.jsx | 155 ++++++ frontend/src/media/llmprovider/cometapi.png | Bin 0 -> 28905 bytes .../GeneralSettings/LLMPreference/index.jsx | 10 + .../Steps/DataHandling/index.jsx | 9 + .../Steps/LLMPreference/index.jsx | 9 + .../AgentConfig/AgentLLMSelection/index.jsx | 2 + locales/README.ja-JP.md | 1 + locales/README.zh-CN.md | 5 +- server/.env.example | 7 +- server/models/systemSettings.js | 5 + server/storage/models/.gitignore | 3 +- server/utils/AiProviders/cometapi/index.js | 472 ++++++++++++++++++ server/utils/agents/aibitat/index.js | 2 + .../agents/aibitat/providers/ai-provider.js | 8 + .../agents/aibitat/providers/cometapi.js | 115 +++++ .../utils/agents/aibitat/providers/index.js | 2 + server/utils/agents/index.js | 7 + server/utils/helpers/customModels.js | 18 + server/utils/helpers/index.js | 8 + server/utils/helpers/updateENV.js | 15 + 23 files changed, 857 insertions(+), 6 deletions(-) create mode 100644 frontend/src/components/LLMSelection/CometApiLLMOptions/index.jsx create mode 100644 frontend/src/media/llmprovider/cometapi.png create mode 100644 server/utils/AiProviders/cometapi/index.js create mode 100644 server/utils/agents/aibitat/providers/cometapi.js diff --git a/.vscode/settings.json b/.vscode/settings.json index e6b76c9e9de..d6b7a83dc62 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,6 +12,7 @@ "comkey", "cooldown", "cooldowns", + "cometapi", "datafile", "Deduplicator", "Dockerized", @@ -60,4 +61,4 @@ ], "eslint.experimental.useFlatConfig": true, "docker.languageserver.formatter.ignoreMultilineInstructions": true -} \ No newline at end of file +} diff --git a/README.md b/README.md index 90243c35a93..88922e65912 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ AnythingLLM divides your documents into objects called `workspaces`. A Workspace - [Novita AI (chat models)](https://novita.ai/model-api/product/llm-api?utm_source=github_anything-llm&utm_medium=github_readme&utm_campaign=link) - [PPIO](https://ppinfra.com?utm_source=github_anything-llm) - [Moonshot AI](https://www.moonshot.ai/) - +- [CometAPI (chat models)](https://api.cometapi.com/) **Embedder models:** - [AnythingLLM Native Embedder](/server/storage/models/README.md) (default) diff --git a/docker/.env.example b/docker/.env.example index dca22fa0493..bd268053664 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -96,6 +96,11 @@ GID='1000' # NOVITA_LLM_API_KEY='your-novita-api-key-here' check on https://novita.ai/settings/key-management # NOVITA_LLM_MODEL_PREF='deepseek/deepseek-r1' +# LLM_PROVIDER='cometapi' +# COMETAPI_LLM_API_KEY='your-cometapi-api-key-here' # Get one at https://api.cometapi.com/console/token +# COMETAPI_LLM_MODEL_PREF='gpt-5-mini' +# COMETAPI_LLM_TIMEOUT_MS=500 # Optional; stream idle timeout in ms (min 500ms) + # LLM_PROVIDER='cohere' # COHERE_API_KEY= # COHERE_MODEL_PREF='command-r' diff --git a/frontend/src/components/LLMSelection/CometApiLLMOptions/index.jsx b/frontend/src/components/LLMSelection/CometApiLLMOptions/index.jsx new file mode 100644 index 00000000000..71fbeec6274 --- /dev/null +++ b/frontend/src/components/LLMSelection/CometApiLLMOptions/index.jsx @@ -0,0 +1,155 @@ +import System from "@/models/system"; +import { CaretDown, CaretUp } from "@phosphor-icons/react"; +import { useState, useEffect } from "react"; + +export default function CometApiLLMOptions({ settings }) { + return ( +
+
+
+ + +
+ {!settings?.credentialsOnly && ( + + )} +
+ +
+ ); +} + +function AdvancedControls({ settings }) { + const [showAdvancedControls, setShowAdvancedControls] = useState(false); + + return ( +
+
+ +
+ +
+ ); +} + +function CometApiModelSelection({ settings }) { + // TODO: For now, CometAPI models list is noisy; show a flat, deduped list without grouping. + // Revisit after CometAPI model list API provides better categorization/metadata. + const [models, setModels] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function findCustomModels() { + setLoading(true); + const { models: fetched = [] } = await System.customModels("cometapi"); + if (fetched?.length > 0) { + // De-duplicate by id (case-insensitive) and sort by name for readability + const seen = new Set(); + const unique = []; + for (const m of fetched) { + const key = String(m.id || m.name || "").toLowerCase(); + if (!seen.has(key)) { + seen.add(key); + unique.push(m); + } + } + unique.sort((a, b) => + String(a.name || a.id).localeCompare(String(b.name || b.id)) + ); + setModels(unique); + } else { + setModels([]); + } + setLoading(false); + } + findCustomModels(); + }, []); + + if (loading || models.length === 0) { + return ( +
+ + +
+ ); + } + + return ( +
+ + + + {models.map((model) => ( + + ))} + +

+ You can type the model id directly or pick from suggestions. +

+
+ ); +} diff --git a/frontend/src/media/llmprovider/cometapi.png b/frontend/src/media/llmprovider/cometapi.png new file mode 100644 index 0000000000000000000000000000000000000000..71e1f64482bfd93b4db5983c4564ed9407940337 GIT binary patch literal 28905 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 , + description: "500+ AI Model API, All In One API. Just In CometAPI", + requiredConfig: ["CometApiLLMApiKey"], + }, { name: "Together AI", value: "togetherai", diff --git a/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx index a6e4ab025c1..442a443d949 100644 --- a/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx +++ b/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx @@ -39,6 +39,7 @@ import PPIOLogo from "@/media/llmprovider/ppio.png"; import PGVectorLogo from "@/media/vectordbs/pgvector.png"; import DPAISLogo from "@/media/llmprovider/dpais.png"; import MoonshotAiLogo from "@/media/llmprovider/moonshotai.png"; +import CometApiLogo from "@/media/llmprovider/cometapi.png"; import React, { useState, useEffect } from "react"; import paths from "@/utils/paths"; @@ -252,6 +253,14 @@ export const LLM_SELECTION_PRIVACY = { ], logo: MoonshotAiLogo, }, + cometapi: { + name: "CometAPI", + description: [ + "Your chats will not be used for training", + "Your prompts and document text used in response creation are visible to CometAPI", + ], + logo: CometApiLogo, + }, }; export const VECTOR_DB_PRIVACY = { diff --git a/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx index 4ce2745d041..e334d85263a 100644 --- a/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx +++ b/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx @@ -28,6 +28,7 @@ import CohereLogo from "@/media/llmprovider/cohere.png"; import PPIOLogo from "@/media/llmprovider/ppio.png"; import DellProAiStudioLogo from "@/media/llmprovider/dpais.png"; import MoonshotAiLogo from "@/media/llmprovider/moonshotai.png"; +import CometApiLogo from "@/media/llmprovider/cometapi.png"; import OpenAiOptions from "@/components/LLMSelection/OpenAiOptions"; import GenericOpenAiOptions from "@/components/LLMSelection/GenericOpenAiOptions"; @@ -57,6 +58,7 @@ import NvidiaNimOptions from "@/components/LLMSelection/NvidiaNimOptions"; import PPIOLLMOptions from "@/components/LLMSelection/PPIOLLMOptions"; import DellProAiStudioOptions from "@/components/LLMSelection/DPAISOptions"; import MoonshotAiOptions from "@/components/LLMSelection/MoonshotAiOptions"; +import CometApiLLMOptions from "@/components/LLMSelection/CometApiLLMOptions"; import LLMItem from "@/components/LLMSelection/LLMItem"; import System from "@/models/system"; @@ -272,6 +274,13 @@ const LLMS = [ options: (settings) => , description: "Run Moonshot AI's powerful LLMs.", }, + { + name: "CometAPI", + value: "cometapi", + logo: CometApiLogo, + options: (settings) => , + description: "OpenAI-compatible chat models from CometAPI.", + }, ]; export default function LLMPreference({ diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentLLMSelection/index.jsx b/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentLLMSelection/index.jsx index 31b7327ba63..5fd8001561d 100644 --- a/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentLLMSelection/index.jsx +++ b/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentLLMSelection/index.jsx @@ -32,6 +32,7 @@ const ENABLED_PROVIDERS = [ "nvidia-nim", "gemini", "moonshotai", + "cometapi", // 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. @@ -45,6 +46,7 @@ const WARN_PERFORMANCE = [ "localai", "openrouter", "novita", + "cometapi", "generic-openai", "textgenwebui", ]; diff --git a/locales/README.ja-JP.md b/locales/README.ja-JP.md index 8920b218fda..d6fef0fa5b7 100644 --- a/locales/README.ja-JP.md +++ b/locales/README.ja-JP.md @@ -91,6 +91,7 @@ AnythingLLMは、ドキュメントを`ワークスペース`と呼ばれるオ - [Cohere](https://cohere.com/) - [KoboldCPP](https://github.com/LostRuins/koboldcpp) - [PPIO](https://ppinfra.com?utm_source=github_anything-llm) +- [CometAPI (チャットモデル)](https://api.cometapi.com/) **埋め込みモデル:** diff --git a/locales/README.zh-CN.md b/locales/README.zh-CN.md index e3c63225a2f..aa328351449 100644 --- a/locales/README.zh-CN.md +++ b/locales/README.zh-CN.md @@ -100,6 +100,7 @@ AnythingLLM将您的文档划分为称为`workspaces` (工作区)的对象。工 - [xAI](https://x.ai/) - [Novita AI (聊天模型)](https://novita.ai/model-api/product/llm-api?utm_source=github_anything-llm&utm_medium=github_readme&utm_campaign=link) - [PPIO (聊天模型)](https://ppinfra.com?utm_source=github_anything-llm) +- [CometAPI (聊天模型)](https://api.cometapi.com/) **支持的嵌入模型:** @@ -200,7 +201,7 @@ _以下是一些与 AnythingLLM 兼容的应用程序,但并非由 Mintplex La ### 怎样关闭 -在服务器或 Docker 的 .env 设置中将 `DISABLE_TELEMETRY` 设置为 "true",即可选择不参与遥测数据收集。你也可以在应用内通过以下路径操作:侧边栏 > `Privacy` (隐私) > 关闭遥测功能。 +在服务器或 Docker 的 .env 设置中将 `DISABLE_TELEMETRY` 设置为 "true",即可选择不参与遥测数据收集。你也可以在应用内通过以下路径操作:侧边栏 > `Privacy` (隐私) > 关闭遥测功能。 ### 你们跟踪收集哪些信息? @@ -214,7 +215,7 @@ _以下是一些与 AnythingLLM 兼容的应用程序,但并非由 Mintplex La 您可以通过查找所有调用`Telemetry.sendTelemetry`的位置来验证这些声明。此外,如果启用,这些事件也会被写入输出日志,因此您也可以看到发送了哪些具体数据。**IP或其他识别信息不会被收集**。Telemetry远程信息收集的方案来自[PostHog](https://posthog.com/) - 一个开源的远程信息收集服务。 -我们非常重视隐私,且不用烦人的弹窗问卷来获取反馈,希望你能理解为什么我们想要知道该工具的使用情况,这样我们才能打造真正值得使用的产品。所有匿名数据 _绝不会_ 与任何第三方共享。 +我们非常重视隐私,且不用烦人的弹窗问卷来获取反馈,希望你能理解为什么我们想要知道该工具的使用情况,这样我们才能打造真正值得使用的产品。所有匿名数据 _绝不会_ 与任何第三方共享。 [在源代码中查看所有信息收集活动](https://github.com/search?q=repo%3AMintplex-Labs%2Fanything-llm%20.sendTelemetry\(&type=code) diff --git a/server/.env.example b/server/.env.example index 0d3d1ecd0e0..b5d45040817 100644 --- a/server/.env.example +++ b/server/.env.example @@ -106,6 +106,11 @@ SIG_SALT='salt' # Please generate random string at least 32 chars long. # COHERE_API_KEY= # COHERE_MODEL_PREF='command-r' +# LLM_PROVIDER='cometapi' +# COMETAPI_LLM_API_KEY='your-cometapi-key-here' # Get one at https://api.cometapi.com/console/token +# COMETAPI_LLM_MODEL_PREF='gpt-5-mini' + + # LLM_PROVIDER='bedrock' # AWS_BEDROCK_LLM_ACCESS_KEY_ID= # AWS_BEDROCK_LLM_ACCESS_KEY= @@ -354,4 +359,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 f0796be0431..064e299c64d 100644 --- a/server/models/systemSettings.js +++ b/server/models/systemSettings.js @@ -610,6 +610,11 @@ const SystemSettings = { DellProAiStudioModelPref: process.env.DPAIS_LLM_MODEL_PREF, DellProAiStudioTokenLimit: process.env.DPAIS_LLM_MODEL_TOKEN_LIMIT ?? 4096, + + // CometAPI LLM Keys + CometApiLLMApiKey: !!process.env.COMETAPI_LLM_API_KEY, + CometApiLLMModelPref: process.env.COMETAPI_LLM_MODEL_PREF, + CometApiLLMTimeout: process.env.COMETAPI_LLM_TIMEOUT_MS, }; }, diff --git a/server/storage/models/.gitignore b/server/storage/models/.gitignore index e73faa05578..7f5c5f8bed1 100644 --- a/server/storage/models/.gitignore +++ b/server/storage/models/.gitignore @@ -10,4 +10,5 @@ togetherAi tesseract ppio context-windows/* -MintplexLabs \ No newline at end of file +MintplexLabs +cometapi \ No newline at end of file diff --git a/server/utils/AiProviders/cometapi/index.js b/server/utils/AiProviders/cometapi/index.js new file mode 100644 index 00000000000..b51228fb100 --- /dev/null +++ b/server/utils/AiProviders/cometapi/index.js @@ -0,0 +1,472 @@ +const { NativeEmbedder } = require("../../EmbeddingEngines/native"); +const { v4: uuidv4 } = require("uuid"); +const { + writeResponseChunk, + clientAbortedHandler, + formatChatHistory, +} = require("../../helpers/chat/responses"); +const fs = require("fs"); +const path = require("path"); +const { safeJsonParse } = require("../../http"); +const { + LLMPerformanceMonitor, +} = require("../../helpers/chat/LLMPerformanceMonitor"); +const cacheFolder = path.resolve( + process.env.STORAGE_DIR + ? path.resolve(process.env.STORAGE_DIR, "models", "cometapi") + : path.resolve(__dirname, `../../../storage/models/cometapi`) +); + +// TODO: When CometAPI's model list is upgraded, this operation needs to be removed +// Model filtering patterns from cometapi.md +const COMETAPI_IGNORE_PATTERNS = [ + // Image generation models + "dall-e", + "dalle", + "midjourney", + "mj_", + "stable-diffusion", + "sd-", + "flux-", + "playground-v", + "ideogram", + "recraft-", + "black-forest-labs", + "/recraft-v3", + "recraftv3", + "stability-ai/", + "sdxl", + // Audio generation models + "suno_", + "tts", + "whisper", + // Video generation models + "runway", + "luma_", + "luma-", + "veo", + "kling_", + "minimax_video", + "hunyuan-t1", + // Utility models + "embedding", + "search-gpts", + "files_retrieve", + "moderation", + // Deepl + "deepl", +]; + +class CometApiLLM { + constructor(embedder = null, modelPreference = null) { + if (!process.env.COMETAPI_LLM_API_KEY) + throw new Error("No CometAPI API key was set."); + + const { OpenAI: OpenAIApi } = require("openai"); + this.basePath = "https://api.cometapi.com/v1"; + this.openai = new OpenAIApi({ + baseURL: this.basePath, + apiKey: process.env.COMETAPI_LLM_API_KEY ?? null, + defaultHeaders: { + "HTTP-Referer": "https://anythingllm.com", + "X-CometAPI-Source": "anythingllm", + }, + }); + this.model = + modelPreference || process.env.COMETAPI_LLM_MODEL_PREF || "gpt-5-mini"; + 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; + this.timeout = this.#parseTimeout(); + + if (!fs.existsSync(cacheFolder)) + fs.mkdirSync(cacheFolder, { recursive: true }); + this.cacheModelPath = path.resolve(cacheFolder, "models.json"); + this.cacheAtPath = path.resolve(cacheFolder, ".cached_at"); + + this.log(`Loaded with model: ${this.model}`); + } + + log(text, ...args) { + console.log(`\x1b[36m[${this.constructor.name}]\x1b[0m ${text}`, ...args); + } + + /** + * CometAPI has various models that never return `finish_reasons` and thus leave the stream open + * which causes issues in subsequent messages. This timeout value forces us to close the stream after + * x milliseconds. This is a configurable value via the COMETAPI_LLM_TIMEOUT_MS value + * @returns {number} The timeout value in milliseconds (default: 500) + */ + #parseTimeout() { + if (isNaN(Number(process.env.COMETAPI_LLM_TIMEOUT_MS))) return 500; + const setValue = Number(process.env.COMETAPI_LLM_TIMEOUT_MS); + if (setValue < 500) return 500; + return setValue; + } + + // 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; + } + + // The CometAPI model API has a lot of models, so we cache this locally in the directory + // as if the cache directory JSON file is stale or does not exist we will fetch from API and store it. + // 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. + // We used to have this as a chore, but given there is an API to get the info - this makes little sense. + async #syncModels() { + if (fs.existsSync(this.cacheModelPath) && !this.#cacheIsStale()) + return false; + + this.log( + "Model cache is not present or stale. Fetching from CometAPI API." + ); + await fetchCometApiModels(); + 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, + ...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( + `CometAPI chat: ${this.model} is not valid for chat completion!` + ); + + const result = await LLMPerformanceMonitor.measureAsyncFunction( + this.openai.chat.completions + .create({ + model: this.model, + messages, + temperature, + }) + .catch((e) => { + throw new Error(e.message); + }) + ); + + 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( + `CometAPI 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 + ); + return measuredStreamRequest; + } + + /** + * Handles the default stream response for a chat. + * @param {import("express").Response} response + * @param {import('../../helpers/chat/LLMPerformanceMonitor').MonitoredStream} stream + * @param {Object} responseProps + * @returns {Promise} + */ + handleStream(response, stream, responseProps) { + const timeoutThresholdMs = this.timeout; + const { uuid = uuidv4(), sources = [] } = responseProps; + + return new Promise(async (resolve) => { + let fullText = ""; + let lastChunkTime = null; // null when first token is still not received. + + // 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 = () => { + stream?.endMeasurement({ + completion_tokens: LLMPerformanceMonitor.countTokens(fullText), + }); + clientAbortedHandler(resolve, fullText); + }; + response.on("close", handleAbort); + + // NOTICE: Not all CometAPI models will return a stop reason + // which keeps the connection open and so the model never finalizes the stream + // like the traditional OpenAI response schema does. So in the case the response stream + // never reaches a formal close state we maintain an interval timer that if we go >=timeoutThresholdMs with + // no new chunks then we kill the stream and assume it to be complete. CometAPI is quite fast + // so this threshold should permit most responses, but we can adjust `timeoutThresholdMs` if + // we find it is too aggressive. + const timeoutCheck = setInterval(() => { + if (lastChunkTime === null) return; + + const now = Number(new Date()); + const diffMs = now - lastChunkTime; + if (diffMs >= timeoutThresholdMs) { + this.log( + `CometAPI stream did not self-close and has been stale for >${timeoutThresholdMs}ms. Closing response stream.` + ); + writeResponseChunk(response, { + uuid, + sources, + type: "textResponseChunk", + textResponse: "", + close: true, + error: false, + }); + clearInterval(timeoutCheck); + response.removeListener("close", handleAbort); + stream?.endMeasurement({ + completion_tokens: LLMPerformanceMonitor.countTokens(fullText), + }); + resolve(fullText); + } + }, 500); + + try { + for await (const chunk of stream) { + const message = chunk?.choices?.[0]; + const token = message?.delta?.content; + lastChunkTime = Number(new Date()); + + if (token) { + fullText += token; + writeResponseChunk(response, { + uuid, + sources: [], + type: "textResponseChunk", + textResponse: token, + close: false, + error: false, + }); + } + + if (message.finish_reason !== null) { + writeResponseChunk(response, { + uuid, + sources, + type: "textResponseChunk", + textResponse: "", + close: true, + error: false, + }); + response.removeListener("close", handleAbort); + stream?.endMeasurement({ + completion_tokens: LLMPerformanceMonitor.countTokens(fullText), + }); + resolve(fullText); + } + } + } catch (e) { + writeResponseChunk(response, { + uuid, + sources, + type: "abort", + textResponse: null, + close: true, + error: e.message, + }); + response.removeListener("close", handleAbort); + stream?.endMeasurement({ + completion_tokens: LLMPerformanceMonitor.countTokens(fullText), + }); + resolve(fullText); + } + }); + } + + // 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); + } +} + +/** + * Fetches available models from CometAPI and filters out non-chat models + * Based on cometapi.md specifications + */ +async function fetchCometApiModels() { + return await fetch(`https://api.cometapi.com/v1/models`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${process.env.COMETAPI_LLM_API_KEY}`, + }, + }) + .then((res) => res.json()) + .then(({ data = [] }) => { + const models = {}; + + // Filter out non-chat models using patterns from cometapi.md + const chatModels = data.filter((model) => { + const modelId = model.id.toLowerCase(); + return !COMETAPI_IGNORE_PATTERNS.some((pattern) => + modelId.includes(pattern.toLowerCase()) + ); + }); + + chatModels.forEach((model) => { + models[model.id] = { + id: model.id, + name: model.id, // CometAPI has limited model info according to cometapi.md + organization: + model.id.split("/")[0] || model.id.split("-")[0] || "CometAPI", + maxLength: model.context_length || 4096, // Conservative default + }; + }); + + // 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("Error fetching CometAPI models:", e); + return {}; + }); +} + +module.exports = { + CometApiLLM, + fetchCometApiModels, +}; diff --git a/server/utils/agents/aibitat/index.js b/server/utils/agents/aibitat/index.js index d6b22d3a9af..683850dfcb9 100644 --- a/server/utils/agents/aibitat/index.js +++ b/server/utils/agents/aibitat/index.js @@ -830,6 +830,8 @@ ${this.getHistory({ to: route.to }) return new Providers.GeminiProvider({ model: config.model }); case "dpais": return new Providers.DellProAiStudioProvider({ model: config.model }); + case "cometapi": + return new Providers.CometApiProvider({ model: config.model }); default: throw new Error( `Unknown provider: ${config.provider}. Please use a valid provider.` diff --git a/server/utils/agents/aibitat/providers/ai-provider.js b/server/utils/agents/aibitat/providers/ai-provider.js index 07867e4c6e4..c2528acd948 100644 --- a/server/utils/agents/aibitat/providers/ai-provider.js +++ b/server/utils/agents/aibitat/providers/ai-provider.js @@ -251,6 +251,14 @@ class Provider { apiKey: null, ...config, }); + case "cometapi": + return new ChatOpenAI({ + configuration: { + baseURL: "https://api.cometapi.com/v1", + }, + apiKey: process.env.COMETAPI_LLM_API_KEY ?? null, + ...config, + }); default: throw new Error(`Unsupported provider ${provider} for this task.`); diff --git a/server/utils/agents/aibitat/providers/cometapi.js b/server/utils/agents/aibitat/providers/cometapi.js new file mode 100644 index 00000000000..87eca7a053b --- /dev/null +++ b/server/utils/agents/aibitat/providers/cometapi.js @@ -0,0 +1,115 @@ +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 CometAPI provider. + */ +class CometApiProvider extends InheritMultiple([Provider, UnTooled]) { + model; + + constructor(config = {}) { + const { model = "gpt-5-mini" } = config; + super(); + const client = new OpenAI({ + baseURL: "https://api.cometapi.com/v1", + apiKey: process.env.COMETAPI_LLM_API_KEY, + maxRetries: 3, + defaultHeaders: { + "HTTP-Referer": "https://anythingllm.com", + "X-CometAPI-Source": "anythingllm", + }, + }); + + 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("CometAPI chat: No results!"); + if (result.choices.length === 0) + throw new Error("CometAPI 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 = []) { + 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, + }; + } + + /** + * Get the cost of the completion. + * + * @param _usage The completion to get the cost for. + * @returns The cost of the completion. + * Stubbed since CometAPI has no cost basis. + */ + getCost() { + return 0; + } +} + +module.exports = CometApiProvider; diff --git a/server/utils/agents/aibitat/providers/index.js b/server/utils/agents/aibitat/providers/index.js index 859ad9de9d5..2146269bb48 100644 --- a/server/utils/agents/aibitat/providers/index.js +++ b/server/utils/agents/aibitat/providers/index.js @@ -24,6 +24,7 @@ const PPIOProvider = require("./ppio.js"); const GeminiProvider = require("./gemini.js"); const DellProAiStudioProvider = require("./dellProAiStudio.js"); const MoonshotAiProvider = require("./moonshotAi.js"); +const CometApiProvider = require("./cometapi.js"); module.exports = { OpenAIProvider, @@ -47,6 +48,7 @@ module.exports = { ApiPieProvider, XAIProvider, NovitaProvider, + CometApiProvider, NvidiaNimProvider, PPIOProvider, GeminiProvider, diff --git a/server/utils/agents/index.js b/server/utils/agents/index.js index 4527ee783b4..46581d3c5ce 100644 --- a/server/utils/agents/index.js +++ b/server/utils/agents/index.js @@ -204,6 +204,11 @@ class AgentHandler { throw new Error("Moonshot AI model must be set to use agents."); break; + case "cometapi": + if (!process.env.COMETAPI_LLM_API_KEY) + throw new Error("CometAPI API Key must be provided to use agents."); + break; + default: throw new Error( "No workspace agent provider set. Please set your agent provider in the workspace's settings" @@ -274,6 +279,8 @@ class AgentHandler { return process.env.GEMINI_LLM_MODEL_PREF ?? "gemini-2.0-flash-lite"; case "dpais": return process.env.DPAIS_LLM_MODEL_PREF; + case "cometapi": + return process.env.COMETAPI_LLM_MODEL_PREF ?? "gpt-5-mini"; default: return null; } diff --git a/server/utils/helpers/customModels.js b/server/utils/helpers/customModels.js index e0a1fb820e4..ea5e738cdfa 100644 --- a/server/utils/helpers/customModels.js +++ b/server/utils/helpers/customModels.js @@ -8,6 +8,7 @@ const { parseLMStudioBasePath } = require("../AiProviders/lmStudio"); const { parseNvidiaNimBasePath } = require("../AiProviders/nvidiaNim"); const { fetchPPIOModels } = require("../AiProviders/ppio"); const { GeminiLLM } = require("../AiProviders/gemini"); +const { fetchCometApiModels } = require("../AiProviders/cometapi"); const SUPPORT_CUSTOM_MODELS = [ "openai", @@ -28,6 +29,7 @@ const SUPPORT_CUSTOM_MODELS = [ "deepseek", "apipie", "novita", + "cometapi", "xai", "gemini", "ppio", @@ -76,6 +78,8 @@ async function getCustomModels(provider = "", apiKey = null, basePath = null) { return await getAPIPieModels(apiKey); case "novita": return await getNovitaModels(); + case "cometapi": + return await getCometApiModels(); case "xai": return await getXAIModels(apiKey); case "nvidia-nim": @@ -453,6 +457,20 @@ async function getNovitaModels() { return { models, error: null }; } +async function getCometApiModels() { + const knownModels = await fetchCometApiModels(); + 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 getAPIPieModels(apiKey = null) { const knownModels = await fetchApiPieModels(apiKey); if (!Object.keys(knownModels).length === 0) diff --git a/server/utils/helpers/index.js b/server/utils/helpers/index.js index bff2873b526..12327698954 100644 --- a/server/utils/helpers/index.js +++ b/server/utils/helpers/index.js @@ -212,6 +212,9 @@ function getLLMProvider({ provider = null, model = null } = {}) { case "dpais": const { DellProAiStudioLLM } = require("../AiProviders/dellProAiStudio"); return new DellProAiStudioLLM(embedder, model); + case "cometapi": + const { CometApiLLM } = require("../AiProviders/cometapi"); + return new CometApiLLM(embedder, model); default: throw new Error( `ENV: No valid LLM_PROVIDER value found in environment! Using ${process.env.LLM_PROVIDER}` @@ -362,6 +365,9 @@ function getLLMProviderClass({ provider = null } = {}) { case "moonshotai": const { MoonshotAiLLM } = require("../AiProviders/moonshotAi"); return MoonshotAiLLM; + case "cometapi": + const { CometApiLLM } = require("../AiProviders/cometapi"); + return CometApiLLM; default: return null; } @@ -430,6 +436,8 @@ function getBaseLLMProviderModel({ provider = null } = {}) { return process.env.DPAIS_LLM_MODEL_PREF; case "moonshotai": return process.env.MOONSHOT_AI_MODEL_PREF; + case "cometapi": + return process.env.COMETAPI_LLM_MODEL_PREF; default: return null; } diff --git a/server/utils/helpers/updateENV.js b/server/utils/helpers/updateENV.js index 124b4b4e77f..6dfbe4fc597 100644 --- a/server/utils/helpers/updateENV.js +++ b/server/utils/helpers/updateENV.js @@ -704,6 +704,20 @@ const KEY_MAPPING = { envKey: "MOONSHOT_AI_MODEL_PREF", checks: [isNotEmpty], }, + + // CometAPI Options + CometApiLLMApiKey: { + envKey: "COMETAPI_LLM_API_KEY", + checks: [isNotEmpty], + }, + CometApiLLMModelPref: { + envKey: "COMETAPI_LLM_MODEL_PREF", + checks: [isNotEmpty], + }, + CometApiLLMTimeout: { + envKey: "COMETAPI_LLM_TIMEOUT_MS", + checks: [], + }, }; function isNotEmpty(input = "") { @@ -813,6 +827,7 @@ function supportedLLM(input = "") { "ppio", "dpais", "moonshotai", + "cometapi", ].includes(input); return validSelection ? null : `${input} is not a valid LLM provider.`; } From de1a277072dad4b1694aa81e6e867890b3e9267d Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Tue, 16 Sep 2025 14:29:08 -0700 Subject: [PATCH 2/2] linting --- .vscode/settings.json | 3 +- frontend/src/media/llmprovider/cometapi.png | Bin 28905 -> 23847 bytes .../GeneralSettings/LLMPreference/index.jsx | 50 +++++++++--------- .../Steps/LLMPreference/index.jsx | 2 +- .../AgentConfig/AgentLLMSelection/index.jsx | 6 --- server/.env.example | 1 + .../utils/AiProviders/cometapi/constants.js | 39 ++++++++++++++ server/utils/AiProviders/cometapi/index.js | 41 +------------- 8 files changed, 68 insertions(+), 74 deletions(-) create mode 100644 server/utils/AiProviders/cometapi/constants.js diff --git a/.vscode/settings.json b/.vscode/settings.json index d6b7a83dc62..e6b76c9e9de 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,7 +12,6 @@ "comkey", "cooldown", "cooldowns", - "cometapi", "datafile", "Deduplicator", "Dockerized", @@ -61,4 +60,4 @@ ], "eslint.experimental.useFlatConfig": true, "docker.languageserver.formatter.ignoreMultilineInstructions": true -} +} \ No newline at end of file diff --git a/frontend/src/media/llmprovider/cometapi.png b/frontend/src/media/llmprovider/cometapi.png index 71e1f64482bfd93b4db5983c4564ed9407940337..40d139ded573867fddb6da3c46630384f028fd2b 100644 GIT binary patch literal 23847 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 z zu5$)86z@W1C?}&NO4pZ6inb(w7vL9|DEXp-0^iV{I}90sB>mRBsEU!=DmS1b={Z#| z4HRwmtZjouNOA!l(l(ZWM+n{!W+{Q(N;Q(AJ8dwSm)1i8k+nH#hsdxjXEU+t8DQI) zun4^^j6Zz&-r_N3PXsU&u#aHpgB=KIc}rgBI|5Lm{9I0%}>o@^%p5%cU0s zA_f3>6^bSlIywZjD8vRismV^1YqX!(tX~8eN;^ab$}|2JL~2`* zw(*VoXkq^^@!z*Ju~)Z7e@r6DhEF~`#Zq&*OqPt)6yPDZ{JiQaFxT$htSE+qvs@NbZ-;njgWyFY zPbHIzVoS*0W`8@*wUQAO31AF6l~E>`G>}Pl)wXy0Te^COd#*RtTdGO4jLM-ZiLJ** z$!-zGu&!XZi>=S1Q`UgJz0}S+AkQ$nyZ^ibF!Lbxl@(bI%fqh?fiB$7d%Tm`S0@I5 z0={{}_+zgIK%s5eJR9~-SN;)Ys7E9`ZtbixVOFkOc|6{E)nB{*I-rBZzSaC1&ADiG z$NC&=cJaAsy6kC8h4ZydnpnGIMEnW6Dukuh08{C$fWs*e&31U~Jd=INWq48)NuT3@ zDBOwD^nsxp=cy9=cHWG;>Lg?ScvS{W(Xdf_twe>ijA6*~6Q4ZS8%bA8jsGn*cSBiur7nio)9wTe;~&Hj%thh1Y}IC_u5A zzes%sP;ap>b;(Cb66S~k4}cOag)LGNKr^q8eP(^`hUCwT=j9CB2%o}|OdDN^nV808 zLEN;D9k?P2@`~C4hz-H@8N_nSYDt=@3c z0EHaj;C788f+RP9ClTr$bQXDX;h#`)dhgq0Iv!8_f#{RFOzM4p|YL^2E2{;zFg$rKf5R|@73qF_bZ*WCGps+eCxN* zmn*P{vc%WJ6aS&`xvI?5S|2(+Im%Wby#oO5>Tf;0vibVbTnX)8H#r3hIr1h*xPM7kJt%;1xoj1hSg_j-Q1n7Mfs~D99?T*?a_{WIS82B zufxB1goS`zZH&G*^*AwQ6kn1zylz>xW$`&6NubFBF4I@uh!jl(c&1*0sb$*`$3x|7 zDb|lT7BODG;DNVuC_vy5DEe&>wnumgjuB`?2qQcpX|-k@)&|b%RJg(+ST93le+8?= zVkYv5oHYiJd)k;O2{B{K!#8X=?GCx`qp_dV9Y;O`a5B0IjRpZ+zuA0={%mN5*T#%$ z0E7$?X71i9$zaJxP{7IYb0E~dx%VWz+1es?#SGM!Ne4-Otzqvg$0EF47JA9gkyI*X z?zYU-B0jfS`qX23gNkt>rqm6f5Y#%`V;kwuA{HY>GhwICty;VKF^{Zv)(?LCh!scW z5WTC8Z*o9-9cO)eeT@bGHGyX?O{Ra+@o4?=#wWR54}KF^-N0{n-PXDd^f8QKmIHja zql|YAe1I3E#K)~c@b`ul*CNF)cPg=@JeYMe6r7bEe&|ZO#(KoKnaj_ z`zl&h_%`lH3{T}cNAM*<$R0zK3)xbrme`5CGnTW9rib@U1Se;!5(wzf-mz`Np|t+M zmGmvA(cWV<3lL&PSijdqSqkr*v9j~uW8tx!n$!JDWPlF_u&rSCm(Sn0i}z0gU&04F zgyWCmF+HzS)aSx>ZPlLoePEekW2;Lx=Ldb8qRBYdZpW})KWF|C2P9WnHmo+16xAy{ zRA7H#S!0)@Gol`y`4P>XXZdAkR(LuFSNU)At-FjpWji7HENVsrxIehB2^aU#pN@eM1aKE8tl>R*Sz zv>t&M6B`ztjEGw?-7hkQWs#?ZTRXcdGI?v3Y);pwk;|MG)SgC+$07%J{ zF|3JM7D{4PwrB#gopkB{UbsY`D1OU4nB)<&MxorVzq#|uS~gs0bU1>aMpn{XNWZ1QB7iA5@>ni!pw((7r1zZSOn%y6a{P~ zPJK5ruc2R!R?Q@O^-)|K`wKsC`woLBmvVPxrnkzY|{&)VP* zz?BfDf-RYb_5=vi%^-Om7_qcLd1ex(N4nJ@E?{2AV$G@km6%QE5VUy6wg{wPlVxFG zDRPnO`c^4Uqj~4DLwHo7@ ztxXQauxos|j6iSM%t$`fNhnJ?o5ZF{YtEB`D_xQ-s(OR73UV&jpl?L4lT7cUac0a^e zN=YpIg@C4u*G=Y(w9Q2;r^!InNA|57{!o!^ZSv)P{K_4hJyQxEVS55V==ZZL8d0$} zH=cq@bC#kj1JuH)4sz@i2hOH5_7-VXiTM}wPPGJ1Ww^hOL!Hk25jmk0cYoZ%KU2ZK z)pAo&eI-#CutFmPrLzc(G}Blg$jITH(!r#dHZyg@JG1T2_4uC_WC?X zu7E-+#}3&qaAA##Nhrte&dq0B?DO-sDM4d$dH8fK_lrh6Shh5(WxA9)=RsIb#mk>p z)SeCRbV&!&T))XSNGSK3=5p00 zw!(4ccSCWt^=QU^3i&yH?h)e2FQpXNKsV`{xjB=|5?ji)>~ulW;{sOv7Jxn1-IjOE zxaRQA2gyEk`n~!K^gK1-yR_!5pVNWjHWlw2FeR5uPH^R54fwK&tGUOg=Sb<19&${F zqEx80a#*EqK=0i7OW!(j7)i-R?)SfpsKCV33C=$r0qS8=lWY7kRVZSXsjxKAXXIVH zf4)`wN(G~gSmnUtBG;cB+j8m3^*hBGy9Px7_rWQ2xqM;c;FHCBVL;ab3O&#mSmk>^ z;{iyM=mu}}m%s9~|BuU@{KX%Lc-u=6#%%ls?{C}Nci~%!+(1x>F(-J1)#gkG0;*QJ zeCwss>3_Y8M;3YVz4`WS2SR1+v#(m#UIjMr4Tty>+VbRO5HqBpMph;#xsC)4SH4rL2 z0Rb#;w1NylMng~qqx2?3ME97T#Nu6zg*J1cqTD};?+&TxCDa<`fr9&WQ~H-*yj@k0 z#${0ar7qCTc$rVEx*qWq|=*Tivmgc2qkx1AWmygh;D98tmRF=S6H#P z`J%8Jln26$PaRpg%ya;+)lZCx;13iykui9W+^%-n!gk{1IKK&$`iljUgDXr~RhI0n zCQ>*F2Ts2m$>nRmrgYTW2;WP=S36N_8+(~JX&Tu7TjgrU%ZI+Nqakbcm;sm@M;=yc z)m2)iB1zHKCWCPK*5&iD>o=nR+vXeqE zMXb?Y7fu%RxDLB%CPlX@CUL4DNSH?2u+5be2zj9!0{jKo+#)0)6PgAbvq;0pga@9Q z>6{)l*y$o*ZPH#QAsT>bGrIuqWN$(M7KS*KvGAQ&+$rTQyB9Yitck9J1{1$Lw#bb!6vmI?7v zl++<=QTU=+Sk0>C1BE&y(xl3za&UK$1FzY^n|)Hn8^(_p_4~_Dl~Z;k7qj3H zy-a)s6|Ndh^J-1JYEAQiFTD?{aW?@p+Xe#b5&~A75wmVGNmv+yg1%vfE7j9-R!S}N zP$JYJMkLgnta$$KIBE-r6h3?j)beC>e^uiq4kjL0P27ah6-AQ~o zAdIym;<3%@hYD*2hZ(yD`t)6bpX83R)N zipGmy#s>C?0Rcv&uKOgMcDr?LL0DA?XLSwQZ2#Qy6};*<_*~LmF$?yaG@kwI6S-ba zW2x|Bc)=ZYij`uw(T#Gi*6aIjy(d4#D)p9_on&ww>NK%(I&DX;g56qgfATZt$wK|? zP2D~9>wKnriWSPS(;o?v+GGQ3QO3Z^Bm|UiEEsDoW28D_^{RRNy|>kSuffTXB?7U&WY{UaaotLP0rl+xKpDJA-}`F zu>x0}R1UJm+0bF`0?kx|y6t(_^ISlqs`+oy*3eDC!~*+qvoAMJg1^mLT+3LaE)(my zfhBlb+l77a0ShP$Bq(INGl6EJc63s?$VO)exFoq#NHjXMtf*oJ^=k0q=-i`6( zS%CuNgX^Ae$-%odQ_s;&2V#o}?0P0lY0b}uA_nY=DX}kgHd#TmtIYJw2dZyDTJ3xk zZ{~|4y(E?fr%ip+`}8+Ru3}s5;ZrTVW*Z3wU+A=)siYK+9_?W3Fb#Rho;{er-qT||gdcgMWy2}s%qiIhV`{W}=Sq^X>IN?ERP@ic;5%Cg=k2+1O4tUmdls|30hPKx5s>B{ z#(-SD%K^3s&~vry{j0txogFg?-B1qtht55Re%%xPwVT{u`KtpXH68uy(#`H}V9p|L z01+tSbpJk@!4BPP^eEju8-;?Z$T6!6d_C40|M|_!gFO@e`ofYaJLhdH_EYlS=TWny zIQ6DpgI-5+0)D#)-#WuxAbV#K9t#9i*4bU2EKp_G*^8stau?2eTbYi0g*N7)Z*#7c(1< zBc9Wj9vr>)lTS-?@z-)lT)ZE51nvjK$BI2$^^?TQAQ8%7qs8-}WRh`mpK0o%wpq&Y zk1XuVz;EK>sxp@b_|I?*VpbrXWIOUY&LJ1Ty96^NKxNIWbz1#&*L^ zFIrg*QCGLi-l41{N>975_h=`y%lO!E?d0Rc@}6ECnRPOyDK6l2^$L4-$(`jTYv1Q0 zlplJdjKzd%^U0)+<;5Xzp8AO4ZlNXPs7Yy-%eA_`#@?wolr9a*?rGiRFQQQkLo!7T|k$ z&1UaZZmr*`4UjBaU+UPlZY~JM#8qm{MuslnAk8euIo=JzvA}ScRyGDe6 zFiv`+<^3$WH-MfOk>@*Lc$&U!S8D3@+x;3=cm~%BP9APxU1EpupO;84&R-2;`JEPe z@?ixd<~IMMIuOih2Qsun8)??R>ljtKYnM-EX%^t4g#cHF2?{P)^1Ox)NQ|DGt)~NH z{MWW+(_#9B@KD%^hv*Vbb~3IJ?tz zoOH0;h+n2{1nD~C^WwKvR6SWM(NNpAUK19at4ZYTwe6=83J{vt-%4?@9+`jui%45W z^+%pFxMuQrs8ioknysRV3sJG36hWp>q?Au3_)x|Ex!13Z8NKaiI81S$rDEmND%;+a z-7`uet@j}xmdK2?WWuuu6^Hz>e&-4`b^e#Q*m9d1EqyR` zYGVx>TD(Fd(Z$P$R>|;v%hfF-dhb&)z&A1rkH{*`8bE-0=6+}`LBvb?_CMF|-{LJX zmyna`Ek7wuzH805s2u~0au##|#P!=~#JQBX|7Up7H65f=zl z_&u{6tNLQak!^#uqz;@#T~U~A;JO(}=Iew`=DvFN{^YFdnEf`BZ*eM%22QCn2l;;(OY znwo{sq8($Oslx#rXQ5LcOOtt zujXUHR360*p=#cM027n(dkJq-TEc>?1Vao9mUaPf9oKoZWb<2!=;J5Qw4_-LAVw3sOF#!&8 zAp$ZXCN4v93_otTo6sk-&GE995G`|SxSUpU?UvNQDs{Mnr|j?*->+(xgD zYOqK>43Th@$b><-^kbY1sblxk7J7EIYDvV2<7bJcAD=}lSS7W~^djEPXG6(<(+S#aLJq(I;JOjjY#g8C@TkEw4e-Z|_iwN5G<8nt4jNcgrIkKJpDI9)# zQrUm85m%sMu6BQSN^?VDQAs${x9FgToi@97Giz)wDzP{N-Q8sJ&X?S+s06TFJG+#h z36kkx8sv;Rc#l$1PVv4`hxL1U6?Y9UxE48z7#}!l%vqwgkwk2O$?As|{F@f@PQ|*XG<#qnYVse}zMA{976Z=1U`R@oMaCSkv` z={8;|HRrCf;x7c=z)^O%>UVf7qhlNH-}BW@s)n+m*B3uRhuOTkJ$X(a%z)6Ib@O)| zI$pJ!B>p#|6m3v*<@%7%99RMU>Q$B3Nh2T-Jz8!;h)B47p zO8a!sFjVZ6n!`Ba)si>rg>e+xs!@Al#ILt#zrbVPl4EICCV{X`uzL8GYzCH^FFh7C zODT=eId***W6c290fC*qlq%9?>6*0OUBs7@Q?jYdzi=}d&npp2w$ww1@;Nd@u z4WGzw3p8`y9-Q z%r@@ayEfmIo8GALPMC!SIDsL>D;S?qu?MAxFSHY*g$LyS22~n+unhg*wF(tFP zttqWwJ?qODdbJ{NQ{)%Xvf?5#hDXnJDCL$766N<;fN~BnL7=p6@cwSl^rFae+)9id zp|KSbP}KT$(;E@szv~XirH}q3NKrGK|9Gz<5osK42&P7y3Hcs)1a#n0xd?1l?kHE+ z|5PnI$lT9PUsZl_LH)U|%lFOR>)-A1kT|Gk?DimiisdS4f4}rIA~$WKbqS8{tbPV; z;jWAp!WLb;gV)~e>bLH(z|3GiP`XxMmyt?dv!=Y#h{xNV`ic%V6rvrh^fSi23v3e( zLNx5M{-GgIv@hvhwhKB0)~3awBRY=ah9~brgKGzZnZ7sS!WEUTzGR-gc_biNg87Cq zZA0-C<*RsG^xkBN#xTh*k={NktfUds2j^SE+&k9+ zBLMOv!k)VgVVK17Vg#t;9aFJ|gVD@Hb|e&V20%wzlOsoQj3XZ?n5l#;jL0S^e<%-N zqB65ndGkw7&=xo1Mxt&ys7>-txDG#V&wcJWcrT;*l{f2>_V3h?l(4wnG|u3+z?|1a zTbBHXA|vU|0eMbnyARD*=Wp*3DdP;Uzq0+^r24abLw(VG{#K2->h_=lRmz%ae2L{P zp6e$hmzG3h3<_(FM+|Y?DQsNa&66`C%Z)*hH3jv6 z1FlZ+dI)q1!E_j+Vf|gJV9CYwo!dphvrex@#zW38-LAJZD{)Aiqz!TnOB-D9da0IMV3;Q`bPNFLVY4h#*$G>45ikul51x@f z8Ag8@RoYM?z>mUJ7PE4zUFk~b=*f;hs|Wdem@ogfcCljH%~6|wD7h&2Sc*ke&Owsp z%#Ika5s(aN(b}-wHFa5pHazg2Mvf}8klzMfmNw0HRxvqq8%D!NGrjB4DAHJjn2?Xp zc4IrKSF!FRK^zL2Ggh9DGPP2lSRQjZn(m+LkA#bb7M30Xml`EwCs%-Dht4tTgrXAm zOaFQPWvqQ2rvz9Q4EUBO3yxu9`To%cz5pW0J-%teuvrcteN*J~%dfI_PBmiK{ERX5 zcvGs{*(-<5kK zVmR&;iiM_a+A!I&iuXu}=r%HD7;D0!v;_UIwN07V0L589xcIV@T3JL$AmybqbM&lQ zb~k5@;R0?=7!``10!%aBw~!~!?ykUju%0DEls&uLqB7?~gbokd6Sd6C*ZfL`l^Wr| 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 , - description: - "Reliable, Scalable, and Cost-Effective for LLMs from Novita AI", - requiredConfig: ["NovitaLLMApiKey"], - }, - { - name: "CometAPI", - value: "cometapi", - logo: CometApiLogo, - options: (settings) => , - description: "500+ AI Model API, All In One API. Just In CometAPI", - requiredConfig: ["CometApiLLMApiKey"], - }, { name: "Together AI", value: "togetherai", @@ -313,6 +296,31 @@ export const AVAILABLE_LLM_PROVIDERS = [ description: "Run Moonshot AI's powerful LLMs.", requiredConfig: ["MoonshotAiApiKey"], }, + { + name: "Novita AI", + value: "novita", + logo: NovitaLogo, + options: (settings) => , + description: + "Reliable, Scalable, and Cost-Effective for LLMs from Novita AI", + requiredConfig: ["NovitaLLMApiKey"], + }, + { + name: "CometAPI", + value: "cometapi", + logo: CometApiLogo, + options: (settings) => , + description: "500+ AI Models all in one API.", + requiredConfig: ["CometApiLLMApiKey"], + }, + { + name: "xAI", + value: "xai", + logo: XAILogo, + options: (settings) => , + description: "Run xAI's powerful LLMs like Grok-2 and more.", + requiredConfig: ["XAIApiKey", "XAIModelPref"], + }, { name: "Generic OpenAI", value: "generic-openai", @@ -327,14 +335,6 @@ export const AVAILABLE_LLM_PROVIDERS = [ "GenericOpenAiKey", ], }, - { - name: "xAI", - value: "xai", - logo: XAILogo, - options: (settings) => , - description: "Run xAI's powerful LLMs like Grok-2 and more.", - requiredConfig: ["XAIApiKey", "XAIModelPref"], - }, ]; export default function GeneralLLMPreference() { diff --git a/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx index e334d85263a..7a16985fe11 100644 --- a/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx +++ b/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx @@ -279,7 +279,7 @@ const LLMS = [ value: "cometapi", logo: CometApiLogo, options: (settings) => , - description: "OpenAI-compatible chat models from CometAPI.", + description: "500+ AI Models all in one API.", }, ]; diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentLLMSelection/index.jsx b/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentLLMSelection/index.jsx index 5fd8001561d..9710243dacb 100644 --- a/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentLLMSelection/index.jsx +++ b/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentLLMSelection/index.jsx @@ -39,15 +39,9 @@ const ENABLED_PROVIDERS = [ ]; const WARN_PERFORMANCE = [ "lmstudio", - "groq", - "azure", "koboldcpp", "ollama", "localai", - "openrouter", - "novita", - "cometapi", - "generic-openai", "textgenwebui", ]; diff --git a/server/.env.example b/server/.env.example index b5d45040817..4e5d3091476 100644 --- a/server/.env.example +++ b/server/.env.example @@ -109,6 +109,7 @@ SIG_SALT='salt' # Please generate random string at least 32 chars long. # LLM_PROVIDER='cometapi' # COMETAPI_LLM_API_KEY='your-cometapi-key-here' # Get one at https://api.cometapi.com/console/token # COMETAPI_LLM_MODEL_PREF='gpt-5-mini' +# COMETAPI_LLM_TIMEOUT_MS=500 # Optional; stream idle timeout in ms (min 500ms) # LLM_PROVIDER='bedrock' diff --git a/server/utils/AiProviders/cometapi/constants.js b/server/utils/AiProviders/cometapi/constants.js new file mode 100644 index 00000000000..2d7a32da4cc --- /dev/null +++ b/server/utils/AiProviders/cometapi/constants.js @@ -0,0 +1,39 @@ +// TODO: When CometAPI's model list is upgraded, this operation needs to be removed +// Model filtering patterns from cometapi.md that are not supported by AnythingLLM +module.exports.COMETAPI_IGNORE_PATTERNS = [ + // Image generation models + "dall-e", + "dalle", + "midjourney", + "mj_", + "stable-diffusion", + "sd-", + "flux-", + "playground-v", + "ideogram", + "recraft-", + "black-forest-labs", + "/recraft-v3", + "recraftv3", + "stability-ai/", + "sdxl", + // Audio generation models + "suno_", + "tts", + "whisper", + // Video generation models + "runway", + "luma_", + "luma-", + "veo", + "kling_", + "minimax_video", + "hunyuan-t1", + // Utility models + "embedding", + "search-gpts", + "files_retrieve", + "moderation", + // Deepl + "deepl", +]; diff --git a/server/utils/AiProviders/cometapi/index.js b/server/utils/AiProviders/cometapi/index.js index b51228fb100..82fb7c1bb3e 100644 --- a/server/utils/AiProviders/cometapi/index.js +++ b/server/utils/AiProviders/cometapi/index.js @@ -11,52 +11,13 @@ const { safeJsonParse } = require("../../http"); const { LLMPerformanceMonitor, } = require("../../helpers/chat/LLMPerformanceMonitor"); +const { COMETAPI_IGNORE_PATTERNS } = require("./constants"); const cacheFolder = path.resolve( process.env.STORAGE_DIR ? path.resolve(process.env.STORAGE_DIR, "models", "cometapi") : path.resolve(__dirname, `../../../storage/models/cometapi`) ); -// TODO: When CometAPI's model list is upgraded, this operation needs to be removed -// Model filtering patterns from cometapi.md -const COMETAPI_IGNORE_PATTERNS = [ - // Image generation models - "dall-e", - "dalle", - "midjourney", - "mj_", - "stable-diffusion", - "sd-", - "flux-", - "playground-v", - "ideogram", - "recraft-", - "black-forest-labs", - "/recraft-v3", - "recraftv3", - "stability-ai/", - "sdxl", - // Audio generation models - "suno_", - "tts", - "whisper", - // Video generation models - "runway", - "luma_", - "luma-", - "veo", - "kling_", - "minimax_video", - "hunyuan-t1", - // Utility models - "embedding", - "search-gpts", - "files_retrieve", - "moderation", - // Deepl - "deepl", -]; - class CometApiLLM { constructor(embedder = null, modelPreference = null) { if (!process.env.COMETAPI_LLM_API_KEY)