@lobehub/chat 1.62.11 → 1.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/changelog/v1.json +12 -0
- package/locales/ar/chat.json +26 -0
- package/locales/ar/models.json +21 -0
- package/locales/bg-BG/chat.json +26 -0
- package/locales/bg-BG/models.json +21 -0
- package/locales/de-DE/chat.json +26 -0
- package/locales/de-DE/models.json +21 -0
- package/locales/en-US/chat.json +26 -0
- package/locales/en-US/models.json +21 -0
- package/locales/es-ES/chat.json +26 -0
- package/locales/es-ES/models.json +21 -0
- package/locales/fa-IR/chat.json +26 -0
- package/locales/fa-IR/models.json +21 -0
- package/locales/fr-FR/chat.json +26 -0
- package/locales/fr-FR/models.json +21 -0
- package/locales/it-IT/chat.json +26 -0
- package/locales/it-IT/models.json +21 -0
- package/locales/ja-JP/chat.json +26 -0
- package/locales/ja-JP/models.json +21 -0
- package/locales/ko-KR/chat.json +26 -0
- package/locales/ko-KR/models.json +21 -0
- package/locales/nl-NL/chat.json +26 -0
- package/locales/nl-NL/models.json +21 -0
- package/locales/pl-PL/chat.json +26 -0
- package/locales/pl-PL/models.json +21 -0
- package/locales/pt-BR/chat.json +26 -0
- package/locales/pt-BR/models.json +21 -0
- package/locales/ru-RU/chat.json +26 -0
- package/locales/ru-RU/models.json +21 -0
- package/locales/tr-TR/chat.json +26 -0
- package/locales/tr-TR/models.json +21 -0
- package/locales/vi-VN/chat.json +26 -0
- package/locales/vi-VN/models.json +21 -0
- package/locales/zh-CN/chat.json +27 -1
- package/locales/zh-CN/models.json +25 -4
- package/locales/zh-TW/chat.json +26 -0
- package/locales/zh-TW/models.json +21 -0
- package/package.json +1 -1
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/index.tsx +1 -0
- package/src/config/aiModels/google.ts +8 -0
- package/src/config/aiModels/groq.ts +111 -95
- package/src/config/aiModels/hunyuan.ts +36 -4
- package/src/config/aiModels/internlm.ts +4 -5
- package/src/config/aiModels/jina.ts +3 -0
- package/src/config/aiModels/mistral.ts +35 -21
- package/src/config/aiModels/novita.ts +293 -32
- package/src/config/aiModels/perplexity.ts +14 -2
- package/src/config/aiModels/qwen.ts +91 -37
- package/src/config/aiModels/sensenova.ts +70 -17
- package/src/config/aiModels/siliconcloud.ts +5 -3
- package/src/config/aiModels/stepfun.ts +19 -0
- package/src/config/aiModels/taichu.ts +4 -2
- package/src/config/aiModels/upstage.ts +24 -11
- package/src/config/modelProviders/openrouter.ts +1 -0
- package/src/config/modelProviders/qwen.ts +2 -1
- package/src/const/settings/agent.ts +1 -0
- package/src/database/repositories/aiInfra/index.test.ts +2 -5
- package/src/database/repositories/aiInfra/index.ts +6 -2
- package/src/database/schemas/message.ts +2 -1
- package/src/database/server/models/aiModel.ts +1 -1
- package/src/database/server/models/aiProvider.ts +6 -1
- package/src/features/ChatInput/ActionBar/Model/ControlsForm.tsx +38 -0
- package/src/features/ChatInput/ActionBar/Model/ExtendControls.tsx +40 -0
- package/src/features/ChatInput/ActionBar/Model/index.tsx +132 -0
- package/src/features/ChatInput/ActionBar/Params/index.tsx +2 -2
- package/src/features/ChatInput/ActionBar/Search/ExaIcon.tsx +15 -0
- package/src/features/ChatInput/ActionBar/Search/ModelBuiltinSearch.tsx +68 -0
- package/src/features/ChatInput/ActionBar/Search/SwitchPanel.tsx +167 -0
- package/src/features/ChatInput/ActionBar/Search/index.tsx +76 -0
- package/src/features/ChatInput/ActionBar/config.ts +4 -2
- package/src/features/Conversation/Messages/Assistant/SearchGrounding.tsx +153 -0
- package/src/features/Conversation/Messages/Assistant/index.tsx +7 -1
- package/src/features/ModelSelect/index.tsx +1 -1
- package/src/features/ModelSwitchPanel/index.tsx +2 -3
- package/src/hooks/useEnabledChatModels.ts +1 -1
- package/src/libs/agent-runtime/google/index.test.ts +142 -36
- package/src/libs/agent-runtime/google/index.ts +26 -51
- package/src/libs/agent-runtime/novita/__snapshots__/index.test.ts.snap +3 -3
- package/src/libs/agent-runtime/openrouter/__snapshots__/index.test.ts.snap +3 -3
- package/src/libs/agent-runtime/openrouter/index.ts +20 -20
- package/src/libs/agent-runtime/perplexity/index.test.ts +2 -2
- package/src/libs/agent-runtime/qwen/index.ts +38 -55
- package/src/libs/agent-runtime/types/chat.ts +6 -2
- package/src/libs/agent-runtime/utils/streams/google-ai.ts +29 -4
- package/src/libs/agent-runtime/utils/streams/openai.ts +1 -1
- package/src/libs/agent-runtime/utils/streams/protocol.ts +1 -1
- package/src/locales/default/chat.ts +28 -0
- package/src/services/chat.ts +10 -0
- package/src/store/agent/slices/chat/__snapshots__/selectors.test.ts.snap +1 -0
- package/src/store/agent/slices/chat/selectors.ts +6 -0
- package/src/store/aiInfra/slices/aiModel/selectors.ts +36 -0
- package/src/store/aiInfra/slices/aiProvider/initialState.ts +2 -2
- package/src/store/aiInfra/slices/aiProvider/selectors.ts +14 -0
- package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +15 -5
- package/src/store/chat/slices/message/action.ts +1 -1
- package/src/store/user/slices/modelList/selectors/modelProvider.ts +1 -1
- package/src/store/user/slices/settings/selectors/__snapshots__/settings.test.ts.snap +1 -0
- package/src/types/agent/index.ts +4 -0
- package/src/types/aiModel.ts +35 -8
- package/src/types/aiProvider.ts +7 -10
- package/src/types/message/base.ts +2 -5
- package/src/types/message/chat.ts +5 -3
- package/src/types/openai/chat.ts +5 -0
- package/src/types/search.ts +29 -0
- package/src/utils/fetch/fetchSSE.ts +11 -11
- package/src/features/ChatInput/ActionBar/ModelSwitch.tsx +0 -20
@@ -2,8 +2,9 @@ import STT from '../STT';
|
|
2
2
|
import Clear from './Clear';
|
3
3
|
import History from './History';
|
4
4
|
import Knowledge from './Knowledge';
|
5
|
-
import
|
5
|
+
import Model from './Model';
|
6
6
|
import Params from './Params';
|
7
|
+
import Search from './Search';
|
7
8
|
import { MainToken, PortalToken } from './Token';
|
8
9
|
import Tools from './Tools';
|
9
10
|
import Upload from './Upload';
|
@@ -14,9 +15,10 @@ export const actionMap = {
|
|
14
15
|
history: History,
|
15
16
|
knowledgeBase: Knowledge,
|
16
17
|
mainToken: MainToken,
|
17
|
-
model:
|
18
|
+
model: Model,
|
18
19
|
params: Params,
|
19
20
|
portalToken: PortalToken,
|
21
|
+
search: Search,
|
20
22
|
stt: STT,
|
21
23
|
temperature: Params,
|
22
24
|
tools: Tools,
|
@@ -0,0 +1,153 @@
|
|
1
|
+
import { Icon, SearchResultCards, Tag } from '@lobehub/ui';
|
2
|
+
import { createStyles } from 'antd-style';
|
3
|
+
import { AnimatePresence, motion } from 'framer-motion';
|
4
|
+
import { ChevronDown, ChevronRight, Globe } from 'lucide-react';
|
5
|
+
import Image from 'next/image';
|
6
|
+
import { rgba } from 'polished';
|
7
|
+
import { memo, useState } from 'react';
|
8
|
+
import { useTranslation } from 'react-i18next';
|
9
|
+
import { Flexbox } from 'react-layout-kit';
|
10
|
+
|
11
|
+
import { GroundingSearch } from '@/types/search';
|
12
|
+
|
13
|
+
const useStyles = createStyles(({ css, token, isDarkMode }) => ({
|
14
|
+
container: css`
|
15
|
+
width: fit-content;
|
16
|
+
padding-block: 4px;
|
17
|
+
padding-inline: 8px;
|
18
|
+
border-radius: 6px;
|
19
|
+
|
20
|
+
color: ${token.colorTextTertiary};
|
21
|
+
|
22
|
+
&:hover {
|
23
|
+
background: ${isDarkMode ? token.colorFillQuaternary : token.colorFillTertiary};
|
24
|
+
}
|
25
|
+
`,
|
26
|
+
expand: css`
|
27
|
+
background: ${isDarkMode ? token.colorFillQuaternary : token.colorFillTertiary} !important;
|
28
|
+
`,
|
29
|
+
shinyText: css`
|
30
|
+
color: ${rgba(token.colorText, 0.45)};
|
31
|
+
|
32
|
+
background: linear-gradient(
|
33
|
+
120deg,
|
34
|
+
${rgba(token.colorTextBase, 0)} 40%,
|
35
|
+
${token.colorTextSecondary} 50%,
|
36
|
+
${rgba(token.colorTextBase, 0)} 60%
|
37
|
+
);
|
38
|
+
background-clip: text;
|
39
|
+
background-size: 200% 100%;
|
40
|
+
|
41
|
+
animation: shine 1.5s linear infinite;
|
42
|
+
|
43
|
+
@keyframes shine {
|
44
|
+
0% {
|
45
|
+
background-position: 100%;
|
46
|
+
}
|
47
|
+
|
48
|
+
100% {
|
49
|
+
background-position: -100%;
|
50
|
+
}
|
51
|
+
}
|
52
|
+
`,
|
53
|
+
title: css`
|
54
|
+
overflow: hidden;
|
55
|
+
display: -webkit-box;
|
56
|
+
-webkit-box-orient: vertical;
|
57
|
+
-webkit-line-clamp: 1;
|
58
|
+
|
59
|
+
font-size: 12px;
|
60
|
+
text-overflow: ellipsis;
|
61
|
+
`,
|
62
|
+
}));
|
63
|
+
|
64
|
+
const SearchGrounding = memo<GroundingSearch>(({ searchQueries, citations }) => {
|
65
|
+
const { t } = useTranslation('chat');
|
66
|
+
const { styles, cx, theme } = useStyles();
|
67
|
+
|
68
|
+
const [showDetail, setShowDetail] = useState(false);
|
69
|
+
|
70
|
+
return (
|
71
|
+
<Flexbox
|
72
|
+
className={cx(styles.container, showDetail && styles.expand)}
|
73
|
+
gap={16}
|
74
|
+
style={{ width: showDetail ? '100%' : undefined }}
|
75
|
+
>
|
76
|
+
<Flexbox
|
77
|
+
distribution={'space-between'}
|
78
|
+
flex={1}
|
79
|
+
gap={8}
|
80
|
+
horizontal
|
81
|
+
onClick={() => {
|
82
|
+
setShowDetail(!showDetail);
|
83
|
+
}}
|
84
|
+
style={{ cursor: 'pointer' }}
|
85
|
+
>
|
86
|
+
<Flexbox align={'center'} gap={8} horizontal>
|
87
|
+
<Icon icon={Globe} />
|
88
|
+
<Flexbox horizontal>{t('search.grounding.title', { count: citations?.length })}</Flexbox>
|
89
|
+
{!showDetail && (
|
90
|
+
<Flexbox horizontal>
|
91
|
+
{citations?.slice(0, 8).map((item, index) => (
|
92
|
+
<Image
|
93
|
+
alt={item.title || item.url}
|
94
|
+
height={16}
|
95
|
+
key={`${item.url}-${index}`}
|
96
|
+
src={`https://icons.duckduckgo.com/ip3/${new URL(item.url).host}.ico`}
|
97
|
+
style={{
|
98
|
+
background: theme.colorBgContainer,
|
99
|
+
borderRadius: 8,
|
100
|
+
marginInline: -2,
|
101
|
+
padding: 2,
|
102
|
+
zIndex: 100 - index,
|
103
|
+
}}
|
104
|
+
unoptimized
|
105
|
+
width={16}
|
106
|
+
/>
|
107
|
+
))}
|
108
|
+
</Flexbox>
|
109
|
+
)}
|
110
|
+
</Flexbox>
|
111
|
+
|
112
|
+
<Flexbox gap={4} horizontal>
|
113
|
+
<Icon icon={showDetail ? ChevronDown : ChevronRight} />
|
114
|
+
</Flexbox>
|
115
|
+
</Flexbox>
|
116
|
+
|
117
|
+
<AnimatePresence initial={false}>
|
118
|
+
{showDetail && (
|
119
|
+
<motion.div
|
120
|
+
animate="open"
|
121
|
+
exit="collapsed"
|
122
|
+
initial="collapsed"
|
123
|
+
style={{ overflow: 'hidden', width: '100%' }}
|
124
|
+
transition={{
|
125
|
+
duration: 0.2,
|
126
|
+
ease: [0.4, 0, 0.2, 1], // 使用 ease-out 缓动函数
|
127
|
+
}}
|
128
|
+
variants={{
|
129
|
+
collapsed: { height: 0, opacity: 0, width: 'auto' },
|
130
|
+
open: { height: 'auto', opacity: 1, width: 'auto' },
|
131
|
+
}}
|
132
|
+
>
|
133
|
+
<Flexbox gap={12}>
|
134
|
+
{searchQueries && (
|
135
|
+
<Flexbox gap={4} horizontal>
|
136
|
+
{t('search.grounding.searchQueries')}
|
137
|
+
<Flexbox gap={8} horizontal>
|
138
|
+
{searchQueries.map((query, index) => (
|
139
|
+
<Tag key={index}>{query}</Tag>
|
140
|
+
))}
|
141
|
+
</Flexbox>
|
142
|
+
</Flexbox>
|
143
|
+
)}
|
144
|
+
{citations && <SearchResultCards dataSource={citations} />}
|
145
|
+
</Flexbox>
|
146
|
+
</motion.div>
|
147
|
+
)}
|
148
|
+
</AnimatePresence>
|
149
|
+
</Flexbox>
|
150
|
+
);
|
151
|
+
});
|
152
|
+
|
153
|
+
export default SearchGrounding;
|
@@ -9,13 +9,14 @@ import { ChatMessage } from '@/types/message';
|
|
9
9
|
import { DefaultMessage } from '../Default';
|
10
10
|
import FileChunks from './FileChunks';
|
11
11
|
import Reasoning from './Reasoning';
|
12
|
+
import SearchGrounding from './SearchGrounding';
|
12
13
|
import Tool from './Tool';
|
13
14
|
|
14
15
|
export const AssistantMessage = memo<
|
15
16
|
ChatMessage & {
|
16
17
|
editableContent: ReactNode;
|
17
18
|
}
|
18
|
-
>(({ id, tools, content, chunksList, ...props }) => {
|
19
|
+
>(({ id, tools, content, chunksList, search, ...props }) => {
|
19
20
|
const editing = useChatStore(chatSelectors.isMessageEditing(id));
|
20
21
|
const generating = useChatStore(chatSelectors.isMessageGenerating(id));
|
21
22
|
|
@@ -23,6 +24,8 @@ export const AssistantMessage = memo<
|
|
23
24
|
|
24
25
|
const isReasoning = useChatStore(aiChatSelectors.isMessageInReasoning(id));
|
25
26
|
|
27
|
+
const showSearch = !!search && !!search.citations?.length;
|
28
|
+
|
26
29
|
// remove \n to avoid empty content
|
27
30
|
// refs: https://github.com/lobehub/lobe-chat/pull/6153
|
28
31
|
const showReasoning =
|
@@ -38,6 +41,9 @@ export const AssistantMessage = memo<
|
|
38
41
|
/>
|
39
42
|
) : (
|
40
43
|
<Flexbox gap={8} id={id}>
|
44
|
+
{showSearch && (
|
45
|
+
<SearchGrounding citations={search?.citations} searchQueries={search?.searchQueries} />
|
46
|
+
)}
|
41
47
|
{!!chunksList && chunksList.length > 0 && <FileChunks data={chunksList} />}
|
42
48
|
{showReasoning && <Reasoning {...props.reasoning} id={id} />}
|
43
49
|
{content && (
|
@@ -4,7 +4,7 @@ import { memo, useMemo } from 'react';
|
|
4
4
|
|
5
5
|
import { ModelItemRender, ProviderItemRender } from '@/components/ModelSelect';
|
6
6
|
import { useEnabledChatModels } from '@/hooks/useEnabledChatModels';
|
7
|
-
import { EnabledProviderWithModels } from '@/types/
|
7
|
+
import { EnabledProviderWithModels } from '@/types/aiProvider';
|
8
8
|
|
9
9
|
const useStyles = createStyles(({ css, prefixCls }) => ({
|
10
10
|
select: css`
|
@@ -14,7 +14,7 @@ import { useEnabledChatModels } from '@/hooks/useEnabledChatModels';
|
|
14
14
|
import { useIsMobile } from '@/hooks/useIsMobile';
|
15
15
|
import { useAgentStore } from '@/store/agent';
|
16
16
|
import { agentSelectors } from '@/store/agent/slices/chat';
|
17
|
-
import { EnabledProviderWithModels } from '@/types/
|
17
|
+
import { EnabledProviderWithModels } from '@/types/aiProvider';
|
18
18
|
|
19
19
|
const useStyles = createStyles(({ css, prefixCls }) => ({
|
20
20
|
menu: css`
|
@@ -68,7 +68,7 @@ const ModelSwitchPanel = memo<PropsWithChildren>(({ children }) => {
|
|
68
68
|
if (items.length === 0)
|
69
69
|
return [
|
70
70
|
{
|
71
|
-
key:
|
71
|
+
key: `${provider.id}-empty`,
|
72
72
|
label: (
|
73
73
|
<Flexbox gap={8} horizontal style={{ color: theme.colorTextTertiary }}>
|
74
74
|
{t('ModelSwitchPanel.emptyModel')}
|
@@ -114,7 +114,6 @@ const ModelSwitchPanel = memo<PropsWithChildren>(({ children }) => {
|
|
114
114
|
},
|
115
115
|
}}
|
116
116
|
placement={isMobile ? 'top' : 'topLeft'}
|
117
|
-
trigger={['click']}
|
118
117
|
>
|
119
118
|
<div className={styles.tag}>{children}</div>
|
120
119
|
</Dropdown>
|
@@ -4,7 +4,7 @@ import { isDeprecatedEdition } from '@/const/version';
|
|
4
4
|
import { useAiInfraStore } from '@/store/aiInfra';
|
5
5
|
import { useUserStore } from '@/store/user';
|
6
6
|
import { modelProviderSelectors } from '@/store/user/selectors';
|
7
|
-
import { EnabledProviderWithModels } from '@/types/
|
7
|
+
import { EnabledProviderWithModels } from '@/types/aiProvider';
|
8
8
|
|
9
9
|
export const useEnabledChatModels = (): EnabledProviderWithModels[] => {
|
10
10
|
const enabledList = useUserStore(modelProviderSelectors.modelProviderListForModelSelect, isEqual);
|
@@ -74,6 +74,145 @@ describe('LobeGoogleAI', () => {
|
|
74
74
|
// 额外的断言可以加入,比如验证返回的流内容等
|
75
75
|
});
|
76
76
|
|
77
|
+
it('should withGrounding', () => {
|
78
|
+
const data = [
|
79
|
+
{
|
80
|
+
candidates: [{ content: { parts: [{ text: 'As' }], role: 'model' } }],
|
81
|
+
usageMetadata: { promptTokenCount: 8, totalTokenCount: 8 },
|
82
|
+
modelVersion: 'gemini-2.0-flash',
|
83
|
+
},
|
84
|
+
{
|
85
|
+
candidates: [
|
86
|
+
{
|
87
|
+
content: { parts: [{ text: ' of February 22, 2025, "Ne Zha ' }], role: 'model' },
|
88
|
+
safetyRatings: [
|
89
|
+
{ category: 'HARM_CATEGORY_HATE_SPEECH', probability: 'NEGLIGIBLE' },
|
90
|
+
{ category: 'HARM_CATEGORY_DANGEROUS_CONTENT', probability: 'MEDIUM' },
|
91
|
+
{ category: 'HARM_CATEGORY_HARASSMENT', probability: 'NEGLIGIBLE' },
|
92
|
+
{ category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', probability: 'NEGLIGIBLE' },
|
93
|
+
],
|
94
|
+
},
|
95
|
+
],
|
96
|
+
usageMetadata: { promptTokenCount: 8, totalTokenCount: 8 },
|
97
|
+
modelVersion: 'gemini-2.0-flash',
|
98
|
+
},
|
99
|
+
{
|
100
|
+
candidates: [
|
101
|
+
{
|
102
|
+
content: {
|
103
|
+
parts: [{ text: '2" has grossed the following:\n\n* **Worldwide:** $1' }],
|
104
|
+
role: 'model',
|
105
|
+
},
|
106
|
+
safetyRatings: [
|
107
|
+
{ category: 'HARM_CATEGORY_HATE_SPEECH', probability: 'NEGLIGIBLE' },
|
108
|
+
{ category: 'HARM_CATEGORY_DANGEROUS_CONTENT', probability: 'NEGLIGIBLE' },
|
109
|
+
{ category: 'HARM_CATEGORY_HARASSMENT', probability: 'NEGLIGIBLE' },
|
110
|
+
{ category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', probability: 'NEGLIGIBLE' },
|
111
|
+
],
|
112
|
+
},
|
113
|
+
],
|
114
|
+
usageMetadata: { promptTokenCount: 8, totalTokenCount: 8 },
|
115
|
+
modelVersion: 'gemini-2.0-flash',
|
116
|
+
},
|
117
|
+
{
|
118
|
+
candidates: [
|
119
|
+
{
|
120
|
+
content: {
|
121
|
+
parts: [
|
122
|
+
{
|
123
|
+
text: '.66 billion\n* **China:** $1.82 billion (CN¥12.35 billion)\n* **US &',
|
124
|
+
},
|
125
|
+
],
|
126
|
+
role: 'model',
|
127
|
+
},
|
128
|
+
safetyRatings: [
|
129
|
+
{ category: 'HARM_CATEGORY_HATE_SPEECH', probability: 'NEGLIGIBLE' },
|
130
|
+
{ category: 'HARM_CATEGORY_DANGEROUS_CONTENT', probability: 'NEGLIGIBLE' },
|
131
|
+
{ category: 'HARM_CATEGORY_HARASSMENT', probability: 'NEGLIGIBLE' },
|
132
|
+
{ category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', probability: 'NEGLIGIBLE' },
|
133
|
+
],
|
134
|
+
},
|
135
|
+
],
|
136
|
+
usageMetadata: { promptTokenCount: 8, totalTokenCount: 8 },
|
137
|
+
modelVersion: 'gemini-2.0-flash',
|
138
|
+
},
|
139
|
+
{
|
140
|
+
candidates: [
|
141
|
+
{
|
142
|
+
content: { parts: [{ text: ' Canada:** $24,744,753\n' }], role: 'model' },
|
143
|
+
finishReason: 'STOP',
|
144
|
+
safetyRatings: [
|
145
|
+
{ category: 'HARM_CATEGORY_HATE_SPEECH', probability: 'NEGLIGIBLE' },
|
146
|
+
{ category: 'HARM_CATEGORY_DANGEROUS_CONTENT', probability: 'NEGLIGIBLE' },
|
147
|
+
{ category: 'HARM_CATEGORY_HARASSMENT', probability: 'NEGLIGIBLE' },
|
148
|
+
{ category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', probability: 'NEGLIGIBLE' },
|
149
|
+
],
|
150
|
+
groundingMetadata: {
|
151
|
+
searchEntryPoint: {
|
152
|
+
renderedContent:
|
153
|
+
'<style>\n.container {\n align-items: center;\n border-radius: 8px;\n display: flex;\n font-family: Google Sans, Roboto, sans-serif;\n font-size: 14px;\n line-height: 20px;\n padding: 8px 12px;\n}\n.chip {\n display: inline-block;\n border: solid 1px;\n border-radius: 16px;\n min-width: 14px;\n padding: 5px 16px;\n text-align: center;\n user-select: none;\n margin: 0 8px;\n -webkit-tap-highlight-color: transparent;\n}\n.carousel {\n overflow: auto;\n scrollbar-width: none;\n white-space: nowrap;\n margin-right: -12px;\n}\n.headline {\n display: flex;\n margin-right: 4px;\n}\n.gradient-container {\n position: relative;\n}\n.gradient {\n position: absolute;\n transform: translate(3px, -9px);\n height: 36px;\n width: 9px;\n}\n@media (prefers-color-scheme: light) {\n .container {\n background-color: #fafafa;\n box-shadow: 0 0 0 1px #0000000f;\n }\n .headline-label {\n color: #1f1f1f;\n }\n .chip {\n background-color: #ffffff;\n border-color: #d2d2d2;\n color: #5e5e5e;\n text-decoration: none;\n }\n .chip:hover {\n background-color: #f2f2f2;\n }\n .chip:focus {\n background-color: #f2f2f2;\n }\n .chip:active {\n background-color: #d8d8d8;\n border-color: #b6b6b6;\n }\n .logo-dark {\n display: none;\n }\n .gradient {\n background: linear-gradient(90deg, #fafafa 15%, #fafafa00 100%);\n }\n}\n@media (prefers-color-scheme: dark) {\n .container {\n background-color: #1f1f1f;\n box-shadow: 0 0 0 1px #ffffff26;\n }\n .headline-label {\n color: #fff;\n }\n .chip {\n background-color: #2c2c2c;\n border-color: #3c4043;\n color: #fff;\n text-decoration: none;\n }\n .chip:hover {\n background-color: #353536;\n }\n .chip:focus {\n background-color: #353536;\n }\n .chip:active {\n background-color: #464849;\n border-color: #53575b;\n }\n .logo-light {\n display: none;\n }\n .gradient {\n background: linear-gradient(90deg, #1f1f1f 15%, #1f1f1f00 100%);\n }\n}\n</style>\n<div class="container">\n <div class="headline">\n <svg class="logo-light" width="18" height="18" viewBox="9 9 35 35" fill="none" xmlns="http://www.w3.org/2000/svg">\n <path fill-rule="evenodd" clip-rule="evenodd" d="M42.8622 27.0064C42.8622 25.7839 42.7525 24.6084 42.5487 23.4799H26.3109V30.1568H35.5897C35.1821 32.3041 33.9596 34.1222 32.1258 35.3448V39.6864H37.7213C40.9814 36.677 42.8622 32.2571 42.8622 27.0064V27.0064Z" fill="#4285F4"/>\n <path fill-rule="evenodd" clip-rule="evenodd" d="M26.3109 43.8555C30.9659 43.8555 34.8687 42.3195 37.7213 39.6863L32.1258 35.3447C30.5898 36.3792 28.6306 37.0061 26.3109 37.0061C21.8282 37.0061 18.0195 33.9811 16.6559 29.906H10.9194V34.3573C13.7563 39.9841 19.5712 43.8555 26.3109 43.8555V43.8555Z" fill="#34A853"/>\n <path fill-rule="evenodd" clip-rule="evenodd" d="M16.6559 29.8904C16.3111 28.8559 16.1074 27.7588 16.1074 26.6146C16.1074 25.4704 16.3111 24.3733 16.6559 23.3388V18.8875H10.9194C9.74388 21.2072 9.06992 23.8247 9.06992 26.6146C9.06992 29.4045 9.74388 32.022 10.9194 34.3417L15.3864 30.8621L16.6559 29.8904V29.8904Z" fill="#FBBC05"/>\n <path fill-rule="evenodd" clip-rule="evenodd" d="M26.3109 16.2386C28.85 16.2386 31.107 17.1164 32.9095 18.8091L37.8466 13.8719C34.853 11.082 30.9659 9.3736 26.3109 9.3736C19.5712 9.3736 13.7563 13.245 10.9194 18.8875L16.6559 23.3388C18.0195 19.2636 21.8282 16.2386 26.3109 16.2386V16.2386Z" fill="#EA4335"/>\n </svg>\n <svg class="logo-dark" width="18" height="18" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">\n <circle cx="24" cy="23" fill="#FFF" r="22"/>\n <path d="M33.76 34.26c2.75-2.56 4.49-6.37 4.49-11.26 0-.89-.08-1.84-.29-3H24.01v5.99h8.03c-.4 2.02-1.5 3.56-3.07 4.56v.75l3.91 2.97h.88z" fill="#4285F4"/>\n <path d="M15.58 25.77A8.845 8.845 0 0 0 24 31.86c1.92 0 3.62-.46 4.97-1.31l4.79 3.71C31.14 36.7 27.65 38 24 38c-5.93 0-11.01-3.4-13.45-8.36l.17-1.01 4.06-2.85h.8z" fill="#34A853"/>\n <path d="M15.59 20.21a8.864 8.864 0 0 0 0 5.58l-5.03 3.86c-.98-2-1.53-4.25-1.53-6.64 0-2.39.55-4.64 1.53-6.64l1-.22 3.81 2.98.22 1.08z" fill="#FBBC05"/>\n <path d="M24 14.14c2.11 0 4.02.75 5.52 1.98l4.36-4.36C31.22 9.43 27.81 8 24 8c-5.93 0-11.01 3.4-13.45 8.36l5.03 3.85A8.86 8.86 0 0 1 24 14.14z" fill="#EA4335"/>\n </svg>\n <div class="gradient-container"><div class="gradient"></div></div>\n </div>\n <div class="carousel">\n <a class="chip" href="https://vertexaisearch.cloud.google.com/grounding-api-redirect/AQXblrycKK-4Q61T9-BeH_jYKcMfCwyI0-TGMMzPcvZuXVtBjnsxXJkWcxxay0giciDNQ5g4dfD8SdUuBIlBLFQE7Fuc8e50WZuKO9u3HVjQXMznQxtzcQ4fHUn1lDlsvKiurKnD-G-Sl6s7_8h3JNMJSsObKg79sP0vQ_f9N7ib5s3tuF35FglH1NLaiTvdpM1DVhaHZc2In94_hV3W-_k=">Nezha Reborn 2 box office</a>\n </div>\n</div>\n',
|
154
|
+
},
|
155
|
+
groundingChunks: [
|
156
|
+
{
|
157
|
+
web: {
|
158
|
+
uri: 'https://vertexaisearch.cloud.google.com/grounding-api-redirect/AQXblrz3Up-UZrEsLlT8zPkpwbakcjDZbojH5RuXL0HAa_0rHfG1WE5h6jADFSzcMxKNZcit_n7OaxnTvZNjp9WFL4NNJmjkqQRJoK_XdeVsnbshWJpm9TJL7KNNwzAl254th8cHxTsQIOPoNxsnrXeebIlMDVb8OuFWfCWUToiRxhv1_Vo=',
|
159
|
+
title: 'screenrant.com',
|
160
|
+
},
|
161
|
+
},
|
162
|
+
{
|
163
|
+
web: {
|
164
|
+
uri: 'https://vertexaisearch.cloud.google.com/grounding-api-redirect/AQXblry4I3hWcwVL-mI75BJYSy72Lb97KF50N2p5PWvH8vuLQQgekFmlw9PDiJ3KouByidcMsja_7IJ3F1S0PguLC0r_uxbcAGfFvJzbiMNdWOhQ7xDSJqObd_mCUa-VFpYzm6cd',
|
165
|
+
title: 'imdb.com',
|
166
|
+
},
|
167
|
+
},
|
168
|
+
],
|
169
|
+
groundingSupports: [
|
170
|
+
{
|
171
|
+
segment: {
|
172
|
+
startIndex: 64,
|
173
|
+
endIndex: 96,
|
174
|
+
text: '* **Worldwide:** $1.66 billion',
|
175
|
+
},
|
176
|
+
groundingChunkIndices: [0],
|
177
|
+
confidenceScores: [0.95218265],
|
178
|
+
},
|
179
|
+
{
|
180
|
+
segment: {
|
181
|
+
startIndex: 146,
|
182
|
+
endIndex: 178,
|
183
|
+
text: '* **US & Canada:** $24,744,753',
|
184
|
+
},
|
185
|
+
groundingChunkIndices: [1],
|
186
|
+
confidenceScores: [0.7182074],
|
187
|
+
},
|
188
|
+
],
|
189
|
+
retrievalMetadata: {},
|
190
|
+
webSearchQueries: ['Nezha Reborn 2 box office'],
|
191
|
+
},
|
192
|
+
},
|
193
|
+
],
|
194
|
+
usageMetadata: {
|
195
|
+
promptTokenCount: 7,
|
196
|
+
candidatesTokenCount: 79,
|
197
|
+
totalTokenCount: 86,
|
198
|
+
promptTokensDetails: [{ modality: 'TEXT', tokenCount: 7 }],
|
199
|
+
candidatesTokensDetails: [{ modality: 'TEXT', tokenCount: 79 }],
|
200
|
+
},
|
201
|
+
modelVersion: 'gemini-2.0-flash',
|
202
|
+
},
|
203
|
+
];
|
204
|
+
const mockStream = new ReadableStream({
|
205
|
+
start(controller) {
|
206
|
+
controller.enqueue('Hello, world!');
|
207
|
+
controller.close();
|
208
|
+
},
|
209
|
+
});
|
210
|
+
|
211
|
+
vi.spyOn(instance['client'], 'getGenerativeModel').mockReturnValue({
|
212
|
+
generateContentStream: vi.fn().mockResolvedValueOnce(mockStream),
|
213
|
+
} as any);
|
214
|
+
});
|
215
|
+
|
77
216
|
it('should call debugStream in DEBUG mode', async () => {
|
78
217
|
// 设置环境变量以启用DEBUG模式
|
79
218
|
process.env.DEBUG_GOOGLE_CHAT_COMPLETION = '1';
|
@@ -368,51 +507,19 @@ describe('LobeGoogleAI', () => {
|
|
368
507
|
it('get default result with gemini-pro', async () => {
|
369
508
|
const messages: OpenAIChatMessage[] = [{ content: 'Hello', role: 'user' }];
|
370
509
|
|
371
|
-
const contents = await instance['buildGoogleMessages'](messages
|
510
|
+
const contents = await instance['buildGoogleMessages'](messages);
|
372
511
|
|
373
512
|
expect(contents).toHaveLength(1);
|
374
513
|
expect(contents).toEqual([{ parts: [{ text: 'Hello' }], role: 'user' }]);
|
375
514
|
});
|
376
515
|
|
377
|
-
it('messages should end with user if using gemini-pro', async () => {
|
378
|
-
const messages: OpenAIChatMessage[] = [
|
379
|
-
{ content: 'Hello', role: 'user' },
|
380
|
-
{ content: 'Hi', role: 'assistant' },
|
381
|
-
];
|
382
|
-
|
383
|
-
const contents = await instance['buildGoogleMessages'](messages, 'gemini-1.0');
|
384
|
-
|
385
|
-
expect(contents).toHaveLength(3);
|
386
|
-
expect(contents).toEqual([
|
387
|
-
{ parts: [{ text: 'Hello' }], role: 'user' },
|
388
|
-
{ parts: [{ text: 'Hi' }], role: 'model' },
|
389
|
-
{ parts: [{ text: '' }], role: 'user' },
|
390
|
-
]);
|
391
|
-
});
|
392
|
-
|
393
|
-
it('should include system role if there is a system role prompt', async () => {
|
394
|
-
const messages: OpenAIChatMessage[] = [
|
395
|
-
{ content: 'you are ChatGPT', role: 'system' },
|
396
|
-
{ content: 'Who are you', role: 'user' },
|
397
|
-
];
|
398
|
-
|
399
|
-
const contents = await instance['buildGoogleMessages'](messages, 'gemini-1.0');
|
400
|
-
|
401
|
-
expect(contents).toHaveLength(3);
|
402
|
-
expect(contents).toEqual([
|
403
|
-
{ parts: [{ text: 'you are ChatGPT' }], role: 'user' },
|
404
|
-
{ parts: [{ text: '' }], role: 'model' },
|
405
|
-
{ parts: [{ text: 'Who are you' }], role: 'user' },
|
406
|
-
]);
|
407
|
-
});
|
408
|
-
|
409
516
|
it('should not modify the length if model is gemini-1.5-pro', async () => {
|
410
517
|
const messages: OpenAIChatMessage[] = [
|
411
518
|
{ content: 'Hello', role: 'user' },
|
412
519
|
{ content: 'Hi', role: 'assistant' },
|
413
520
|
];
|
414
521
|
|
415
|
-
const contents = await instance['buildGoogleMessages'](messages
|
522
|
+
const contents = await instance['buildGoogleMessages'](messages);
|
416
523
|
|
417
524
|
expect(contents).toHaveLength(2);
|
418
525
|
expect(contents).toEqual([
|
@@ -431,10 +538,9 @@ describe('LobeGoogleAI', () => {
|
|
431
538
|
role: 'user',
|
432
539
|
},
|
433
540
|
];
|
434
|
-
const model = 'gemini-1.5-flash-latest';
|
435
541
|
|
436
542
|
// 调用 buildGoogleMessages 方法
|
437
|
-
const contents = await instance['buildGoogleMessages'](messages
|
543
|
+
const contents = await instance['buildGoogleMessages'](messages);
|
438
544
|
|
439
545
|
expect(contents).toHaveLength(1);
|
440
546
|
expect(contents).toEqual([
|
@@ -5,12 +5,13 @@ import {
|
|
5
5
|
FunctionDeclaration,
|
6
6
|
Tool as GoogleFunctionCallTool,
|
7
7
|
GoogleGenerativeAI,
|
8
|
+
GoogleSearchRetrievalTool,
|
8
9
|
Part,
|
9
10
|
SchemaType,
|
10
11
|
} from '@google/generative-ai';
|
11
12
|
|
12
|
-
import type { ChatModelCard } from '@/types/llm';
|
13
13
|
import { VertexAIStream } from '@/libs/agent-runtime/utils/streams/vertex-ai';
|
14
|
+
import type { ChatModelCard } from '@/types/llm';
|
14
15
|
import { imageUrlToBase64 } from '@/utils/imageToBase64';
|
15
16
|
import { safeParseJSON } from '@/utils/safeParseJSON';
|
16
17
|
|
@@ -86,7 +87,7 @@ export class LobeGoogleAI implements LobeRuntimeAI {
|
|
86
87
|
const payload = this.buildPayload(rawPayload);
|
87
88
|
const model = payload.model;
|
88
89
|
|
89
|
-
const contents = await this.buildGoogleMessages(payload.messages
|
90
|
+
const contents = await this.buildGoogleMessages(payload.messages);
|
90
91
|
|
91
92
|
const geminiStreamResult = await this.client
|
92
93
|
.getGenerativeModel(
|
@@ -123,7 +124,7 @@ export class LobeGoogleAI implements LobeRuntimeAI {
|
|
123
124
|
.generateContentStream({
|
124
125
|
contents,
|
125
126
|
systemInstruction: payload.system as string,
|
126
|
-
tools: this.buildGoogleTools(payload.tools),
|
127
|
+
tools: this.buildGoogleTools(payload.tools, payload),
|
127
128
|
});
|
128
129
|
|
129
130
|
const googleStream = convertIterableToStream(geminiStreamResult.stream);
|
@@ -168,26 +169,30 @@ export class LobeGoogleAI implements LobeRuntimeAI {
|
|
168
169
|
.map((model) => {
|
169
170
|
const modelName = model.name.replace(/^models\//, '');
|
170
171
|
|
171
|
-
const knownModel = LOBE_DEFAULT_MODEL_LIST.find(
|
172
|
+
const knownModel = LOBE_DEFAULT_MODEL_LIST.find(
|
173
|
+
(m) => modelName.toLowerCase() === m.id.toLowerCase(),
|
174
|
+
);
|
172
175
|
|
173
176
|
return {
|
174
177
|
contextWindowTokens: model.inputTokenLimit + model.outputTokenLimit,
|
175
178
|
displayName: model.displayName,
|
176
179
|
enabled: knownModel?.enabled || false,
|
177
180
|
functionCall:
|
178
|
-
modelName.toLowerCase().includes('gemini') &&
|
179
|
-
|
180
|
-
||
|
181
|
+
(modelName.toLowerCase().includes('gemini') &&
|
182
|
+
!modelName.toLowerCase().includes('thinking')) ||
|
183
|
+
knownModel?.abilities?.functionCall ||
|
184
|
+
false,
|
181
185
|
id: modelName,
|
182
186
|
reasoning:
|
183
|
-
modelName.toLowerCase().includes('thinking')
|
184
|
-
|
185
|
-
|
187
|
+
modelName.toLowerCase().includes('thinking') ||
|
188
|
+
knownModel?.abilities?.reasoning ||
|
189
|
+
false,
|
186
190
|
vision:
|
187
|
-
modelName.toLowerCase().includes('vision')
|
188
|
-
|
189
|
-
|
190
|
-
||
|
191
|
+
modelName.toLowerCase().includes('vision') ||
|
192
|
+
(modelName.toLowerCase().includes('gemini') &&
|
193
|
+
!modelName.toLowerCase().includes('gemini-1.0')) ||
|
194
|
+
knownModel?.abilities?.vision ||
|
195
|
+
false,
|
191
196
|
};
|
192
197
|
})
|
193
198
|
.filter(Boolean) as ChatModelCard[];
|
@@ -266,43 +271,7 @@ export class LobeGoogleAI implements LobeRuntimeAI {
|
|
266
271
|
};
|
267
272
|
|
268
273
|
// convert messages from the OpenAI format to Google GenAI SDK
|
269
|
-
private buildGoogleMessages = async (
|
270
|
-
messages: OpenAIChatMessage[],
|
271
|
-
model: string,
|
272
|
-
): Promise<Content[]> => {
|
273
|
-
// if the model is gemini-1.0 we need to pair messages
|
274
|
-
if (model.startsWith('gemini-1.0')) {
|
275
|
-
const contents: Content[] = [];
|
276
|
-
let lastRole = 'model';
|
277
|
-
|
278
|
-
for (const message of messages) {
|
279
|
-
// current to filter function message
|
280
|
-
if (message.role === 'function') {
|
281
|
-
continue;
|
282
|
-
}
|
283
|
-
const googleMessage = await this.convertOAIMessagesToGoogleMessage(message);
|
284
|
-
|
285
|
-
// if the last message is a model message and the current message is a model message
|
286
|
-
// then we need to add a user message to separate them
|
287
|
-
if (lastRole === googleMessage.role) {
|
288
|
-
contents.push({ parts: [{ text: '' }], role: lastRole === 'user' ? 'model' : 'user' });
|
289
|
-
}
|
290
|
-
|
291
|
-
// add the current message to the contents
|
292
|
-
contents.push(googleMessage);
|
293
|
-
|
294
|
-
// update the last role
|
295
|
-
lastRole = googleMessage.role;
|
296
|
-
}
|
297
|
-
|
298
|
-
// if the last message is a user message, then we need to add a model message to separate them
|
299
|
-
if (lastRole === 'model') {
|
300
|
-
contents.push({ parts: [{ text: '' }], role: 'user' });
|
301
|
-
}
|
302
|
-
|
303
|
-
return contents;
|
304
|
-
}
|
305
|
-
|
274
|
+
private buildGoogleMessages = async (messages: OpenAIChatMessage[]): Promise<Content[]> => {
|
306
275
|
const pools = messages
|
307
276
|
.filter((message) => message.role !== 'function')
|
308
277
|
.map(async (msg) => await this.convertOAIMessagesToGoogleMessage(msg));
|
@@ -353,7 +322,13 @@ export class LobeGoogleAI implements LobeRuntimeAI {
|
|
353
322
|
|
354
323
|
private buildGoogleTools(
|
355
324
|
tools: ChatCompletionTool[] | undefined,
|
325
|
+
payload?: ChatStreamPayload,
|
356
326
|
): GoogleFunctionCallTool[] | undefined {
|
327
|
+
// 目前 Tools (例如 googleSearch) 无法与其他 FunctionCall 同时使用
|
328
|
+
if (payload?.enabledSearch) {
|
329
|
+
return [{ googleSearch: {} } as GoogleSearchRetrievalTool];
|
330
|
+
}
|
331
|
+
|
357
332
|
if (!tools || tools.length === 0) return;
|
358
333
|
|
359
334
|
return [
|
@@ -26,7 +26,7 @@ exports[`NovitaAI > models > should get models 1`] = `
|
|
26
26
|
"contextWindowTokens": 8192,
|
27
27
|
"description": "Meta's latest class of models, Llama 3.1, launched with a variety of sizes and configurations. The 8B instruct-tuned version is particularly fast and efficient. It has demonstrated strong performance in human evaluations, outperforming several leading closed-source models.",
|
28
28
|
"displayName": "meta-llama/llama-3.1-8b-instruct",
|
29
|
-
"enabled":
|
29
|
+
"enabled": false,
|
30
30
|
"functionCall": false,
|
31
31
|
"id": "meta-llama/llama-3.1-8b-instruct",
|
32
32
|
"reasoning": false,
|
@@ -36,7 +36,7 @@ exports[`NovitaAI > models > should get models 1`] = `
|
|
36
36
|
"contextWindowTokens": 8192,
|
37
37
|
"description": "Meta's latest class of models, Llama 3.1, has launched with a variety of sizes and configurations. The 70B instruct-tuned version is optimized for high-quality dialogue use cases. It has demonstrated strong performance in human evaluations compared to leading closed-source models.",
|
38
38
|
"displayName": "meta-llama/llama-3.1-70b-instruct",
|
39
|
-
"enabled":
|
39
|
+
"enabled": false,
|
40
40
|
"functionCall": false,
|
41
41
|
"id": "meta-llama/llama-3.1-70b-instruct",
|
42
42
|
"reasoning": false,
|
@@ -46,7 +46,7 @@ exports[`NovitaAI > models > should get models 1`] = `
|
|
46
46
|
"contextWindowTokens": 32768,
|
47
47
|
"description": "Meta's latest class of models, Llama 3.1, launched with a variety of sizes and configurations. This 405B instruct-tuned version is optimized for high-quality dialogue use cases. It has demonstrated strong performance compared to leading closed-source models, including GPT-4o and Claude 3.5 Sonnet, in evaluations.",
|
48
48
|
"displayName": "meta-llama/llama-3.1-405b-instruct",
|
49
|
-
"enabled":
|
49
|
+
"enabled": false,
|
50
50
|
"functionCall": false,
|
51
51
|
"id": "meta-llama/llama-3.1-405b-instruct",
|
52
52
|
"reasoning": false,
|
@@ -407,7 +407,7 @@ It has demonstrated strong performance compared to leading closed-source models
|
|
407
407
|
|
408
408
|
To read more about the model release, [click here](https://ai.meta.com/blog/meta-llama-3/). Usage of this model is subject to [Meta's Acceptable Use Policy](https://llama.meta.com/llama3/use-policy/).",
|
409
409
|
"displayName": "Meta: Llama 3.1 70B Instruct",
|
410
|
-
"enabled":
|
410
|
+
"enabled": false,
|
411
411
|
"functionCall": false,
|
412
412
|
"id": "meta-llama/llama-3.1-70b-instruct",
|
413
413
|
"maxTokens": undefined,
|
@@ -439,7 +439,7 @@ It has demonstrated strong performance compared to leading closed-source models
|
|
439
439
|
|
440
440
|
To read more about the model release, [click here](https://ai.meta.com/blog/meta-llama-3/). Usage of this model is subject to [Meta's Acceptable Use Policy](https://llama.meta.com/llama3/use-policy/).",
|
441
441
|
"displayName": "Meta: Llama 3.1 8B Instruct",
|
442
|
-
"enabled":
|
442
|
+
"enabled": false,
|
443
443
|
"functionCall": false,
|
444
444
|
"id": "meta-llama/llama-3.1-8b-instruct",
|
445
445
|
"maxTokens": undefined,
|
@@ -456,7 +456,7 @@ It has demonstrated strong performance compared to leading closed-source models
|
|
456
456
|
|
457
457
|
To read more about the model release, [click here](https://ai.meta.com/blog/meta-llama-3/). Usage of this model is subject to [Meta's Acceptable Use Policy](https://llama.meta.com/llama3/use-policy/).",
|
458
458
|
"displayName": "Meta: Llama 3.1 405B Instruct",
|
459
|
-
"enabled":
|
459
|
+
"enabled": false,
|
460
460
|
"functionCall": false,
|
461
461
|
"id": "meta-llama/llama-3.1-405b-instruct",
|
462
462
|
"maxTokens": undefined,
|