diff --git a/Dockerfile b/Dockerfile index 710297ce6ea5..5f5a97d2acb9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -158,6 +158,8 @@ ENV \ DEEPSEEK_API_KEY="" DEEPSEEK_MODEL_LIST="" \ # Fireworks AI FIREWORKSAI_API_KEY="" FIREWORKSAI_MODEL_LIST="" \ + # Gitee AI + GITEE_AI_API_KEY="" GITEE_AI_MODEL_LIST="" \ # GitHub GITHUB_TOKEN="" GITHUB_MODEL_LIST="" \ # Google diff --git a/Dockerfile.database b/Dockerfile.database index c730b825d013..ce99a5e3773e 100644 --- a/Dockerfile.database +++ b/Dockerfile.database @@ -193,6 +193,8 @@ ENV \ DEEPSEEK_API_KEY="" DEEPSEEK_MODEL_LIST="" \ # Fireworks AI FIREWORKSAI_API_KEY="" FIREWORKSAI_MODEL_LIST="" \ + # Gitee AI + GITEE_AI_API_KEY="" GITEE_AI_MODEL_LIST="" \ # GitHub GITHUB_TOKEN="" GITHUB_MODEL_LIST="" \ # Google diff --git a/src/app/(main)/settings/llm/ProviderList/providers.tsx b/src/app/(main)/settings/llm/ProviderList/providers.tsx index f49cdd22cea1..1e90871fce22 100644 --- a/src/app/(main)/settings/llm/ProviderList/providers.tsx +++ b/src/app/(main)/settings/llm/ProviderList/providers.tsx @@ -7,6 +7,7 @@ import { BaichuanProviderCard, DeepSeekProviderCard, FireworksAIProviderCard, + GiteeAIProviderCard, GoogleProviderCard, GroqProviderCard, HunyuanProviderCard, @@ -88,6 +89,7 @@ export const useProviderList = (): ProviderItem[] => { TaichuProviderCard, InternLMProviderCard, SiliconCloudProviderCard, + GiteeAIProviderCard, ], [ AzureProvider, diff --git a/src/config/llm.ts b/src/config/llm.ts index 87a364a7bef6..e515b51105dd 100644 --- a/src/config/llm.ts +++ b/src/config/llm.ts @@ -94,6 +94,9 @@ export const getLLMConfig = () => { ENABLED_SILICONCLOUD: z.boolean(), SILICONCLOUD_API_KEY: z.string().optional(), + ENABLED_GITEE_AI: z.boolean(), + GITEE_AI_API_KEY: z.string().optional(), + ENABLED_UPSTAGE: z.boolean(), UPSTAGE_API_KEY: z.string().optional(), @@ -210,6 +213,9 @@ export const getLLMConfig = () => { ENABLED_SILICONCLOUD: !!process.env.SILICONCLOUD_API_KEY, SILICONCLOUD_API_KEY: process.env.SILICONCLOUD_API_KEY, + ENABLED_GITEE_AI: !!process.env.GITEE_AI_API_KEY, + GITEE_AI_API_KEY: process.env.GITEE_AI_API_KEY, + ENABLED_UPSTAGE: !!process.env.UPSTAGE_API_KEY, UPSTAGE_API_KEY: process.env.UPSTAGE_API_KEY, diff --git a/src/config/modelProviders/giteeai.ts b/src/config/modelProviders/giteeai.ts new file mode 100644 index 000000000000..49f997b2f2a4 --- /dev/null +++ b/src/config/modelProviders/giteeai.ts @@ -0,0 +1,66 @@ +import { ModelProviderCard } from '@/types/llm'; + +// ref: https://ai.gitee.com/serverless-api/packages/1493 +const GiteeAI: ModelProviderCard = { + chatModels: [ + { + description: 'Qwen2.5-72B-Instruct 支持 16k 上下文, 生成长文本超过 8K 。支持 function call 与外部系统无缝交互,极大提升了灵活性和扩展性。模型知识明显增加,并且大大提高了编码和数学能力, 多语言支持超过 29 种', + displayName: 'Qwen2.5 72B Instruct', + enabled: true, + functionCall: true, + id: 'Qwen2.5-72B-Instruct', + tokens: 16_000, + }, + { + description: 'Qwen2 是 Qwen 模型的最新系列,支持 128k 上下文,对比当前最优的开源模型,Qwen2-72B 在自然语言理解、知识、代码、数学及多语言等多项能力上均显著超越当前领先的模型。', + displayName: 'Qwen2 72B Instruct', + id: 'Qwen2-72B-Instruct', + tokens: 6000, + }, + { + description: 'Qwen2 是 Qwen 模型的最新系列,能够超越同等规模的最优开源模型甚至更大规模的模型,Qwen2 7B 在多个评测上取得显著的优势,尤其是代码及中文理解上。', + displayName: 'Qwen2 7B Instruct', + id: 'Qwen2-7B-Instruct', + tokens: 32_000, + }, + { + description: 'GLM-4-9B-Chat 在语义、数学、推理、代码和知识等多方面均表现出较高性能。还具备网页浏览、代码执行、自定义工具调用和长文本推理。 支持包括日语,韩语,德语在内的 26 种语言。', + displayName: 'GLM4 9B Chat', + enabled: true, + id: 'glm-4-9b-chat', + tokens: 32_000, + }, + { + description: 'Yi-1.5-34B 在保持原系列模型优秀的通用语言能力的前提下,通过增量训练 5 千亿高质量 token,大幅提高了数学逻辑、代码能力。', + displayName: 'Yi 34B Chat', + enabled: true, + id: 'Yi-34B-Chat', + tokens: 4000, + }, + { + description: 'DeepSeek Coder 33B 是一个代码语言模型, 基于 2 万亿数据训练而成,其中 87% 为代码, 13% 为中英文语言。模型引入 16K 窗口大小和填空任务,提供项目级别的代码补全和片段填充功能。', + displayName: 'DeepSeek Coder 33B Instruct', + enabled: true, + id: 'deepseek-coder-33B-instruct', + tokens: 8000, + }, + { + description: 'CodeGeeX4-ALL-9B 是一个多语言代码生成模型,支持包括代码补全和生成、代码解释器、网络搜索、函数调用、仓库级代码问答在内的全面功能,覆盖软件开发的各种场景。是参数少于 10B 的顶尖代码生成模型。', + displayName: 'CodeGeeX4 All 9B', + enabled: true, + id: 'codegeex4-all-9b', + tokens: 40_000, + }, + ], + checkModel: 'Qwen2-7B-Instruct', + description: + 'Gitee AI 的 Serverless API 为 AI 开发者提供开箱即用的大模型推理 API 服务。', + disableBrowserRequest: true, + id: 'giteeai', + modelList: { showModelFetcher: true }, + modelsUrl: 'https://ai.gitee.com/docs/openapi/v1#tag/serverless/POST/chat/completions', + name: 'Gitee AI', + url: 'https://ai.gitee.com', +}; + +export default GiteeAI; diff --git a/src/config/modelProviders/index.ts b/src/config/modelProviders/index.ts index c79ee701497f..961736a34fff 100644 --- a/src/config/modelProviders/index.ts +++ b/src/config/modelProviders/index.ts @@ -9,6 +9,7 @@ import BedrockProvider from './bedrock'; import CloudflareProvider from './cloudflare'; import DeepSeekProvider from './deepseek'; import FireworksAIProvider from './fireworksai'; +import GiteeAIProvider from './giteeai'; import GithubProvider from './github'; import GoogleProvider from './google'; import GroqProvider from './groq'; @@ -64,6 +65,7 @@ export const LOBE_DEFAULT_MODEL_LIST: ChatModelCard[] = [ CloudflareProvider.chatModels, Ai360Provider.chatModels, SiliconCloudProvider.chatModels, + GiteeAIProvider.chatModels, UpstageProvider.chatModels, SparkProvider.chatModels, Ai21Provider.chatModels, @@ -109,6 +111,7 @@ export const DEFAULT_MODEL_PROVIDER_LIST = [ TaichuProvider, InternLMProvider, SiliconCloudProvider, + GiteeAIProvider, ]; export const filterEnabledModels = (provider: ModelProviderCard) => { @@ -129,6 +132,7 @@ export { default as BedrockProviderCard } from './bedrock'; export { default as CloudflareProviderCard } from './cloudflare'; export { default as DeepSeekProviderCard } from './deepseek'; export { default as FireworksAIProviderCard } from './fireworksai'; +export { default as GiteeAIProviderCard } from './giteeai'; export { default as GithubProviderCard } from './github'; export { default as GoogleProviderCard } from './google'; export { default as GroqProviderCard } from './groq'; diff --git a/src/libs/agent-runtime/AgentRuntime.ts b/src/libs/agent-runtime/AgentRuntime.ts index 8ee4a61b2d62..6cc1d0332943 100644 --- a/src/libs/agent-runtime/AgentRuntime.ts +++ b/src/libs/agent-runtime/AgentRuntime.ts @@ -12,6 +12,7 @@ import { LobeBedrockAI, LobeBedrockAIParams } from './bedrock'; import { LobeCloudflareAI, LobeCloudflareParams } from './cloudflare'; import { LobeDeepSeekAI } from './deepseek'; import { LobeFireworksAI } from './fireworksai'; +import { LobeGiteeAI } from './giteeai'; import { LobeGithubAI } from './github'; import { LobeGoogleAI } from './google'; import { LobeGroq } from './groq'; @@ -137,6 +138,7 @@ class AgentRuntime { cloudflare: Partial; deepseek: Partial; fireworksai: Partial; + giteeai: Partial; github: Partial; google: { apiKey?: string; baseURL?: string }; groq: Partial; @@ -303,6 +305,11 @@ class AgentRuntime { break; } + case ModelProvider.GiteeAI: { + runtimeModel = new LobeGiteeAI(params.giteeai); + break; + } + case ModelProvider.Upstage: { runtimeModel = new LobeUpstageAI(params.upstage); break; diff --git a/src/libs/agent-runtime/giteeai/index.test.ts b/src/libs/agent-runtime/giteeai/index.test.ts new file mode 100644 index 000000000000..f9c2866afe59 --- /dev/null +++ b/src/libs/agent-runtime/giteeai/index.test.ts @@ -0,0 +1,255 @@ +// @vitest-environment node +import OpenAI from 'openai'; +import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { + ChatStreamCallbacks, + LobeOpenAICompatibleRuntime, + ModelProvider, +} from '@/libs/agent-runtime'; + +import * as debugStreamModule from '../utils/debugStream'; +import { LobeGiteeAI } from './index'; + +const provider = ModelProvider.GiteeAI; +const defaultBaseURL = 'https://ai.gitee.com/v1'; + +const bizErrorType = 'ProviderBizError'; +const invalidErrorType = 'InvalidProviderAPIKey'; + +// Mock the console.error to avoid polluting test output +vi.spyOn(console, 'error').mockImplementation(() => {}); + +let instance: LobeOpenAICompatibleRuntime; + +beforeEach(() => { + instance = new LobeGiteeAI({ apiKey: 'test' }); + + // 使用 vi.spyOn 来模拟 chat.completions.create 方法 + vi.spyOn(instance['client'].chat.completions, 'create').mockResolvedValue( + new ReadableStream() as any, + ); +}); + +afterEach(() => { + vi.clearAllMocks(); +}); + +describe('LobeGiteeAI', () => { + describe('init', () => { + it('should correctly initialize with an API key', async () => { + const instance = new LobeGiteeAI({ apiKey: 'test_api_key' }); + expect(instance).toBeInstanceOf(LobeGiteeAI); + expect(instance.baseURL).toEqual(defaultBaseURL); + }); + }); + + describe('chat', () => { + describe('Error', () => { + it('should return OpenAIBizError with an openai error response when OpenAI.APIError is thrown', async () => { + // Arrange + const apiError = new OpenAI.APIError( + 400, + { + status: 400, + error: { + message: 'Bad Request', + }, + }, + 'Error message', + {}, + ); + + vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(apiError); + + // Act + try { + await instance.chat({ + messages: [{ content: 'Hello', role: 'user' }], + model: 'Qwen2-7B-Instruct', + temperature: 0, + }); + } catch (e) { + expect(e).toEqual({ + endpoint: defaultBaseURL, + error: { + error: { message: 'Bad Request' }, + status: 400, + }, + errorType: bizErrorType, + provider, + }); + } + }); + + it('should throw AgentRuntimeError with NoOpenAIAPIKey if no apiKey is provided', async () => { + try { + new LobeGiteeAI({}); + } catch (e) { + expect(e).toEqual({ errorType: invalidErrorType }); + } + }); + + it('should return OpenAIBizError with the cause when OpenAI.APIError is thrown with cause', async () => { + // Arrange + const errorInfo = { + stack: 'abc', + cause: { + message: 'api is undefined', + }, + }; + const apiError = new OpenAI.APIError(400, errorInfo, 'module error', {}); + + vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(apiError); + + // Act + try { + await instance.chat({ + messages: [{ content: 'Hello', role: 'user' }], + model: 'Qwen2-7B-Instruct', + temperature: 0, + }); + } catch (e) { + expect(e).toEqual({ + endpoint: defaultBaseURL, + error: { + cause: { message: 'api is undefined' }, + stack: 'abc', + }, + errorType: bizErrorType, + provider, + }); + } + }); + + it('should return OpenAIBizError with an cause response with desensitize Url', async () => { + // Arrange + const errorInfo = { + stack: 'abc', + cause: { message: 'api is undefined' }, + }; + const apiError = new OpenAI.APIError(400, errorInfo, 'module error', {}); + + instance = new LobeGiteeAI({ + apiKey: 'test', + + baseURL: 'https://api.abc.com/v1', + }); + + vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(apiError); + + // Act + try { + await instance.chat({ + messages: [{ content: 'Hello', role: 'user' }], + model: 'Qwen2-7B-Instruct', + temperature: 0, + }); + } catch (e) { + expect(e).toEqual({ + endpoint: 'https://api.***.com/v1', + error: { + cause: { message: 'api is undefined' }, + stack: 'abc', + }, + errorType: bizErrorType, + provider, + }); + } + }); + + it('should throw an InvalidGiteeAIAPIKey error type on 401 status code', async () => { + // Mock the API call to simulate a 401 error + const error = new Error('Unauthorized') as any; + error.status = 401; + vi.mocked(instance['client'].chat.completions.create).mockRejectedValue(error); + + try { + await instance.chat({ + messages: [{ content: 'Hello', role: 'user' }], + model: 'Qwen2-7B-Instruct', + temperature: 0, + }); + } catch (e) { + // Expect the chat method to throw an error with InvalidGiteeAIAPIKey + expect(e).toEqual({ + endpoint: defaultBaseURL, + error: new Error('Unauthorized'), + errorType: invalidErrorType, + provider, + }); + } + }); + + it('should return AgentRuntimeError for non-OpenAI errors', async () => { + // Arrange + const genericError = new Error('Generic Error'); + + vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(genericError); + + // Act + try { + await instance.chat({ + messages: [{ content: 'Hello', role: 'user' }], + model: 'Qwen2-7B-Instruct', + temperature: 0, + }); + } catch (e) { + expect(e).toEqual({ + endpoint: defaultBaseURL, + errorType: 'AgentRuntimeError', + provider, + error: { + name: genericError.name, + cause: genericError.cause, + message: genericError.message, + stack: genericError.stack, + }, + }); + } + }); + }); + + describe('DEBUG', () => { + it('should call debugStream and return StreamingTextResponse when DEBUG_GITEE_AI_CHAT_COMPLETION is 1', async () => { + // Arrange + const mockProdStream = new ReadableStream() as any; // 模拟的 prod 流 + const mockDebugStream = new ReadableStream({ + start(controller) { + controller.enqueue('Debug stream content'); + controller.close(); + }, + }) as any; + mockDebugStream.toReadableStream = () => mockDebugStream; // 添加 toReadableStream 方法 + + // 模拟 chat.completions.create 返回值,包括模拟的 tee 方法 + (instance['client'].chat.completions.create as Mock).mockResolvedValue({ + tee: () => [mockProdStream, { toReadableStream: () => mockDebugStream }], + }); + + // 保存原始环境变量值 + const originalDebugValue = process.env.DEBUG_GITEE_AI_CHAT_COMPLETION; + + // 模拟环境变量 + process.env.DEBUG_GITEE_AI_CHAT_COMPLETION = '1'; + vi.spyOn(debugStreamModule, 'debugStream').mockImplementation(() => Promise.resolve()); + + // 执行测试 + // 运行你的测试函数,确保它会在条件满足时调用 debugStream + // 假设的测试函数调用,你可能需要根据实际情况调整 + await instance.chat({ + messages: [{ content: 'Hello', role: 'user' }], + model: 'Qwen2-7B-Instruct', + stream: true, + temperature: 0, + }); + + // 验证 debugStream 被调用 + expect(debugStreamModule.debugStream).toHaveBeenCalled(); + + // 恢复原始环境变量值 + process.env.DEBUG_GITEE_AI_CHAT_COMPLETION = originalDebugValue; + }); + }); + }); +}); diff --git a/src/libs/agent-runtime/giteeai/index.ts b/src/libs/agent-runtime/giteeai/index.ts new file mode 100644 index 000000000000..a84af7571a5f --- /dev/null +++ b/src/libs/agent-runtime/giteeai/index.ts @@ -0,0 +1,10 @@ +import { ModelProvider } from '../types'; +import { LobeOpenAICompatibleFactory } from '../utils/openaiCompatibleFactory'; + +export const LobeGiteeAI = LobeOpenAICompatibleFactory({ + baseURL: 'https://ai.gitee.com/v1', + debug: { + chatCompletion: () => process.env.DEBUG_GITEE_AI_CHAT_COMPLETION === '1', + }, + provider: ModelProvider.GiteeAI, +}); diff --git a/src/libs/agent-runtime/togetherai/__snapshots__/index.test.ts.snap b/src/libs/agent-runtime/togetherai/__snapshots__/index.test.ts.snap index 4a969451ce8d..7e7a2edf7cd3 100644 --- a/src/libs/agent-runtime/togetherai/__snapshots__/index.test.ts.snap +++ b/src/libs/agent-runtime/togetherai/__snapshots__/index.test.ts.snap @@ -565,7 +565,7 @@ exports[`LobeTogetherAI > models > should get models 1`] = ` { "description": "The Yi series models are large language models trained from scratch by developers at 01.AI", "displayName": "01-ai Yi Chat (34B)", - "enabled": false, + "enabled": true, "functionCall": false, "id": "zero-one-ai/Yi-34B-Chat", "maxOutput": 4096, diff --git a/src/libs/agent-runtime/types/type.ts b/src/libs/agent-runtime/types/type.ts index 6629f2bf9920..16f0b4f72948 100644 --- a/src/libs/agent-runtime/types/type.ts +++ b/src/libs/agent-runtime/types/type.ts @@ -31,6 +31,7 @@ export enum ModelProvider { Cloudflare = 'cloudflare', DeepSeek = 'deepseek', FireworksAI = 'fireworksai', + GiteeAI = 'giteeai', Github = 'github', Google = 'google', Groq = 'groq', diff --git a/src/server/globalConfig/index.ts b/src/server/globalConfig/index.ts index 97e1d6c1f0c4..089157fa548c 100644 --- a/src/server/globalConfig/index.ts +++ b/src/server/globalConfig/index.ts @@ -29,6 +29,10 @@ export const getServerGlobalConfig = () => { enabledKey: 'ENABLED_AWS_BEDROCK', modelListKey: 'AWS_BEDROCK_MODEL_LIST', }, + giteeai: { + enabledKey: 'ENABLED_GITEE_AI', + modelListKey: 'GITEE_AI_MODEL_LIST', + }, ollama: { fetchOnClient: !process.env.OLLAMA_PROXY_URL, }, diff --git a/src/server/modules/AgentRuntime/index.ts b/src/server/modules/AgentRuntime/index.ts index 0cfa54ef7158..86e54f38ba2b 100644 --- a/src/server/modules/AgentRuntime/index.ts +++ b/src/server/modules/AgentRuntime/index.ts @@ -233,6 +233,13 @@ const getLlmOptionsFromPayload = (provider: string, payload: JWTPayload) => { return { apiKey, baseURL }; } + case ModelProvider.GiteeAI: { + const { GITEE_AI_API_KEY } = getLLMConfig(); + + const apiKey = apiKeyManager.pick(payload?.apiKey || GITEE_AI_API_KEY); + + return { apiKey }; + } case ModelProvider.HuggingFace: { const { HUGGINGFACE_API_KEY } = getLLMConfig(); diff --git a/src/types/user/settings/keyVaults.ts b/src/types/user/settings/keyVaults.ts index 492cf7449834..f6156c22363a 100644 --- a/src/types/user/settings/keyVaults.ts +++ b/src/types/user/settings/keyVaults.ts @@ -41,6 +41,7 @@ export interface UserKeyVaults { cloudflare?: CloudflareKeyVault; deepseek?: OpenAICompatibleKeyVault; fireworksai?: OpenAICompatibleKeyVault; + giteeai?: OpenAICompatibleKeyVault; github?: OpenAICompatibleKeyVault; google?: OpenAICompatibleKeyVault; groq?: OpenAICompatibleKeyVault;