From 5656f3999924ebe9c7b9cdf0db02f56fee1f0442 Mon Sep 17 00:00:00 2001 From: Zhijie He Date: Wed, 25 Dec 2024 11:40:20 +0800 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20refactor=20sen?= =?UTF-8?q?senova=20provider=20with=20LobeOpenAICompatibleFactory=20(#5116?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 2 +- Dockerfile.database | 2 +- .../llm/ProviderList/SenseNova/index.tsx | 44 ---- .../settings/llm/ProviderList/providers.tsx | 6 +- src/config/llm.ts | 9 +- src/const/auth.ts | 3 - .../Error/APIKeyForm/SenseNova.tsx | 49 ---- .../Conversation/Error/APIKeyForm/index.tsx | 3 - src/libs/agent-runtime/AgentRuntime.ts | 2 +- src/libs/agent-runtime/index.ts | 1 - .../agent-runtime/sensenova/authToken.test.ts | 18 -- src/libs/agent-runtime/sensenova/authToken.ts | 27 --- .../agent-runtime/sensenova/index.test.ts | 226 ++++++------------ src/libs/agent-runtime/sensenova/index.ts | 121 ++-------- src/locales/default/modelProvider.ts | 17 -- src/server/modules/AgentRuntime/index.ts | 15 -- src/services/_auth.ts | 14 -- .../slices/modelList/selectors/keyVaults.ts | 2 - .../slices/modelList/selectors/modelConfig.ts | 2 - src/types/user/settings/keyVaults.ts | 7 +- 20 files changed, 110 insertions(+), 460 deletions(-) delete mode 100644 src/app/(main)/settings/llm/ProviderList/SenseNova/index.tsx delete mode 100644 src/features/Conversation/Error/APIKeyForm/SenseNova.tsx delete mode 100644 src/libs/agent-runtime/sensenova/authToken.test.ts delete mode 100644 src/libs/agent-runtime/sensenova/authToken.ts diff --git a/Dockerfile b/Dockerfile index 4256fcdcde2ce..369f52b9364dc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -196,7 +196,7 @@ ENV \ # Qwen QWEN_API_KEY="" QWEN_MODEL_LIST="" QWEN_PROXY_URL="" \ # SenseNova - SENSENOVA_ACCESS_KEY_ID="" SENSENOVA_ACCESS_KEY_SECRET="" SENSENOVA_MODEL_LIST="" \ + SENSENOVA_API_KEY="" SENSENOVA_MODEL_LIST="" \ # SiliconCloud SILICONCLOUD_API_KEY="" SILICONCLOUD_MODEL_LIST="" SILICONCLOUD_PROXY_URL="" \ # Spark diff --git a/Dockerfile.database b/Dockerfile.database index 59ce47bd96270..144e2c1ca27a7 100644 --- a/Dockerfile.database +++ b/Dockerfile.database @@ -231,7 +231,7 @@ ENV \ # Qwen QWEN_API_KEY="" QWEN_MODEL_LIST="" QWEN_PROXY_URL="" \ # SenseNova - SENSENOVA_ACCESS_KEY_ID="" SENSENOVA_ACCESS_KEY_SECRET="" SENSENOVA_MODEL_LIST="" \ + SENSENOVA_API_KEY="" SENSENOVA_MODEL_LIST="" \ # SiliconCloud SILICONCLOUD_API_KEY="" SILICONCLOUD_MODEL_LIST="" SILICONCLOUD_PROXY_URL="" \ # Spark diff --git a/src/app/(main)/settings/llm/ProviderList/SenseNova/index.tsx b/src/app/(main)/settings/llm/ProviderList/SenseNova/index.tsx deleted file mode 100644 index c109d5c4ee7c5..0000000000000 --- a/src/app/(main)/settings/llm/ProviderList/SenseNova/index.tsx +++ /dev/null @@ -1,44 +0,0 @@ -'use client'; - -import { Input } from 'antd'; -import { useTranslation } from 'react-i18next'; - -import { SenseNovaProviderCard } from '@/config/modelProviders'; -import { GlobalLLMProviderKey } from '@/types/user/settings'; - -import { KeyVaultsConfigKey } from '../../const'; -import { ProviderItem } from '../../type'; - -const providerKey: GlobalLLMProviderKey = 'sensenova'; - -export const useSenseNovaProvider = (): ProviderItem => { - const { t } = useTranslation('modelProvider'); - - return { - ...SenseNovaProviderCard, - apiKeyItems: [ - { - children: ( - - ), - desc: t(`${providerKey}.sensenovaAccessKeyID.desc`), - label: t(`${providerKey}.sensenovaAccessKeyID.title`), - name: [KeyVaultsConfigKey, providerKey, 'sensenovaAccessKeyID'], - }, - { - children: ( - - ), - desc: t(`${providerKey}.sensenovaAccessKeySecret.desc`), - label: t(`${providerKey}.sensenovaAccessKeySecret.title`), - name: [KeyVaultsConfigKey, providerKey, 'sensenovaAccessKeySecret'], - }, - ], - }; -}; diff --git a/src/app/(main)/settings/llm/ProviderList/providers.tsx b/src/app/(main)/settings/llm/ProviderList/providers.tsx index eefe7bc424f40..f108683bfa07f 100644 --- a/src/app/(main)/settings/llm/ProviderList/providers.tsx +++ b/src/app/(main)/settings/llm/ProviderList/providers.tsx @@ -20,6 +20,7 @@ import { OpenRouterProviderCard, PerplexityProviderCard, QwenProviderCard, + SenseNovaProviderCard, SiliconCloudProviderCard, SparkProviderCard, StepfunProviderCard, @@ -39,7 +40,6 @@ import { useGithubProvider } from './Github'; import { useHuggingFaceProvider } from './HuggingFace'; import { useOllamaProvider } from './Ollama'; import { useOpenAIProvider } from './OpenAI'; -import { useSenseNovaProvider } from './SenseNova'; import { useWenxinProvider } from './Wenxin'; export const useProviderList = (): ProviderItem[] => { @@ -51,7 +51,6 @@ export const useProviderList = (): ProviderItem[] => { const GithubProvider = useGithubProvider(); const HuggingFaceProvider = useHuggingFaceProvider(); const WenxinProvider = useWenxinProvider(); - const SenseNovaProvider = useSenseNovaProvider(); return useMemo( () => [ @@ -81,7 +80,7 @@ export const useProviderList = (): ProviderItem[] => { SparkProviderCard, ZhiPuProviderCard, ZeroOneProviderCard, - SenseNovaProvider, + SenseNovaProviderCard, StepfunProviderCard, MoonshotProviderCard, BaichuanProviderCard, @@ -102,7 +101,6 @@ export const useProviderList = (): ProviderItem[] => { GithubProvider, WenxinProvider, HuggingFaceProvider, - SenseNovaProvider, ], ); }; diff --git a/src/config/llm.ts b/src/config/llm.ts index cd04a3af0ae04..89b0ac242638c 100644 --- a/src/config/llm.ts +++ b/src/config/llm.ts @@ -113,8 +113,7 @@ export const getLLMConfig = () => { HUGGINGFACE_API_KEY: z.string().optional(), ENABLED_SENSENOVA: z.boolean(), - SENSENOVA_ACCESS_KEY_ID: z.string().optional(), - SENSENOVA_ACCESS_KEY_SECRET: z.string().optional(), + SENSENOVA_API_KEY: z.string().optional(), ENABLED_XAI: z.boolean(), XAI_API_KEY: z.string().optional(), @@ -234,10 +233,8 @@ export const getLLMConfig = () => { ENABLED_HUGGINGFACE: !!process.env.HUGGINGFACE_API_KEY, HUGGINGFACE_API_KEY: process.env.HUGGINGFACE_API_KEY, - ENABLED_SENSENOVA: - !!process.env.SENSENOVA_ACCESS_KEY_ID && !!process.env.SENSENOVA_ACCESS_KEY_SECRET, - SENSENOVA_ACCESS_KEY_ID: process.env.SENSENOVA_ACCESS_KEY_ID, - SENSENOVA_ACCESS_KEY_SECRET: process.env.SENSENOVA_ACCESS_KEY_SECRET, + ENABLED_SENSENOVA: !!process.env.SENSENOVA_API_KEY, + SENSENOVA_API_KEY: process.env.SENSENOVA_API_KEY, ENABLED_XAI: !!process.env.XAI_API_KEY, XAI_API_KEY: process.env.XAI_API_KEY, diff --git a/src/const/auth.ts b/src/const/auth.ts index fe3626aef2886..0858275a4b71a 100644 --- a/src/const/auth.ts +++ b/src/const/auth.ts @@ -42,9 +42,6 @@ export interface JWTPayload { wenxinAccessKey?: string; wenxinSecretKey?: string; - sensenovaAccessKeyID?: string; - sensenovaAccessKeySecret?: string; - /** * user id * in client db mode it's a uuid diff --git a/src/features/Conversation/Error/APIKeyForm/SenseNova.tsx b/src/features/Conversation/Error/APIKeyForm/SenseNova.tsx deleted file mode 100644 index dbf970b1c6d69..0000000000000 --- a/src/features/Conversation/Error/APIKeyForm/SenseNova.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { SenseNova } from '@lobehub/icons'; -import { Input } from 'antd'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { ModelProvider } from '@/libs/agent-runtime'; -import { useUserStore } from '@/store/user'; -import { keyVaultsConfigSelectors } from '@/store/user/selectors'; - -import { FormAction } from '../style'; - -const SenseNovaForm = memo(() => { - const { t } = useTranslation('modelProvider'); - - const [sensenovaAccessKeyID, sensenovaAccessKeySecret, setConfig] = useUserStore((s) => [ - keyVaultsConfigSelectors.sensenovaConfig(s).sensenovaAccessKeyID, - keyVaultsConfigSelectors.sensenovaConfig(s).sensenovaAccessKeySecret, - s.updateKeyVaultConfig, - ]); - - return ( - } - description={t('sensenova.unlock.description')} - title={t('sensenova.unlock.title')} - > - { - setConfig(ModelProvider.SenseNova, { sensenovaAccessKeyID: e.target.value }); - }} - placeholder={t('sensenova.sensenovaAccessKeyID.placeholder')} - type={'block'} - value={sensenovaAccessKeyID} - /> - { - setConfig(ModelProvider.SenseNova, { sensenovaAccessKeySecret: e.target.value }); - }} - placeholder={t('sensenova.sensenovaAccessKeySecret.placeholder')} - type={'block'} - value={sensenovaAccessKeySecret} - /> - - ); -}); - -export default SenseNovaForm; diff --git a/src/features/Conversation/Error/APIKeyForm/index.tsx b/src/features/Conversation/Error/APIKeyForm/index.tsx index 7b53b69d89458..5ba78f4f0ba34 100644 --- a/src/features/Conversation/Error/APIKeyForm/index.tsx +++ b/src/features/Conversation/Error/APIKeyForm/index.tsx @@ -10,7 +10,6 @@ import { GlobalLLMProviderKey } from '@/types/user/settings'; import BedrockForm from './Bedrock'; import ProviderApiKeyForm from './ProviderApiKeyForm'; -import SenseNovaForm from './SenseNova'; import WenxinForm from './Wenxin'; interface APIKeyFormProps { @@ -67,8 +66,6 @@ const APIKeyForm = memo(({ id, provider }) => {
{provider === ModelProvider.Bedrock ? ( - ) : provider === ModelProvider.SenseNova ? ( - ) : provider === ModelProvider.Wenxin ? ( ) : ( diff --git a/src/libs/agent-runtime/AgentRuntime.ts b/src/libs/agent-runtime/AgentRuntime.ts index ebdf75bfaa05c..c70c5d4c153e8 100644 --- a/src/libs/agent-runtime/AgentRuntime.ts +++ b/src/libs/agent-runtime/AgentRuntime.ts @@ -333,7 +333,7 @@ class AgentRuntime { } case ModelProvider.SenseNova: { - runtimeModel = await LobeSenseNovaAI.fromAPIKey(params.sensenova); + runtimeModel = new LobeSenseNovaAI(params.sensenova); break; } diff --git a/src/libs/agent-runtime/index.ts b/src/libs/agent-runtime/index.ts index 5776b9451e2c9..308cd40ca4529 100644 --- a/src/libs/agent-runtime/index.ts +++ b/src/libs/agent-runtime/index.ts @@ -15,7 +15,6 @@ export { LobeOpenAI } from './openai'; export { LobeOpenRouterAI } from './openrouter'; export { LobePerplexityAI } from './perplexity'; export { LobeQwenAI } from './qwen'; -export { LobeSenseNovaAI } from './sensenova'; export { LobeTogetherAI } from './togetherai'; export * from './types'; export { AgentRuntimeError } from './utils/createError'; diff --git a/src/libs/agent-runtime/sensenova/authToken.test.ts b/src/libs/agent-runtime/sensenova/authToken.test.ts deleted file mode 100644 index 1539d5017b652..0000000000000 --- a/src/libs/agent-runtime/sensenova/authToken.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -// @vitest-environment node -import { generateApiToken } from './authToken'; - -describe('generateApiToken', () => { - it('should throw an error if no apiKey is provided', async () => { - await expect(generateApiToken()).rejects.toThrow('Invalid apiKey'); - }); - - it('should throw an error if apiKey is invalid', async () => { - await expect(generateApiToken('invalid')).rejects.toThrow('Invalid apiKey'); - }); - - it('should return a token if a valid apiKey is provided', async () => { - const apiKey = 'id:secret'; - const token = await generateApiToken(apiKey); - expect(token).toBeDefined(); - }); -}); diff --git a/src/libs/agent-runtime/sensenova/authToken.ts b/src/libs/agent-runtime/sensenova/authToken.ts deleted file mode 100644 index 74bb32d9e7e77..0000000000000 --- a/src/libs/agent-runtime/sensenova/authToken.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { SignJWT } from 'jose'; - -// https://console.sensecore.cn/help/docs/model-as-a-service/nova/overview/Authorization -export const generateApiToken = async (apiKey?: string): Promise => { - if (!apiKey) { - throw new Error('Invalid apiKey'); - } - - const [id, secret] = apiKey.split(':'); - if (!id || !secret) { - throw new Error('Invalid apiKey'); - } - - const currentTime = Math.floor(Date.now() / 1000); - - const payload = { - exp: currentTime + 1800, - iss: id, - nbf: currentTime - 5, - }; - - const jwt = await new SignJWT(payload) - .setProtectedHeader({ alg: 'HS256', typ: 'JWT' }) - .sign(new TextEncoder().encode(secret)); - - return jwt; -}; diff --git a/src/libs/agent-runtime/sensenova/index.test.ts b/src/libs/agent-runtime/sensenova/index.test.ts index 08760c369373b..3a147a40d2093 100644 --- a/src/libs/agent-runtime/sensenova/index.test.ts +++ b/src/libs/agent-runtime/sensenova/index.test.ts @@ -1,142 +1,49 @@ // @vitest-environment node -import { OpenAI } from 'openai'; +import OpenAI from 'openai'; import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { ChatStreamCallbacks, LobeOpenAI } from '@/libs/agent-runtime'; -import * as debugStreamModule from '@/libs/agent-runtime/utils/debugStream'; +import { LobeOpenAICompatibleRuntime } from '@/libs/agent-runtime'; +import { ModelProvider } from '@/libs/agent-runtime'; +import { AgentRuntimeErrorType } from '@/libs/agent-runtime'; -import * as authTokenModule from './authToken'; +import * as debugStreamModule from '../utils/debugStream'; import { LobeSenseNovaAI } from './index'; -const bizErrorType = 'ProviderBizError'; -const invalidErrorType = 'InvalidProviderAPIKey'; +const provider = ModelProvider.SenseNova; +const defaultBaseURL = 'https://api.sensenova.cn/compatible-mode/v1'; +const bizErrorType = AgentRuntimeErrorType.ProviderBizError; +const invalidErrorType = AgentRuntimeErrorType.InvalidProviderAPIKey; -// Mock相关依赖 -vi.mock('./authToken'); +// Mock the console.error to avoid polluting test output +vi.spyOn(console, 'error').mockImplementation(() => {}); -describe('LobeSenseNovaAI', () => { - beforeEach(() => { - // Mock generateApiToken - vi.spyOn(authTokenModule, 'generateApiToken').mockResolvedValue('mocked_token'); - }); +let instance: LobeOpenAICompatibleRuntime; - afterEach(() => { - vi.restoreAllMocks(); - }); +beforeEach(() => { + instance = new LobeSenseNovaAI({ apiKey: 'test' }); - describe('fromAPIKey', () => { - it('should correctly initialize with an API key', async () => { - const lobeSenseNovaAI = await LobeSenseNovaAI.fromAPIKey({ apiKey: 'test_api_key' }); - expect(lobeSenseNovaAI).toBeInstanceOf(LobeSenseNovaAI); - expect(lobeSenseNovaAI.baseURL).toEqual('https://api.sensenova.cn/compatible-mode/v1'); - }); + // 使用 vi.spyOn 来模拟 chat.completions.create 方法 + vi.spyOn(instance['client'].chat.completions, 'create').mockResolvedValue( + new ReadableStream() as any, + ); +}); - it('should throw an error if API key is invalid', async () => { - vi.spyOn(authTokenModule, 'generateApiToken').mockRejectedValue(new Error('Invalid API Key')); - try { - await LobeSenseNovaAI.fromAPIKey({ apiKey: 'asd' }); - } catch (e) { - expect(e).toEqual({ errorType: invalidErrorType }); - } +afterEach(() => { + vi.clearAllMocks(); +}); + +describe('LobeSenseNovaAI', () => { + describe('init', () => { + it('should correctly initialize with an API key', async () => { + const instance = new LobeSenseNovaAI({ apiKey: 'test_api_key' }); + expect(instance).toBeInstanceOf(LobeSenseNovaAI); + expect(instance.baseURL).toEqual(defaultBaseURL); }); }); describe('chat', () => { - let instance: LobeSenseNovaAI; - - beforeEach(async () => { - instance = await LobeSenseNovaAI.fromAPIKey({ - apiKey: 'test_api_key', - }); - - // Mock chat.completions.create - vi.spyOn(instance['client'].chat.completions, 'create').mockResolvedValue( - new ReadableStream() as any, - ); - }); - - it('should return a StreamingTextResponse on successful API call', async () => { - const result = await instance.chat({ - messages: [{ content: 'Hello', role: 'user' }], - model: 'SenseChat', - temperature: 0, - }); - expect(result).toBeInstanceOf(Response); - }); - - it('should handle callback and headers correctly', async () => { - // 模拟 chat.completions.create 方法返回一个可读流 - const mockCreateMethod = vi - .spyOn(instance['client'].chat.completions, 'create') - .mockResolvedValue( - new ReadableStream({ - start(controller) { - controller.enqueue({ - id: 'chatcmpl-8xDx5AETP8mESQN7UB30GxTN2H1SO', - object: 'chat.completion.chunk', - created: 1709125675, - model: 'gpt-3.5-turbo-0125', - system_fingerprint: 'fp_86156a94a0', - choices: [ - { index: 0, delta: { content: 'hello' }, logprobs: null, finish_reason: null }, - ], - }); - controller.close(); - }, - }) as any, - ); - - // 准备 callback 和 headers - const mockCallback: ChatStreamCallbacks = { - onStart: vi.fn(), - onToken: vi.fn(), - }; - const mockHeaders = { 'Custom-Header': 'TestValue' }; - - // 执行测试 - const result = await instance.chat( - { - messages: [{ content: 'Hello', role: 'user' }], - model: 'SenseChat', - temperature: 0, - }, - { callback: mockCallback, headers: mockHeaders }, - ); - - // 验证 callback 被调用 - await result.text(); // 确保流被消费 - - // 验证 headers 被正确传递 - expect(result.headers.get('Custom-Header')).toEqual('TestValue'); - - // 清理 - mockCreateMethod.mockRestore(); - }); - - it('should transform messages correctly', async () => { - const spyOn = vi.spyOn(instance['client'].chat.completions, 'create'); - - await instance.chat({ - frequency_penalty: 0, - messages: [ - { content: 'Hello', role: 'user' }, - { content: [{ type: 'text', text: 'Hello again' }], role: 'user' }, - ], - model: 'SenseChat', - temperature: 0, - top_p: 1, - }); - - const calledWithParams = spyOn.mock.calls[0][0]; - - expect(calledWithParams.frequency_penalty).toBeUndefined(); // frequency_penalty 0 should be undefined - expect(calledWithParams.messages[1].content).toEqual([{ type: 'text', text: 'Hello again' }]); - expect(calledWithParams.temperature).toBeUndefined(); // temperature 0 should be undefined - expect(calledWithParams.top_p).toBeUndefined(); // top_p 1 should be undefined - }); - describe('Error', () => { - it('should return SenseNovaAIBizError with an openai error response when OpenAI.APIError is thrown', async () => { + it('should return QwenBizError with an openai error response when OpenAI.APIError is thrown', async () => { // Arrange const apiError = new OpenAI.APIError( 400, @@ -156,31 +63,31 @@ describe('LobeSenseNovaAI', () => { try { await instance.chat({ messages: [{ content: 'Hello', role: 'user' }], - model: 'SenseChat', - temperature: 0, + model: 'max-32k', + temperature: 0.999, }); } catch (e) { expect(e).toEqual({ - endpoint: 'https://api.sensenova.cn/compatible-mode/v1', + endpoint: defaultBaseURL, error: { error: { message: 'Bad Request' }, status: 400, }, errorType: bizErrorType, - provider: 'sensenova', + provider, }); } }); - it('should throw AgentRuntimeError with NoOpenAIAPIKey if no apiKey is provided', async () => { + it('should throw AgentRuntimeError with InvalidQwenAPIKey if no apiKey is provided', async () => { try { - await LobeSenseNovaAI.fromAPIKey({ apiKey: '' }); + new LobeSenseNovaAI({}); } catch (e) { expect(e).toEqual({ errorType: invalidErrorType }); } }); - it('should return OpenAIBizError with the cause when OpenAI.APIError is thrown with cause', async () => { + it('should return QwenBizError with the cause when OpenAI.APIError is thrown with cause', async () => { // Arrange const errorInfo = { stack: 'abc', @@ -196,23 +103,23 @@ describe('LobeSenseNovaAI', () => { try { await instance.chat({ messages: [{ content: 'Hello', role: 'user' }], - model: 'SenseChat', - temperature: 0.2, + model: 'max-32k', + temperature: 0.999, }); } catch (e) { expect(e).toEqual({ - endpoint: 'https://api.sensenova.cn/compatible-mode/v1', + endpoint: defaultBaseURL, error: { cause: { message: 'api is undefined' }, stack: 'abc', }, errorType: bizErrorType, - provider: 'sensenova', + provider, }); } }); - it('should return OpenAIBizError with an cause response with desensitize Url', async () => { + it('should return QwenBizError with an cause response with desensitize Url', async () => { // Arrange const errorInfo = { stack: 'abc', @@ -220,10 +127,10 @@ describe('LobeSenseNovaAI', () => { }; const apiError = new OpenAI.APIError(400, errorInfo, 'module error', {}); - instance = await LobeSenseNovaAI.fromAPIKey({ + instance = new LobeSenseNovaAI({ apiKey: 'test', - baseURL: 'https://abc.com/v2', + baseURL: 'https://api.abc.com/v1', }); vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(apiError); @@ -232,18 +139,40 @@ describe('LobeSenseNovaAI', () => { try { await instance.chat({ messages: [{ content: 'Hello', role: 'user' }], - model: 'gpt-3.5-turbo', - temperature: 0, + model: 'max-32k', + temperature: 0.999, }); } catch (e) { expect(e).toEqual({ - endpoint: 'https://***.com/v2', + endpoint: 'https://api.***.com/v1', error: { cause: { message: 'api is undefined' }, stack: 'abc', }, errorType: bizErrorType, - provider: 'sensenova', + provider, + }); + } + }); + + it('should throw an InvalidQwenAPIKey error type on 401 status code', async () => { + // Mock the API call to simulate a 401 error + const error = new Error('InvalidApiKey') as any; + error.status = 401; + vi.mocked(instance['client'].chat.completions.create).mockRejectedValue(error); + + try { + await instance.chat({ + messages: [{ content: 'Hello', role: 'user' }], + model: 'max-32k', + temperature: 0.999, + }); + } catch (e) { + expect(e).toEqual({ + endpoint: defaultBaseURL, + error: new Error('InvalidApiKey'), + errorType: invalidErrorType, + provider, }); } }); @@ -258,14 +187,14 @@ describe('LobeSenseNovaAI', () => { try { await instance.chat({ messages: [{ content: 'Hello', role: 'user' }], - model: 'SenseChat', - temperature: 0, + model: 'max-32k', + temperature: 0.999, }); } catch (e) { expect(e).toEqual({ - endpoint: 'https://api.sensenova.cn/compatible-mode/v1', + endpoint: defaultBaseURL, errorType: 'AgentRuntimeError', - provider: 'sensenova', + provider, error: { name: genericError.name, cause: genericError.cause, @@ -278,7 +207,7 @@ describe('LobeSenseNovaAI', () => { }); describe('DEBUG', () => { - it('should call debugStream and return StreamingTextResponse when DEBUG_OPENAI_CHAT_COMPLETION is 1', async () => { + it('should call debugStream and return StreamingTextResponse when DEBUG_SENSENOVA_CHAT_COMPLETION is 1', async () => { // Arrange const mockProdStream = new ReadableStream() as any; // 模拟的 prod 流 const mockDebugStream = new ReadableStream({ @@ -306,8 +235,9 @@ describe('LobeSenseNovaAI', () => { // 假设的测试函数调用,你可能需要根据实际情况调整 await instance.chat({ messages: [{ content: 'Hello', role: 'user' }], - model: 'SenseChat', - temperature: 0, + model: 'max-32k', + stream: true, + temperature: 0.999, }); // 验证 debugStream 被调用 diff --git a/src/libs/agent-runtime/sensenova/index.ts b/src/libs/agent-runtime/sensenova/index.ts index 59e1f592b7727..e95a22f05d5f0 100644 --- a/src/libs/agent-runtime/sensenova/index.ts +++ b/src/libs/agent-runtime/sensenova/index.ts @@ -1,98 +1,23 @@ -import OpenAI, { ClientOptions } from 'openai'; - -import { LobeRuntimeAI } from '../BaseAI'; -import { AgentRuntimeErrorType } from '../error'; -import { ChatCompetitionOptions, ChatStreamPayload, ModelProvider } from '../types'; -import { AgentRuntimeError } from '../utils/createError'; -import { debugStream } from '../utils/debugStream'; -import { desensitizeUrl } from '../utils/desensitizeUrl'; -import { handleOpenAIError } from '../utils/handleOpenAIError'; -import { convertOpenAIMessages } from '../utils/openaiHelpers'; -import { StreamingResponse } from '../utils/response'; -import { OpenAIStream } from '../utils/streams'; -import { generateApiToken } from './authToken'; - -const DEFAULT_BASE_URL = 'https://api.sensenova.cn/compatible-mode/v1'; - -export class LobeSenseNovaAI implements LobeRuntimeAI { - private client: OpenAI; - - baseURL: string; - - constructor(oai: OpenAI) { - this.client = oai; - this.baseURL = this.client.baseURL; - } - - static async fromAPIKey({ apiKey, baseURL = DEFAULT_BASE_URL, ...res }: ClientOptions = {}) { - const invalidSenseNovaAPIKey = AgentRuntimeError.createError( - AgentRuntimeErrorType.InvalidProviderAPIKey, - ); - - if (!apiKey) throw invalidSenseNovaAPIKey; - - let token: string; - - try { - token = await generateApiToken(apiKey); - } catch { - throw invalidSenseNovaAPIKey; - } - - const header = { Authorization: `Bearer ${token}` }; - - const llm = new OpenAI({ apiKey, baseURL, defaultHeaders: header, ...res }); - - return new LobeSenseNovaAI(llm); - } - - async chat(payload: ChatStreamPayload, options?: ChatCompetitionOptions) { - try { - const params = await this.buildCompletionsParams(payload); - - const response = await this.client.chat.completions.create( - params as unknown as OpenAI.ChatCompletionCreateParamsStreaming, - ); - - const [prod, debug] = response.tee(); - - if (process.env.DEBUG_SENSENOVA_CHAT_COMPLETION === '1') { - debugStream(debug.toReadableStream()).catch(console.error); - } - - return StreamingResponse(OpenAIStream(prod), { - headers: options?.headers, - }); - } catch (error) { - const { errorResult, RuntimeError } = handleOpenAIError(error); - - const errorType = RuntimeError || AgentRuntimeErrorType.ProviderBizError; - let desensitizedEndpoint = this.baseURL; - - if (this.baseURL !== DEFAULT_BASE_URL) { - desensitizedEndpoint = desensitizeUrl(this.baseURL); - } - throw AgentRuntimeError.chat({ - endpoint: desensitizedEndpoint, - error: errorResult, - errorType, - provider: ModelProvider.SenseNova, - }); - } - } - - private async buildCompletionsParams(payload: ChatStreamPayload) { - const { frequency_penalty, messages, temperature, top_p, ...params } = payload; - - return { - messages: await convertOpenAIMessages(messages as any), - ...params, - frequency_penalty: (frequency_penalty !== undefined && frequency_penalty > 0 && frequency_penalty <= 2) ? frequency_penalty : undefined, - stream: true, - temperature: (temperature !== undefined && temperature > 0 && temperature <= 2) ? temperature : undefined, - top_p: (top_p !== undefined && top_p > 0 && top_p < 1) ? top_p : undefined, - }; - } -} - -export default LobeSenseNovaAI; +import { ModelProvider } from '../types'; +import { LobeOpenAICompatibleFactory } from '../utils/openaiCompatibleFactory'; + +export const LobeSenseNovaAI = LobeOpenAICompatibleFactory({ + baseURL: 'https://api.sensenova.cn/compatible-mode/v1', + chatCompletion: { + handlePayload: (payload) => { + const { frequency_penalty, temperature, top_p, ...rest } = payload; + + return { + ...rest, + frequency_penalty: (frequency_penalty !== undefined && frequency_penalty > 0 && frequency_penalty <= 2) ? frequency_penalty : undefined, + stream: true, + temperature: (temperature !== undefined && temperature > 0 && temperature <= 2) ? temperature : undefined, + top_p: (top_p !== undefined && top_p > 0 && top_p < 1) ? top_p : undefined, + } as any; + }, + }, + debug: { + chatCompletion: () => process.env.DEBUG_SENSENOVA_CHAT_COMPLETION === '1', + }, + provider: ModelProvider.SenseNova, +}); diff --git a/src/locales/default/modelProvider.ts b/src/locales/default/modelProvider.ts index 73d4eb2359ad0..a2662c1bf47ee 100644 --- a/src/locales/default/modelProvider.ts +++ b/src/locales/default/modelProvider.ts @@ -134,23 +134,6 @@ export default { title: '下载指定的 Ollama 模型', }, }, - sensenova: { - sensenovaAccessKeyID: { - desc: '填入 SenseNova Access Key ID', - placeholder: 'SenseNova Access Key ID', - title: 'Access Key ID', - }, - sensenovaAccessKeySecret: { - desc: '填入 SenseNova Access Key Secret', - placeholder: 'SenseNova Access Key Secret', - title: 'Access Key Secret', - }, - unlock: { - description: - '输入你的 Access Key ID / Access Key Secret 即可开始会话。应用不会记录你的鉴权配置', - title: '使用自定义 SenseNova 鉴权信息', - }, - }, wenxin: { accessKey: { desc: '填入百度千帆平台的 Access Key', diff --git a/src/server/modules/AgentRuntime/index.ts b/src/server/modules/AgentRuntime/index.ts index 353efafd19b38..73f559109ddd5 100644 --- a/src/server/modules/AgentRuntime/index.ts +++ b/src/server/modules/AgentRuntime/index.ts @@ -100,21 +100,6 @@ const getLlmOptionsFromPayload = (provider: string, payload: JWTPayload) => { return { apiKey }; } - - case ModelProvider.SenseNova: { - const { SENSENOVA_ACCESS_KEY_ID, SENSENOVA_ACCESS_KEY_SECRET } = llmConfig; - - const sensenovaAccessKeyID = apiKeyManager.pick( - payload?.sensenovaAccessKeyID || SENSENOVA_ACCESS_KEY_ID, - ); - const sensenovaAccessKeySecret = apiKeyManager.pick( - payload?.sensenovaAccessKeySecret || SENSENOVA_ACCESS_KEY_SECRET, - ); - - const apiKey = sensenovaAccessKeyID + ':' + sensenovaAccessKeySecret; - - return { apiKey }; - } } }; diff --git a/src/services/_auth.ts b/src/services/_auth.ts index 8b73330300d63..b8f63accecb4f 100644 --- a/src/services/_auth.ts +++ b/src/services/_auth.ts @@ -25,20 +25,6 @@ export const getProviderAuthPayload = (provider: string) => { }; } - case ModelProvider.SenseNova: { - const { sensenovaAccessKeyID, sensenovaAccessKeySecret } = keyVaultsConfigSelectors.sensenovaConfig( - useUserStore.getState(), - ); - - const apiKey = (sensenovaAccessKeyID || '') + ':' + (sensenovaAccessKeySecret || '') - - return { - apiKey, - sensenovaAccessKeyID: sensenovaAccessKeyID, - sensenovaAccessKeySecret: sensenovaAccessKeySecret, - }; - } - case ModelProvider.Wenxin: { const { secretKey, accessKey } = keyVaultsConfigSelectors.wenxinConfig( useUserStore.getState(), diff --git a/src/store/user/slices/modelList/selectors/keyVaults.ts b/src/store/user/slices/modelList/selectors/keyVaults.ts index 0ec5188461df9..684c95baa28b1 100644 --- a/src/store/user/slices/modelList/selectors/keyVaults.ts +++ b/src/store/user/slices/modelList/selectors/keyVaults.ts @@ -16,7 +16,6 @@ const openAIConfig = (s: UserStore) => keyVaultsSettings(s).openai || {}; const bedrockConfig = (s: UserStore) => keyVaultsSettings(s).bedrock || {}; const wenxinConfig = (s: UserStore) => keyVaultsSettings(s).wenxin || {}; const ollamaConfig = (s: UserStore) => keyVaultsSettings(s).ollama || {}; -const sensenovaConfig = (s: UserStore) => keyVaultsSettings(s).sensenova || {}; const azureConfig = (s: UserStore) => keyVaultsSettings(s).azure || {}; const cloudflareConfig = (s: UserStore) => keyVaultsSettings(s).cloudflare || {}; const getVaultByProvider = (provider: GlobalLLMProviderKey) => (s: UserStore) => @@ -46,6 +45,5 @@ export const keyVaultsConfigSelectors = { ollamaConfig, openAIConfig, password, - sensenovaConfig, wenxinConfig, }; diff --git a/src/store/user/slices/modelList/selectors/modelConfig.ts b/src/store/user/slices/modelList/selectors/modelConfig.ts index 7f68dcacacb01..1a6d9854e7509 100644 --- a/src/store/user/slices/modelList/selectors/modelConfig.ts +++ b/src/store/user/slices/modelList/selectors/modelConfig.ts @@ -70,7 +70,6 @@ const bedrockConfig = (s: UserStore) => currentLLMSettings(s).bedrock; const ollamaConfig = (s: UserStore) => currentLLMSettings(s).ollama; const azureConfig = (s: UserStore) => currentLLMSettings(s).azure; const cloudflareConfig = (s: UserStore) => currentLLMSettings(s).cloudflare; -const sensenovaConfig = (s: UserStore) => currentLLMSettings(s).sensenova; const isAzureEnabled = (s: UserStore) => currentLLMSettings(s).azure.enabled; @@ -89,5 +88,4 @@ export const modelConfigSelectors = { ollamaConfig, openAIConfig, - sensenovaConfig, }; diff --git a/src/types/user/settings/keyVaults.ts b/src/types/user/settings/keyVaults.ts index 2e17b53865a6a..c7dfc030b2eda 100644 --- a/src/types/user/settings/keyVaults.ts +++ b/src/types/user/settings/keyVaults.ts @@ -21,11 +21,6 @@ export interface CloudflareKeyVault { baseURLOrAccountID?: string; } -export interface SenseNovaKeyVault { - sensenovaAccessKeyID?: string; - sensenovaAccessKeySecret?: string; -} - export interface WenxinKeyVault { accessKey?: string; secretKey?: string; @@ -60,7 +55,7 @@ export interface UserKeyVaults { password?: string; perplexity?: OpenAICompatibleKeyVault; qwen?: OpenAICompatibleKeyVault; - sensenova?: SenseNovaKeyVault; + sensenova?: OpenAICompatibleKeyVault; siliconcloud?: OpenAICompatibleKeyVault; spark?: OpenAICompatibleKeyVault; stepfun?: OpenAICompatibleKeyVault;