@lobehub/chat 1.128.3 → 1.128.5
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 +2 -2
- package/packages/model-bank/src/aiModels/xai.ts +3 -3
- package/packages/model-runtime/src/core/openaiCompatibleFactory/index.test.ts +24 -4
- package/packages/model-runtime/src/core/streams/google-ai.test.ts +89 -4
- package/packages/model-runtime/src/core/streams/google-ai.ts +13 -1
- package/packages/model-runtime/src/core/streams/openai/openai.test.ts +3 -3
- package/packages/model-runtime/src/core/streams/openai/openai.ts +3 -2
- package/packages/model-runtime/src/core/streams/openai/responsesStream.ts +3 -2
- package/packages/model-runtime/src/core/streams/protocol.test.ts +80 -1
- package/packages/model-runtime/src/core/streams/protocol.ts +25 -2
- package/packages/model-runtime/src/providers/google/index.test.ts +155 -0
- package/packages/model-runtime/src/providers/google/index.ts +65 -11
- package/src/app/[variants]/(main)/discover/(list)/(home)/Client.tsx +1 -1
- package/src/config/modelProviders/qwen.ts +1 -1
- package/src/config/modelProviders/siliconcloud.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,56 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
### [Version 1.128.5](https://github.com/lobehub/lobe-chat/compare/v1.128.4...v1.128.5)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2025-09-13**</sup>
|
|
8
|
+
|
|
9
|
+
#### 🐛 Bug Fixes
|
|
10
|
+
|
|
11
|
+
- **misc**: Google stream error unable to abort request.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### What's fixed
|
|
19
|
+
|
|
20
|
+
- **misc**: Google stream error unable to abort request, closes [#9180](https://github.com/lobehub/lobe-chat/issues/9180) ([78eaead](https://github.com/lobehub/lobe-chat/commit/78eaead))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
### [Version 1.128.4](https://github.com/lobehub/lobe-chat/compare/v1.128.3...v1.128.4)
|
|
31
|
+
|
|
32
|
+
<sup>Released on **2025-09-13**</sup>
|
|
33
|
+
|
|
34
|
+
#### 💄 Styles
|
|
35
|
+
|
|
36
|
+
- **misc**: Fix discover plugin link.
|
|
37
|
+
|
|
38
|
+
<br/>
|
|
39
|
+
|
|
40
|
+
<details>
|
|
41
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
42
|
+
|
|
43
|
+
#### Styles
|
|
44
|
+
|
|
45
|
+
- **misc**: Fix discover plugin link, closes [#9240](https://github.com/lobehub/lobe-chat/issues/9240) ([cfb2246](https://github.com/lobehub/lobe-chat/commit/cfb2246))
|
|
46
|
+
|
|
47
|
+
</details>
|
|
48
|
+
|
|
49
|
+
<div align="right">
|
|
50
|
+
|
|
51
|
+
[](#readme-top)
|
|
52
|
+
|
|
53
|
+
</div>
|
|
54
|
+
|
|
5
55
|
### [Version 1.128.3](https://github.com/lobehub/lobe-chat/compare/v1.128.2...v1.128.3)
|
|
6
56
|
|
|
7
57
|
<sup>Released on **2025-09-13**</sup>
|
package/changelog/v1.json
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"children": {
|
|
4
|
+
"fixes": [
|
|
5
|
+
"Google stream error unable to abort request."
|
|
6
|
+
]
|
|
7
|
+
},
|
|
8
|
+
"date": "2025-09-13",
|
|
9
|
+
"version": "1.128.5"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"children": {
|
|
13
|
+
"improvements": [
|
|
14
|
+
"Fix discover plugin link."
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"date": "2025-09-13",
|
|
18
|
+
"version": "1.128.4"
|
|
19
|
+
},
|
|
2
20
|
{
|
|
3
21
|
"children": {
|
|
4
22
|
"fixes": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/chat",
|
|
3
|
-
"version": "1.128.
|
|
3
|
+
"version": "1.128.5",
|
|
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",
|
|
@@ -137,7 +137,7 @@
|
|
|
137
137
|
"@electric-sql/pglite": "0.2.17",
|
|
138
138
|
"@emotion/react": "^11.14.0",
|
|
139
139
|
"@fal-ai/client": "^1.6.2",
|
|
140
|
-
"@formkit/auto-animate": "^0.
|
|
140
|
+
"@formkit/auto-animate": "^0.9.0",
|
|
141
141
|
"@google/genai": "^1.19.0",
|
|
142
142
|
"@huggingface/inference": "^2.8.1",
|
|
143
143
|
"@icons-pack/react-simple-icons": "9.6.0",
|
|
@@ -86,7 +86,7 @@ const xaiChatModels: AIChatModelCard[] = [
|
|
|
86
86
|
description:
|
|
87
87
|
'旗舰级模型,擅长数据提取、编程和文本摘要等企业级应用,拥有金融、医疗、法律和科学等领域的深厚知识。',
|
|
88
88
|
displayName: 'Grok 3 (Fast mode)',
|
|
89
|
-
id: 'grok-3-fast',
|
|
89
|
+
id: 'grok-3-fast', // legacy
|
|
90
90
|
pricing: {
|
|
91
91
|
units: [
|
|
92
92
|
{ name: 'textInput_cacheRead', rate: 1.25, strategy: 'fixed', unit: 'millionTokens' },
|
|
@@ -136,7 +136,7 @@ const xaiChatModels: AIChatModelCard[] = [
|
|
|
136
136
|
description:
|
|
137
137
|
'轻量级模型,回话前会先思考。运行快速、智能,适用于不需要深层领域知识的逻辑任务,并能获取原始的思维轨迹。',
|
|
138
138
|
displayName: 'Grok 3 Mini (Fast mode)',
|
|
139
|
-
id: 'grok-3-mini-fast',
|
|
139
|
+
id: 'grok-3-mini-fast', // legacy
|
|
140
140
|
pricing: {
|
|
141
141
|
units: [
|
|
142
142
|
{ name: 'textInput_cacheRead', rate: 0.15, strategy: 'fixed', unit: 'millionTokens' },
|
|
@@ -181,7 +181,7 @@ const xaiChatModels: AIChatModelCard[] = [
|
|
|
181
181
|
contextWindowTokens: 32_768,
|
|
182
182
|
description: '该模型在准确性、指令遵循和多语言能力方面有所改进。',
|
|
183
183
|
displayName: 'Grok 2 Vision 1212',
|
|
184
|
-
id: 'grok-2-vision-1212',
|
|
184
|
+
id: 'grok-2-vision-1212', // legacy
|
|
185
185
|
pricing: {
|
|
186
186
|
units: [
|
|
187
187
|
{ name: 'textInput', rate: 2, strategy: 'fixed', unit: 'millionTokens' },
|
|
@@ -58,6 +58,14 @@ afterEach(() => {
|
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
describe('LobeOpenAICompatibleFactory', () => {
|
|
61
|
+
// Polyfill File for Node environment used in image tests
|
|
62
|
+
if (typeof File === 'undefined') {
|
|
63
|
+
// @ts-ignore
|
|
64
|
+
global.File = class MockFile {
|
|
65
|
+
constructor(public parts: any[], public name: string, public opts?: any) {}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
61
69
|
describe('init', () => {
|
|
62
70
|
it('should correctly initialize with an API key', async () => {
|
|
63
71
|
const instance = new LobeMockProvider({ apiKey: 'test_api_key' });
|
|
@@ -148,10 +156,22 @@ describe('LobeOpenAICompatibleFactory', () => {
|
|
|
148
156
|
|
|
149
157
|
const decoder = new TextDecoder();
|
|
150
158
|
const reader = result.body!.getReader();
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
159
|
+
|
|
160
|
+
// Collect all chunks
|
|
161
|
+
const chunks = [];
|
|
162
|
+
while (true) {
|
|
163
|
+
const { value, done } = await reader.read();
|
|
164
|
+
if (done) break;
|
|
165
|
+
chunks.push(decoder.decode(value));
|
|
166
|
+
}
|
|
167
|
+
// Assert that all expected chunk patterns are present
|
|
168
|
+
expect(chunks).toEqual(
|
|
169
|
+
expect.arrayContaining([
|
|
170
|
+
'id: a\n',
|
|
171
|
+
'event: text\n',
|
|
172
|
+
'data: "hello"\n\n',
|
|
173
|
+
]),
|
|
174
|
+
);
|
|
155
175
|
});
|
|
156
176
|
|
|
157
177
|
// https://github.com/lobehub/lobe-chat/issues/2752
|
|
@@ -2,7 +2,7 @@ import { GenerateContentResponse } from '@google/genai';
|
|
|
2
2
|
import { describe, expect, it, vi } from 'vitest';
|
|
3
3
|
|
|
4
4
|
import * as uuidModule from '../../utils/uuid';
|
|
5
|
-
import { GoogleGenerativeAIStream } from './google-ai';
|
|
5
|
+
import { GoogleGenerativeAIStream, LOBE_ERROR_KEY } from './google-ai';
|
|
6
6
|
|
|
7
7
|
describe('GoogleGenerativeAIStream', () => {
|
|
8
8
|
it('should transform Google Generative AI stream to protocol stream', async () => {
|
|
@@ -21,7 +21,21 @@ describe('GoogleGenerativeAIStream', () => {
|
|
|
21
21
|
controller.enqueue(
|
|
22
22
|
mockGenerateContentResponse('', [{ name: 'testFunction', args: { arg1: 'value1' } }]),
|
|
23
23
|
);
|
|
24
|
-
|
|
24
|
+
|
|
25
|
+
// final chunk should include finishReason and usageMetadata to mark terminal event
|
|
26
|
+
controller.enqueue({
|
|
27
|
+
text: ' world!',
|
|
28
|
+
candidates: [
|
|
29
|
+
{ content: { role: 'model' }, finishReason: 'STOP', index: 0 },
|
|
30
|
+
],
|
|
31
|
+
usageMetadata: {
|
|
32
|
+
promptTokenCount: 1,
|
|
33
|
+
totalTokenCount: 1,
|
|
34
|
+
promptTokensDetails: [{ modality: 'TEXT', tokenCount: 1 }],
|
|
35
|
+
},
|
|
36
|
+
modelVersion: 'gemini-test',
|
|
37
|
+
} as unknown as GenerateContentResponse);
|
|
38
|
+
|
|
25
39
|
controller.close();
|
|
26
40
|
},
|
|
27
41
|
});
|
|
@@ -63,6 +77,14 @@ describe('GoogleGenerativeAIStream', () => {
|
|
|
63
77
|
'id: chat_1\n',
|
|
64
78
|
'event: text\n',
|
|
65
79
|
`data: " world!"\n\n`,
|
|
80
|
+
// stop
|
|
81
|
+
'id: chat_1\n',
|
|
82
|
+
'event: stop\n',
|
|
83
|
+
`data: "STOP"\n\n`,
|
|
84
|
+
// usage
|
|
85
|
+
'id: chat_1\n',
|
|
86
|
+
'event: usage\n',
|
|
87
|
+
`data: {"inputTextTokens":1,"outputImageTokens":0,"outputTextTokens":0,"totalInputTokens":1,"totalOutputTokens":0,"totalTokens":1}\n\n`,
|
|
66
88
|
]);
|
|
67
89
|
|
|
68
90
|
expect(onStartMock).toHaveBeenCalledTimes(1);
|
|
@@ -73,8 +95,23 @@ describe('GoogleGenerativeAIStream', () => {
|
|
|
73
95
|
});
|
|
74
96
|
|
|
75
97
|
it('should handle empty stream', async () => {
|
|
98
|
+
vi.spyOn(uuidModule, 'nanoid').mockReturnValueOnce('E5M9dFKw');
|
|
76
99
|
const mockGoogleStream = new ReadableStream({
|
|
77
100
|
start(controller) {
|
|
101
|
+
controller.enqueue({
|
|
102
|
+
candidates: [{ content: { role: 'model' }, finishReason: 'STOP', index: 0 }],
|
|
103
|
+
usageMetadata: {
|
|
104
|
+
promptTokenCount: 0,
|
|
105
|
+
cachedContentTokenCount: 0,
|
|
106
|
+
totalTokenCount: 0,
|
|
107
|
+
promptTokensDetails: [
|
|
108
|
+
{ modality: 'TEXT', tokenCount: 0 },
|
|
109
|
+
{ modality: 'IMAGE', tokenCount: 0 },
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
modelVersion: 'gemini-test',
|
|
113
|
+
} as unknown as GenerateContentResponse);
|
|
114
|
+
|
|
78
115
|
controller.close();
|
|
79
116
|
},
|
|
80
117
|
});
|
|
@@ -89,7 +126,14 @@ describe('GoogleGenerativeAIStream', () => {
|
|
|
89
126
|
chunks.push(decoder.decode(chunk, { stream: true }));
|
|
90
127
|
}
|
|
91
128
|
|
|
92
|
-
expect(chunks).toEqual([
|
|
129
|
+
expect(chunks).toEqual([
|
|
130
|
+
'id: chat_E5M9dFKw\n',
|
|
131
|
+
'event: stop\n',
|
|
132
|
+
`data: "STOP"\n\n`,
|
|
133
|
+
'id: chat_E5M9dFKw\n',
|
|
134
|
+
'event: usage\n',
|
|
135
|
+
`data: {"inputCachedTokens":0,"inputImageTokens":0,"inputTextTokens":0,"outputImageTokens":0,"outputTextTokens":0,"totalInputTokens":0,"totalOutputTokens":0,"totalTokens":0}\n\n`,
|
|
136
|
+
]);
|
|
93
137
|
});
|
|
94
138
|
|
|
95
139
|
it('should handle image', async () => {
|
|
@@ -102,13 +146,17 @@ describe('GoogleGenerativeAIStream', () => {
|
|
|
102
146
|
parts: [{ inlineData: { mimeType: 'image/png', data: 'iVBORw0KGgoAA' } }],
|
|
103
147
|
role: 'model',
|
|
104
148
|
},
|
|
149
|
+
finishReason: 'STOP',
|
|
105
150
|
index: 0,
|
|
106
151
|
},
|
|
107
152
|
],
|
|
108
153
|
usageMetadata: {
|
|
109
154
|
promptTokenCount: 6,
|
|
110
155
|
totalTokenCount: 6,
|
|
111
|
-
promptTokensDetails: [
|
|
156
|
+
promptTokensDetails: [
|
|
157
|
+
{ modality: 'TEXT', tokenCount: 6 },
|
|
158
|
+
{ modality: 'IMAGE', tokenCount: 0 },
|
|
159
|
+
],
|
|
112
160
|
},
|
|
113
161
|
modelVersion: 'gemini-2.0-flash-exp',
|
|
114
162
|
};
|
|
@@ -136,6 +184,14 @@ describe('GoogleGenerativeAIStream', () => {
|
|
|
136
184
|
'id: chat_1\n',
|
|
137
185
|
'event: base64_image\n',
|
|
138
186
|
`data: "data:image/png;base64,iVBORw0KGgoAA"\n\n`,
|
|
187
|
+
// stop
|
|
188
|
+
'id: chat_1\n',
|
|
189
|
+
'event: stop\n',
|
|
190
|
+
`data: "STOP"\n\n`,
|
|
191
|
+
// usage
|
|
192
|
+
'id: chat_1\n',
|
|
193
|
+
'event: usage\n',
|
|
194
|
+
`data: {"inputImageTokens":0,"inputTextTokens":6,"outputImageTokens":0,"outputTextTokens":0,"totalInputTokens":6,"totalOutputTokens":0,"totalTokens":6}\n\n`,
|
|
139
195
|
]);
|
|
140
196
|
});
|
|
141
197
|
|
|
@@ -855,4 +911,33 @@ describe('GoogleGenerativeAIStream', () => {
|
|
|
855
911
|
`data: {"body":{"context":{"promptFeedback":{"blockReason":"PROHIBITED_CONTENT"}},"message":"您的请求可能包含违禁内容。请调整您的请求,确保内容符合使用规范。","provider":"google"},"type":"ProviderBizError"}\n\n`,
|
|
856
912
|
]);
|
|
857
913
|
});
|
|
914
|
+
|
|
915
|
+
it('should pass through injected lobe error marker', async () => {
|
|
916
|
+
vi.spyOn(uuidModule, 'nanoid').mockReturnValueOnce('1');
|
|
917
|
+
|
|
918
|
+
const errorPayload = { message: 'internal error', code: 123 };
|
|
919
|
+
|
|
920
|
+
const mockGoogleStream = new ReadableStream({
|
|
921
|
+
start(controller) {
|
|
922
|
+
controller.enqueue({ [LOBE_ERROR_KEY]: errorPayload });
|
|
923
|
+
controller.close();
|
|
924
|
+
},
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
const protocolStream = GoogleGenerativeAIStream(mockGoogleStream);
|
|
928
|
+
|
|
929
|
+
const decoder = new TextDecoder();
|
|
930
|
+
const chunks = [];
|
|
931
|
+
|
|
932
|
+
// @ts-ignore
|
|
933
|
+
for await (const chunk of protocolStream) {
|
|
934
|
+
chunks.push(decoder.decode(chunk, { stream: true }));
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
expect(chunks).toEqual([
|
|
938
|
+
'id: chat_1\n',
|
|
939
|
+
'event: error\n',
|
|
940
|
+
`data: ${JSON.stringify(errorPayload)}\n\n`,
|
|
941
|
+
]);
|
|
942
|
+
});
|
|
858
943
|
});
|
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
generateToolCallId,
|
|
17
17
|
} from './protocol';
|
|
18
18
|
|
|
19
|
+
export const LOBE_ERROR_KEY = '__lobe_error';
|
|
20
|
+
|
|
19
21
|
const getBlockReasonMessage = (blockReason: string): string => {
|
|
20
22
|
const blockReasonMessages = errorLocale.response.GoogleAIBlockReason;
|
|
21
23
|
|
|
@@ -29,6 +31,14 @@ const transformGoogleGenerativeAIStream = (
|
|
|
29
31
|
chunk: GenerateContentResponse,
|
|
30
32
|
context: StreamContext,
|
|
31
33
|
): StreamProtocolChunk | StreamProtocolChunk[] => {
|
|
34
|
+
// Handle injected internal error marker to pass through detailed error info
|
|
35
|
+
if ((chunk as any)?.[LOBE_ERROR_KEY]) {
|
|
36
|
+
return {
|
|
37
|
+
data: (chunk as any)[LOBE_ERROR_KEY],
|
|
38
|
+
id: context?.id || 'error',
|
|
39
|
+
type: 'error',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
32
42
|
// Handle promptFeedback with blockReason (e.g., PROHIBITED_CONTENT)
|
|
33
43
|
if ('promptFeedback' in chunk && (chunk as any).promptFeedback?.blockReason) {
|
|
34
44
|
const blockReason = (chunk as any).promptFeedback.blockReason;
|
|
@@ -216,6 +226,8 @@ export const GoogleGenerativeAIStream = (
|
|
|
216
226
|
.pipeThrough(
|
|
217
227
|
createTokenSpeedCalculator(transformGoogleGenerativeAIStream, { inputStartAt, streamStack }),
|
|
218
228
|
)
|
|
219
|
-
.pipeThrough(
|
|
229
|
+
.pipeThrough(
|
|
230
|
+
createSSEProtocolTransformer((c) => c, streamStack, { requireTerminalEvent: true }),
|
|
231
|
+
)
|
|
220
232
|
.pipeThrough(createCallbacksTransformer(callbacks));
|
|
221
233
|
};
|
|
@@ -396,7 +396,7 @@ describe('OpenAIStream', () => {
|
|
|
396
396
|
expect(chunks).toEqual([
|
|
397
397
|
'id: first_chunk_error\n',
|
|
398
398
|
'event: error\n',
|
|
399
|
-
`data: {"body":{"errorType":"ProviderBizError","message":"Test error"},"type":"ProviderBizError"}\n\n`,
|
|
399
|
+
`data: {"body":{"errorType":"ProviderBizError","message":"Test error"},"message":"Test error","type":"ProviderBizError"}\n\n`,
|
|
400
400
|
]);
|
|
401
401
|
});
|
|
402
402
|
|
|
@@ -427,7 +427,7 @@ describe('OpenAIStream', () => {
|
|
|
427
427
|
expect(chunks).toEqual([
|
|
428
428
|
'id: first_chunk_error\n',
|
|
429
429
|
'event: error\n',
|
|
430
|
-
`data: {"body":{"message":"Custom error","errorType":"PermissionDenied","provider":"grok"},"type":"PermissionDenied"}\n\n`,
|
|
430
|
+
`data: {"body":{"message":"Custom error","errorType":"PermissionDenied","provider":"grok"},"message":"Custom error","type":"PermissionDenied"}\n\n`,
|
|
431
431
|
]);
|
|
432
432
|
});
|
|
433
433
|
|
|
@@ -2481,4 +2481,4 @@ describe('OpenAIStream', () => {
|
|
|
2481
2481
|
`data: "${base64_2}"\n\n`,
|
|
2482
2482
|
]);
|
|
2483
2483
|
});
|
|
2484
|
-
});
|
|
2484
|
+
});
|
|
@@ -57,8 +57,9 @@ const transformOpenAIStream = (
|
|
|
57
57
|
|
|
58
58
|
const errorData = {
|
|
59
59
|
body: chunk,
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
message: 'message' in chunk ? typeof chunk.message === 'string' ? chunk.message : JSON.stringify(chunk) : JSON.stringify(chunk),
|
|
61
|
+
type: 'errorType' in chunk ? chunk.errorType as typeof AgentRuntimeErrorType.ProviderBizError : AgentRuntimeErrorType.ProviderBizError,
|
|
62
|
+
} satisfies ChatMessageError;
|
|
62
63
|
return { data: errorData, id: 'first_chunk_error', type: 'error' };
|
|
63
64
|
}
|
|
64
65
|
|
|
@@ -45,8 +45,9 @@ const transformOpenAIStream = (
|
|
|
45
45
|
|
|
46
46
|
const errorData = {
|
|
47
47
|
body: chunk,
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
message: 'message' in chunk ? typeof chunk.message === 'string' ? chunk.message : JSON.stringify(chunk) : JSON.stringify(chunk),
|
|
49
|
+
type: 'errorType' in chunk ? chunk.errorType as typeof AgentRuntimeErrorType.ProviderBizError : AgentRuntimeErrorType.ProviderBizError,
|
|
50
|
+
} satisfies ChatMessageError;
|
|
50
51
|
return { data: errorData, id: 'first_chunk_error', type: 'error' };
|
|
51
52
|
}
|
|
52
53
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
|
|
3
|
-
import { createSSEDataExtractor, createTokenSpeedCalculator } from './protocol';
|
|
3
|
+
import { createSSEDataExtractor, createTokenSpeedCalculator, createSSEProtocolTransformer } from './protocol';
|
|
4
4
|
|
|
5
5
|
describe('createSSEDataExtractor', () => {
|
|
6
6
|
// Helper function to convert string to Uint8Array
|
|
@@ -233,3 +233,82 @@ describe('createTokenSpeedCalculator', async () => {
|
|
|
233
233
|
expect(speedChunk.data.ttft).not.toBeNaN();
|
|
234
234
|
});
|
|
235
235
|
});
|
|
236
|
+
|
|
237
|
+
describe('createSSEProtocolTransformer', () => {
|
|
238
|
+
const processChunk = async (transformer: TransformStream, chunk: any) => {
|
|
239
|
+
const results: any[] = [];
|
|
240
|
+
const readable = new ReadableStream({
|
|
241
|
+
start(controller) {
|
|
242
|
+
controller.enqueue(chunk);
|
|
243
|
+
controller.close();
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const writable = new WritableStream({
|
|
248
|
+
write(chunk) {
|
|
249
|
+
results.push(chunk);
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
await readable.pipeThrough(transformer).pipeTo(writable);
|
|
254
|
+
|
|
255
|
+
return results;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
it('should convert chunk into SSE formatted lines without enforcing terminal (default)', async () => {
|
|
259
|
+
const transformerFn = (chunk: any) => ({ type: 'text', id: chunk.id, data: chunk.data });
|
|
260
|
+
const transformer = createSSEProtocolTransformer(transformerFn as any);
|
|
261
|
+
|
|
262
|
+
const input = { id: '1', data: 'hello' };
|
|
263
|
+
const results = await processChunk(transformer, input);
|
|
264
|
+
|
|
265
|
+
// Should only output the text event, no injected error on flush (default not enforced)
|
|
266
|
+
expect(results).toEqual([
|
|
267
|
+
`id: 1\n`,
|
|
268
|
+
`event: text\n`,
|
|
269
|
+
`data: ${JSON.stringify('hello')}\n\n`,
|
|
270
|
+
]);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('should not emit flush error if a terminal event was received (enforced)', async () => {
|
|
274
|
+
const transformerFn = (chunk: any) => ({ type: 'stop', id: chunk.id, data: chunk.data });
|
|
275
|
+
const transformer = createSSEProtocolTransformer(transformerFn as any, { id: 'stream_ok' }, { requireTerminalEvent: true });
|
|
276
|
+
|
|
277
|
+
const input = { id: 'ok', data: 'bye' };
|
|
278
|
+
const results = await processChunk(transformer, input);
|
|
279
|
+
|
|
280
|
+
// Only the stop event lines should be present (no extra error event from flush)
|
|
281
|
+
expect(results).toEqual([
|
|
282
|
+
`id: ok\n`,
|
|
283
|
+
`event: stop\n`,
|
|
284
|
+
`data: ${JSON.stringify('bye')}\n\n`,
|
|
285
|
+
]);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('should emit an error event on flush when no terminal event received (enforced)', async () => {
|
|
289
|
+
const transformerFn = (chunk: any) => ({ type: 'text', id: chunk.id, data: chunk.data });
|
|
290
|
+
const streamStack = { id: 'stream_missing_term' } as any;
|
|
291
|
+
const transformer = createSSEProtocolTransformer(transformerFn as any, streamStack, { requireTerminalEvent: true });
|
|
292
|
+
|
|
293
|
+
const input = { id: '1', data: 'partial' };
|
|
294
|
+
const results = await processChunk(transformer, input);
|
|
295
|
+
|
|
296
|
+
// original 3 lines + 3 lines from flush error
|
|
297
|
+
expect(results).toHaveLength(6);
|
|
298
|
+
|
|
299
|
+
// last three lines should be the injected error event
|
|
300
|
+
const lastThree = results.slice(-3);
|
|
301
|
+
const expectedData = {
|
|
302
|
+
body: { name: 'Stream parsing error', reason: 'unexpected_end' },
|
|
303
|
+
message: 'Stream ended unexpectedly',
|
|
304
|
+
name: 'Stream parsing error',
|
|
305
|
+
type: 'StreamChunkError',
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
expect(lastThree).toEqual([
|
|
309
|
+
`id: ${streamStack.id}\n`,
|
|
310
|
+
`event: error\n`,
|
|
311
|
+
`data: ${JSON.stringify(expectedData)}\n\n`,
|
|
312
|
+
]);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
@@ -166,8 +166,27 @@ export const convertIterableToStream = <T>(stream: AsyncIterable<T>) => {
|
|
|
166
166
|
export const createSSEProtocolTransformer = (
|
|
167
167
|
transformer: (chunk: any, stack: StreamContext) => StreamProtocolChunk | StreamProtocolChunk[],
|
|
168
168
|
streamStack?: StreamContext,
|
|
169
|
-
|
|
170
|
-
|
|
169
|
+
options?: { requireTerminalEvent?: boolean },
|
|
170
|
+
) => {
|
|
171
|
+
let hasTerminalEvent = false;
|
|
172
|
+
const requireTerminalEvent = Boolean(options?.requireTerminalEvent);
|
|
173
|
+
|
|
174
|
+
return new TransformStream({
|
|
175
|
+
flush(controller) {
|
|
176
|
+
// If the upstream closes without sending a terminal event, emit a final error event
|
|
177
|
+
if (requireTerminalEvent && !hasTerminalEvent) {
|
|
178
|
+
const id = streamStack?.id || 'stream_end';
|
|
179
|
+
const data = {
|
|
180
|
+
body: { name: 'Stream parsing error', reason: 'unexpected_end' },
|
|
181
|
+
message: 'Stream ended unexpectedly',
|
|
182
|
+
name: 'Stream parsing error',
|
|
183
|
+
type: 'StreamChunkError',
|
|
184
|
+
};
|
|
185
|
+
controller.enqueue(`id: ${id}\n`);
|
|
186
|
+
controller.enqueue(`event: error\n`);
|
|
187
|
+
controller.enqueue(`data: ${JSON.stringify(data)}\n\n`);
|
|
188
|
+
}
|
|
189
|
+
},
|
|
171
190
|
transform: (chunk, controller) => {
|
|
172
191
|
const result = transformer(chunk, streamStack || { id: '' });
|
|
173
192
|
|
|
@@ -177,9 +196,13 @@ export const createSSEProtocolTransformer = (
|
|
|
177
196
|
controller.enqueue(`id: ${id}\n`);
|
|
178
197
|
controller.enqueue(`event: ${type}\n`);
|
|
179
198
|
controller.enqueue(`data: ${JSON.stringify(data)}\n\n`);
|
|
199
|
+
|
|
200
|
+
// mark terminal when receiving any of these events
|
|
201
|
+
if (type === 'stop' || type === 'usage' || type === 'error') hasTerminalEvent = true;
|
|
180
202
|
});
|
|
181
203
|
},
|
|
182
204
|
});
|
|
205
|
+
};
|
|
183
206
|
|
|
184
207
|
export function createCallbacksTransformer(cb: ChatStreamCallbacks | undefined) {
|
|
185
208
|
const textEncoder = new TextEncoder();
|
|
@@ -8,6 +8,8 @@ import { ChatStreamPayload } from '@/types/openai/chat';
|
|
|
8
8
|
|
|
9
9
|
import * as debugStreamModule from '../../utils/debugStream';
|
|
10
10
|
import * as imageToBase64Module from '../../utils/imageToBase64';
|
|
11
|
+
import { LOBE_ERROR_KEY } from '../../core/streams/google-ai';
|
|
12
|
+
import { AgentRuntimeErrorType } from '../../types/error';
|
|
11
13
|
import { LobeGoogleAI } from './index';
|
|
12
14
|
|
|
13
15
|
const provider = 'google';
|
|
@@ -825,5 +827,158 @@ describe('LobeGoogleAI', () => {
|
|
|
825
827
|
});
|
|
826
828
|
});
|
|
827
829
|
});
|
|
830
|
+
|
|
831
|
+
describe('createEnhancedStream', () => {
|
|
832
|
+
it('should handle stream cancellation with data gracefully', async () => {
|
|
833
|
+
const mockStream = (async function* () {
|
|
834
|
+
yield { text: 'Hello' };
|
|
835
|
+
yield { text: ' world' };
|
|
836
|
+
})();
|
|
837
|
+
|
|
838
|
+
const abortController = new AbortController();
|
|
839
|
+
const enhancedStream = instance['createEnhancedStream'](mockStream, abortController.signal);
|
|
840
|
+
|
|
841
|
+
const reader = enhancedStream.getReader();
|
|
842
|
+
const chunks: any[] = [];
|
|
843
|
+
|
|
844
|
+
// Read first value then cancel to trigger error chunk
|
|
845
|
+
chunks.push((await reader.read()).value);
|
|
846
|
+
abortController.abort();
|
|
847
|
+
|
|
848
|
+
// Read all remaining chunks
|
|
849
|
+
let result;
|
|
850
|
+
while (!(result = await reader.read()).done) {
|
|
851
|
+
chunks.push(result.value);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Batch-assert the entire chunks array
|
|
855
|
+
expect(chunks).toEqual([
|
|
856
|
+
{ text: 'Hello' },
|
|
857
|
+
{
|
|
858
|
+
[LOBE_ERROR_KEY]: {
|
|
859
|
+
body: { name: 'Stream cancelled', provider, reason: 'aborted' },
|
|
860
|
+
message: 'Stream cancelled',
|
|
861
|
+
name: 'Stream cancelled',
|
|
862
|
+
type: AgentRuntimeErrorType.StreamChunkError,
|
|
863
|
+
},
|
|
864
|
+
},
|
|
865
|
+
]);
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
it('should handle stream cancellation without data', async () => {
|
|
869
|
+
const mockStream = (async function* () {
|
|
870
|
+
// Empty stream
|
|
871
|
+
})();
|
|
872
|
+
|
|
873
|
+
const abortController = new AbortController();
|
|
874
|
+
const enhancedStream = instance['createEnhancedStream'](mockStream, abortController.signal);
|
|
875
|
+
|
|
876
|
+
const reader = enhancedStream.getReader();
|
|
877
|
+
|
|
878
|
+
// Cancel immediately
|
|
879
|
+
abortController.abort();
|
|
880
|
+
|
|
881
|
+
// Should be closed without any chunks
|
|
882
|
+
const chunk = await reader.read();
|
|
883
|
+
expect(chunk.done).toBe(true);
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
it('should handle AbortError with data', async () => {
|
|
887
|
+
const mockStream = (async function* () {
|
|
888
|
+
yield { text: 'Hello' };
|
|
889
|
+
throw new Error('aborted');
|
|
890
|
+
})();
|
|
891
|
+
|
|
892
|
+
const abortController = new AbortController();
|
|
893
|
+
const enhancedStream = instance['createEnhancedStream'](mockStream, abortController.signal);
|
|
894
|
+
|
|
895
|
+
const reader = enhancedStream.getReader();
|
|
896
|
+
const chunks: any[] = [];
|
|
897
|
+
|
|
898
|
+
// Read first value then collect remaining chunks (error included)
|
|
899
|
+
chunks.push((await reader.read()).value);
|
|
900
|
+
let result;
|
|
901
|
+
while (!(result = await reader.read()).done) {
|
|
902
|
+
chunks.push(result.value);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// Assert both data and error chunk together
|
|
906
|
+
expect(chunks).toEqual([
|
|
907
|
+
{ text: 'Hello' },
|
|
908
|
+
{
|
|
909
|
+
[LOBE_ERROR_KEY]: {
|
|
910
|
+
body: { name: 'Stream cancelled', provider, reason: 'aborted' },
|
|
911
|
+
message: 'Stream cancelled',
|
|
912
|
+
name: 'Stream cancelled',
|
|
913
|
+
type: AgentRuntimeErrorType.StreamChunkError,
|
|
914
|
+
},
|
|
915
|
+
},
|
|
916
|
+
]);
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
it('should handle AbortError without data', async () => {
|
|
920
|
+
const mockStream = (async function* () {
|
|
921
|
+
throw new Error('aborted');
|
|
922
|
+
})();
|
|
923
|
+
|
|
924
|
+
const abortController = new AbortController();
|
|
925
|
+
const enhancedStream = instance['createEnhancedStream'](mockStream, abortController.signal);
|
|
926
|
+
|
|
927
|
+
const reader = enhancedStream.getReader();
|
|
928
|
+
const chunks: any[] = [];
|
|
929
|
+
|
|
930
|
+
// Read error chunk
|
|
931
|
+
const chunk1 = await reader.read();
|
|
932
|
+
chunks.push(chunk1.value);
|
|
933
|
+
|
|
934
|
+
// Stream should be closed
|
|
935
|
+
const chunk2 = await reader.read();
|
|
936
|
+
expect(chunk2.done).toBe(true);
|
|
937
|
+
|
|
938
|
+
expect(chunks[0][LOBE_ERROR_KEY]).toEqual({
|
|
939
|
+
body: {
|
|
940
|
+
message: 'aborted',
|
|
941
|
+
name: 'AbortError',
|
|
942
|
+
provider,
|
|
943
|
+
stack: expect.any(String),
|
|
944
|
+
},
|
|
945
|
+
message: 'aborted',
|
|
946
|
+
name: 'AbortError',
|
|
947
|
+
type: AgentRuntimeErrorType.StreamChunkError,
|
|
948
|
+
});
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
it('should handle other stream parsing errors', async () => {
|
|
952
|
+
const mockStream = (async function* () {
|
|
953
|
+
yield { text: 'Hello' };
|
|
954
|
+
throw new Error('Network error');
|
|
955
|
+
})();
|
|
956
|
+
|
|
957
|
+
const abortController = new AbortController();
|
|
958
|
+
const enhancedStream = instance['createEnhancedStream'](mockStream, abortController.signal);
|
|
959
|
+
|
|
960
|
+
const reader = enhancedStream.getReader();
|
|
961
|
+
const chunks: any[] = [];
|
|
962
|
+
|
|
963
|
+
// Read first value then collect remaining chunks (parsing error)
|
|
964
|
+
chunks.push((await reader.read()).value);
|
|
965
|
+
let result;
|
|
966
|
+
while (!(result = await reader.read()).done) {
|
|
967
|
+
chunks.push(result.value);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
expect(chunks).toEqual([
|
|
971
|
+
{ text: 'Hello' },
|
|
972
|
+
{
|
|
973
|
+
[LOBE_ERROR_KEY]: {
|
|
974
|
+
body: { message: 'Network error', provider },
|
|
975
|
+
message: 'Network error',
|
|
976
|
+
name: 'Stream parsing error',
|
|
977
|
+
type: AgentRuntimeErrorType.ProviderBizError,
|
|
978
|
+
},
|
|
979
|
+
},
|
|
980
|
+
]);
|
|
981
|
+
});
|
|
982
|
+
});
|
|
828
983
|
});
|
|
829
984
|
});
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
ThinkingConfig,
|
|
11
11
|
} from '@google/genai';
|
|
12
12
|
|
|
13
|
+
import { LOBE_ERROR_KEY } from '../../core/streams/google-ai';
|
|
13
14
|
import { LobeRuntimeAI } from '../../core/BaseAI';
|
|
14
15
|
import { GoogleGenerativeAIStream, VertexAIStream } from '../../core/streams';
|
|
15
16
|
import {
|
|
@@ -29,6 +30,9 @@ import { StreamingResponse } from '../../utils/response';
|
|
|
29
30
|
import { safeParseJSON } from '../../utils/safeParseJSON';
|
|
30
31
|
import { parseDataUri } from '../../utils/uriParser';
|
|
31
32
|
import { createGoogleImage } from './createImage';
|
|
33
|
+
import debug from 'debug';
|
|
34
|
+
|
|
35
|
+
const log = debug('model-runtime:google');
|
|
32
36
|
|
|
33
37
|
const modelsOffSafetySettings = new Set(['gemini-2.0-flash-exp']);
|
|
34
38
|
|
|
@@ -244,7 +248,7 @@ export class LobeGoogleAI implements LobeRuntimeAI {
|
|
|
244
248
|
|
|
245
249
|
// 移除之前的静默处理,统一抛出错误
|
|
246
250
|
if (isAbortError(err)) {
|
|
247
|
-
|
|
251
|
+
log('Request was cancelled');
|
|
248
252
|
throw AgentRuntimeError.chat({
|
|
249
253
|
error: { message: 'Request was cancelled' },
|
|
250
254
|
errorType: AgentRuntimeErrorType.ProviderBizError,
|
|
@@ -252,7 +256,7 @@ export class LobeGoogleAI implements LobeRuntimeAI {
|
|
|
252
256
|
});
|
|
253
257
|
}
|
|
254
258
|
|
|
255
|
-
|
|
259
|
+
log('Error: %O', err);
|
|
256
260
|
const { errorType, error } = parseGoogleErrorMessage(err.message);
|
|
257
261
|
|
|
258
262
|
throw AgentRuntimeError.chat({ error, errorType, provider: this.provider });
|
|
@@ -268,6 +272,8 @@ export class LobeGoogleAI implements LobeRuntimeAI {
|
|
|
268
272
|
}
|
|
269
273
|
|
|
270
274
|
private createEnhancedStream(originalStream: any, signal: AbortSignal): ReadableStream {
|
|
275
|
+
// capture provider for error payloads inside the stream closure
|
|
276
|
+
const provider = this.provider;
|
|
271
277
|
return new ReadableStream({
|
|
272
278
|
async start(controller) {
|
|
273
279
|
let hasData = false;
|
|
@@ -277,12 +283,23 @@ export class LobeGoogleAI implements LobeRuntimeAI {
|
|
|
277
283
|
if (signal.aborted) {
|
|
278
284
|
// 如果有数据已经输出,优雅地关闭流而不是抛出错误
|
|
279
285
|
if (hasData) {
|
|
280
|
-
|
|
286
|
+
log('Stream cancelled gracefully, preserving existing output');
|
|
287
|
+
// 显式注入取消错误,避免走 SSE 兜底 unexpected_end
|
|
288
|
+
controller.enqueue({
|
|
289
|
+
[LOBE_ERROR_KEY]: {
|
|
290
|
+
body: { name: 'Stream cancelled', provider, reason: 'aborted' },
|
|
291
|
+
message: 'Stream cancelled',
|
|
292
|
+
name: 'Stream cancelled',
|
|
293
|
+
type: AgentRuntimeErrorType.StreamChunkError,
|
|
294
|
+
},
|
|
295
|
+
});
|
|
281
296
|
controller.close();
|
|
282
297
|
return;
|
|
283
298
|
} else {
|
|
284
|
-
//
|
|
285
|
-
|
|
299
|
+
// 如果还没有数据输出,直接关闭流,由下游 SSE 在 flush 阶段补发错误事件
|
|
300
|
+
log('Stream cancelled before any output');
|
|
301
|
+
controller.close();
|
|
302
|
+
return;
|
|
286
303
|
}
|
|
287
304
|
}
|
|
288
305
|
|
|
@@ -296,18 +313,55 @@ export class LobeGoogleAI implements LobeRuntimeAI {
|
|
|
296
313
|
if (isAbortError(err) || signal.aborted) {
|
|
297
314
|
// 如果有数据已经输出,优雅地关闭流
|
|
298
315
|
if (hasData) {
|
|
299
|
-
|
|
316
|
+
log('Stream reading cancelled gracefully, preserving existing output');
|
|
317
|
+
// 显式注入取消错误,避免走 SSE 兜底 unexpected_end
|
|
318
|
+
controller.enqueue({
|
|
319
|
+
[LOBE_ERROR_KEY]: {
|
|
320
|
+
body: { name: 'Stream cancelled', provider, reason: 'aborted' },
|
|
321
|
+
message: 'Stream cancelled',
|
|
322
|
+
name: 'Stream cancelled',
|
|
323
|
+
type: AgentRuntimeErrorType.StreamChunkError,
|
|
324
|
+
},
|
|
325
|
+
});
|
|
300
326
|
controller.close();
|
|
301
327
|
return;
|
|
302
328
|
} else {
|
|
303
|
-
|
|
304
|
-
|
|
329
|
+
log('Stream reading cancelled before any output');
|
|
330
|
+
// 注入一个带详细错误信息的错误标记,交由下游 google-ai transformer 输出 error 事件
|
|
331
|
+
controller.enqueue({
|
|
332
|
+
[LOBE_ERROR_KEY]: {
|
|
333
|
+
body: {
|
|
334
|
+
message: err.message,
|
|
335
|
+
name: 'AbortError',
|
|
336
|
+
provider,
|
|
337
|
+
stack: err.stack,
|
|
338
|
+
},
|
|
339
|
+
message: err.message || 'Request was cancelled',
|
|
340
|
+
name: 'AbortError',
|
|
341
|
+
type: AgentRuntimeErrorType.StreamChunkError,
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
controller.close();
|
|
305
345
|
return;
|
|
306
346
|
}
|
|
307
347
|
} else {
|
|
308
348
|
// 处理其他流解析错误
|
|
309
|
-
|
|
310
|
-
|
|
349
|
+
log('Stream parsing error: %O', err);
|
|
350
|
+
// 尝试解析 Google 错误并提取 code/message/status
|
|
351
|
+
const { error: parsedError, errorType } = parseGoogleErrorMessage(
|
|
352
|
+
err?.message || String(err),
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
// 注入一个带详细错误信息的错误标记,交由下游 google-ai transformer 输出 error 事件
|
|
356
|
+
controller.enqueue({
|
|
357
|
+
[LOBE_ERROR_KEY]: {
|
|
358
|
+
body: { ...parsedError, provider },
|
|
359
|
+
message: parsedError?.message || err.message || 'Stream parsing error',
|
|
360
|
+
name: 'Stream parsing error',
|
|
361
|
+
type: errorType ?? AgentRuntimeErrorType.StreamChunkError,
|
|
362
|
+
},
|
|
363
|
+
});
|
|
364
|
+
controller.close();
|
|
311
365
|
return;
|
|
312
366
|
}
|
|
313
367
|
}
|
|
@@ -348,7 +402,7 @@ export class LobeGoogleAI implements LobeRuntimeAI {
|
|
|
348
402
|
|
|
349
403
|
return processModelList(processedModels, MODEL_LIST_CONFIGS.google);
|
|
350
404
|
} catch (error) {
|
|
351
|
-
|
|
405
|
+
log('Failed to fetch Google models: %O', error);
|
|
352
406
|
throw error;
|
|
353
407
|
}
|
|
354
408
|
}
|
|
@@ -34,7 +34,7 @@ const Client = memo<{ mobile?: boolean }>(() => {
|
|
|
34
34
|
</Title>
|
|
35
35
|
<AssistantList data={assistantList.items} rows={4} />
|
|
36
36
|
<div />
|
|
37
|
-
<Title more={t('home.more')} moreLink={'/discover/
|
|
37
|
+
<Title more={t('home.more')} moreLink={'/discover/mcp'}>
|
|
38
38
|
{t('home.featuredTools')}
|
|
39
39
|
</Title>
|
|
40
40
|
<McpList data={mcpList.items} rows={4} />
|
|
@@ -247,7 +247,7 @@ const Qwen: ModelProviderCard = {
|
|
|
247
247
|
releasedAt: '2025-02-05',
|
|
248
248
|
},
|
|
249
249
|
],
|
|
250
|
-
checkModel: 'qwen-flash
|
|
250
|
+
checkModel: 'qwen-flash',
|
|
251
251
|
description:
|
|
252
252
|
'通义千问是阿里云自主研发的超大规模语言模型,具有强大的自然语言理解和生成能力。它可以回答各种问题、创作文字内容、表达观点看法、撰写代码等,在多个领域发挥作用。',
|
|
253
253
|
disableBrowserRequest: true,
|
|
@@ -419,7 +419,7 @@ const SiliconCloud: ModelProviderCard = {
|
|
|
419
419
|
vision: true,
|
|
420
420
|
},
|
|
421
421
|
],
|
|
422
|
-
checkModel: 'Pro/Qwen/Qwen2-
|
|
422
|
+
checkModel: 'Pro/Qwen/Qwen2-7B-Instruct',
|
|
423
423
|
description: 'SiliconCloud,基于优秀开源基础模型的高性价比 GenAI 云服务',
|
|
424
424
|
id: 'siliconcloud',
|
|
425
425
|
modelList: { showModelFetcher: true },
|