@lobehub/chat 1.70.11 → 1.71.1
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/.github/workflows/sync-database-schema.yml +25 -0
- package/CHANGELOG.md +58 -0
- package/changelog/v1.json +21 -0
- package/docs/developer/database-schema.dbml +569 -0
- package/locales/ar/components.json +1 -0
- package/locales/ar/models.json +6 -0
- package/locales/bg-BG/components.json +1 -0
- package/locales/bg-BG/models.json +6 -0
- package/locales/de-DE/components.json +1 -0
- package/locales/de-DE/models.json +6 -0
- package/locales/en-US/components.json +1 -0
- package/locales/en-US/models.json +6 -0
- package/locales/es-ES/components.json +1 -0
- package/locales/es-ES/models.json +6 -0
- package/locales/fa-IR/components.json +1 -0
- package/locales/fa-IR/models.json +6 -0
- package/locales/fr-FR/components.json +1 -0
- package/locales/fr-FR/models.json +6 -0
- package/locales/it-IT/components.json +1 -0
- package/locales/it-IT/models.json +6 -0
- package/locales/ja-JP/components.json +1 -0
- package/locales/ja-JP/models.json +6 -0
- package/locales/ko-KR/components.json +1 -0
- package/locales/ko-KR/models.json +6 -0
- package/locales/nl-NL/components.json +1 -0
- package/locales/nl-NL/models.json +6 -0
- package/locales/pl-PL/components.json +1 -0
- package/locales/pl-PL/models.json +6 -0
- package/locales/pt-BR/components.json +1 -0
- package/locales/pt-BR/models.json +6 -0
- package/locales/ru-RU/components.json +1 -0
- package/locales/ru-RU/models.json +6 -0
- package/locales/tr-TR/components.json +1 -0
- package/locales/tr-TR/models.json +6 -0
- package/locales/vi-VN/components.json +1 -0
- package/locales/vi-VN/models.json +6 -0
- package/locales/zh-CN/components.json +1 -0
- package/locales/zh-CN/models.json +6 -0
- package/locales/zh-TW/components.json +1 -0
- package/locales/zh-TW/models.json +6 -0
- package/package.json +6 -2
- package/scripts/dbmlWorkflow/index.ts +11 -0
- package/src/components/ModelSelect/index.tsx +16 -0
- package/src/config/aiModels/google.ts +36 -0
- package/src/config/aiModels/vertexai.ts +24 -6
- package/src/config/modelProviders/vertexai.ts +1 -1
- package/src/database/client/migrations.json +4 -4
- package/src/database/server/models/message.ts +20 -9
- package/src/database/server/models/user.test.ts +58 -0
- package/src/features/AlertBanner/CloudBanner.tsx +1 -1
- package/src/features/Conversation/Messages/Assistant/index.tsx +4 -1
- package/src/features/Conversation/Messages/User/index.tsx +4 -4
- package/src/libs/agent-runtime/google/index.ts +23 -5
- package/src/libs/agent-runtime/utils/streams/google-ai.test.ts +189 -0
- package/src/libs/agent-runtime/utils/streams/google-ai.ts +70 -23
- package/src/libs/agent-runtime/utils/streams/index.ts +1 -0
- package/src/libs/agent-runtime/utils/streams/protocol.ts +2 -0
- package/src/locales/default/components.ts +1 -0
- package/src/services/chat.ts +33 -15
- package/src/services/file/client.ts +3 -1
- package/src/services/message/server.ts +2 -2
- package/src/services/message/type.ts +2 -2
- package/src/services/upload.ts +82 -1
- package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +44 -4
- package/src/store/chat/slices/message/action.ts +3 -0
- package/src/store/file/slices/upload/action.ts +36 -13
- package/src/store/file/store.ts +2 -0
- package/src/types/aiModel.ts +4 -1
- package/src/types/files/upload.ts +7 -0
- package/src/types/message/base.ts +22 -1
- package/src/types/message/chat.ts +1 -6
- package/src/types/message/image.ts +11 -0
- package/src/types/message/index.ts +1 -0
- package/src/utils/fetch/fetchSSE.ts +24 -1
@@ -21,6 +21,7 @@ import {
|
|
21
21
|
CreateMessageParams,
|
22
22
|
MessageItem,
|
23
23
|
ModelRankItem,
|
24
|
+
UpdateMessageParams,
|
24
25
|
} from '@/types/message';
|
25
26
|
import { merge } from '@/utils/merge';
|
26
27
|
import { today } from '@/utils/time';
|
@@ -497,15 +498,25 @@ export class MessageModel {
|
|
497
498
|
};
|
498
499
|
// **************** Update *************** //
|
499
500
|
|
500
|
-
update = async (id: string, message: Partial<
|
501
|
-
return this.db
|
502
|
-
.
|
503
|
-
.
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
}
|
508
|
-
|
501
|
+
update = async (id: string, { imageList, ...message }: Partial<UpdateMessageParams>) => {
|
502
|
+
return this.db.transaction(async (trx) => {
|
503
|
+
// 1. insert message files
|
504
|
+
if (imageList && imageList.length > 0) {
|
505
|
+
await trx
|
506
|
+
.insert(messagesFiles)
|
507
|
+
.values(imageList.map((file) => ({ fileId: file.id, messageId: id })));
|
508
|
+
}
|
509
|
+
|
510
|
+
return trx
|
511
|
+
.update(messages)
|
512
|
+
.set({
|
513
|
+
...message,
|
514
|
+
// TODO: need a better way to handle this
|
515
|
+
// TODO: but I forget why 🤡
|
516
|
+
role: message.role as any,
|
517
|
+
})
|
518
|
+
.where(and(eq(messages.id, id), eq(messages.userId, this.userId)));
|
519
|
+
});
|
509
520
|
};
|
510
521
|
|
511
522
|
updatePluginState = async (id: string, state: Record<string, any>) => {
|
@@ -0,0 +1,58 @@
|
|
1
|
+
// @vitest-environment node
|
2
|
+
import { TRPCError } from '@trpc/server';
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
4
|
+
|
5
|
+
import { UserModel, UserNotFoundError } from '@/database/server/models/user';
|
6
|
+
|
7
|
+
describe('UserNotFoundError', () => {
|
8
|
+
it('should extend TRPCError with correct code and message', () => {
|
9
|
+
const error = new UserNotFoundError();
|
10
|
+
|
11
|
+
expect(error).toBeInstanceOf(TRPCError);
|
12
|
+
expect(error.code).toBe('UNAUTHORIZED');
|
13
|
+
expect(error.message).toBe('user not found');
|
14
|
+
});
|
15
|
+
});
|
16
|
+
|
17
|
+
describe('UserModel', () => {
|
18
|
+
const mockDb = {
|
19
|
+
query: {
|
20
|
+
users: {
|
21
|
+
findFirst: vi.fn(),
|
22
|
+
},
|
23
|
+
},
|
24
|
+
};
|
25
|
+
|
26
|
+
const mockUserId = 'test-user-id';
|
27
|
+
const userModel = new UserModel(mockDb as any, mockUserId);
|
28
|
+
|
29
|
+
describe('getUserRegistrationDuration', () => {
|
30
|
+
it('should return default values when user not found', async () => {
|
31
|
+
mockDb.query.users.findFirst.mockResolvedValue(null);
|
32
|
+
|
33
|
+
const result = await userModel.getUserRegistrationDuration();
|
34
|
+
|
35
|
+
expect(result).toEqual({
|
36
|
+
createdAt: expect.any(String),
|
37
|
+
duration: 1,
|
38
|
+
updatedAt: expect.any(String),
|
39
|
+
});
|
40
|
+
});
|
41
|
+
|
42
|
+
it('should calculate duration correctly for existing user', async () => {
|
43
|
+
const createdAt = new Date('2024-01-01');
|
44
|
+
mockDb.query.users.findFirst.mockResolvedValue({
|
45
|
+
createdAt,
|
46
|
+
});
|
47
|
+
|
48
|
+
const result = await userModel.getUserRegistrationDuration();
|
49
|
+
|
50
|
+
expect(result).toEqual({
|
51
|
+
createdAt: '2024-01-01',
|
52
|
+
duration: expect.any(Number),
|
53
|
+
updatedAt: expect.any(String),
|
54
|
+
});
|
55
|
+
expect(result.duration).toBeGreaterThan(0);
|
56
|
+
});
|
57
|
+
});
|
58
|
+
});
|
@@ -61,7 +61,7 @@ const CloudBanner = memo<{ mobile?: boolean }>(({ mobile }) => {
|
|
61
61
|
<b>{t('alert.cloud.title', { name: LOBE_CHAT_CLOUD })}:</b>
|
62
62
|
<span>
|
63
63
|
{t(mobile ? 'alert.cloud.descOnMobile' : 'alert.cloud.desc', {
|
64
|
-
credit: new Intl.NumberFormat('en-US').format(
|
64
|
+
credit: new Intl.NumberFormat('en-US').format(500_000),
|
65
65
|
name: LOBE_CHAT_CLOUD,
|
66
66
|
})}
|
67
67
|
</span>
|
@@ -2,6 +2,7 @@ import { ReactNode, memo } from 'react';
|
|
2
2
|
import { Flexbox } from 'react-layout-kit';
|
3
3
|
|
4
4
|
import { LOADING_FLAT } from '@/const/message';
|
5
|
+
import ImageFileListViewer from '@/features/Conversation/Messages/User/ImageFileListViewer';
|
5
6
|
import { useChatStore } from '@/store/chat';
|
6
7
|
import { aiChatSelectors, chatSelectors } from '@/store/chat/selectors';
|
7
8
|
import { ChatMessage } from '@/types/message';
|
@@ -17,7 +18,7 @@ export const AssistantMessage = memo<
|
|
17
18
|
ChatMessage & {
|
18
19
|
editableContent: ReactNode;
|
19
20
|
}
|
20
|
-
>(({ id, tools, content, chunksList, search, ...props }) => {
|
21
|
+
>(({ id, tools, content, chunksList, search, imageList, ...props }) => {
|
21
22
|
const editing = useChatStore(chatSelectors.isMessageEditing(id));
|
22
23
|
const generating = useChatStore(chatSelectors.isMessageGenerating(id));
|
23
24
|
|
@@ -28,6 +29,7 @@ export const AssistantMessage = memo<
|
|
28
29
|
const isIntentUnderstanding = useChatStore(aiChatSelectors.isIntentUnderstanding(id));
|
29
30
|
|
30
31
|
const showSearch = !!search && !!search.citations?.length;
|
32
|
+
const showImageItems = !!imageList && imageList.length > 0;
|
31
33
|
|
32
34
|
// remove \n to avoid empty content
|
33
35
|
// refs: https://github.com/lobehub/lobe-chat/pull/6153
|
@@ -64,6 +66,7 @@ export const AssistantMessage = memo<
|
|
64
66
|
/>
|
65
67
|
)
|
66
68
|
)}
|
69
|
+
{showImageItems && <ImageFileListViewer items={imageList} />}
|
67
70
|
{tools && (
|
68
71
|
<Flexbox gap={8}>
|
69
72
|
{tools.map((toolCall, index) => (
|
@@ -12,16 +12,16 @@ export const UserMessage = memo<
|
|
12
12
|
ChatMessage & {
|
13
13
|
editableContent: ReactNode;
|
14
14
|
}
|
15
|
-
>(({ id, editableContent, content,
|
15
|
+
>(({ id, editableContent, content, imageList, fileList }) => {
|
16
16
|
if (content === LOADING_FLAT) return <BubblesLoading />;
|
17
17
|
|
18
18
|
return (
|
19
19
|
<Flexbox gap={8} id={id}>
|
20
20
|
{editableContent}
|
21
|
-
{
|
22
|
-
{
|
21
|
+
{imageList && imageList?.length > 0 && <ImageFileListViewer items={imageList} />}
|
22
|
+
{fileList && fileList?.length > 0 && (
|
23
23
|
<div style={{ marginTop: 8 }}>
|
24
|
-
<FileListViewer items={
|
24
|
+
<FileListViewer items={fileList} />
|
25
25
|
</div>
|
26
26
|
)}
|
27
27
|
</Flexbox>
|
@@ -10,7 +10,6 @@ import {
|
|
10
10
|
SchemaType,
|
11
11
|
} from '@google/generative-ai';
|
12
12
|
|
13
|
-
import { VertexAIStream } from '@/libs/agent-runtime/utils/streams/vertex-ai';
|
14
13
|
import type { ChatModelCard } from '@/types/llm';
|
15
14
|
import { imageUrlToBase64 } from '@/utils/imageToBase64';
|
16
15
|
import { safeParseJSON } from '@/utils/safeParseJSON';
|
@@ -28,9 +27,25 @@ import { ModelProvider } from '../types/type';
|
|
28
27
|
import { AgentRuntimeError } from '../utils/createError';
|
29
28
|
import { debugStream } from '../utils/debugStream';
|
30
29
|
import { StreamingResponse } from '../utils/response';
|
31
|
-
import {
|
30
|
+
import {
|
31
|
+
GoogleGenerativeAIStream,
|
32
|
+
VertexAIStream,
|
33
|
+
convertIterableToStream,
|
34
|
+
} from '../utils/streams';
|
32
35
|
import { parseDataUri } from '../utils/uriParser';
|
33
36
|
|
37
|
+
const modelsOffSafetySettings = new Set(['gemini-2.0-flash-exp']);
|
38
|
+
|
39
|
+
const modelsWithModalities = new Set([
|
40
|
+
'gemini-2.0-flash-exp',
|
41
|
+
'gemini-2.0-flash-exp-image-generation',
|
42
|
+
]);
|
43
|
+
|
44
|
+
const modelsDisableInstuction = new Set([
|
45
|
+
'gemini-2.0-flash-exp',
|
46
|
+
'gemini-2.0-flash-exp-image-generation',
|
47
|
+
]);
|
48
|
+
|
34
49
|
export interface GoogleModelCard {
|
35
50
|
displayName: string;
|
36
51
|
inputTokenLimit: number;
|
@@ -50,8 +65,7 @@ enum HarmBlockThreshold {
|
|
50
65
|
}
|
51
66
|
|
52
67
|
function getThreshold(model: string): HarmBlockThreshold {
|
53
|
-
|
54
|
-
if (useOFF.includes(model)) {
|
68
|
+
if (modelsOffSafetySettings.has(model)) {
|
55
69
|
return 'OFF' as HarmBlockThreshold; // https://discuss.ai.google.dev/t/59352
|
56
70
|
}
|
57
71
|
return HarmBlockThreshold.BLOCK_NONE;
|
@@ -94,6 +108,8 @@ export class LobeGoogleAI implements LobeRuntimeAI {
|
|
94
108
|
{
|
95
109
|
generationConfig: {
|
96
110
|
maxOutputTokens: payload.max_tokens,
|
111
|
+
// @ts-expect-error - Google SDK 0.24.0 doesn't have this property for now with
|
112
|
+
response_modalities: modelsWithModalities.has(model) ? ['Text', 'Image'] : undefined,
|
97
113
|
temperature: payload.temperature,
|
98
114
|
topP: payload.top_p,
|
99
115
|
},
|
@@ -123,7 +139,9 @@ export class LobeGoogleAI implements LobeRuntimeAI {
|
|
123
139
|
)
|
124
140
|
.generateContentStream({
|
125
141
|
contents,
|
126
|
-
systemInstruction:
|
142
|
+
systemInstruction: modelsDisableInstuction.has(model)
|
143
|
+
? undefined
|
144
|
+
: (payload.system as string),
|
127
145
|
tools: this.buildGoogleTools(payload.tools, payload),
|
128
146
|
});
|
129
147
|
|
@@ -94,4 +94,193 @@ describe('GoogleGenerativeAIStream', () => {
|
|
94
94
|
|
95
95
|
expect(chunks).toEqual([]);
|
96
96
|
});
|
97
|
+
|
98
|
+
it('should handle image', async () => {
|
99
|
+
vi.spyOn(uuidModule, 'nanoid').mockReturnValueOnce('1');
|
100
|
+
|
101
|
+
const data = {
|
102
|
+
candidates: [
|
103
|
+
{
|
104
|
+
content: {
|
105
|
+
parts: [{ inlineData: { mimeType: 'image/png', data: 'iVBORw0KGgoAA' } }],
|
106
|
+
role: 'model',
|
107
|
+
},
|
108
|
+
index: 0,
|
109
|
+
},
|
110
|
+
],
|
111
|
+
usageMetadata: {
|
112
|
+
promptTokenCount: 6,
|
113
|
+
totalTokenCount: 6,
|
114
|
+
promptTokensDetails: [{ modality: 'TEXT', tokenCount: 6 }],
|
115
|
+
},
|
116
|
+
modelVersion: 'gemini-2.0-flash-exp',
|
117
|
+
};
|
118
|
+
const mockGenerateContentResponse = (text: string, functionCalls?: any[]) =>
|
119
|
+
({
|
120
|
+
text: () => text,
|
121
|
+
functionCall: () => functionCalls?.[0],
|
122
|
+
functionCalls: () => functionCalls,
|
123
|
+
}) as EnhancedGenerateContentResponse;
|
124
|
+
|
125
|
+
const mockGoogleStream = new ReadableStream({
|
126
|
+
start(controller) {
|
127
|
+
controller.enqueue(data);
|
128
|
+
|
129
|
+
controller.close();
|
130
|
+
},
|
131
|
+
});
|
132
|
+
|
133
|
+
const protocolStream = GoogleGenerativeAIStream(mockGoogleStream);
|
134
|
+
|
135
|
+
const decoder = new TextDecoder();
|
136
|
+
const chunks = [];
|
137
|
+
|
138
|
+
// @ts-ignore
|
139
|
+
for await (const chunk of protocolStream) {
|
140
|
+
chunks.push(decoder.decode(chunk, { stream: true }));
|
141
|
+
}
|
142
|
+
|
143
|
+
expect(chunks).toEqual([
|
144
|
+
// image
|
145
|
+
'id: chat_1\n',
|
146
|
+
'event: base64_image\n',
|
147
|
+
`data: "data:image/png;base64,iVBORw0KGgoAA"\n\n`,
|
148
|
+
]);
|
149
|
+
});
|
150
|
+
|
151
|
+
it('should handle token count', async () => {
|
152
|
+
vi.spyOn(uuidModule, 'nanoid').mockReturnValueOnce('1');
|
153
|
+
|
154
|
+
const data = {
|
155
|
+
candidates: [{ content: { role: 'model' }, finishReason: 'STOP', index: 0 }],
|
156
|
+
usageMetadata: {
|
157
|
+
promptTokenCount: 266,
|
158
|
+
totalTokenCount: 266,
|
159
|
+
promptTokensDetails: [
|
160
|
+
{ modality: 'TEXT', tokenCount: 8 },
|
161
|
+
{ modality: 'IMAGE', tokenCount: 258 },
|
162
|
+
],
|
163
|
+
},
|
164
|
+
modelVersion: 'gemini-2.0-flash-exp',
|
165
|
+
};
|
166
|
+
|
167
|
+
const mockGoogleStream = new ReadableStream({
|
168
|
+
start(controller) {
|
169
|
+
controller.enqueue(data);
|
170
|
+
|
171
|
+
controller.close();
|
172
|
+
},
|
173
|
+
});
|
174
|
+
|
175
|
+
const protocolStream = GoogleGenerativeAIStream(mockGoogleStream);
|
176
|
+
|
177
|
+
const decoder = new TextDecoder();
|
178
|
+
const chunks = [];
|
179
|
+
|
180
|
+
// @ts-ignore
|
181
|
+
for await (const chunk of protocolStream) {
|
182
|
+
chunks.push(decoder.decode(chunk, { stream: true }));
|
183
|
+
}
|
184
|
+
|
185
|
+
expect(chunks).toEqual([
|
186
|
+
// stop
|
187
|
+
'id: chat_1\n',
|
188
|
+
'event: stop\n',
|
189
|
+
`data: "STOP"\n\n`,
|
190
|
+
// usage
|
191
|
+
'id: chat_1\n',
|
192
|
+
'event: usage\n',
|
193
|
+
`data: {"inputImageTokens":258,"inputTextTokens":8,"totalInputTokens":266,"totalTokens":266}\n\n`,
|
194
|
+
]);
|
195
|
+
});
|
196
|
+
|
197
|
+
it('should handle stop with content', async () => {
|
198
|
+
vi.spyOn(uuidModule, 'nanoid').mockReturnValueOnce('1');
|
199
|
+
|
200
|
+
const data = [
|
201
|
+
{
|
202
|
+
candidates: [
|
203
|
+
{
|
204
|
+
content: { parts: [{ text: '234' }], role: 'model' },
|
205
|
+
safetyRatings: [
|
206
|
+
{ category: 'HARM_CATEGORY_HATE_SPEECH', probability: 'NEGLIGIBLE' },
|
207
|
+
{ category: 'HARM_CATEGORY_DANGEROUS_CONTENT', probability: 'NEGLIGIBLE' },
|
208
|
+
{ category: 'HARM_CATEGORY_HARASSMENT', probability: 'NEGLIGIBLE' },
|
209
|
+
{ category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', probability: 'NEGLIGIBLE' },
|
210
|
+
],
|
211
|
+
},
|
212
|
+
],
|
213
|
+
text: () => '234',
|
214
|
+
usageMetadata: {
|
215
|
+
promptTokenCount: 20,
|
216
|
+
totalTokenCount: 20,
|
217
|
+
promptTokensDetails: [{ modality: 'TEXT', tokenCount: 20 }],
|
218
|
+
},
|
219
|
+
modelVersion: 'gemini-2.0-flash-exp-image-generation',
|
220
|
+
},
|
221
|
+
{
|
222
|
+
text: () => '567890\n',
|
223
|
+
candidates: [
|
224
|
+
{
|
225
|
+
content: { parts: [{ text: '567890\n' }], role: 'model' },
|
226
|
+
finishReason: 'STOP',
|
227
|
+
safetyRatings: [
|
228
|
+
{ category: 'HARM_CATEGORY_HATE_SPEECH', probability: 'NEGLIGIBLE' },
|
229
|
+
{ category: 'HARM_CATEGORY_DANGEROUS_CONTENT', probability: 'NEGLIGIBLE' },
|
230
|
+
{ category: 'HARM_CATEGORY_HARASSMENT', probability: 'NEGLIGIBLE' },
|
231
|
+
{ category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', probability: 'NEGLIGIBLE' },
|
232
|
+
],
|
233
|
+
},
|
234
|
+
],
|
235
|
+
usageMetadata: {
|
236
|
+
promptTokenCount: 19,
|
237
|
+
candidatesTokenCount: 11,
|
238
|
+
totalTokenCount: 30,
|
239
|
+
promptTokensDetails: [{ modality: 'TEXT', tokenCount: 19 }],
|
240
|
+
candidatesTokensDetails: [{ modality: 'TEXT', tokenCount: 11 }],
|
241
|
+
},
|
242
|
+
modelVersion: 'gemini-2.0-flash-exp-image-generation',
|
243
|
+
},
|
244
|
+
];
|
245
|
+
|
246
|
+
const mockGoogleStream = new ReadableStream({
|
247
|
+
start(controller) {
|
248
|
+
data.forEach((item) => {
|
249
|
+
controller.enqueue(item);
|
250
|
+
});
|
251
|
+
|
252
|
+
controller.close();
|
253
|
+
},
|
254
|
+
});
|
255
|
+
|
256
|
+
const protocolStream = GoogleGenerativeAIStream(mockGoogleStream);
|
257
|
+
|
258
|
+
const decoder = new TextDecoder();
|
259
|
+
const chunks = [];
|
260
|
+
|
261
|
+
// @ts-ignore
|
262
|
+
for await (const chunk of protocolStream) {
|
263
|
+
chunks.push(decoder.decode(chunk, { stream: true }));
|
264
|
+
}
|
265
|
+
|
266
|
+
expect(chunks).toEqual(
|
267
|
+
[
|
268
|
+
'id: chat_1',
|
269
|
+
'event: text',
|
270
|
+
'data: "234"\n',
|
271
|
+
|
272
|
+
'id: chat_1',
|
273
|
+
'event: text',
|
274
|
+
`data: "567890\\n"\n`,
|
275
|
+
// stop
|
276
|
+
'id: chat_1',
|
277
|
+
'event: stop',
|
278
|
+
`data: "STOP"\n`,
|
279
|
+
// usage
|
280
|
+
'id: chat_1',
|
281
|
+
'event: usage',
|
282
|
+
`data: {"inputTextTokens":19,"totalInputTokens":19,"totalOutputTokens":11,"totalTokens":30}\n`,
|
283
|
+
].map((i) => i + '\n'),
|
284
|
+
);
|
285
|
+
});
|
97
286
|
});
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import { EnhancedGenerateContentResponse } from '@google/generative-ai';
|
2
2
|
|
3
|
+
import { ModelTokensUsage } from '@/types/message';
|
3
4
|
import { GroundingSearch } from '@/types/search';
|
4
5
|
import { nanoid } from '@/utils/uuid';
|
5
6
|
|
@@ -18,7 +19,7 @@ const transformGoogleGenerativeAIStream = (
|
|
18
19
|
context: StreamContext,
|
19
20
|
): StreamProtocolChunk | StreamProtocolChunk[] => {
|
20
21
|
// maybe need another structure to add support for multiple choices
|
21
|
-
const functionCalls = chunk.functionCalls();
|
22
|
+
const functionCalls = chunk.functionCalls?.();
|
22
23
|
|
23
24
|
if (functionCalls) {
|
24
25
|
return {
|
@@ -37,30 +38,76 @@ const transformGoogleGenerativeAIStream = (
|
|
37
38
|
type: 'tool_calls',
|
38
39
|
};
|
39
40
|
}
|
40
|
-
const text = chunk.text();
|
41
41
|
|
42
|
-
|
43
|
-
const { webSearchQueries, groundingSupports, groundingChunks } =
|
44
|
-
chunk.candidates[0].groundingMetadata;
|
45
|
-
console.log({ groundingChunks, groundingSupports, webSearchQueries });
|
42
|
+
const text = chunk.text?.();
|
46
43
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
44
|
+
if (chunk.candidates) {
|
45
|
+
const candidate = chunk.candidates[0];
|
46
|
+
|
47
|
+
// return the grounding
|
48
|
+
if (candidate.groundingMetadata) {
|
49
|
+
const { webSearchQueries, groundingChunks } = candidate.groundingMetadata;
|
50
|
+
|
51
|
+
return [
|
52
|
+
{ data: text, id: context.id, type: 'text' },
|
53
|
+
{
|
54
|
+
data: {
|
55
|
+
citations: groundingChunks?.map((chunk) => ({
|
56
|
+
// google 返回的 uri 是经过 google 自己处理过的 url,因此无法展现真实的 favicon
|
57
|
+
// 需要使用 title 作为替换
|
58
|
+
favicon: chunk.web?.title,
|
59
|
+
title: chunk.web?.title,
|
60
|
+
url: chunk.web?.uri,
|
61
|
+
})),
|
62
|
+
searchQueries: webSearchQueries,
|
63
|
+
} as GroundingSearch,
|
64
|
+
id: context.id,
|
65
|
+
type: 'grounding',
|
66
|
+
},
|
67
|
+
];
|
68
|
+
}
|
69
|
+
|
70
|
+
if (candidate.finishReason) {
|
71
|
+
if (chunk.usageMetadata) {
|
72
|
+
const usage = chunk.usageMetadata;
|
73
|
+
return [
|
74
|
+
!!text ? { data: text, id: context?.id, type: 'text' } : undefined,
|
75
|
+
{ data: candidate.finishReason, id: context?.id, type: 'stop' },
|
76
|
+
{
|
77
|
+
data: {
|
78
|
+
// TODO: Google SDK 0.24.0 don't have promptTokensDetails types
|
79
|
+
inputImageTokens: (usage as any).promptTokensDetails?.find(
|
80
|
+
(i: any) => i.modality === 'IMAGE',
|
81
|
+
)?.tokenCount,
|
82
|
+
inputTextTokens: (usage as any).promptTokensDetails?.find(
|
83
|
+
(i: any) => i.modality === 'TEXT',
|
84
|
+
)?.tokenCount,
|
85
|
+
totalInputTokens: usage.promptTokenCount,
|
86
|
+
totalOutputTokens: usage.candidatesTokenCount,
|
87
|
+
totalTokens: usage.totalTokenCount,
|
88
|
+
} as ModelTokensUsage,
|
89
|
+
id: context?.id,
|
90
|
+
type: 'usage',
|
91
|
+
},
|
92
|
+
].filter(Boolean) as StreamProtocolChunk[];
|
93
|
+
}
|
94
|
+
return { data: candidate.finishReason, id: context?.id, type: 'stop' };
|
95
|
+
}
|
96
|
+
|
97
|
+
if (!!text?.trim()) return { data: text, id: context?.id, type: 'text' };
|
98
|
+
|
99
|
+
// streaming the image
|
100
|
+
if (Array.isArray(candidate.content.parts) && candidate.content.parts.length > 0) {
|
101
|
+
const part = candidate.content.parts[0];
|
102
|
+
|
103
|
+
if (part && part.inlineData && part.inlineData.data && part.inlineData.mimeType) {
|
104
|
+
return {
|
105
|
+
data: `data:${part.inlineData.mimeType};base64,${part.inlineData.data}`,
|
106
|
+
id: context.id,
|
107
|
+
type: 'base64_image',
|
108
|
+
};
|
109
|
+
}
|
110
|
+
}
|
64
111
|
}
|
65
112
|
|
66
113
|
return {
|
@@ -78,6 +78,7 @@ export default {
|
|
78
78
|
custom: '自定义模型,默认设定同时支持函数调用与视觉识别,请根据实际情况验证上述能力的可用性',
|
79
79
|
file: '该模型支持上传文件读取与识别',
|
80
80
|
functionCall: '该模型支持函数调用(Function Call)',
|
81
|
+
imageOutput: '该模型支持生成图片',
|
81
82
|
reasoning: '该模型支持深度思考',
|
82
83
|
search: '该模型支持联网搜索',
|
83
84
|
tokens: '该模型单个会话最多支持 {{tokens}} Tokens',
|
package/src/services/chat.ts
CHANGED
@@ -438,6 +438,8 @@ class ChatService {
|
|
438
438
|
provider: params.provider!,
|
439
439
|
});
|
440
440
|
|
441
|
+
// remove plugins
|
442
|
+
delete params.plugins;
|
441
443
|
await this.getChatCompletion(
|
442
444
|
{ ...params, messages: oaiMessages, tools },
|
443
445
|
{
|
@@ -474,7 +476,7 @@ class ChatService {
|
|
474
476
|
// handle content type for vision model
|
475
477
|
// for the models with visual ability, add image url to content
|
476
478
|
// refs: https://platform.openai.com/docs/guides/vision/quick-start
|
477
|
-
const
|
479
|
+
const getUserContent = (m: ChatMessage) => {
|
478
480
|
// only if message doesn't have images and files, then return the plain content
|
479
481
|
if ((!m.imageList || m.imageList.length === 0) && (!m.fileList || m.fileList.length === 0))
|
480
482
|
return m.content;
|
@@ -490,27 +492,43 @@ class ChatService {
|
|
490
492
|
] as UserMessageContentPart[];
|
491
493
|
};
|
492
494
|
|
495
|
+
const getAssistantContent = (m: ChatMessage) => {
|
496
|
+
// signature is a signal of anthropic thinking mode
|
497
|
+
const shouldIncludeThinking = m.reasoning && !!m.reasoning?.signature;
|
498
|
+
|
499
|
+
if (shouldIncludeThinking) {
|
500
|
+
return [
|
501
|
+
{
|
502
|
+
signature: m.reasoning!.signature,
|
503
|
+
thinking: m.reasoning!.content,
|
504
|
+
type: 'thinking',
|
505
|
+
},
|
506
|
+
{ text: m.content, type: 'text' },
|
507
|
+
] as UserMessageContentPart[];
|
508
|
+
}
|
509
|
+
// only if message doesn't have images and files, then return the plain content
|
510
|
+
|
511
|
+
if (m.imageList && m.imageList.length > 0) {
|
512
|
+
return [
|
513
|
+
!!m.content ? { text: m.content, type: 'text' } : undefined,
|
514
|
+
...m.imageList.map(
|
515
|
+
(i) => ({ image_url: { detail: 'auto', url: i.url }, type: 'image_url' }) as const,
|
516
|
+
),
|
517
|
+
].filter(Boolean) as UserMessageContentPart[];
|
518
|
+
}
|
519
|
+
|
520
|
+
return m.content;
|
521
|
+
};
|
522
|
+
|
493
523
|
let postMessages = messages.map((m): OpenAIChatMessage => {
|
494
524
|
const supportTools = isCanUseFC(model, provider);
|
495
525
|
switch (m.role) {
|
496
526
|
case 'user': {
|
497
|
-
return { content:
|
527
|
+
return { content: getUserContent(m), role: m.role };
|
498
528
|
}
|
499
529
|
|
500
530
|
case 'assistant': {
|
501
|
-
|
502
|
-
const shouldIncludeThinking = m.reasoning && !!m.reasoning?.signature;
|
503
|
-
|
504
|
-
const content = shouldIncludeThinking
|
505
|
-
? [
|
506
|
-
{
|
507
|
-
signature: m.reasoning!.signature,
|
508
|
-
thinking: m.reasoning!.content,
|
509
|
-
type: 'thinking',
|
510
|
-
} as any,
|
511
|
-
{ text: m.content, type: 'text' },
|
512
|
-
]
|
513
|
-
: m.content;
|
531
|
+
const content = getAssistantContent(m);
|
514
532
|
|
515
533
|
if (!supportTools) {
|
516
534
|
return { content, role: m.role };
|
@@ -11,6 +11,8 @@ export class ClientService extends BaseClientService implements IFileService {
|
|
11
11
|
}
|
12
12
|
|
13
13
|
createFile: IFileService['createFile'] = async (file) => {
|
14
|
+
const { isExist } = await this.fileModel.checkHash(file.hash!);
|
15
|
+
|
14
16
|
// save to local storage
|
15
17
|
// we may want to save to a remote server later
|
16
18
|
const res = await this.fileModel.create(
|
@@ -23,7 +25,7 @@ export class ClientService extends BaseClientService implements IFileService {
|
|
23
25
|
size: file.size,
|
24
26
|
url: file.url!,
|
25
27
|
},
|
26
|
-
|
28
|
+
!isExist,
|
27
29
|
);
|
28
30
|
|
29
31
|
// get file to base64 url
|
@@ -64,8 +64,8 @@ export class ServerService implements IMessageService {
|
|
64
64
|
return lambdaClient.message.updateMessagePlugin.mutate({ id, value: { arguments: args } });
|
65
65
|
};
|
66
66
|
|
67
|
-
updateMessage: IMessageService['updateMessage'] = async (id,
|
68
|
-
return lambdaClient.message.update.mutate({ id, value
|
67
|
+
updateMessage: IMessageService['updateMessage'] = async (id, value) => {
|
68
|
+
return lambdaClient.message.update.mutate({ id, value });
|
69
69
|
};
|
70
70
|
|
71
71
|
updateMessageTranslate: IMessageService['updateMessageTranslate'] = async (id, translate) => {
|