@lobehub/chat 1.129.2 → 1.129.3
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 +25 -0
- package/changelog/v1.json +9 -0
- package/package.json +1 -1
- package/packages/model-runtime/src/core/RouterRuntime/baseRuntimeMap.ts +2 -0
- package/packages/model-runtime/src/providers/newapi/index.ts +17 -2
- package/packages/model-runtime/src/providers/qwen/createImage.test.ts +110 -0
- package/packages/model-runtime/src/providers/qwen/createImage.ts +100 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
### [Version 1.129.3](https://github.com/lobehub/lobe-chat/compare/v1.129.2...v1.129.3)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2025-09-17**</sup>
|
|
8
|
+
|
|
9
|
+
#### 🐛 Bug Fixes
|
|
10
|
+
|
|
11
|
+
- **misc**: Add qwen provider support for image-edit model.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### What's fixed
|
|
19
|
+
|
|
20
|
+
- **misc**: Add qwen provider support for image-edit model, closes [#9277](https://github.com/lobehub/lobe-chat/issues/9277) [#9184](https://github.com/lobehub/lobe-chat/issues/9184) ([e137b33](https://github.com/lobehub/lobe-chat/commit/e137b33))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
5
30
|
### [Version 1.129.2](https://github.com/lobehub/lobe-chat/compare/v1.129.1...v1.129.2)
|
|
6
31
|
|
|
7
32
|
<sup>Released on **2025-09-17**</sup>
|
package/changelog/v1.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/chat",
|
|
3
|
-
"version": "1.129.
|
|
3
|
+
"version": "1.129.3",
|
|
4
4
|
"description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"framework",
|
|
@@ -4,6 +4,7 @@ import { LobeCloudflareAI } from '../../providers/cloudflare';
|
|
|
4
4
|
import { LobeFalAI } from '../../providers/fal';
|
|
5
5
|
import { LobeGoogleAI } from '../../providers/google';
|
|
6
6
|
import { LobeOpenAI } from '../../providers/openai';
|
|
7
|
+
import { LobeQwenAI } from '../../providers/qwen';
|
|
7
8
|
import { LobeXAI } from '../../providers/xai';
|
|
8
9
|
|
|
9
10
|
export const baseRuntimeMap = {
|
|
@@ -13,5 +14,6 @@ export const baseRuntimeMap = {
|
|
|
13
14
|
fal: LobeFalAI,
|
|
14
15
|
google: LobeGoogleAI,
|
|
15
16
|
openai: LobeOpenAI,
|
|
17
|
+
qwen: LobeQwenAI,
|
|
16
18
|
xai: LobeXAI,
|
|
17
19
|
};
|
|
@@ -37,11 +37,11 @@ const handlePayload = (payload: ChatStreamPayload) => {
|
|
|
37
37
|
return payload as any;
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
-
// 根据 owned_by
|
|
40
|
+
// 根据 owned_by 字段判断提供商(基于 NewAPI 的 channel name)
|
|
41
41
|
const getProviderFromOwnedBy = (ownedBy: string): string => {
|
|
42
42
|
const normalizedOwnedBy = ownedBy.toLowerCase();
|
|
43
43
|
|
|
44
|
-
if (normalizedOwnedBy.includes('
|
|
44
|
+
if (normalizedOwnedBy.includes('claude') || normalizedOwnedBy.includes('anthropic')) {
|
|
45
45
|
return 'anthropic';
|
|
46
46
|
}
|
|
47
47
|
if (normalizedOwnedBy.includes('google') || normalizedOwnedBy.includes('gemini')) {
|
|
@@ -50,6 +50,9 @@ const getProviderFromOwnedBy = (ownedBy: string): string => {
|
|
|
50
50
|
if (normalizedOwnedBy.includes('xai') || normalizedOwnedBy.includes('grok')) {
|
|
51
51
|
return 'xai';
|
|
52
52
|
}
|
|
53
|
+
if (normalizedOwnedBy.includes('ali') || normalizedOwnedBy.includes('qwen')) {
|
|
54
|
+
return 'qwen';
|
|
55
|
+
}
|
|
53
56
|
|
|
54
57
|
// 默认为 openai
|
|
55
58
|
return 'openai';
|
|
@@ -149,6 +152,8 @@ export const LobeNewAPIAI = createRouterRuntime({
|
|
|
149
152
|
detectedProvider = 'google';
|
|
150
153
|
} else if (model.supported_endpoint_types.includes('xai')) {
|
|
151
154
|
detectedProvider = 'xai';
|
|
155
|
+
} else if (model.supported_endpoint_types.includes('qwen')) {
|
|
156
|
+
detectedProvider = 'qwen';
|
|
152
157
|
}
|
|
153
158
|
}
|
|
154
159
|
// 优先级2:使用 owned_by 字段
|
|
@@ -211,6 +216,16 @@ export const LobeNewAPIAI = createRouterRuntime({
|
|
|
211
216
|
baseURL: urlJoin(userBaseURL, '/v1'),
|
|
212
217
|
},
|
|
213
218
|
},
|
|
219
|
+
{
|
|
220
|
+
apiType: 'qwen',
|
|
221
|
+
models: LOBE_DEFAULT_MODEL_LIST.map((m) => m.id).filter(
|
|
222
|
+
(id) => detectModelProvider(id) === 'qwen',
|
|
223
|
+
),
|
|
224
|
+
options: {
|
|
225
|
+
...options,
|
|
226
|
+
baseURL: urlJoin(userBaseURL, '/v1'),
|
|
227
|
+
},
|
|
228
|
+
},
|
|
214
229
|
{
|
|
215
230
|
apiType: 'openai',
|
|
216
231
|
options: {
|
|
@@ -591,4 +591,114 @@ describe('createQwenImage', () => {
|
|
|
591
591
|
expect(fetch).toHaveBeenCalledTimes(4);
|
|
592
592
|
});
|
|
593
593
|
});
|
|
594
|
+
|
|
595
|
+
describe('qwen-image-edit model', () => {
|
|
596
|
+
it('should successfully generate image with qwen-image-edit model', async () => {
|
|
597
|
+
const mockImageUrl =
|
|
598
|
+
'https://dashscope.oss-cn-beijing.aliyuncs.com/aigc/test-generated-image.jpg';
|
|
599
|
+
|
|
600
|
+
// Mock fetch for multimodal-generation API
|
|
601
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
602
|
+
ok: true,
|
|
603
|
+
json: async () => ({
|
|
604
|
+
output: {
|
|
605
|
+
choices: [
|
|
606
|
+
{
|
|
607
|
+
message: {
|
|
608
|
+
content: [{ image: mockImageUrl }],
|
|
609
|
+
},
|
|
610
|
+
},
|
|
611
|
+
],
|
|
612
|
+
},
|
|
613
|
+
request_id: 'req-edit-123',
|
|
614
|
+
}),
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
const payload: CreateImagePayload = {
|
|
618
|
+
model: 'qwen-image-edit',
|
|
619
|
+
params: {
|
|
620
|
+
prompt: 'Edit this image to add a cat',
|
|
621
|
+
imageUrl: 'https://example.com/source-image.jpg',
|
|
622
|
+
},
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
const result = await createQwenImage(payload, mockOptions);
|
|
626
|
+
|
|
627
|
+
expect(result).toEqual({
|
|
628
|
+
imageUrl: mockImageUrl,
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
expect(fetch).toHaveBeenCalled();
|
|
632
|
+
const [url, options] = (fetch as any).mock.calls[0];
|
|
633
|
+
|
|
634
|
+
expect(url).toBe(
|
|
635
|
+
'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation',
|
|
636
|
+
);
|
|
637
|
+
expect(options.method).toBe('POST');
|
|
638
|
+
expect(options.headers).toEqual({
|
|
639
|
+
'Authorization': 'Bearer test-api-key',
|
|
640
|
+
'Content-Type': 'application/json',
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
const body = JSON.parse(options.body);
|
|
644
|
+
expect(body).toEqual({
|
|
645
|
+
input: {
|
|
646
|
+
messages: [
|
|
647
|
+
{
|
|
648
|
+
content: [
|
|
649
|
+
{ image: 'https://example.com/source-image.jpg' },
|
|
650
|
+
{ text: 'Edit this image to add a cat' },
|
|
651
|
+
],
|
|
652
|
+
role: 'user',
|
|
653
|
+
},
|
|
654
|
+
],
|
|
655
|
+
},
|
|
656
|
+
model: 'qwen-image-edit',
|
|
657
|
+
parameters: {},
|
|
658
|
+
});
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
it('should throw error when imageUrl is missing for qwen-image-edit', async () => {
|
|
662
|
+
const payload: CreateImagePayload = {
|
|
663
|
+
model: 'qwen-image-edit',
|
|
664
|
+
params: {
|
|
665
|
+
prompt: 'Edit this image',
|
|
666
|
+
// imageUrl is missing
|
|
667
|
+
},
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
await expect(createQwenImage(payload, mockOptions)).rejects.toEqual(
|
|
671
|
+
expect.objectContaining({
|
|
672
|
+
errorType: 'ProviderBizError',
|
|
673
|
+
provider: 'qwen',
|
|
674
|
+
}),
|
|
675
|
+
);
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
it('should handle qwen-image-edit API errors', async () => {
|
|
679
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
680
|
+
ok: false,
|
|
681
|
+
status: 400,
|
|
682
|
+
statusText: 'Bad Request',
|
|
683
|
+
json: async () => ({
|
|
684
|
+
message: 'Invalid image format',
|
|
685
|
+
}),
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
const payload: CreateImagePayload = {
|
|
689
|
+
model: 'qwen-image-edit',
|
|
690
|
+
params: {
|
|
691
|
+
prompt: 'Edit this image',
|
|
692
|
+
imageUrl: 'https://example.com/invalid-image.jpg',
|
|
693
|
+
},
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
await expect(createQwenImage(payload, mockOptions)).rejects.toEqual(
|
|
697
|
+
expect.objectContaining({
|
|
698
|
+
errorType: 'ProviderBizError',
|
|
699
|
+
provider: 'qwen',
|
|
700
|
+
}),
|
|
701
|
+
);
|
|
702
|
+
});
|
|
703
|
+
});
|
|
594
704
|
});
|
|
@@ -19,8 +19,22 @@ interface QwenImageTaskResponse {
|
|
|
19
19
|
request_id: string;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
// Interface for qwen-image-edit multimodal-generation response
|
|
23
|
+
interface QwenImageEditResponse {
|
|
24
|
+
output: {
|
|
25
|
+
choices: Array<{
|
|
26
|
+
message: {
|
|
27
|
+
content: Array<{
|
|
28
|
+
image: string;
|
|
29
|
+
}>;
|
|
30
|
+
};
|
|
31
|
+
}>;
|
|
32
|
+
};
|
|
33
|
+
request_id: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
22
36
|
/**
|
|
23
|
-
* Create an image generation task with Qwen API
|
|
37
|
+
* Create an image generation task with Qwen API for text-to-image models
|
|
24
38
|
*/
|
|
25
39
|
async function createImageTask(payload: CreateImagePayload, apiKey: string): Promise<string> {
|
|
26
40
|
const { model, params } = payload;
|
|
@@ -72,6 +86,78 @@ async function createImageTask(payload: CreateImagePayload, apiKey: string): Pro
|
|
|
72
86
|
return data.output.task_id;
|
|
73
87
|
}
|
|
74
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Create image with Qwen image-edit API for image-to-image models
|
|
91
|
+
* This is a synchronous API that returns the result directly
|
|
92
|
+
*/
|
|
93
|
+
async function createImageEdit(
|
|
94
|
+
payload: CreateImagePayload,
|
|
95
|
+
apiKey: string,
|
|
96
|
+
): Promise<CreateImageResponse> {
|
|
97
|
+
const { model, params } = payload;
|
|
98
|
+
const endpoint = `https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation`;
|
|
99
|
+
log('Creating image edit with model: %s, endpoint: %s', model, endpoint);
|
|
100
|
+
|
|
101
|
+
if (!params.imageUrl) {
|
|
102
|
+
throw new Error('imageUrl is required for qwen-image-edit model');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const response = await fetch(endpoint, {
|
|
106
|
+
body: JSON.stringify({
|
|
107
|
+
input: {
|
|
108
|
+
messages: [
|
|
109
|
+
{
|
|
110
|
+
content: [{ image: params.imageUrl }, { text: params.prompt }],
|
|
111
|
+
role: 'user',
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
model,
|
|
116
|
+
parameters: {
|
|
117
|
+
// watermark defaults to false (no watermark) unless explicitly requested
|
|
118
|
+
},
|
|
119
|
+
}),
|
|
120
|
+
headers: {
|
|
121
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
122
|
+
'Content-Type': 'application/json',
|
|
123
|
+
},
|
|
124
|
+
method: 'POST',
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
let errorData;
|
|
129
|
+
try {
|
|
130
|
+
errorData = await response.json();
|
|
131
|
+
} catch {
|
|
132
|
+
// Failed to parse JSON error response
|
|
133
|
+
}
|
|
134
|
+
throw new Error(
|
|
135
|
+
`Failed to create image edit (${response.status}): ${errorData?.message || response.statusText}`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const data: QwenImageEditResponse = await response.json();
|
|
140
|
+
|
|
141
|
+
if (!data.output.choices || data.output.choices.length === 0) {
|
|
142
|
+
throw new Error('No image choices returned from qwen-image-edit API');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const choice = data.output.choices[0];
|
|
146
|
+
if (!choice.message.content || choice.message.content.length === 0) {
|
|
147
|
+
throw new Error('No image content returned from qwen-image-edit API');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const imageContent = choice.message.content.find((content) => 'image' in content);
|
|
151
|
+
if (!imageContent) {
|
|
152
|
+
throw new Error('No image found in response content');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const imageUrl = imageContent.image;
|
|
156
|
+
log('Image edit generated successfully: %s', imageUrl);
|
|
157
|
+
|
|
158
|
+
return { imageUrl };
|
|
159
|
+
}
|
|
160
|
+
|
|
75
161
|
/**
|
|
76
162
|
* Query the status of an image generation task
|
|
77
163
|
*/
|
|
@@ -102,15 +188,26 @@ async function queryTaskStatus(taskId: string, apiKey: string): Promise<QwenImag
|
|
|
102
188
|
}
|
|
103
189
|
|
|
104
190
|
/**
|
|
105
|
-
* Create image using Qwen
|
|
106
|
-
*
|
|
191
|
+
* Create image using Qwen API
|
|
192
|
+
* Supports both text-to-image (async with polling) and image-to-image (sync) workflows
|
|
107
193
|
*/
|
|
108
194
|
export async function createQwenImage(
|
|
109
195
|
payload: CreateImagePayload,
|
|
110
196
|
options: CreateImageOptions,
|
|
111
197
|
): Promise<CreateImageResponse> {
|
|
112
198
|
const { apiKey, provider } = options;
|
|
199
|
+
const { model } = payload;
|
|
200
|
+
|
|
113
201
|
try {
|
|
202
|
+
// Check if this is qwen-image-edit model for image-to-image
|
|
203
|
+
if (model === 'qwen-image-edit') {
|
|
204
|
+
log('Using multimodal-generation API for qwen-image-edit model');
|
|
205
|
+
return await createImageEdit(payload, apiKey);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Default to text-to-image workflow for other qwen models
|
|
209
|
+
log('Using text2image API for model: %s', model);
|
|
210
|
+
|
|
114
211
|
// 1. Create image generation task
|
|
115
212
|
const taskId = await createImageTask(payload, apiKey);
|
|
116
213
|
|