@lobehub/chat 1.118.7 → 1.119.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/changelog/v1.json +18 -0
- package/package.json +1 -1
- package/packages/model-bank/src/aiModels/aihubmix.ts +61 -6
- package/packages/model-bank/src/aiModels/azure.ts +63 -2
- package/packages/model-bank/src/aiModels/deepseek.ts +9 -9
- package/packages/model-bank/src/aiModels/modelscope.ts +18 -9
- package/packages/model-bank/src/aiModels/novita.ts +149 -10
- package/packages/model-bank/src/aiModels/openrouter.ts +38 -0
- package/packages/model-bank/src/aiModels/qwen.ts +31 -15
- package/packages/model-bank/src/aiModels/siliconcloud.ts +39 -1
- package/packages/model-bank/src/aiModels/vertexai.ts +0 -4
- package/packages/model-bank/src/aiModels/volcengine.ts +27 -0
- package/packages/model-bank/src/aiModels/xai.ts +1 -2
- package/packages/model-runtime/src/azureOpenai/index.test.ts +147 -0
- package/packages/model-runtime/src/azureOpenai/index.ts +115 -1
- package/packages/model-runtime/src/deepseek/index.ts +2 -25
- package/packages/model-runtime/src/google/index.ts +1 -1
- package/packages/model-runtime/src/utils/modelParse.ts +3 -3
- package/packages/model-runtime/src/utils/streams/google-ai.ts +5 -1
- package/packages/model-runtime/src/volcengine/index.ts +1 -0
- package/src/config/modelProviders/modelscope.ts +1 -0
@@ -3,6 +3,25 @@ import { AIChatModelCard, AIImageModelCard } from '@/types/aiModel';
|
|
3
3
|
// https://help.aliyun.com/zh/model-studio/models?spm=a2c4g.11186623
|
4
4
|
|
5
5
|
const qwenChatModels: AIChatModelCard[] = [
|
6
|
+
{
|
7
|
+
abilities: {
|
8
|
+
functionCall: true,
|
9
|
+
reasoning: true,
|
10
|
+
},
|
11
|
+
contextWindowTokens: 131_072,
|
12
|
+
description: 'DeepSeek-V3.1 模型为混合推理架构模型,同时支持思考模式与非思考模式。',
|
13
|
+
displayName: 'DeepSeek V3.1',
|
14
|
+
id: 'deepseek-v3.1',
|
15
|
+
maxOutput: 65_536,
|
16
|
+
pricing: {
|
17
|
+
currency: 'CNY',
|
18
|
+
units: [
|
19
|
+
{ name: 'textInput', rate: 4, strategy: 'fixed', unit: 'millionTokens' },
|
20
|
+
{ name: 'textOutput', rate: 12, strategy: 'fixed', unit: 'millionTokens' },
|
21
|
+
],
|
22
|
+
},
|
23
|
+
type: 'chat',
|
24
|
+
},
|
6
25
|
{
|
7
26
|
abilities: {
|
8
27
|
search: true,
|
@@ -11,7 +30,6 @@ const qwenChatModels: AIChatModelCard[] = [
|
|
11
30
|
description:
|
12
31
|
'总参数 1T,激活参数 32B。 非思维模型中,在前沿知识、数学和编码方面达到了顶尖水平,更擅长通用 Agent 任务。 针对代理任务进行了精心优化,不仅能回答问题,还能采取行动。 最适用于即兴、通用聊天和代理体验,是一款无需长时间思考的反射级模型。',
|
13
32
|
displayName: 'Kimi K2 Instruct',
|
14
|
-
enabled: true,
|
15
33
|
id: 'Moonshot-Kimi-K2-Instruct',
|
16
34
|
maxOutput: 8192,
|
17
35
|
organization: 'Qwen',
|
@@ -45,7 +63,7 @@ const qwenChatModels: AIChatModelCard[] = [
|
|
45
63
|
pricing: {
|
46
64
|
currency: 'CNY',
|
47
65
|
units: [
|
48
|
-
{ name: 'textInput_cacheRead', rate: 2
|
66
|
+
{ name: 'textInput_cacheRead', rate: 6 * 0.2, strategy: 'fixed', unit: 'millionTokens' }, // tokens 32K ~ 128K
|
49
67
|
{ name: 'textInput', rate: 6, strategy: 'fixed', unit: 'millionTokens' },
|
50
68
|
{ name: 'textOutput', rate: 24, strategy: 'fixed', unit: 'millionTokens' },
|
51
69
|
],
|
@@ -70,7 +88,7 @@ const qwenChatModels: AIChatModelCard[] = [
|
|
70
88
|
pricing: {
|
71
89
|
currency: 'CNY',
|
72
90
|
units: [
|
73
|
-
{ name: 'textInput_cacheRead', rate: 0.
|
91
|
+
{ name: 'textInput_cacheRead', rate: 1.5 * 0.2, strategy: 'fixed', unit: 'millionTokens' }, // tokens 32K ~ 128K
|
74
92
|
{ name: 'textInput', rate: 1.5, strategy: 'fixed', unit: 'millionTokens' },
|
75
93
|
{ name: 'textOutput', rate: 6, strategy: 'fixed', unit: 'millionTokens' },
|
76
94
|
],
|
@@ -155,7 +173,6 @@ const qwenChatModels: AIChatModelCard[] = [
|
|
155
173
|
description:
|
156
174
|
'基于Qwen3的思考模式开源模型,相较上一版本(通义千问3-30B-A3B)逻辑能力、通用能力、知识增强及创作能力均有大幅提升,适用于高难度强推理场景。',
|
157
175
|
displayName: 'Qwen3 30B A3B Thinking 2507',
|
158
|
-
enabled: true,
|
159
176
|
id: 'qwen3-30b-a3b-thinking-2507',
|
160
177
|
maxOutput: 32_768,
|
161
178
|
organization: 'Qwen',
|
@@ -180,7 +197,6 @@ const qwenChatModels: AIChatModelCard[] = [
|
|
180
197
|
description:
|
181
198
|
'相较上一版本(Qwen3-30B-A3B)中英文和多语言整体通用能力有大幅提升。主观开放类任务专项优化,显著更加符合用户偏好,能够提供更有帮助性的回复。',
|
182
199
|
displayName: 'Qwen3 30B A3B Instruct 2507',
|
183
|
-
enabled: true,
|
184
200
|
id: 'qwen3-30b-a3b-instruct-2507',
|
185
201
|
maxOutput: 32_768,
|
186
202
|
organization: 'Qwen',
|
@@ -466,9 +482,9 @@ const qwenChatModels: AIChatModelCard[] = [
|
|
466
482
|
name: 'textInput_cacheRead',
|
467
483
|
strategy: 'tiered',
|
468
484
|
tiers: [
|
469
|
-
{ rate: 0.15 * 0.
|
470
|
-
{ rate: 0.6 * 0.
|
471
|
-
{ rate: 1.2 * 0.
|
485
|
+
{ rate: 0.15 * 0.2, upTo: 0.128 },
|
486
|
+
{ rate: 0.6 * 0.2, upTo: 0.256 },
|
487
|
+
{ rate: 1.2 * 0.2, upTo: 'infinity' },
|
472
488
|
],
|
473
489
|
unit: 'millionTokens',
|
474
490
|
},
|
@@ -500,7 +516,7 @@ const qwenChatModels: AIChatModelCard[] = [
|
|
500
516
|
pricing: {
|
501
517
|
currency: 'CNY',
|
502
518
|
units: [
|
503
|
-
{ name: 'textInput_cacheRead', rate: 0.
|
519
|
+
{ name: 'textInput_cacheRead', rate: 0.3 * 0.2, strategy: 'fixed', unit: 'millionTokens' },
|
504
520
|
{ name: 'textInput', rate: 0.3, strategy: 'fixed', unit: 'millionTokens' },
|
505
521
|
{ name: 'textOutput', rate: 3, strategy: 'fixed', unit: 'millionTokens' },
|
506
522
|
],
|
@@ -534,9 +550,9 @@ const qwenChatModels: AIChatModelCard[] = [
|
|
534
550
|
{
|
535
551
|
lookup: {
|
536
552
|
prices: {
|
537
|
-
'[0, 128_000]': 0.8 * 0.
|
538
|
-
'[128_000, 256_000]': 2.4 * 0.
|
539
|
-
'[256_000, infinity]': 4.8 * 0.
|
553
|
+
'[0, 128_000]': 0.8 * 0.2,
|
554
|
+
'[128_000, 256_000]': 2.4 * 0.2,
|
555
|
+
'[256_000, infinity]': 4.8 * 0.2,
|
540
556
|
},
|
541
557
|
pricingParams: ['textInputRange'],
|
542
558
|
},
|
@@ -602,7 +618,7 @@ const qwenChatModels: AIChatModelCard[] = [
|
|
602
618
|
pricing: {
|
603
619
|
currency: 'CNY',
|
604
620
|
units: [
|
605
|
-
{ name: 'textInput_cacheRead', rate: 0.
|
621
|
+
{ name: 'textInput_cacheRead', rate: 2.4 * 0.2, strategy: 'fixed', unit: 'millionTokens' },
|
606
622
|
{ name: 'textInput', rate: 2.4, strategy: 'fixed', unit: 'millionTokens' },
|
607
623
|
{ name: 'textOutput', rate: 9.6, strategy: 'fixed', unit: 'millionTokens' },
|
608
624
|
],
|
@@ -695,7 +711,7 @@ const qwenChatModels: AIChatModelCard[] = [
|
|
695
711
|
pricing: {
|
696
712
|
currency: 'CNY',
|
697
713
|
units: [
|
698
|
-
{ name: 'textInput_cacheRead', rate: 0.8 * 0.
|
714
|
+
{ name: 'textInput_cacheRead', rate: 0.8 * 0.2, strategy: 'fixed', unit: 'millionTokens' },
|
699
715
|
{ name: 'textInput', rate: 0.8, strategy: 'fixed', unit: 'millionTokens' },
|
700
716
|
{ name: 'textOutput', rate: 2, strategy: 'fixed', unit: 'millionTokens' },
|
701
717
|
],
|
@@ -719,7 +735,7 @@ const qwenChatModels: AIChatModelCard[] = [
|
|
719
735
|
pricing: {
|
720
736
|
currency: 'CNY',
|
721
737
|
units: [
|
722
|
-
{ name: 'textInput_cacheRead', rate: 1.6 * 0.
|
738
|
+
{ name: 'textInput_cacheRead', rate: 1.6 * 0.2, strategy: 'fixed', unit: 'millionTokens' },
|
723
739
|
{ name: 'textInput', rate: 1.6, strategy: 'fixed', unit: 'millionTokens' },
|
724
740
|
{ name: 'textOutput', rate: 4, strategy: 'fixed', unit: 'millionTokens' },
|
725
741
|
],
|
@@ -2,6 +2,45 @@ import { AIChatModelCard, AIImageModelCard } from '@/types/aiModel';
|
|
2
2
|
|
3
3
|
// https://siliconflow.cn/zh-cn/models
|
4
4
|
const siliconcloudChatModels: AIChatModelCard[] = [
|
5
|
+
{
|
6
|
+
abilities: {
|
7
|
+
functionCall: true,
|
8
|
+
reasoning: true,
|
9
|
+
},
|
10
|
+
contextWindowTokens: 163_840,
|
11
|
+
description:
|
12
|
+
'DeepSeek-V3.1 是由深度求索(DeepSeek AI)发布的混合模式大语言模型,它在前代模型的基础上进行了多方面的重要升级。该模型的一大创新是集成了“思考模式”(Thinking Mode)和“非思考模式”(Non-thinking Mode)于一体,用户可以通过调整聊天模板灵活切换,以适应不同的任务需求。通过专门的训练后优化,V3.1 在工具调用和 Agent 任务方面的性能得到了显著增强,能够更好地支持外部搜索工具和执行多步复杂任务。该模型基于 DeepSeek-V3.1-Base 进行后训练,通过两阶段长文本扩展方法,大幅增加了训练数据量,使其在处理长文档和长篇代码方面表现更佳。作为一个开源模型,DeepSeek-V3.1 在编码、数学和推理等多个基准测试中展现了与顶尖闭源模型相媲美的能力,同时凭借其混合专家(MoE)架构,在保持巨大模型容量的同时,有效降低了推理成本。',
|
13
|
+
displayName: 'DeepSeek V3.1',
|
14
|
+
enabled: true,
|
15
|
+
id: 'deepseek-ai/DeepSeek-V3.1',
|
16
|
+
pricing: {
|
17
|
+
currency: 'CNY',
|
18
|
+
units: [
|
19
|
+
{ name: 'textInput', rate: 4, strategy: 'fixed', unit: 'millionTokens' },
|
20
|
+
{ name: 'textOutput', rate: 12, strategy: 'fixed', unit: 'millionTokens' },
|
21
|
+
],
|
22
|
+
},
|
23
|
+
type: 'chat',
|
24
|
+
},
|
25
|
+
{
|
26
|
+
abilities: {
|
27
|
+
functionCall: true,
|
28
|
+
reasoning: true,
|
29
|
+
},
|
30
|
+
contextWindowTokens: 163_840,
|
31
|
+
description:
|
32
|
+
'DeepSeek-V3.1 是由深度求索(DeepSeek AI)发布的混合模式大语言模型,它在前代模型的基础上进行了多方面的重要升级。该模型的一大创新是集成了“思考模式”(Thinking Mode)和“非思考模式”(Non-thinking Mode)于一体,用户可以通过调整聊天模板灵活切换,以适应不同的任务需求。通过专门的训练后优化,V3.1 在工具调用和 Agent 任务方面的性能得到了显著增强,能够更好地支持外部搜索工具和执行多步复杂任务。该模型基于 DeepSeek-V3.1-Base 进行后训练,通过两阶段长文本扩展方法,大幅增加了训练数据量,使其在处理长文档和长篇代码方面表现更佳。作为一个开源模型,DeepSeek-V3.1 在编码、数学和推理等多个基准测试中展现了与顶尖闭源模型相媲美的能力,同时凭借其混合专家(MoE)架构,在保持巨大模型容量的同时,有效降低了推理成本。',
|
33
|
+
displayName: 'DeepSeek V3.1 (Pro)',
|
34
|
+
id: 'Pro/deepseek-ai/DeepSeek-V3.1',
|
35
|
+
pricing: {
|
36
|
+
currency: 'CNY',
|
37
|
+
units: [
|
38
|
+
{ name: 'textInput', rate: 4, strategy: 'fixed', unit: 'millionTokens' },
|
39
|
+
{ name: 'textOutput', rate: 12, strategy: 'fixed', unit: 'millionTokens' },
|
40
|
+
],
|
41
|
+
},
|
42
|
+
type: 'chat',
|
43
|
+
},
|
5
44
|
{
|
6
45
|
abilities: {
|
7
46
|
functionCall: true,
|
@@ -452,7 +491,6 @@ const siliconcloudChatModels: AIChatModelCard[] = [
|
|
452
491
|
description:
|
453
492
|
'Qwen3是一款能力大幅提升的新一代通义千问大模型,在推理、通用、Agent和多语言等多个核心能力上均达到业界领先水平,并支持思考模式切换。',
|
454
493
|
displayName: 'Qwen3 8B (Free)',
|
455
|
-
enabled: true,
|
456
494
|
id: 'Qwen/Qwen3-8B',
|
457
495
|
organization: 'Qwen',
|
458
496
|
pricing: {
|
@@ -40,7 +40,6 @@ const vertexaiChatModels: AIChatModelCard[] = [
|
|
40
40
|
description:
|
41
41
|
'Gemini 2.5 Pro Preview 是 Google 最先进的思维模型,能够对代码、数学和STEM领域的复杂问题进行推理,以及使用长上下文分析大型数据集、代码库和文档。',
|
42
42
|
displayName: 'Gemini 2.5 Pro Preview 05-06',
|
43
|
-
enabled: true,
|
44
43
|
id: 'gemini-2.5-pro-preview-05-06',
|
45
44
|
maxOutput: 65_536,
|
46
45
|
pricing: {
|
@@ -109,7 +108,6 @@ const vertexaiChatModels: AIChatModelCard[] = [
|
|
109
108
|
contextWindowTokens: 1_048_576 + 65_536,
|
110
109
|
description: 'Gemini 2.5 Flash Preview 是 Google 性价比最高的模型,提供全面的功能。',
|
111
110
|
displayName: 'Gemini 2.5 Flash Preview 04-17',
|
112
|
-
enabled: true,
|
113
111
|
id: 'gemini-2.5-flash-preview-04-17',
|
114
112
|
maxOutput: 65_536,
|
115
113
|
pricing: {
|
@@ -153,7 +151,6 @@ const vertexaiChatModels: AIChatModelCard[] = [
|
|
153
151
|
contextWindowTokens: 1_000_000 + 64_000,
|
154
152
|
description: 'Gemini 2.5 Flash-Lite 是 Google 最小、性价比最高的模型,专为大规模使用而设计。',
|
155
153
|
displayName: 'Gemini 2.5 Flash-Lite',
|
156
|
-
enabled: true,
|
157
154
|
id: 'gemini-2.5-flash-lite',
|
158
155
|
maxOutput: 64_000,
|
159
156
|
pricing: {
|
@@ -180,7 +177,6 @@ const vertexaiChatModels: AIChatModelCard[] = [
|
|
180
177
|
description:
|
181
178
|
'Gemini 2.5 Flash-Lite Preview 是 Google 最小、性价比最高的模型,专为大规模使用而设计。',
|
182
179
|
displayName: 'Gemini 2.5 Flash-Lite Preview 06-17',
|
183
|
-
enabled: true,
|
184
180
|
id: 'gemini-2.5-flash-lite-preview-06-17',
|
185
181
|
maxOutput: 64_000,
|
186
182
|
pricing: {
|
@@ -4,6 +4,33 @@ import { AIChatModelCard, AIImageModelCard } from '@/types/aiModel';
|
|
4
4
|
// pricing https://console.volcengine.com/ark/region:ark+cn-beijing/openManagement
|
5
5
|
|
6
6
|
const doubaoChatModels: AIChatModelCard[] = [
|
7
|
+
{
|
8
|
+
abilities: {
|
9
|
+
functionCall: true,
|
10
|
+
reasoning: true,
|
11
|
+
},
|
12
|
+
config: {
|
13
|
+
deploymentName: 'deepseek-v3-1-250821',
|
14
|
+
},
|
15
|
+
contextWindowTokens: 131_072,
|
16
|
+
description:
|
17
|
+
'DeepSeek-V3.1 是深度求索全新推出的混合推理模型,支持思考与非思考2种推理模式,较 DeepSeek-R1-0528 思考效率更高。经 Post-Training 优化,Agent 工具使用与智能体任务表现大幅提升。支持 128k 上下文窗口,输出长度支持最大 64k tokens。',
|
18
|
+
displayName: 'DeepSeek V3.1',
|
19
|
+
id: 'deepseek-v3.1',
|
20
|
+
maxOutput: 32_768,
|
21
|
+
pricing: {
|
22
|
+
currency: 'CNY',
|
23
|
+
units: [
|
24
|
+
{ name: 'textInput_cacheRead', rate: 0.8, strategy: 'fixed', unit: 'millionTokens' },
|
25
|
+
{ name: 'textInput', rate: 4, strategy: 'fixed', unit: 'millionTokens' },
|
26
|
+
{ name: 'textOutput', rate: 12, strategy: 'fixed', unit: 'millionTokens' },
|
27
|
+
],
|
28
|
+
},
|
29
|
+
settings: {
|
30
|
+
extendParams: ['enableReasoning'],
|
31
|
+
},
|
32
|
+
type: 'chat',
|
33
|
+
},
|
7
34
|
{
|
8
35
|
abilities: {
|
9
36
|
functionCall: true,
|
@@ -11,7 +11,6 @@ const xaiChatModels: AIChatModelCard[] = [
|
|
11
11
|
description:
|
12
12
|
'我们很高兴推出 grok-code-fast-1,这是一款快速且经济高效的推理模型,在代理编码方面表现出色。',
|
13
13
|
displayName: 'Grok Code Fast 1',
|
14
|
-
enabled: true,
|
15
14
|
id: 'grok-code-fast-1',
|
16
15
|
pricing: {
|
17
16
|
units: [
|
@@ -20,7 +19,7 @@ const xaiChatModels: AIChatModelCard[] = [
|
|
20
19
|
{ name: 'textOutput', rate: 1.5, strategy: 'fixed', unit: 'millionTokens' },
|
21
20
|
],
|
22
21
|
},
|
23
|
-
releasedAt: '2025-08-
|
22
|
+
releasedAt: '2025-08-27',
|
24
23
|
// settings: {
|
25
24
|
// reasoning_effort is not supported by grok-code. Specifying reasoning_effort parameter will get an error response.
|
26
25
|
// extendParams: ['reasoningEffort'],
|
@@ -340,6 +340,153 @@ describe('LobeAzureOpenAI', () => {
|
|
340
340
|
});
|
341
341
|
});
|
342
342
|
|
343
|
+
describe('createImage', () => {
|
344
|
+
beforeEach(() => {
|
345
|
+
// ensure images namespace exists and is spy-able
|
346
|
+
expect(instance['client'].images).toBeTruthy();
|
347
|
+
});
|
348
|
+
|
349
|
+
it('should generate image and return url from object response', async () => {
|
350
|
+
const url = 'https://example.com/image.png';
|
351
|
+
const generateSpy = vi
|
352
|
+
.spyOn(instance['client'].images, 'generate')
|
353
|
+
.mockResolvedValue({ data: [{ url }] } as any);
|
354
|
+
|
355
|
+
const res = await instance.createImage({
|
356
|
+
model: 'gpt-image-1',
|
357
|
+
params: { prompt: 'a cat' },
|
358
|
+
});
|
359
|
+
|
360
|
+
expect(generateSpy).toHaveBeenCalledTimes(1);
|
361
|
+
const args = vi.mocked(generateSpy).mock.calls[0][0] as any;
|
362
|
+
expect(args).not.toHaveProperty('image');
|
363
|
+
expect(res).toEqual({ imageUrl: url });
|
364
|
+
});
|
365
|
+
|
366
|
+
it('should parse string JSON response from images.generate', async () => {
|
367
|
+
const url = 'https://example.com/str.png';
|
368
|
+
const payload = JSON.stringify({ data: [{ url }] });
|
369
|
+
vi.spyOn(instance['client'].images, 'generate').mockResolvedValue(payload as any);
|
370
|
+
|
371
|
+
const res = await instance.createImage({ model: 'gpt-image-1', params: { prompt: 'dog' } });
|
372
|
+
expect(res).toEqual({ imageUrl: url });
|
373
|
+
});
|
374
|
+
|
375
|
+
it('should parse bodyAsText JSON response', async () => {
|
376
|
+
const url = 'https://example.com/bodyAsText.png';
|
377
|
+
const bodyAsText = JSON.stringify({ data: [{ url }] });
|
378
|
+
vi.spyOn(instance['client'].images, 'generate').mockResolvedValue({ bodyAsText } as any);
|
379
|
+
|
380
|
+
const res = await instance.createImage({ model: 'gpt-image-1', params: { prompt: 'bird' } });
|
381
|
+
expect(res).toEqual({ imageUrl: url });
|
382
|
+
});
|
383
|
+
|
384
|
+
it('should parse body JSON response', async () => {
|
385
|
+
const url = 'https://example.com/body.png';
|
386
|
+
const body = JSON.stringify({ data: [{ url }] });
|
387
|
+
vi.spyOn(instance['client'].images, 'generate').mockResolvedValue({ body } as any);
|
388
|
+
|
389
|
+
const res = await instance.createImage({ model: 'gpt-image-1', params: { prompt: 'fish' } });
|
390
|
+
expect(res).toEqual({ imageUrl: url });
|
391
|
+
});
|
392
|
+
|
393
|
+
it('should prefer b64_json and return data URL', async () => {
|
394
|
+
const b64 = 'AAA';
|
395
|
+
vi.spyOn(instance['client'].images, 'generate').mockResolvedValue({
|
396
|
+
data: [{ b64_json: b64 }],
|
397
|
+
} as any);
|
398
|
+
|
399
|
+
const res = await instance.createImage({ model: 'gpt-image-1', params: { prompt: 'sun' } });
|
400
|
+
expect(res.imageUrl).toBe(`data:image/png;base64,${b64}`);
|
401
|
+
});
|
402
|
+
|
403
|
+
it('should throw wrapped error for empty data array', async () => {
|
404
|
+
vi.spyOn(instance['client'].images, 'generate').mockResolvedValue({ data: [] } as any);
|
405
|
+
|
406
|
+
await expect(
|
407
|
+
instance.createImage({ model: 'gpt-image-1', params: { prompt: 'moon' } }),
|
408
|
+
).rejects.toMatchObject({
|
409
|
+
endpoint: 'https://***.openai.azure.com/',
|
410
|
+
errorType: 'AgentRuntimeError',
|
411
|
+
provider: 'azure',
|
412
|
+
error: {
|
413
|
+
name: 'Error',
|
414
|
+
cause: undefined,
|
415
|
+
message: expect.stringContaining('Invalid image response: missing or empty data array'),
|
416
|
+
},
|
417
|
+
});
|
418
|
+
});
|
419
|
+
|
420
|
+
it('should throw wrapped error when missing both b64_json and url', async () => {
|
421
|
+
vi.spyOn(instance['client'].images, 'generate').mockResolvedValue({
|
422
|
+
data: [{}],
|
423
|
+
} as any);
|
424
|
+
|
425
|
+
await expect(
|
426
|
+
instance.createImage({ model: 'gpt-image-1', params: { prompt: 'stars' } }),
|
427
|
+
).rejects.toEqual({
|
428
|
+
endpoint: 'https://***.openai.azure.com/',
|
429
|
+
errorType: 'AgentRuntimeError',
|
430
|
+
provider: 'azure',
|
431
|
+
error: {
|
432
|
+
name: 'Error',
|
433
|
+
cause: undefined,
|
434
|
+
message: 'Invalid image response: missing both b64_json and url fields',
|
435
|
+
},
|
436
|
+
});
|
437
|
+
});
|
438
|
+
|
439
|
+
it('should call images.edit when imageUrl provided and strip size:auto', async () => {
|
440
|
+
const url = 'https://example.com/edited.png';
|
441
|
+
const editSpy = vi
|
442
|
+
.spyOn(instance['client'].images, 'edit')
|
443
|
+
.mockResolvedValue({ data: [{ url }] } as any);
|
444
|
+
|
445
|
+
const helpers = await import('../utils/openaiHelpers');
|
446
|
+
vi.spyOn(helpers, 'convertImageUrlToFile').mockResolvedValue({} as any);
|
447
|
+
|
448
|
+
const res = await instance.createImage({
|
449
|
+
model: 'gpt-image-1',
|
450
|
+
params: { prompt: 'edit', imageUrl: 'https://example.com/in.png', size: 'auto' as any },
|
451
|
+
});
|
452
|
+
|
453
|
+
expect(editSpy).toHaveBeenCalledTimes(1);
|
454
|
+
const arg = vi.mocked(editSpy).mock.calls[0][0] as any;
|
455
|
+
expect(arg).not.toHaveProperty('size');
|
456
|
+
expect(res).toEqual({ imageUrl: url });
|
457
|
+
});
|
458
|
+
|
459
|
+
it('should convert multiple imageUrls and pass images array to edit', async () => {
|
460
|
+
const url = 'https://example.com/edited2.png';
|
461
|
+
const editSpy = vi
|
462
|
+
.spyOn(instance['client'].images, 'edit')
|
463
|
+
.mockResolvedValue({ data: [{ url }] } as any);
|
464
|
+
|
465
|
+
const helpers = await import('../utils/openaiHelpers');
|
466
|
+
const spy = vi.spyOn(helpers, 'convertImageUrlToFile').mockResolvedValue({} as any);
|
467
|
+
|
468
|
+
await instance.createImage({
|
469
|
+
model: 'gpt-image-1',
|
470
|
+
params: { prompt: 'edit', imageUrls: ['u1', 'u2'] },
|
471
|
+
});
|
472
|
+
|
473
|
+
expect(spy).toHaveBeenCalledTimes(2);
|
474
|
+
const arg = vi.mocked(editSpy).mock.calls[0][0] as any;
|
475
|
+
expect(arg).toHaveProperty('image');
|
476
|
+
});
|
477
|
+
|
478
|
+
it('should not include image in generate options', async () => {
|
479
|
+
const generateSpy = vi
|
480
|
+
.spyOn(instance['client'].images, 'generate')
|
481
|
+
.mockResolvedValue({ data: [{ url: 'https://x/y.png' }] } as any);
|
482
|
+
|
483
|
+
await instance.createImage({ model: 'gpt-image-1', params: { prompt: 'no image' } });
|
484
|
+
|
485
|
+
const arg = vi.mocked(generateSpy).mock.calls[0][0] as any;
|
486
|
+
expect(arg).not.toHaveProperty('image');
|
487
|
+
});
|
488
|
+
});
|
489
|
+
|
343
490
|
describe('private method', () => {
|
344
491
|
describe('tocamelCase', () => {
|
345
492
|
it('should convert string to camel case', () => {
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import debug from 'debug';
|
1
2
|
import OpenAI, { AzureOpenAI } from 'openai';
|
2
3
|
import type { Stream } from 'openai/streaming';
|
3
4
|
|
@@ -13,13 +14,15 @@ import {
|
|
13
14
|
EmbeddingsPayload,
|
14
15
|
ModelProvider,
|
15
16
|
} from '../types';
|
17
|
+
import { CreateImagePayload, CreateImageResponse } from '../types/image';
|
16
18
|
import { AgentRuntimeError } from '../utils/createError';
|
17
19
|
import { debugStream } from '../utils/debugStream';
|
18
20
|
import { transformResponseToStream } from '../utils/openaiCompatibleFactory';
|
19
|
-
import { convertOpenAIMessages } from '../utils/openaiHelpers';
|
21
|
+
import { convertImageUrlToFile, convertOpenAIMessages } from '../utils/openaiHelpers';
|
20
22
|
import { StreamingResponse } from '../utils/response';
|
21
23
|
import { OpenAIStream } from '../utils/streams';
|
22
24
|
|
25
|
+
const azureImageLogger = debug('lobe-image:azure');
|
23
26
|
export class LobeAzureOpenAI implements LobeRuntimeAI {
|
24
27
|
client: AzureOpenAI;
|
25
28
|
|
@@ -116,6 +119,117 @@ export class LobeAzureOpenAI implements LobeRuntimeAI {
|
|
116
119
|
}
|
117
120
|
}
|
118
121
|
|
122
|
+
// Create image using Azure OpenAI Images API (gpt-image-1 or DALL·E deployments)
|
123
|
+
async createImage(payload: CreateImagePayload): Promise<CreateImageResponse> {
|
124
|
+
const { model, params } = payload;
|
125
|
+
azureImageLogger('Creating image with model: %s and params: %O', model, params);
|
126
|
+
|
127
|
+
try {
|
128
|
+
// Clone params and remap imageUrls/imageUrl -> image
|
129
|
+
const userInput: Record<string, any> = { ...params };
|
130
|
+
|
131
|
+
// Convert imageUrls to 'image' for edit API
|
132
|
+
if (Array.isArray(userInput.imageUrls) && userInput.imageUrls.length > 0) {
|
133
|
+
const imageFiles = await Promise.all(
|
134
|
+
userInput.imageUrls.map((url: string) => convertImageUrlToFile(url)),
|
135
|
+
);
|
136
|
+
userInput.image = imageFiles.length === 1 ? imageFiles[0] : imageFiles;
|
137
|
+
}
|
138
|
+
|
139
|
+
// Backward compatibility: single imageUrl -> image
|
140
|
+
if (userInput.imageUrl && !userInput.image) {
|
141
|
+
userInput.image = await convertImageUrlToFile(userInput.imageUrl);
|
142
|
+
}
|
143
|
+
|
144
|
+
// Remove non-API parameters to avoid unknown_parameter errors
|
145
|
+
delete userInput.imageUrls;
|
146
|
+
delete userInput.imageUrl;
|
147
|
+
|
148
|
+
const isImageEdit = Boolean(userInput.image);
|
149
|
+
|
150
|
+
azureImageLogger('Is Image Edit: ' + isImageEdit);
|
151
|
+
// Azure/OpenAI Images: remove unsupported/auto values where appropriate
|
152
|
+
if (userInput.size === 'auto') delete userInput.size;
|
153
|
+
|
154
|
+
// Build options: do not force response_format for gpt-image-1
|
155
|
+
const options: any = {
|
156
|
+
model,
|
157
|
+
n: 1,
|
158
|
+
...(isImageEdit ? { input_fidelity: 'high' } : {}),
|
159
|
+
...userInput,
|
160
|
+
};
|
161
|
+
|
162
|
+
// For generate, ensure no 'image' field is sent
|
163
|
+
if (!isImageEdit) delete options.image;
|
164
|
+
|
165
|
+
// Call Azure Images API
|
166
|
+
const img = isImageEdit
|
167
|
+
? await this.client.images.edit(options)
|
168
|
+
: await this.client.images.generate(options);
|
169
|
+
|
170
|
+
// Normalize possible string JSON response -- Sometimes Azure Image API returns a text/plain Content-Type
|
171
|
+
let result: any = img as any;
|
172
|
+
if (typeof result === 'string') {
|
173
|
+
try {
|
174
|
+
result = JSON.parse(result);
|
175
|
+
} catch {
|
176
|
+
const truncated = result.length > 500 ? result.slice(0, 500) + '...[truncated]' : result;
|
177
|
+
azureImageLogger(
|
178
|
+
`Failed to parse string response from images API. Raw response: ${truncated}`,
|
179
|
+
);
|
180
|
+
throw new Error('Invalid image response: expected JSON string but parsing failed');
|
181
|
+
}
|
182
|
+
} else if (result && typeof result === 'object') {
|
183
|
+
// Handle common Azure REST shapes
|
184
|
+
if (typeof (result as any).bodyAsText === 'string') {
|
185
|
+
try {
|
186
|
+
result = JSON.parse((result as any).bodyAsText);
|
187
|
+
} catch {
|
188
|
+
const rawText = (result as any).bodyAsText;
|
189
|
+
const truncated =
|
190
|
+
rawText.length > 500 ? rawText.slice(0, 500) + '...[truncated]' : rawText;
|
191
|
+
azureImageLogger(
|
192
|
+
`Failed to parse bodyAsText from images API. Raw response: ${truncated}`,
|
193
|
+
);
|
194
|
+
throw new Error('Invalid image response: bodyAsText not valid JSON');
|
195
|
+
}
|
196
|
+
} else if (typeof (result as any).body === 'string') {
|
197
|
+
try {
|
198
|
+
result = JSON.parse((result as any).body);
|
199
|
+
} catch {
|
200
|
+
azureImageLogger('Failed to parse body from images API response');
|
201
|
+
throw new Error('Invalid image response: body not valid JSON');
|
202
|
+
}
|
203
|
+
}
|
204
|
+
}
|
205
|
+
|
206
|
+
// Validate response
|
207
|
+
if (!result || !result.data || !Array.isArray(result.data) || result.data.length === 0) {
|
208
|
+
throw new Error(
|
209
|
+
`Invalid image response: missing or empty data array. Response: ${JSON.stringify(result)}`,
|
210
|
+
);
|
211
|
+
}
|
212
|
+
|
213
|
+
const imageData: any = result.data[0];
|
214
|
+
if (!imageData)
|
215
|
+
throw new Error('Invalid image response: first data item is null or undefined');
|
216
|
+
|
217
|
+
// Prefer base64 if provided, otherwise URL
|
218
|
+
if (imageData.b64_json) {
|
219
|
+
const mimeType = 'image/png';
|
220
|
+
return { imageUrl: `data:${mimeType};base64,${imageData.b64_json}` };
|
221
|
+
}
|
222
|
+
|
223
|
+
if (imageData.url) {
|
224
|
+
return { imageUrl: imageData.url };
|
225
|
+
}
|
226
|
+
|
227
|
+
throw new Error('Invalid image response: missing both b64_json and url fields');
|
228
|
+
} catch (e) {
|
229
|
+
return this.handleError(e, model);
|
230
|
+
}
|
231
|
+
}
|
232
|
+
|
119
233
|
protected handleError(e: any, model?: string): never {
|
120
234
|
let error = e as { [key: string]: any; code: string; message: string };
|
121
235
|
|
@@ -1,6 +1,5 @@
|
|
1
|
-
import type { ChatModelCard } from '@/types/llm';
|
2
|
-
|
3
1
|
import { ModelProvider } from '../types';
|
2
|
+
import { MODEL_LIST_CONFIGS, processModelList } from '../utils/modelParse';
|
4
3
|
import { createOpenAICompatibleRuntime } from '../utils/openaiCompatibleFactory';
|
5
4
|
|
6
5
|
export interface DeepSeekModelCard {
|
@@ -18,29 +17,7 @@ export const LobeDeepSeekAI = createOpenAICompatibleRuntime({
|
|
18
17
|
const modelsPage = (await client.models.list()) as any;
|
19
18
|
const modelList: DeepSeekModelCard[] = modelsPage.data;
|
20
19
|
|
21
|
-
return modelList
|
22
|
-
.map((model) => {
|
23
|
-
const knownModel = LOBE_DEFAULT_MODEL_LIST.find(
|
24
|
-
(m) => model.id.toLowerCase() === m.id.toLowerCase(),
|
25
|
-
);
|
26
|
-
|
27
|
-
return {
|
28
|
-
contextWindowTokens: knownModel?.contextWindowTokens ?? undefined,
|
29
|
-
displayName: knownModel?.displayName ?? undefined,
|
30
|
-
enabled: knownModel?.enabled || false,
|
31
|
-
functionCall:
|
32
|
-
!model.id.toLowerCase().includes('reasoner') ||
|
33
|
-
knownModel?.abilities?.functionCall ||
|
34
|
-
false,
|
35
|
-
id: model.id,
|
36
|
-
reasoning:
|
37
|
-
model.id.toLowerCase().includes('reasoner') ||
|
38
|
-
knownModel?.abilities?.reasoning ||
|
39
|
-
false,
|
40
|
-
vision: knownModel?.abilities?.vision || false,
|
41
|
-
};
|
42
|
-
})
|
43
|
-
.filter(Boolean) as ChatModelCard[];
|
20
|
+
return processModelList(modelList, MODEL_LIST_CONFIGS.deepseek, 'deepseek');
|
44
21
|
},
|
45
22
|
provider: ModelProvider.DeepSeek,
|
46
23
|
});
|
@@ -134,7 +134,7 @@ export class LobeGoogleAI implements LobeRuntimeAI {
|
|
134
134
|
const thinkingConfig: ThinkingConfig = {
|
135
135
|
includeThoughts:
|
136
136
|
!!thinkingBudget ||
|
137
|
-
|
137
|
+
(!thinkingBudget && model && (model.includes('-2.5-') || model.includes('thinking')))
|
138
138
|
? true
|
139
139
|
: undefined,
|
140
140
|
// https://ai.google.dev/gemini-api/docs/thinking#set-budget
|
@@ -17,8 +17,8 @@ export const MODEL_LIST_CONFIGS = {
|
|
17
17
|
visionKeywords: ['claude'],
|
18
18
|
},
|
19
19
|
deepseek: {
|
20
|
-
functionCallKeywords: ['v3', 'r1'],
|
21
|
-
reasoningKeywords: ['r1'],
|
20
|
+
functionCallKeywords: ['v3', 'r1', 'deepseek-chat'],
|
21
|
+
reasoningKeywords: ['r1', 'deepseek-reasoner', 'v3.1'],
|
22
22
|
},
|
23
23
|
google: {
|
24
24
|
functionCallKeywords: ['gemini'],
|
@@ -67,7 +67,7 @@ export const MODEL_LIST_CONFIGS = {
|
|
67
67
|
},
|
68
68
|
xai: {
|
69
69
|
functionCallKeywords: ['grok'],
|
70
|
-
reasoningKeywords: ['mini', 'grok-4'],
|
70
|
+
reasoningKeywords: ['mini', 'grok-4', 'grok-code-fast'],
|
71
71
|
visionKeywords: ['vision', 'grok-4'],
|
72
72
|
},
|
73
73
|
zeroone: {
|
@@ -167,9 +167,13 @@ const transformGoogleGenerativeAIStream = (
|
|
167
167
|
if (candidate.finishReason) {
|
168
168
|
const chunks: StreamProtocolChunk[] = [imageChunk];
|
169
169
|
if (chunk.usageMetadata) {
|
170
|
+
// usageChunks already includes the 'stop' chunk as its first entry when usage exists,
|
171
|
+
// so append usageChunks to avoid sending a duplicate 'stop'.
|
170
172
|
chunks.push(...usageChunks);
|
173
|
+
} else {
|
174
|
+
// No usage metadata, we need to send the stop chunk explicitly.
|
175
|
+
chunks.push({ data: candidate.finishReason, id: context?.id, type: 'stop' });
|
171
176
|
}
|
172
|
-
chunks.push({ data: candidate.finishReason, id: context?.id, type: 'stop' });
|
173
177
|
return chunks;
|
174
178
|
}
|
175
179
|
|