@lobehub/chat 1.70.11 → 1.71.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/.github/workflows/sync-database-schema.yml +25 -0
- package/CHANGELOG.md +25 -0
- package/changelog/v1.json +9 -0
- package/docs/developer/database-schema.dbml +569 -0
- package/locales/ar/models.json +3 -0
- package/locales/bg-BG/models.json +3 -0
- package/locales/de-DE/models.json +3 -0
- package/locales/en-US/models.json +3 -0
- package/locales/es-ES/models.json +3 -0
- package/locales/fa-IR/models.json +3 -0
- package/locales/fr-FR/models.json +3 -0
- package/locales/it-IT/models.json +3 -0
- package/locales/ja-JP/models.json +3 -0
- package/locales/ko-KR/models.json +3 -0
- package/locales/nl-NL/models.json +3 -0
- package/locales/pl-PL/models.json +3 -0
- package/locales/pt-BR/models.json +3 -0
- package/locales/ru-RU/models.json +3 -0
- package/locales/tr-TR/models.json +3 -0
- package/locales/vi-VN/models.json +3 -0
- package/locales/zh-CN/models.json +3 -0
- package/locales/zh-TW/models.json +3 -0
- package/package.json +6 -2
- package/scripts/dbmlWorkflow/index.ts +11 -0
- package/src/config/aiModels/google.ts +17 -0
- 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 +8 -2
- package/src/libs/agent-runtime/utils/streams/google-ai.test.ts +99 -0
- package/src/libs/agent-runtime/utils/streams/google-ai.ts +69 -23
- package/src/libs/agent-runtime/utils/streams/protocol.ts +2 -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/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
@@ -833,6 +833,9 @@
|
|
833
833
|
"gemini-2.0-flash-001": {
|
834
834
|
"description": "Gemini 2.0 Flash propose des fonctionnalités et des améliorations de nouvelle génération, y compris une vitesse exceptionnelle, l'utilisation d'outils natifs, la génération multimodale et une fenêtre de contexte de 1M tokens."
|
835
835
|
},
|
836
|
+
"gemini-2.0-flash-exp": {
|
837
|
+
"description": "Modèle variant Gemini 2.0 Flash, optimisé pour des objectifs tels que le rapport coût-efficacité et la faible latence."
|
838
|
+
},
|
836
839
|
"gemini-2.0-flash-lite": {
|
837
840
|
"description": "Une variante du modèle Gemini 2.0 Flash, optimisée pour des objectifs tels que le rapport coût-efficacité et la faible latence."
|
838
841
|
},
|
@@ -833,6 +833,9 @@
|
|
833
833
|
"gemini-2.0-flash-001": {
|
834
834
|
"description": "Gemini 2.0 Flash offre funzionalità e miglioramenti di nuova generazione, tra cui velocità eccezionale, utilizzo di strumenti nativi, generazione multimodale e una finestra di contesto di 1M token."
|
835
835
|
},
|
836
|
+
"gemini-2.0-flash-exp": {
|
837
|
+
"description": "Gemini 2.0 Flash è una variante del modello ottimizzata per obiettivi come il rapporto costo-efficacia e la bassa latenza."
|
838
|
+
},
|
836
839
|
"gemini-2.0-flash-lite": {
|
837
840
|
"description": "Gemini 2.0 Flash è una variante del modello Flash, ottimizzata per obiettivi come il rapporto costo-efficacia e la bassa latenza."
|
838
841
|
},
|
@@ -833,6 +833,9 @@
|
|
833
833
|
"gemini-2.0-flash-001": {
|
834
834
|
"description": "Gemini 2.0 Flashは、卓越した速度、ネイティブツールの使用、マルチモーダル生成、1Mトークンのコンテキストウィンドウを含む次世代の機能と改善を提供します。"
|
835
835
|
},
|
836
|
+
"gemini-2.0-flash-exp": {
|
837
|
+
"description": "Gemini 2.0 Flash モデルのバリアントで、コスト効率と低遅延などの目標に最適化されています。"
|
838
|
+
},
|
836
839
|
"gemini-2.0-flash-lite": {
|
837
840
|
"description": "Gemini 2.0 Flashモデルのバリアントで、コスト効率と低遅延などの目標に最適化されています。"
|
838
841
|
},
|
@@ -833,6 +833,9 @@
|
|
833
833
|
"gemini-2.0-flash-001": {
|
834
834
|
"description": "Gemini 2.0 Flash는 뛰어난 속도, 원주율 도구 사용, 다중 모달 생성 및 1M 토큰 문맥 창을 포함한 차세대 기능과 개선 사항을 제공합니다."
|
835
835
|
},
|
836
|
+
"gemini-2.0-flash-exp": {
|
837
|
+
"description": "Gemini 2.0 Flash 모델 변형으로, 비용 효율성과 저지연 등의 목표를 위해 최적화되었습니다."
|
838
|
+
},
|
836
839
|
"gemini-2.0-flash-lite": {
|
837
840
|
"description": "Gemini 2.0 플래시 모델 변형으로, 비용 효율성과 낮은 지연 시간 등의 목표를 위해 최적화되었습니다."
|
838
841
|
},
|
@@ -833,6 +833,9 @@
|
|
833
833
|
"gemini-2.0-flash-001": {
|
834
834
|
"description": "Gemini 2.0 Flash biedt next-gen functies en verbeteringen, waaronder uitstekende snelheid, native toolgebruik, multimodale generatie en een contextvenster van 1M tokens."
|
835
835
|
},
|
836
|
+
"gemini-2.0-flash-exp": {
|
837
|
+
"description": "Gemini 2.0 Flash modelvariant, geoptimaliseerd voor kosteneffectiviteit en lage latentie."
|
838
|
+
},
|
836
839
|
"gemini-2.0-flash-lite": {
|
837
840
|
"description": "Gemini 2.0 Flash is een modelvariant die is geoptimaliseerd voor kosteneffectiviteit en lage latentie."
|
838
841
|
},
|
@@ -833,6 +833,9 @@
|
|
833
833
|
"gemini-2.0-flash-001": {
|
834
834
|
"description": "Gemini 2.0 Flash oferuje funkcje i ulepszenia nowej generacji, w tym doskonałą prędkość, natywne korzystanie z narzędzi, generowanie multimodalne oraz okno kontekstowe o długości 1M tokenów."
|
835
835
|
},
|
836
|
+
"gemini-2.0-flash-exp": {
|
837
|
+
"description": "Gemini 2.0 Flash to wariant modelu, zoptymalizowany pod kątem efektywności kosztowej i niskiego opóźnienia."
|
838
|
+
},
|
836
839
|
"gemini-2.0-flash-lite": {
|
837
840
|
"description": "Gemini 2.0 Flash to wariant modelu, zoptymalizowany pod kątem efektywności kosztowej i niskiego opóźnienia."
|
838
841
|
},
|
@@ -833,6 +833,9 @@
|
|
833
833
|
"gemini-2.0-flash-001": {
|
834
834
|
"description": "Gemini 2.0 Flash oferece funcionalidades e melhorias de próxima geração, incluindo velocidade excepcional, uso nativo de ferramentas, geração multimodal e uma janela de contexto de 1M tokens."
|
835
835
|
},
|
836
|
+
"gemini-2.0-flash-exp": {
|
837
|
+
"description": "Variante do modelo Gemini 2.0 Flash, otimizada para custo-benefício e baixa latência."
|
838
|
+
},
|
836
839
|
"gemini-2.0-flash-lite": {
|
837
840
|
"description": "Variante do modelo Gemini 2.0 Flash, otimizada para custo-benefício e baixa latência."
|
838
841
|
},
|
@@ -833,6 +833,9 @@
|
|
833
833
|
"gemini-2.0-flash-001": {
|
834
834
|
"description": "Gemini 2.0 Flash предлагает функции следующего поколения и улучшения, включая выдающуюся скорость, использование встроенных инструментов, многомодальную генерацию и контекстное окно на 1M токенов."
|
835
835
|
},
|
836
|
+
"gemini-2.0-flash-exp": {
|
837
|
+
"description": "Модельный вариант Gemini 2.0 Flash, оптимизированный для достижения таких целей, как экономическая эффективность и низкая задержка."
|
838
|
+
},
|
836
839
|
"gemini-2.0-flash-lite": {
|
837
840
|
"description": "Модельный вариант Gemini 2.0 Flash, оптимизированный для достижения таких целей, как экономическая эффективность и низкая задержка."
|
838
841
|
},
|
@@ -833,6 +833,9 @@
|
|
833
833
|
"gemini-2.0-flash-001": {
|
834
834
|
"description": "Gemini 2.0 Flash, mükemmel hız, yerel araç kullanımı, çok modlu üretim ve 1M token bağlam penceresi dahil olmak üzere bir sonraki nesil özellikler ve iyileştirmeler sunar."
|
835
835
|
},
|
836
|
+
"gemini-2.0-flash-exp": {
|
837
|
+
"description": "Gemini 2.0 Flash modeli varyantı, maliyet etkinliği ve düşük gecikme gibi hedefler için optimize edilmiştir."
|
838
|
+
},
|
836
839
|
"gemini-2.0-flash-lite": {
|
837
840
|
"description": "Gemini 2.0 Flash model varyantı, maliyet etkinliği ve düşük gecikme gibi hedefler için optimize edilmiştir."
|
838
841
|
},
|
@@ -833,6 +833,9 @@
|
|
833
833
|
"gemini-2.0-flash-001": {
|
834
834
|
"description": "Gemini 2.0 Flash cung cấp các tính năng và cải tiến thế hệ tiếp theo, bao gồm tốc độ vượt trội, sử dụng công cụ bản địa, tạo đa phương tiện và cửa sổ ngữ cảnh 1M token."
|
835
835
|
},
|
836
|
+
"gemini-2.0-flash-exp": {
|
837
|
+
"description": "Biến thể mô hình Gemini 2.0 Flash, được tối ưu hóa cho hiệu quả chi phí và độ trễ thấp."
|
838
|
+
},
|
836
839
|
"gemini-2.0-flash-lite": {
|
837
840
|
"description": "Biến thể mô hình Gemini 2.0 Flash được tối ưu hóa cho hiệu quả chi phí và độ trễ thấp."
|
838
841
|
},
|
@@ -833,6 +833,9 @@
|
|
833
833
|
"gemini-2.0-flash-001": {
|
834
834
|
"description": "Gemini 2.0 Flash 提供下一代功能和改进,包括卓越的速度、原生工具使用、多模态生成和1M令牌上下文窗口。"
|
835
835
|
},
|
836
|
+
"gemini-2.0-flash-exp": {
|
837
|
+
"description": "Gemini 2.0 Flash 模型变体,针对成本效益和低延迟等目标进行了优化。"
|
838
|
+
},
|
836
839
|
"gemini-2.0-flash-lite": {
|
837
840
|
"description": "Gemini 2.0 Flash 模型变体,针对成本效益和低延迟等目标进行了优化。"
|
838
841
|
},
|
@@ -833,6 +833,9 @@
|
|
833
833
|
"gemini-2.0-flash-001": {
|
834
834
|
"description": "Gemini 2.0 Flash 提供下一代功能和改進,包括卓越的速度、原生工具使用、多模態生成和1M令牌上下文窗口。"
|
835
835
|
},
|
836
|
+
"gemini-2.0-flash-exp": {
|
837
|
+
"description": "Gemini 2.0 Flash 模型變體,針對成本效益和低延遲等目標進行了優化。"
|
838
|
+
},
|
836
839
|
"gemini-2.0-flash-lite": {
|
837
840
|
"description": "Gemini 2.0 Flash 模型變體,針對成本效益和低延遲等目標進行了優化。"
|
838
841
|
},
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@lobehub/chat",
|
3
|
-
"version": "1.
|
3
|
+
"version": "1.71.0",
|
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",
|
@@ -35,12 +35,13 @@
|
|
35
35
|
"build-sitemap": "tsx ./scripts/buildSitemapIndex/index.ts",
|
36
36
|
"build:analyze": "ANALYZE=true next build",
|
37
37
|
"build:docker": "DOCKER=true next build && npm run build-sitemap",
|
38
|
-
"db:generate": "drizzle-kit generate && npm run db:generate-client",
|
38
|
+
"db:generate": "drizzle-kit generate && npm run db:generate-client && npm run workflow:dbml",
|
39
39
|
"db:generate-client": "tsx ./scripts/migrateClientDB/compile-migrations.ts",
|
40
40
|
"db:migrate": "MIGRATION_DB=1 tsx ./scripts/migrateServerDB/index.ts",
|
41
41
|
"db:push": "drizzle-kit push",
|
42
42
|
"db:push-test": "NODE_ENV=test drizzle-kit push",
|
43
43
|
"db:studio": "drizzle-kit studio",
|
44
|
+
"db:visualize": "dbdocs build docs/developer/database-schema.dbml --project lobe-chat",
|
44
45
|
"db:z-pull": "drizzle-kit introspect",
|
45
46
|
"dev": "next dev --turbopack -p 3010",
|
46
47
|
"docs:i18n": "lobe-i18n md && npm run lint:md && npm run lint:mdx",
|
@@ -72,6 +73,7 @@
|
|
72
73
|
"workflow:cdn": "tsx ./scripts/cdnWorkflow/index.ts",
|
73
74
|
"workflow:changelog": "tsx ./scripts/changelogWorkflow/index.ts",
|
74
75
|
"workflow:countCharters": "tsx scripts/countEnWord.ts",
|
76
|
+
"workflow:dbml": "tsx ./scripts/dbmlWorkflow/index.ts",
|
75
77
|
"workflow:docs": "tsx ./scripts/docsWorkflow/index.ts",
|
76
78
|
"workflow:i18n": "tsx ./scripts/i18nWorkflow/index.ts",
|
77
79
|
"workflow:mdx": "tsx ./scripts/mdxWorkflow/index.ts",
|
@@ -289,8 +291,10 @@
|
|
289
291
|
"commitlint": "^19.6.1",
|
290
292
|
"consola": "^3.3.3",
|
291
293
|
"crypto-js": "^4.2.0",
|
294
|
+
"dbdocs": "^0.14.3",
|
292
295
|
"dotenv": "^16.4.7",
|
293
296
|
"dpdm-fast": "^1.0.7",
|
297
|
+
"drizzle-dbml-generator": "^0.10.0",
|
294
298
|
"drizzle-kit": "^0.30.1",
|
295
299
|
"eslint": "^8.57.1",
|
296
300
|
"eslint-plugin-mdx": "^3.1.5",
|
@@ -0,0 +1,11 @@
|
|
1
|
+
import { pgGenerate } from 'drizzle-dbml-generator';
|
2
|
+
import { join } from 'node:path';
|
3
|
+
|
4
|
+
import * as schema from '../../src/database/schemas';
|
5
|
+
|
6
|
+
const out = join(__dirname,'../../docs/developer/database-schema.dbml');
|
7
|
+
const relational = true;
|
8
|
+
|
9
|
+
pgGenerate({ out, relational, schema });
|
10
|
+
|
11
|
+
console.log('🏁 dbml generated successful!');
|
@@ -92,6 +92,23 @@ const googleChatModels: AIChatModelCard[] = [
|
|
92
92
|
releasedAt: '2025-02-05',
|
93
93
|
type: 'chat',
|
94
94
|
},
|
95
|
+
{
|
96
|
+
abilities: {
|
97
|
+
vision: true,
|
98
|
+
},
|
99
|
+
contextWindowTokens: 32_768,
|
100
|
+
description: 'Gemini 2.0 Flash 模型变体,针对成本效益和低延迟等目标进行了优化。',
|
101
|
+
displayName: 'Gemini 2.0 Flash Exp',
|
102
|
+
enabled: true,
|
103
|
+
id: 'gemini-2.0-flash-exp',
|
104
|
+
maxOutput: 8192,
|
105
|
+
pricing: {
|
106
|
+
input: 0,
|
107
|
+
output: 0,
|
108
|
+
},
|
109
|
+
releasedAt: '2025-02-05',
|
110
|
+
type: 'chat',
|
111
|
+
},
|
95
112
|
{
|
96
113
|
abilities: {
|
97
114
|
vision: true,
|
@@ -316,12 +316,12 @@
|
|
316
316
|
},
|
317
317
|
{
|
318
318
|
"sql": [
|
319
|
-
"CREATE INDEX \"messages_topic_id_idx\" ON \"messages\" USING btree (\"topic_id\");",
|
320
|
-
"\nCREATE INDEX \"messages_parent_id_idx\" ON \"messages\" USING btree (\"parent_id\");",
|
321
|
-
"\nCREATE INDEX \"messages_quota_id_idx\" ON \"messages\" USING btree (\"quota_id\")
|
319
|
+
"CREATE INDEX IF NOT EXISTS \"messages_topic_id_idx\" ON \"messages\" USING btree (\"topic_id\");",
|
320
|
+
"\nCREATE INDEX IF NOT EXISTS \"messages_parent_id_idx\" ON \"messages\" USING btree (\"parent_id\");",
|
321
|
+
"\nCREATE INDEX IF NOT EXISTS \"messages_quota_id_idx\" ON \"messages\" USING btree (\"quota_id\");\n"
|
322
322
|
],
|
323
323
|
"bps": true,
|
324
324
|
"folderMillis": 1741844738677,
|
325
|
-
"hash": "
|
325
|
+
"hash": "2a7a98be2e49361391444d6fabf3fb5db0bcb6a65e5540e9c3d426ceeb1f7f3a"
|
326
326
|
}
|
327
327
|
]
|
@@ -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>
|
@@ -31,6 +31,9 @@ import { StreamingResponse } from '../utils/response';
|
|
31
31
|
import { GoogleGenerativeAIStream, convertIterableToStream } from '../utils/streams';
|
32
32
|
import { parseDataUri } from '../utils/uriParser';
|
33
33
|
|
34
|
+
const modelsOffSafetySettings = new Set(['gemini-2.0-flash-exp']);
|
35
|
+
const modelsWithModalities = new Set(['gemini-2.0-flash-exp']);
|
36
|
+
|
34
37
|
export interface GoogleModelCard {
|
35
38
|
displayName: string;
|
36
39
|
inputTokenLimit: number;
|
@@ -50,8 +53,7 @@ enum HarmBlockThreshold {
|
|
50
53
|
}
|
51
54
|
|
52
55
|
function getThreshold(model: string): HarmBlockThreshold {
|
53
|
-
|
54
|
-
if (useOFF.includes(model)) {
|
56
|
+
if (modelsOffSafetySettings.has(model)) {
|
55
57
|
return 'OFF' as HarmBlockThreshold; // https://discuss.ai.google.dev/t/59352
|
56
58
|
}
|
57
59
|
return HarmBlockThreshold.BLOCK_NONE;
|
@@ -94,6 +96,10 @@ export class LobeGoogleAI implements LobeRuntimeAI {
|
|
94
96
|
{
|
95
97
|
generationConfig: {
|
96
98
|
maxOutputTokens: payload.max_tokens,
|
99
|
+
// @ts-expect-error - Google SDK 0.24.0 doesn't have this property for now with
|
100
|
+
response_modalities: modelsWithModalities.has(model)
|
101
|
+
? ['Text', 'Image']
|
102
|
+
: undefined,
|
97
103
|
temperature: payload.temperature,
|
98
104
|
topP: payload.top_p,
|
99
105
|
},
|
@@ -94,4 +94,103 @@ 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
|
+
});
|
97
196
|
});
|
@@ -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,75 @@ 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
|
+
{ data: candidate.finishReason, id: context?.id, type: 'stop' },
|
75
|
+
{
|
76
|
+
data: {
|
77
|
+
// TODO: Google SDK 0.24.0 don't have promptTokensDetails types
|
78
|
+
inputImageTokens: (usage as any).promptTokensDetails?.find(
|
79
|
+
(i: any) => i.modality === 'IMAGE',
|
80
|
+
)?.tokenCount,
|
81
|
+
inputTextTokens: (usage as any).promptTokensDetails?.find(
|
82
|
+
(i: any) => i.modality === 'TEXT',
|
83
|
+
)?.tokenCount,
|
84
|
+
totalInputTokens: usage.promptTokenCount,
|
85
|
+
totalOutputTokens: usage.candidatesTokenCount,
|
86
|
+
totalTokens: usage.totalTokenCount,
|
87
|
+
} as ModelTokensUsage,
|
88
|
+
id: context?.id,
|
89
|
+
type: 'usage',
|
90
|
+
},
|
91
|
+
];
|
92
|
+
}
|
93
|
+
return { data: candidate.finishReason, id: context?.id, type: 'stop' };
|
94
|
+
}
|
95
|
+
|
96
|
+
if (!!text?.trim()) return { data: text, id: context?.id, type: 'text' };
|
97
|
+
|
98
|
+
// streaming the image
|
99
|
+
if (Array.isArray(candidate.content.parts) && candidate.content.parts.length > 0) {
|
100
|
+
const part = candidate.content.parts[0];
|
101
|
+
|
102
|
+
if (part && part.inlineData && part.inlineData.data && part.inlineData.mimeType) {
|
103
|
+
return {
|
104
|
+
data: `data:${part.inlineData.mimeType};base64,${part.inlineData.data}`,
|
105
|
+
id: context.id,
|
106
|
+
type: 'base64_image',
|
107
|
+
};
|
108
|
+
}
|
109
|
+
}
|
64
110
|
}
|
65
111
|
|
66
112
|
return {
|