Skip to content

Commit 9c8597f

Browse files
authored
💄 style: add SenseNova-V6 series & SenseChat-Vision support (#7439)
1 parent ced3575 commit 9c8597f

File tree

4 files changed

+275
-9
lines changed

4 files changed

+275
-9
lines changed

src/config/aiModels/sensenova.ts

+120-5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,62 @@ import { AIChatModelCard } from '@/types/aiModel';
44
// https://www.sensecore.cn/help/docs/model-as-a-service/nova/release
55

66
const sensenovaChatModels: AIChatModelCard[] = [
7+
{
8+
abilities: {
9+
reasoning: true,
10+
vision: true,
11+
},
12+
contextWindowTokens: 131_072,
13+
description:
14+
'兼顾视觉、语言深度推理,实现慢思考和深度推理,呈现完整的思维链过程。',
15+
displayName: 'SenseNova V6 Reasoner',
16+
enabled: true,
17+
id: 'SenseNova-V6-Reasoner',
18+
pricing: {
19+
currency: 'CNY',
20+
input: 4,
21+
output: 16,
22+
},
23+
releasedAt: '2025-04-14',
24+
type: 'chat',
25+
},
26+
{
27+
abilities: {
28+
reasoning: true,
29+
vision: true,
30+
},
31+
contextWindowTokens: 131_072,
32+
description:
33+
'实现图片、文本、视频能力的原生统一,突破传统多模态分立局限,在多模基础能力、语言基础能力等核心维度全面领先,文理兼修,在多项测评中多次位列国内外第一梯队水平。',
34+
displayName: 'SenseNova V6 Turbo',
35+
enabled: true,
36+
id: 'SenseNova-V6-Turbo',
37+
pricing: {
38+
currency: 'CNY',
39+
input: 1.5,
40+
output: 4.5,
41+
},
42+
releasedAt: '2025-04-14',
43+
type: 'chat',
44+
},
45+
{
46+
abilities: {
47+
vision: true,
48+
},
49+
contextWindowTokens: 131_072,
50+
description:
51+
'实现图片、文本、视频能力的原生统一,突破传统多模态分立局限,在OpenCompass和SuperCLUE评测中斩获双冠军。',
52+
displayName: 'SenseNova V6 Pro',
53+
enabled: true,
54+
id: 'SenseNova-V6-Pro',
55+
pricing: {
56+
currency: 'CNY',
57+
input: 9,
58+
output: 3,
59+
},
60+
releasedAt: '2025-04-14',
61+
type: 'chat',
62+
},
763
{
864
abilities: {
965
functionCall: true,
@@ -12,7 +68,6 @@ const sensenovaChatModels: AIChatModelCard[] = [
1268
description:
1369
'是基于V5.5的最新版本,较上版本在中英文基础能力,聊天,理科知识, 文科知识,写作,数理逻辑,字数控制 等几个维度的表现有显著提升。',
1470
displayName: 'SenseChat 5.5 1202',
15-
enabled: true,
1671
id: 'SenseChat-5-1202',
1772
pricing: {
1873
currency: 'CNY',
@@ -30,7 +85,6 @@ const sensenovaChatModels: AIChatModelCard[] = [
3085
description:
3186
'是最新的轻量版本模型,达到全量模型90%以上能力,显著降低推理成本。',
3287
displayName: 'SenseChat Turbo 1202',
33-
enabled: true,
3488
id: 'SenseChat-Turbo-1202',
3589
pricing: {
3690
currency: 'CNY',
@@ -48,7 +102,6 @@ const sensenovaChatModels: AIChatModelCard[] = [
48102
description:
49103
'最新版本模型 (V5.5),128K上下文长度,在数学推理、英文对话、指令跟随以及长文本理解等领域能力显著提升,比肩GPT-4o。',
50104
displayName: 'SenseChat 5.5',
51-
enabled: true,
52105
id: 'SenseChat-5',
53106
pricing: {
54107
currency: 'CNY',
@@ -58,10 +111,12 @@ const sensenovaChatModels: AIChatModelCard[] = [
58111
type: 'chat',
59112
},
60113
{
114+
abilities: {
115+
vision: true,
116+
},
61117
contextWindowTokens: 32_768,
62118
description: '最新版本模型 (V5.5),支持多图的输入,全面实现模型基础能力优化,在对象属性识别、空间关系、动作事件识别、场景理解、情感识别、逻辑常识推理和文本理解生成上都实现了较大提升。',
63119
displayName: 'SenseChat 5.5 Vision',
64-
enabled: true,
65120
id: 'SenseChat-Vision',
66121
pricing: {
67122
currency: 'CNY',
@@ -78,7 +133,6 @@ const sensenovaChatModels: AIChatModelCard[] = [
78133
contextWindowTokens: 32_768,
79134
description: '适用于快速问答、模型微调场景',
80135
displayName: 'SenseChat 5.0 Turbo',
81-
enabled: true,
82136
id: 'SenseChat-Turbo',
83137
pricing: {
84138
currency: 'CNY',
@@ -160,6 +214,67 @@ const sensenovaChatModels: AIChatModelCard[] = [
160214
},
161215
type: 'chat',
162216
},
217+
{
218+
contextWindowTokens: 32_768,
219+
description:
220+
'DeepSeek-V3 是一款由深度求索公司自研的MoE模型。DeepSeek-V3 多项评测成绩超越了 Qwen2.5-72B 和 Llama-3.1-405B 等其他开源模型,并在性能上和世界顶尖的闭源模型 GPT-4o 以及 Claude-3.5-Sonnet 不分伯仲。',
221+
displayName: 'DeepSeek V3',
222+
id: 'DeepSeek-V3',
223+
pricing: {
224+
currency: 'CNY',
225+
input: 2,
226+
output: 8,
227+
},
228+
type: 'chat',
229+
},
230+
{
231+
abilities: {
232+
reasoning: true,
233+
},
234+
contextWindowTokens: 32_768,
235+
description:
236+
'DeepSeek-R1 在后训练阶段大规模使用了强化学习技术,在仅有极少标注数据的情况下,极大提升了模型推理能力。在数学、代码、自然语言推理等任务上,性能比肩 OpenAI o1 正式版。',
237+
displayName: 'DeepSeek R1',
238+
id: 'DeepSeek-R1',
239+
pricing: {
240+
currency: 'CNY',
241+
input: 4,
242+
output: 16,
243+
},
244+
type: 'chat',
245+
},
246+
{
247+
abilities: {
248+
reasoning: true,
249+
},
250+
contextWindowTokens: 32_768,
251+
description:
252+
'DeepSeek-R1-Distill 模型是在开源模型的基础上通过微调训练得到的,训练过程中使用了由 DeepSeek-R1 生成的样本数据。',
253+
displayName: 'DeepSeek R1 Distill Qwen 14B',
254+
id: 'DeepSeek-R1-Distill-Qwen-14B',
255+
pricing: {
256+
currency: 'CNY',
257+
input: 0,
258+
output: 0,
259+
},
260+
type: 'chat',
261+
},
262+
{
263+
abilities: {
264+
reasoning: true,
265+
},
266+
contextWindowTokens: 8192,
267+
description:
268+
'DeepSeek-R1-Distill 模型是在开源模型的基础上通过微调训练得到的,训练过程中使用了由 DeepSeek-R1 生成的样本数据。',
269+
displayName: 'DeepSeek R1 Distill Qwen 32B',
270+
id: 'DeepSeek-R1-Distill-Qwen-32B',
271+
pricing: {
272+
currency: 'CNY',
273+
input: 0,
274+
output: 0,
275+
},
276+
type: 'chat',
277+
},
163278
];
164279

165280
export const allModels = [...sensenovaChatModels];

src/libs/agent-runtime/sensenova/index.ts

+17-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { ModelProvider } from '../types';
22
import { LobeOpenAICompatibleFactory } from '../utils/openaiCompatibleFactory';
33

4+
import { convertSenseNovaMessage } from '../utils/sensenovaHelpers';
5+
46
import type { ChatModelCard } from '@/types/llm';
57

68
export interface SenseNovaModelCard {
@@ -11,14 +13,20 @@ export const LobeSenseNovaAI = LobeOpenAICompatibleFactory({
1113
baseURL: 'https://api.sensenova.cn/compatible-mode/v1',
1214
chatCompletion: {
1315
handlePayload: (payload) => {
14-
const { frequency_penalty, temperature, top_p, ...rest } = payload;
16+
const { frequency_penalty, messages, model, temperature, top_p, ...rest } = payload;
1517

1618
return {
1719
...rest,
1820
frequency_penalty:
1921
frequency_penalty !== undefined && frequency_penalty > 0 && frequency_penalty <= 2
2022
? frequency_penalty
2123
: undefined,
24+
messages: messages.map((message) =>
25+
message.role !== 'user' || !/^Sense(Nova-V6|Chat-Vision)/.test(model)
26+
? message
27+
: { ...message, content: convertSenseNovaMessage(message.content) }
28+
) as any[],
29+
model,
2230
stream: true,
2331
temperature:
2432
temperature !== undefined && temperature > 0 && temperature <= 2
@@ -35,12 +43,17 @@ export const LobeSenseNovaAI = LobeOpenAICompatibleFactory({
3543
const { LOBE_DEFAULT_MODEL_LIST } = await import('@/config/aiModels');
3644

3745
const functionCallKeywords = [
38-
'deepseek-v3',
3946
'sensechat-5',
4047
];
4148

49+
const visionKeywords = [
50+
'vision',
51+
'sensenova-v6',
52+
];
53+
4254
const reasoningKeywords = [
43-
'deepseek-r1'
55+
'deepseek-r1',
56+
'sensenova-v6',
4457
];
4558

4659
client.baseURL = 'https://api.sensenova.cn/v1/llm';
@@ -66,7 +79,7 @@ export const LobeSenseNovaAI = LobeOpenAICompatibleFactory({
6679
|| knownModel?.abilities?.reasoning
6780
|| false,
6881
vision:
69-
model.id.toLowerCase().includes('vision')
82+
visionKeywords.some(keyword => model.id.toLowerCase().includes(keyword))
7083
|| knownModel?.abilities?.vision
7184
|| false,
7285
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { convertSenseNovaMessage } from './sensenovaHelpers';
3+
4+
describe('convertSenseNovaMessage', () => {
5+
it('should convert string content to text type array', () => {
6+
const content = 'Hello world';
7+
const result = convertSenseNovaMessage(content);
8+
9+
expect(result).toEqual([{ type: 'text', text: 'Hello world' }]);
10+
});
11+
12+
it('should handle array content with text type', () => {
13+
const content = [
14+
{ type: 'text', text: 'Hello world' }
15+
];
16+
const result = convertSenseNovaMessage(content);
17+
18+
expect(result).toEqual([{ type: 'text', text: 'Hello world' }]);
19+
});
20+
21+
it('should convert image_url with base64 format to image_base64', () => {
22+
const content = [
23+
{ type: 'image_url', image_url: { url: 'data:image/jpeg;base64,ABCDEF123456' } }
24+
];
25+
const result = convertSenseNovaMessage(content);
26+
27+
expect(result).toEqual([
28+
{ type: 'image_base64', image_base64: 'ABCDEF123456' }
29+
]);
30+
});
31+
32+
it('should keep image_url format for non-base64 urls', () => {
33+
const content = [
34+
{ type: 'image_url', image_url: { url: 'https://example.com/image.jpg' } }
35+
];
36+
const result = convertSenseNovaMessage(content);
37+
38+
expect(result).toEqual([
39+
{ type: 'image_url', image_url: 'https://example.com/image.jpg' }
40+
]);
41+
});
42+
43+
it('should handle mixed content types', () => {
44+
const content = [
45+
{ type: 'text', text: 'Hello world' },
46+
{ type: 'image_url', image_url: { url: 'data:image/jpeg;base64,ABCDEF123456' } },
47+
{ type: 'image_url', image_url: { url: 'https://example.com/image.jpg' } }
48+
];
49+
const result = convertSenseNovaMessage(content);
50+
51+
expect(result).toEqual([
52+
{ type: 'text', text: 'Hello world' },
53+
{ type: 'image_base64', image_base64: 'ABCDEF123456' },
54+
{ type: 'image_url', image_url: 'https://example.com/image.jpg' }
55+
]);
56+
});
57+
58+
it('should filter out invalid items', () => {
59+
const content = [
60+
{ type: 'text', text: 'Hello world' },
61+
{ type: 'unknown', value: 'should be filtered' },
62+
{ type: 'image_url', image_url: { notUrl: 'missing url field' } }
63+
];
64+
const result = convertSenseNovaMessage(content);
65+
66+
expect(result).toEqual([
67+
{ type: 'text', text: 'Hello world' }
68+
]);
69+
});
70+
71+
it('should handle the example input format correctly', () => {
72+
const messages = [
73+
{
74+
content: [
75+
{
76+
content: "Hi",
77+
role: "user"
78+
},
79+
{
80+
image_url: {
81+
detail: "auto",
82+
url: "data:image/jpeg;base64,ABCDEF123456"
83+
},
84+
type: "image_url"
85+
}
86+
],
87+
role: "user"
88+
}
89+
];
90+
91+
// This is simulating how you might use convertSenseNovaMessage with the example input
92+
// Note: The actual function only converts the content part, not the entire messages array
93+
const content = messages[0].content;
94+
95+
// This is how the function would be expected to handle a mixed array like this
96+
// However, the actual test would need to be adjusted based on how your function
97+
// is intended to handle this specific format with nested content objects
98+
const result = convertSenseNovaMessage([
99+
{ type: 'text', text: "Hi" },
100+
{ type: 'image_url', image_url: { url: "data:image/jpeg;base64,ABCDEF123456" } }
101+
]);
102+
103+
expect(result).toEqual([
104+
{ type: 'text', text: "Hi" },
105+
{ type: 'image_base64', image_base64: "ABCDEF123456" }
106+
]);
107+
});
108+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
export const convertSenseNovaMessage = (content: any) => {
2+
3+
// 如果为单条 string 类 content,则格式转换为 text 类
4+
if (typeof content === 'string') {
5+
return [{ text: content, type: 'text' }];
6+
}
7+
8+
// 如果内容包含图片内容,则需要对 array 类 content,进行格式转换
9+
return content
10+
?.map((item: any) => {
11+
// 如果为 content,则格式转换为 text 类
12+
if (item.type === 'text') return item;
13+
14+
// 如果为 image_url,则格式转换为 image_url 类
15+
if (item.type === 'image_url' && item.image_url?.url) {
16+
const url = item.image_url.url;
17+
18+
// 如果 image_url 为 base64 格式,则返回 image_base64 类,否则返回 image_url 类
19+
return url.startsWith('data:image/jpeg;base64')
20+
? {
21+
image_base64: url.split(',')[1],
22+
type: 'image_base64',
23+
}
24+
: { image_url: url, type: 'image_url' };
25+
}
26+
27+
return null;
28+
})
29+
.filter(Boolean);
30+
};

0 commit comments

Comments
 (0)