From 90e474abcb01bf27cbb9d6bb119e78fd458a6610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B9=E7=A8=8B?= <1787003204@qq.com> Date: Wed, 26 Nov 2025 06:19:32 +0800 Subject: [PATCH] Support Gitee AI(LLM Provider) (#3361) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Support Gitee AI(LLM Provider) * refactor(server): 重构 GiteeAI 模型窗口限制功能,暂时将窗口限制硬编码,计划使用外部 API 数据和缓存 * updates for Gitee AI * use legacy lookup since gitee does not enable getting token context windows * add more missing records * reorder imports --------- Co-authored-by: 方程 Co-authored-by: timothycarambat --- README.md | 1 + docker/.env.example | 5 + .../LLMSelection/GiteeAIOptions/index.jsx | 116 +++++ frontend/src/media/llmprovider/giteeai.png | Bin 0 -> 5765 bytes .../GeneralSettings/LLMPreference/index.jsx | 10 + .../Steps/DataHandling/index.jsx | 8 + .../Steps/LLMPreference/index.jsx | 9 + .../AgentConfig/AgentLLMSelection/index.jsx | 1 + server/.env.example | 5 + server/endpoints/utils.js | 3 + server/models/systemSettings.js | 5 + server/storage/models/.gitignore | 3 +- server/utils/AiProviders/giteeai/index.js | 397 ++++++++++++++++++ server/utils/AiProviders/modelMap/legacy.js | 22 + server/utils/agents/aibitat/index.js | 2 + .../agents/aibitat/providers/ai-provider.js | 8 + .../utils/agents/aibitat/providers/giteeai.js | 85 ++++ .../utils/agents/aibitat/providers/index.js | 2 + server/utils/agents/index.js | 8 +- server/utils/helpers/customModels.js | 17 + server/utils/helpers/index.js | 8 + server/utils/helpers/updateENV.js | 15 + 22 files changed, 726 insertions(+), 4 deletions(-) create mode 100644 frontend/src/components/LLMSelection/GiteeAIOptions/index.jsx create mode 100644 frontend/src/media/llmprovider/giteeai.png create mode 100644 server/utils/AiProviders/giteeai/index.js create mode 100644 server/utils/agents/aibitat/providers/giteeai.js diff --git a/README.md b/README.md index 8221d34f..bc140949 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,7 @@ AnythingLLM divides your documents into objects called `workspaces`. A Workspace - [Z.AI (chat models)](https://z.ai/model-api) - [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) +- [Gitee AI](https://ai.gitee.com/) - [Moonshot AI](https://www.moonshot.ai/) - [Microsoft Foundry Local](https://github.com/microsoft/Foundry-Local) - [CometAPI (chat models)](https://api.cometapi.com/) diff --git a/docker/.env.example b/docker/.env.example index 0cf05f3c..76131cff 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -157,6 +157,11 @@ GID='1000' # FOUNDRY_MODEL_PREF='phi-3.5-mini' # FOUNDRY_MODEL_TOKEN_LIMIT=4096 +# LLM_PROVIDER='giteeai' +# GITEE_AI_API_KEY= +# GITEE_AI_MODEL_PREF= +# GITEE_AI_MODEL_TOKEN_LIMIT= + ########################################### ######## Embedding API SElECTION ########## ########################################### diff --git a/frontend/src/components/LLMSelection/GiteeAIOptions/index.jsx b/frontend/src/components/LLMSelection/GiteeAIOptions/index.jsx new file mode 100644 index 00000000..fa6fa910 --- /dev/null +++ b/frontend/src/components/LLMSelection/GiteeAIOptions/index.jsx @@ -0,0 +1,116 @@ +import { useState, useEffect } from "react"; +import System from "@/models/system"; + +export default function GiteeAIOptions({ settings }) { + return ( +
+
+ + +
+ {!settings?.credentialsOnly && ( + <> + +
+ + e.target.blur()} + defaultValue={settings?.GiteeAITokenLimit} + required={true} + autoComplete="off" + /> +
+ + )} +
+ ); +} + +function GiteeAIModelSelection({ settings }) { + const [groupedModels, setGroupedModels] = useState({}); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function findCustomModels() { + setLoading(true); + const { models = [] } = await System.customModels("giteeai"); + if (models?.length > 0) { + const modelsByOrganization = models.reduce((acc, model) => { + acc[model.organization] = acc[model.organization] || []; + acc[model.organization].push(model); + return acc; + }, {}); + setGroupedModels(modelsByOrganization); + } + + setLoading(false); + } + findCustomModels(); + }, []); + + if (loading) { + return ( +
+ + +
+ ); + } + + return ( +
+ + +
+ ); +} diff --git a/frontend/src/media/llmprovider/giteeai.png b/frontend/src/media/llmprovider/giteeai.png new file mode 100644 index 0000000000000000000000000000000000000000..a1ef5fdade648ab07c48675d7271196bb16af83f GIT binary patch literal 5765 zcmbVQ^;;8;^B-e_$%p|Xq@)Ct4h0DvJyHe;j0O>eQKLk9lP(Df>5zsI(m7H<0YO4( zrYIdMB`9Cs-#_8=!^P{Kd+zSJ=bn3>yBGs~O&TgTDgXdLqlHv6ypqHJ5rpE3*O(FV zuEaG@B-$GQfHM9^WPq$3)+-~Kx1pvAfH2Onc?CdD%6iHGK>bVTg&i0GU}n=&Q#SS| z+s&QufNL_pi<`;Y?b_ioX?_?3m;9;|jc4_~h6+2hHoL{HkO8}C>t*)&LMp>o4Kfbi zk4$R=F&29E=_jo+ut%$i`HK`K#W@urN34gQ(sg9SUy?QVD2Xj{l~z?bRaV;g_Plhs zZcq8L?0jM0sjFoh6=us?D%?X)-9OL=v||N}z#>t^|4BKp=ylMpYPxk-)?zK6l7eDz zbv4X(g9xk#xnCqCl$zCE3f;93oqT#BWR8PnjYaj&nXkS3{{4I6rITwlulsT<;b>nr zo4siHX>{@$kEEhvUta?VIskyFcA;2~jv~uzYGRI7e+>;qBHtez)E%F=D=xw`Uv?j! z?yq~Hv*G6)vvT22_GYo3{Ri6 zdd~Dm0ed-Us}s7LZcB;4z;$paG=m&d@2CeC>)j7~`I>G#eD;YKNn9iyWX8>jJaM*L zlb$NtxH%L9d@uFlB|@Z5B$cr?dSt{D^p=r%n0d-PC}8s^1oa;xUhYuK+xzVwY9po0W8BDGnj^_P1j!i9FzGgI{URDRhNhDsJJ;8n( zesBMLJ>Ax?PsQ0t4p&_}cLUo0gV?dcs+Lg%Es+2!$4g3kYjF4<+KNdT5^hx*Q1K+S zaX0(>P5dGm!osEhk`)qxUzJ3GZOjl-JPR$y2gTEIcF+CsD7xIPwibalc(hL$%*kPr z!Vu04Z?*ETJZlAre5% zX0|UcK7&+7bX-YYh&MWe6&CWunLcvp+|aX5=HU{Z4AqQSY2)8v?;|i@JKnFmz2zm* z_*HC>XF#tLzI=Jxr`h?I+yCt^HV+x^EX$`(ZL|TesgAX|As2GX2aE$ZTm8Dh`^ykD z$4a?~1qwcV5MR6w9EK!1{Ix5jOOin;R0HjJz{j-?lo&p=bs-=J5qfqHf;!ebe7d``=&EryKo2G0RocrSvG$P)CHI{)*Iq;*8vssjze@K|gTFsvH zJFlrgMznEKHd-0qHKb?qU>U>gTm;N-#|&9%67Si{FT=7CU2chD&zj6!R~d4as+5ah z0T1Fhdnotm`N2nngXaf_dUV1P5(bLG*{>0{_gqa!*N@HepQ8Q8U;yUeL<-5*oo7+1 zk|5d3VI+@7kd+iYL<%mZ5BlSe7ahYyG9}Ypzqk%0rgj_8bKd`}`M#rzKFl&IftJ!0 zIOMpR5Ovyl{k}|TX`*k~{xPr8BHsb~rXY*y0Z+cOI7!Y#3s$f73QUSowdhPG=PyPnx7@V*SOtVf%yJ!Z9E`M z=HX)&=aF*O(c%&W^^+6=YMM=k%?)Zh%JBQV-B#W5uFPHloSOr%jni5soUMdj7XR() zx$M6YaVv;k)&CczNdZ?7f*Ky41hS{EuJ^-9LxJ1LU`T*3)MK*RfX((Nw)f+_VZ{zK zLK#JU@U`So7`H+B5%%`_2g)1r3JN4OZuAgW&1~(E@Fy6^FcC&xt;)Rlj=#v>jK-eA z6pM%UHO(|-rJgLsAQ~FD0GrBI-tpRAve2uQU|Ln!A#=BR;`Q@6aP4*6+x>JN8gK9X z=z8{hFy!p(DjC_>Mi~@T83ki7id(oof}5 z_ot!m;Pnya$&n%?qFpml(qYTQ~ zIdl_)1N8KMfEQ+bF+dz9Z7`yr{m#96Je4!54zb?;H&$tcT z(FXo~ted@_#)RcXo@EVmBJhH4J*!3eenE|fcz8%1jeZTr|0X2`5mYw7KEjy;

|K(%GJ3w7Ln;NrGaBQ>~oWw{_zRCvD!z=i-r( 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 literal 0 HcmV?d00001 diff --git a/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx b/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx index 19177607..1a50539a 100644 --- a/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx +++ b/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx @@ -36,6 +36,7 @@ import DellProAiStudioLogo from "@/media/llmprovider/dpais.png"; import MoonshotAiLogo from "@/media/llmprovider/moonshotai.png"; import CometApiLogo from "@/media/llmprovider/cometapi.png"; import FoundryLogo from "@/media/llmprovider/foundry-local.png"; +import GiteeAILogo from "@/media/llmprovider/giteeai.png"; import PreLoader from "@/components/Preloader"; import OpenAiOptions from "@/components/LLMSelection/OpenAiOptions"; @@ -69,6 +70,7 @@ import PPIOLLMOptions from "@/components/LLMSelection/PPIOLLMOptions"; import DellProAiStudioOptions from "@/components/LLMSelection/DPAISOptions"; import MoonshotAiOptions from "@/components/LLMSelection/MoonshotAiOptions"; import FoundryOptions from "@/components/LLMSelection/FoundryOptions"; +import GiteeAIOptions from "@/components/LLMSelection/GiteeAIOptions/index.jsx"; import LLMItem from "@/components/LLMSelection/LLMItem"; import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react"; @@ -345,6 +347,14 @@ export const AVAILABLE_LLM_PROVIDERS = [ description: "Run Z.AI's powerful GLM models.", requiredConfig: ["ZAiApiKey"], }, + { + name: "GiteeAI", + value: "giteeai", + logo: GiteeAILogo, + options: (settings) => , + description: "Run GiteeAI's powerful LLMs.", + requiredConfig: ["GiteeAIApiKey"], + }, { name: "Generic OpenAI", value: "generic-openai", diff --git a/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx index 3c804ec8..76568582 100644 --- a/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx +++ b/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx @@ -42,6 +42,7 @@ import DPAISLogo from "@/media/llmprovider/dpais.png"; import MoonshotAiLogo from "@/media/llmprovider/moonshotai.png"; import CometApiLogo from "@/media/llmprovider/cometapi.png"; import FoundryLogo from "@/media/llmprovider/foundry-local.png"; +import GiteeAILogo from "@/media/llmprovider/giteeai.png"; import React, { useState, useEffect } from "react"; import paths from "@/utils/paths"; @@ -279,6 +280,13 @@ export const LLM_SELECTION_PRIVACY = { ], logo: FoundryLogo, }, + giteeai: { + name: "GiteeAI", + description: [ + "Your model and chat contents are visible to GiteeAI in accordance with their terms of service.", + ], + logo: GiteeAILogo, + }, }; 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 ed4b02f7..a0cf2ae8 100644 --- a/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx +++ b/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx @@ -30,6 +30,7 @@ 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 GiteeAILogo from "@/media/llmprovider/giteeai.png"; import OpenAiOptions from "@/components/LLMSelection/OpenAiOptions"; import GenericOpenAiOptions from "@/components/LLMSelection/GenericOpenAiOptions"; @@ -61,6 +62,7 @@ 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 GiteeAiOptions from "@/components/LLMSelection/GiteeAIOptions"; import LLMItem from "@/components/LLMSelection/LLMItem"; import System from "@/models/system"; @@ -290,6 +292,13 @@ const LLMS = [ options: (settings) => , description: "500+ AI Models all in one API.", }, + { + name: "GiteeAI", + value: "giteeai", + logo: GiteeAILogo, + options: (settings) => , + description: "Run GiteeAI's powerful LLMs.", + }, ]; 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 a1309203..020c5016 100644 --- a/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentLLMSelection/index.jsx +++ b/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentLLMSelection/index.jsx @@ -35,6 +35,7 @@ const ENABLED_PROVIDERS = [ "cometapi", "foundry", "zai", + "giteeai", // 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 c7c02a0a..47aef59d 100644 --- a/server/.env.example +++ b/server/.env.example @@ -156,6 +156,11 @@ SIG_SALT='salt' # Please generate random string at least 32 chars long. # FOUNDRY_MODEL_PREF='phi-3.5-mini' # FOUNDRY_MODEL_TOKEN_LIMIT=4096 +# LLM_PROVIDER='giteeai' +# GITEE_AI_API_KEY= +# GITEE_AI_MODEL_PREF= +# GITEE_AI_MODEL_TOKEN_LIMIT= + ########################################### ######## Embedding API SElECTION ########## ########################################### diff --git a/server/endpoints/utils.js b/server/endpoints/utils.js index 136245dc..fc5b4133 100644 --- a/server/endpoints/utils.js +++ b/server/endpoints/utils.js @@ -148,6 +148,9 @@ function getModelTag() { case "zai": model = process.env.ZAI_MODEL_PREF; break; + case "giteeai": + model = process.env.GITEE_AI_MODEL_PREF; + break; default: model = "--"; break; diff --git a/server/models/systemSettings.js b/server/models/systemSettings.js index 34693396..28e44ca6 100644 --- a/server/models/systemSettings.js +++ b/server/models/systemSettings.js @@ -641,6 +641,11 @@ const SystemSettings = { // Z.AI Keys ZAiApiKey: !!process.env.ZAI_API_KEY, ZAiModelPref: process.env.ZAI_MODEL_PREF, + + // GiteeAI API Keys + GiteeAIApiKey: !!process.env.GITEE_AI_API_KEY, + GiteeAIModelPref: process.env.GITEE_AI_MODEL_PREF, + GiteeAITokenLimit: process.env.GITEE_AI_MODEL_TOKEN_LIMIT || 8192, }; }, diff --git a/server/storage/models/.gitignore b/server/storage/models/.gitignore index 9181a534..6bda7b7a 100644 --- a/server/storage/models/.gitignore +++ b/server/storage/models/.gitignore @@ -12,4 +12,5 @@ ppio context-windows/* MintplexLabs cometapi -fireworks \ No newline at end of file +fireworks +giteeai \ No newline at end of file diff --git a/server/utils/AiProviders/giteeai/index.js b/server/utils/AiProviders/giteeai/index.js new file mode 100644 index 00000000..e74a6d55 --- /dev/null +++ b/server/utils/AiProviders/giteeai/index.js @@ -0,0 +1,397 @@ +const fs = require("fs"); +const path = require("path"); +const { v4: uuidv4 } = require("uuid"); +const { safeJsonParse, toValidNumber } = require("../../http"); +const LEGACY_MODEL_MAP = require("../modelMap/legacy"); +const { NativeEmbedder } = require("../../EmbeddingEngines/native"); +const { + LLMPerformanceMonitor, +} = require("../../helpers/chat/LLMPerformanceMonitor"); +const { + writeResponseChunk, + clientAbortedHandler, +} = require("../../helpers/chat/responses"); +const cacheFolder = path.resolve( + process.env.STORAGE_DIR + ? path.resolve(process.env.STORAGE_DIR, "models", "giteeai") + : path.resolve(__dirname, `../../../storage/models/giteeai`) +); + +class GiteeAILLM { + constructor(embedder = null, modelPreference = null) { + if (!process.env.GITEE_AI_API_KEY) + throw new Error("No Gitee AI API key was set."); + const { OpenAI: OpenAIApi } = require("openai"); + + this.openai = new OpenAIApi({ + apiKey: process.env.GITEE_AI_API_KEY, + baseURL: "https://ai.gitee.com/v1", + }); + this.model = modelPreference || process.env.GITEE_AI_MODEL_PREF || ""; + this.limits = { + history: this.promptWindowLimit() * 0.15, + system: this.promptWindowLimit() * 0.15, + user: this.promptWindowLimit() * 0.7, + }; + + this.embedder = embedder ?? new NativeEmbedder(); + this.defaultTemp = 0.7; + + if (!fs.existsSync(cacheFolder)) + fs.mkdirSync(cacheFolder, { recursive: true }); + this.cacheModelPath = path.resolve(cacheFolder, "models.json"); + this.cacheAtPath = path.resolve(cacheFolder, ".cached_at"); + this.log("Initialized with model:", this.model); + } + + log(text, ...args) { + console.log(`\x1b[36m[${this.constructor.name}]\x1b[0m ${text}`, ...args); + } + + // This checks if the .cached_at file has a timestamp that is more than 1Week (in millis) + // from the current date. If it is, then we will refetch the API so that all the models are up + // to date. + #cacheIsStale() { + const MAX_STALE = 6.048e8; // 1 Week in MS + if (!fs.existsSync(this.cacheAtPath)) return true; + const now = Number(new Date()); + const timestampMs = Number(fs.readFileSync(this.cacheAtPath)); + return now - timestampMs > MAX_STALE; + } + + // This function fetches the models from the GiteeAI API and caches them locally. + async #syncModels() { + if (fs.existsSync(this.cacheModelPath) && !this.#cacheIsStale()) + return false; + + this.log("Model cache is not present or stale. Fetching from GiteeAI API."); + await giteeAiModels(); + return; + } + + models() { + if (!fs.existsSync(this.cacheModelPath)) return {}; + return safeJsonParse( + fs.readFileSync(this.cacheModelPath, { encoding: "utf-8" }), + {} + ); + } + + #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("") + ); + } + + streamingEnabled() { + return "streamGetChatCompletion" in this; + } + + static promptWindowLimit(model) { + return ( + toValidNumber(process.env.GITEE_AI_MODEL_TOKEN_LIMIT) || + LEGACY_MODEL_MAP.giteeai[model] || + 8192 + ); + } + + promptWindowLimit() { + return ( + toValidNumber(process.env.GITEE_AI_MODEL_TOKEN_LIMIT) || + LEGACY_MODEL_MAP.giteeai[this.model] || + 8192 + ); + } + + async isValidChatCompletionModel(modelName = "") { + return true; + } + + constructPrompt({ + systemPrompt = "", + contextTexts = [], + chatHistory = [], + userPrompt = "", + }) { + const prompt = { + role: "system", + content: `${systemPrompt}${this.#appendContext(contextTexts)}`, + }; + return [prompt, ...chatHistory, { role: "user", content: userPrompt }]; + } + + /** + * Parses and prepends reasoning from the response and returns the full text response. + * @param {Object} response + * @returns {string} + */ + #parseReasoningFromResponse({ message }) { + let textResponse = message?.content; + if ( + !!message?.reasoning_content && + message.reasoning_content.trim().length > 0 + ) + textResponse = `${message.reasoning_content}${textResponse}`; + return textResponse; + } + + async getChatCompletion(messages = null, { temperature = 0.7 }) { + 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 + ) + throw new Error( + `Invalid response body returned from GiteeAI: ${JSON.stringify(result.output)}` + ); + + return { + textResponse: this.#parseReasoningFromResponse(result.output.choices[0]), + 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 }) { + const measuredStreamRequest = await LLMPerformanceMonitor.measureStream( + this.openai.chat.completions.create({ + model: this.model, + stream: true, + messages, + temperature, + }), + messages, + false + ); + + return measuredStreamRequest; + } + + // TODO: This is a copy of the generic handleStream function in responses.js + // to specifically handle the GiteeAI reasoning model `reasoning_content` field. + // When or if ever possible, we should refactor this to be in the generic function. + handleStream(response, stream, responseProps) { + const { uuid = uuidv4(), sources = [] } = responseProps; + let hasUsageMetrics = false; + let usage = { + completion_tokens: 0, + }; + + return new Promise(async (resolve) => { + let fullText = ""; + let reasoningText = ""; + + // 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(usage); + clientAbortedHandler(resolve, fullText); + }; + response.on("close", handleAbort); + + try { + for await (const chunk of stream) { + const message = chunk?.choices?.[0]; + const token = message?.delta?.content; + const reasoningToken = message?.delta?.reasoning_content; + + if ( + chunk.hasOwnProperty("usage") && // exists + !!chunk.usage && // is not null + Object.values(chunk.usage).length > 0 // has values + ) { + if (chunk.usage.hasOwnProperty("prompt_tokens")) { + usage.prompt_tokens = Number(chunk.usage.prompt_tokens); + } + + if (chunk.usage.hasOwnProperty("completion_tokens")) { + hasUsageMetrics = true; // to stop estimating counter + usage.completion_tokens = Number(chunk.usage.completion_tokens); + } + } + + // Reasoning models will always return the reasoning text before the token text. + if (reasoningToken) { + // If the reasoning text is empty (''), we need to initialize it + // and send the first chunk of reasoning text. + if (reasoningText.length === 0) { + writeResponseChunk(response, { + uuid, + sources: [], + type: "textResponseChunk", + textResponse: `${reasoningToken}`, + close: false, + error: false, + }); + reasoningText += `${reasoningToken}`; + continue; + } else { + writeResponseChunk(response, { + uuid, + sources: [], + type: "textResponseChunk", + textResponse: reasoningToken, + close: false, + error: false, + }); + reasoningText += reasoningToken; + } + } + + // If the reasoning text is not empty, but the reasoning token is empty + // and the token text is not empty we need to close the reasoning text and begin sending the token text. + if (!!reasoningText && !reasoningToken && token) { + writeResponseChunk(response, { + uuid, + sources: [], + type: "textResponseChunk", + textResponse: ``, + close: false, + error: false, + }); + fullText += `${reasoningText}`; + reasoningText = ""; + } + + if (token) { + fullText += token; + // If we never saw a usage metric, we can estimate them by number of completion chunks + if (!hasUsageMetrics) usage.completion_tokens++; + writeResponseChunk(response, { + uuid, + sources: [], + type: "textResponseChunk", + textResponse: token, + close: false, + error: false, + }); + } + + // LocalAi returns '' and others return null on chunks - the last chunk is not "" or null. + // Either way, the key `finish_reason` must be present to determine ending chunk. + if ( + message?.hasOwnProperty("finish_reason") && // Got valid message and it is an object with finish_reason + message.finish_reason !== "" && + message.finish_reason !== null + ) { + writeResponseChunk(response, { + uuid, + sources, + type: "textResponseChunk", + textResponse: "", + close: true, + error: false, + }); + response.removeListener("close", handleAbort); + stream?.endMeasurement(usage); + resolve(fullText); + break; // Break streaming when a valid finish_reason is first encountered + } + } + } catch (e) { + console.log(`\x1b[43m\x1b[34m[STREAMING ERROR]\x1b[0m ${e.message}`); + writeResponseChunk(response, { + uuid, + type: "abort", + textResponse: null, + sources: [], + close: true, + error: e.message, + }); + stream?.endMeasurement(usage); + resolve(fullText); // Return what we currently have - if anything. + } + }); + } + + 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 giteeAiModels() { + const url = new URL("https://ai.gitee.com/v1/models"); + url.searchParams.set("type", "text2text"); + return await fetch(url.toString(), { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${process.env.GITEE_AI_API_KEY}`, + }, + }) + .then((res) => res.json()) + .then(({ data = [] }) => data) + .then((models = []) => { + const validModels = {}; + models.forEach( + (model) => + (validModels[model.id] = { + id: model.id, + name: model.id, + organization: model.owned_by, + }) + ); + // Cache all response information + if (!fs.existsSync(cacheFolder)) + fs.mkdirSync(cacheFolder, { recursive: true }); + fs.writeFileSync( + path.resolve(cacheFolder, "models.json"), + JSON.stringify(validModels), + { + encoding: "utf-8", + } + ); + fs.writeFileSync( + path.resolve(cacheFolder, ".cached_at"), + String(Number(new Date())), + { + encoding: "utf-8", + } + ); + + return validModels; + }) + .catch((e) => { + console.error(e); + return {}; + }); +} + +module.exports = { + GiteeAILLM, + giteeAiModels, +}; diff --git a/server/utils/AiProviders/modelMap/legacy.js b/server/utils/AiProviders/modelMap/legacy.js index 1187cf51..d8de4e54 100644 --- a/server/utils/AiProviders/modelMap/legacy.js +++ b/server/utils/AiProviders/modelMap/legacy.js @@ -120,5 +120,27 @@ const LEGACY_MODEL_MAP = { xai: { "grok-beta": 131072, }, + giteeai: { + "Qwen2.5-72B-Instruct": 16_384, + "Qwen2.5-14B-Instruct": 24_576, + "Qwen2-7B-Instruct": 24_576, + "Qwen2.5-32B-Instruct": 32_768, + "Qwen2-72B-Instruct": 32_768, + "Qwen2-VL-72B": 32_768, + "QwQ-32B-Preview": 32_768, + "Yi-34B-Chat": 4_096, + "glm-4-9b-chat": 32_768, + "deepseek-coder-33B-instruct": 8_192, + "codegeex4-all-9b": 32_768, + "InternVL2-8B": 32_768, + "InternVL2.5-26B": 32_768, + "InternVL2.5-78B": 32_768, + "DeepSeek-R1-Distill-Qwen-32B": 32_768, + "DeepSeek-R1-Distill-Qwen-1.5B": 32_768, + "DeepSeek-R1-Distill-Qwen-14B": 32_768, + "DeepSeek-R1-Distill-Qwen-7B": 32_768, + "DeepSeek-V3": 32_768, + "DeepSeek-R1": 32_768, + }, }; module.exports = LEGACY_MODEL_MAP; diff --git a/server/utils/agents/aibitat/index.js b/server/utils/agents/aibitat/index.js index 9edfbc4c..add1adb1 100644 --- a/server/utils/agents/aibitat/index.js +++ b/server/utils/agents/aibitat/index.js @@ -988,6 +988,8 @@ ${this.getHistory({ to: route.to }) return new Providers.CometApiProvider({ model: config.model }); case "foundry": return new Providers.FoundryProvider({ model: config.model }); + case "giteeai": + return new Providers.GiteeAIProvider({ 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 ec9884a1..c1a41909 100644 --- a/server/utils/agents/aibitat/providers/ai-provider.js +++ b/server/utils/agents/aibitat/providers/ai-provider.js @@ -231,6 +231,14 @@ class Provider { apiKey: process.env.COMETAPI_LLM_API_KEY ?? null, ...config, }); + case "giteeai": + return new ChatOpenAI({ + configuration: { + baseURL: "https://ai.gitee.com/v1", + }, + apiKey: process.env.GITEE_AI_API_KEY ?? null, + ...config, + }); // OSS Model Runners // case "anythingllm_ollama": // return new ChatOllama({ diff --git a/server/utils/agents/aibitat/providers/giteeai.js b/server/utils/agents/aibitat/providers/giteeai.js new file mode 100644 index 00000000..261760a8 --- /dev/null +++ b/server/utils/agents/aibitat/providers/giteeai.js @@ -0,0 +1,85 @@ +const OpenAI = require("openai"); +const Provider = require("./ai-provider.js"); +const InheritMultiple = require("./helpers/classes.js"); +const UnTooled = require("./helpers/untooled.js"); + +class GiteeAIProvider extends InheritMultiple([Provider, UnTooled]) { + model; + + constructor(config = {}) { + super(); + const { model = "DeepSeek-R1" } = config; + this._client = new OpenAI({ + baseURL: "https://ai.gitee.com/v1", + apiKey: process.env.GITEE_AI_API_KEY ?? null, + maxRetries: 3, + }); + this.model = model; + this.verbose = true; + } + + get client() { + return this._client; + } + + get supportsAgentStreaming() { + return true; + } + + async #handleFunctionCallChat({ messages = [] }) { + return await this.client.chat.completions + .create({ + model: this.model, + messages, + }) + .then((result) => { + if (!result.hasOwnProperty("choices")) + throw new Error("GiteeAI chat: No results!"); + if (result.choices.length === 0) + throw new Error("GiteeAI chat: No results length!"); + return result.choices[0].message.content; + }) + .catch((_) => { + return null; + }); + } + + async #handleFunctionCallStream({ messages = [] }) { + return await this.client.chat.completions.create({ + model: this.model, + stream: true, + messages, + }); + } + + async stream(messages, functions = [], eventHandler = null) { + return await UnTooled.prototype.stream.call( + this, + messages, + functions, + this.#handleFunctionCallStream.bind(this), + eventHandler + ); + } + + async complete(messages, functions = []) { + return await UnTooled.prototype.complete.call( + this, + messages, + functions, + this.#handleFunctionCallChat.bind(this) + ); + } + + /** + * Get the cost of the completion. + * + * @param _usage The completion to get the cost for. + * @returns The cost of the completion. + */ + getCost(_usage) { + return 0; + } +} + +module.exports = GiteeAIProvider; diff --git a/server/utils/agents/aibitat/providers/index.js b/server/utils/agents/aibitat/providers/index.js index f927c82c..9ac8465f 100644 --- a/server/utils/agents/aibitat/providers/index.js +++ b/server/utils/agents/aibitat/providers/index.js @@ -27,6 +27,7 @@ const DellProAiStudioProvider = require("./dellProAiStudio.js"); const MoonshotAiProvider = require("./moonshotAi.js"); const CometApiProvider = require("./cometapi.js"); const FoundryProvider = require("./foundry.js"); +const GiteeAIProvider = require("./giteeai.js"); module.exports = { OpenAIProvider, @@ -58,4 +59,5 @@ module.exports = { DellProAiStudioProvider, MoonshotAiProvider, FoundryProvider, + GiteeAIProvider, }; diff --git a/server/utils/agents/index.js b/server/utils/agents/index.js index 10972594..b2d95676 100644 --- a/server/utils/agents/index.js +++ b/server/utils/agents/index.js @@ -208,17 +208,17 @@ class AgentHandler { if (!process.env.MOONSHOT_AI_MODEL_PREF) 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; - case "foundry": if (!process.env.FOUNDRY_BASE_PATH) throw new Error("Foundry base path must be provided to use agents."); break; - + case "giteeai": + if (!process.env.GITEE_AI_API_KEY) + throw new Error("GiteeAI API Key must be provided to use agents."); default: throw new Error( "No workspace agent provider set. Please set your agent provider in the workspace's settings" @@ -295,6 +295,8 @@ class AgentHandler { return process.env.COMETAPI_LLM_MODEL_PREF ?? "gpt-5-mini"; case "foundry": return process.env.FOUNDRY_MODEL_PREF ?? null; + case "giteeai": + return process.env.GITEE_AI_MODEL_PREF ?? null; default: return null; } diff --git a/server/utils/helpers/customModels.js b/server/utils/helpers/customModels.js index 27371909..4a06a1b3 100644 --- a/server/utils/helpers/customModels.js +++ b/server/utils/helpers/customModels.js @@ -42,6 +42,7 @@ const SUPPORT_CUSTOM_MODELS = [ "foundry", "cohere", "zai", + "giteeai", // Embedding Engines "native-embedder", "cohere-embedder", @@ -113,6 +114,8 @@ async function getCustomModels(provider = "", apiKey = null, basePath = null) { return await getCohereModels(apiKey, "embed"); case "openrouter-embedder": return await getOpenRouterEmbeddingModels(); + case "giteeai": + return await getGiteeAIModels(apiKey); default: return { models: [], error: "Invalid provider for custom models" }; } @@ -596,6 +599,20 @@ async function getDeepSeekModels(apiKey = null) { return { models, error: null }; } +async function getGiteeAIModels() { + const { giteeAiModels } = require("../AiProviders/giteeai"); + const modelMap = await giteeAiModels(); + if (!Object.keys(modelMap).length === 0) return { models: [], error: null }; + const models = Object.values(modelMap).map((model) => { + return { + id: model.id, + organization: model.organization ?? "GiteeAI", + name: model.id, + }; + }); + return { models, error: null }; +} + async function getXAIModels(_apiKey = null) { const { OpenAI: OpenAIApi } = require("openai"); const apiKey = diff --git a/server/utils/helpers/index.js b/server/utils/helpers/index.js index f1cc1fde..01e24926 100644 --- a/server/utils/helpers/index.js +++ b/server/utils/helpers/index.js @@ -225,6 +225,9 @@ function getLLMProvider({ provider = null, model = null } = {}) { case "zai": const { ZAiLLM } = require("../AiProviders/zai"); return new ZAiLLM(embedder, model); + case "giteeai": + const { GiteeAILLM } = require("../AiProviders/giteeai"); + return new GiteeAILLM(embedder, model); default: throw new Error( `ENV: No valid LLM_PROVIDER value found in environment! Using ${process.env.LLM_PROVIDER}` @@ -387,6 +390,9 @@ function getLLMProviderClass({ provider = null } = {}) { case "zai": const { ZAiLLM } = require("../AiProviders/zai"); return ZAiLLM; + case "giteeai": + const { GiteeAILLM } = require("../AiProviders/giteeai"); + return GiteeAILLM; default: return null; } @@ -461,6 +467,8 @@ function getBaseLLMProviderModel({ provider = null } = {}) { return process.env.FOUNDRY_MODEL_PREF; case "zai": return process.env.ZAI_MODEL_PREF; + case "giteeai": + return process.env.GITEE_AI_MODEL_PREF; default: return null; } diff --git a/server/utils/helpers/updateENV.js b/server/utils/helpers/updateENV.js index 5bfe58f1..43b48794 100644 --- a/server/utils/helpers/updateENV.js +++ b/server/utils/helpers/updateENV.js @@ -775,6 +775,20 @@ const KEY_MAPPING = { envKey: "ZAI_MODEL_PREF", checks: [isNotEmpty], }, + + // GiteeAI Options + GiteeAIApiKey: { + envKey: "GITEE_AI_API_KEY", + checks: [isNotEmpty], + }, + GiteeAIModelPref: { + envKey: "GITEE_AI_MODEL_PREF", + checks: [isNotEmpty], + }, + GiteeAITokenLimit: { + envKey: "GITEE_AI_MODEL_TOKEN_LIMIT", + checks: [nonZero], + }, }; function isNotEmpty(input = "") { @@ -887,6 +901,7 @@ function supportedLLM(input = "") { "cometapi", "foundry", "zai", + "giteeai", ].includes(input); return validSelection ? null : `${input} is not a valid LLM provider.`; }