@lobehub/chat 1.69.4 → 1.69.6
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 +52 -0
- package/changelog/v1.json +18 -0
- package/docker-compose/setup.sh +92 -1
- package/docs/self-hosting/advanced/auth/clerk.mdx +1 -0
- package/locales/ar/components.json +2 -0
- package/locales/ar/models.json +3 -0
- package/locales/bg-BG/components.json +2 -0
- package/locales/bg-BG/models.json +3 -0
- package/locales/de-DE/components.json +2 -0
- package/locales/de-DE/models.json +3 -0
- package/locales/en-US/components.json +2 -0
- package/locales/en-US/models.json +3 -0
- package/locales/es-ES/components.json +2 -0
- package/locales/es-ES/models.json +3 -0
- package/locales/fa-IR/components.json +2 -0
- package/locales/fa-IR/models.json +3 -0
- package/locales/fr-FR/components.json +2 -0
- package/locales/fr-FR/models.json +3 -0
- package/locales/it-IT/components.json +2 -0
- package/locales/it-IT/models.json +3 -0
- package/locales/ja-JP/components.json +2 -0
- package/locales/ja-JP/models.json +3 -0
- package/locales/ko-KR/components.json +2 -0
- package/locales/ko-KR/models.json +3 -0
- package/locales/nl-NL/components.json +2 -0
- package/locales/nl-NL/models.json +3 -0
- package/locales/pl-PL/components.json +2 -0
- package/locales/pl-PL/models.json +3 -0
- package/locales/pt-BR/components.json +2 -0
- package/locales/pt-BR/models.json +3 -0
- package/locales/ru-RU/components.json +2 -0
- package/locales/ru-RU/models.json +3 -0
- package/locales/tr-TR/components.json +2 -0
- package/locales/tr-TR/models.json +3 -0
- package/locales/vi-VN/components.json +2 -0
- package/locales/vi-VN/models.json +3 -0
- package/locales/zh-CN/components.json +3 -1
- package/locales/zh-CN/models.json +3 -0
- package/locales/zh-TW/components.json +2 -0
- package/locales/zh-TW/models.json +3 -0
- package/package.json +4 -4
- package/packages/web-crawler/package.json +1 -1
- package/packages/web-crawler/src/crawImpl/__tests__/browserless.test.ts +94 -0
- package/packages/web-crawler/src/crawImpl/browserless.ts +1 -1
- package/packages/web-crawler/src/crawImpl/naive.ts +1 -1
- package/packages/web-crawler/src/utils/__snapshots__/htmlToMarkdown.test.ts.snap +2 -382
- package/packages/web-crawler/src/utils/htmlToMarkdown.ts +32 -2
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/MessageFromUrl.tsx +31 -0
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/index.tsx +45 -39
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/InboxWelcome/AgentsSuggest.tsx +5 -1
- package/src/config/aiModels/openrouter.ts +26 -1
- package/src/config/modelProviders/openai.ts +3 -0
- package/src/database/client/db.ts +3 -3
- package/src/features/Conversation/components/MarkdownElements/LobeArtifact/Render/index.tsx +51 -52
- package/src/features/ModelSwitchPanel/index.tsx +37 -8
- package/src/libs/agent-runtime/anthropic/index.ts +5 -2
- package/src/libs/agent-runtime/openrouter/index.test.ts +33 -0
- package/src/libs/agent-runtime/openrouter/index.ts +11 -2
- package/src/libs/agent-runtime/openrouter/type.ts +19 -0
- package/src/locales/default/components.ts +3 -1
- package/src/services/__tests__/chat.test.ts +123 -0
- package/src/services/chat.ts +19 -19
- package/src/store/user/slices/modelList/action.ts +17 -16
- package/src/utils/fetch/__tests__/fetchSSE.test.ts +3 -2
- package/src/utils/fetch/fetchSSE.ts +1 -1
@@ -1,7 +1,7 @@
|
|
1
1
|
import { Button, Space } from 'antd';
|
2
2
|
import { createStyles } from 'antd-style';
|
3
3
|
import { rgba } from 'polished';
|
4
|
-
import { memo, useEffect, useState } from 'react';
|
4
|
+
import { Suspense, memo, useEffect, useState } from 'react';
|
5
5
|
import { useTranslation } from 'react-i18next';
|
6
6
|
import { Flexbox } from 'react-layout-kit';
|
7
7
|
|
@@ -13,6 +13,7 @@ import { useChatStore } from '@/store/chat';
|
|
13
13
|
import { chatSelectors } from '@/store/chat/selectors';
|
14
14
|
import { isMacOS } from '@/utils/platform';
|
15
15
|
|
16
|
+
import MessageFromUrl from './MessageFromUrl';
|
16
17
|
import SendMore from './SendMore';
|
17
18
|
import ShortcutHint from './ShortcutHint';
|
18
19
|
|
@@ -67,49 +68,54 @@ const Footer = memo<FooterProps>(({ onExpandChange, expand }) => {
|
|
67
68
|
}, [setIsMac]);
|
68
69
|
|
69
70
|
return (
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
<
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
>
|
92
|
-
{t('input.stop')}
|
93
|
-
</Button>
|
94
|
-
) : (
|
95
|
-
<Space.Compact>
|
71
|
+
<>
|
72
|
+
<Suspense fallback={null}>
|
73
|
+
<MessageFromUrl />
|
74
|
+
</Suspense>
|
75
|
+
<Flexbox
|
76
|
+
align={'end'}
|
77
|
+
className={styles.overrideAntdIcon}
|
78
|
+
distribution={'space-between'}
|
79
|
+
flex={'none'}
|
80
|
+
gap={8}
|
81
|
+
horizontal
|
82
|
+
padding={'0 24px'}
|
83
|
+
>
|
84
|
+
<Flexbox align={'center'} gap={8} horizontal style={{ overflow: 'hidden' }}>
|
85
|
+
{expand && <LocalFiles />}
|
86
|
+
</Flexbox>
|
87
|
+
<Flexbox align={'center'} flex={'none'} gap={8} horizontal>
|
88
|
+
<ShortcutHint />
|
89
|
+
<SaveTopic />
|
90
|
+
<Flexbox style={{ minWidth: 92 }}>
|
91
|
+
{isAIGenerating ? (
|
96
92
|
<Button
|
97
|
-
|
98
|
-
|
99
|
-
onClick={
|
100
|
-
sendMessage();
|
101
|
-
onExpandChange?.(false);
|
102
|
-
}}
|
103
|
-
type={'primary'}
|
93
|
+
className={styles.loadingButton}
|
94
|
+
icon={<StopLoadingIcon />}
|
95
|
+
onClick={stopGenerateMessage}
|
104
96
|
>
|
105
|
-
{t('input.
|
97
|
+
{t('input.stop')}
|
106
98
|
</Button>
|
107
|
-
|
108
|
-
|
109
|
-
|
99
|
+
) : (
|
100
|
+
<Space.Compact>
|
101
|
+
<Button
|
102
|
+
disabled={!canSend}
|
103
|
+
loading={!canSend}
|
104
|
+
onClick={() => {
|
105
|
+
sendMessage();
|
106
|
+
onExpandChange?.(false);
|
107
|
+
}}
|
108
|
+
type={'primary'}
|
109
|
+
>
|
110
|
+
{t('input.send')}
|
111
|
+
</Button>
|
112
|
+
<SendMore disabled={!canSend} isMac={isMac} />
|
113
|
+
</Space.Compact>
|
114
|
+
)}
|
115
|
+
</Flexbox>
|
110
116
|
</Flexbox>
|
111
117
|
</Flexbox>
|
112
|
-
|
118
|
+
</>
|
113
119
|
);
|
114
120
|
});
|
115
121
|
|
@@ -105,7 +105,11 @@ const AgentsSuggest = memo<{ mobile?: boolean }>(({ mobile }) => {
|
|
105
105
|
: assistantList
|
106
106
|
.slice(sliceStart, sliceStart + agentLength)
|
107
107
|
.map((item: DiscoverAssistantItem) => (
|
108
|
-
<Link
|
108
|
+
<Link
|
109
|
+
href={urlJoin('/discover/assistant/', item.identifier)}
|
110
|
+
key={item.identifier}
|
111
|
+
prefetch={false}
|
112
|
+
>
|
109
113
|
<Flexbox className={styles.card} gap={8} horizontal>
|
110
114
|
<Avatar avatar={item.meta.avatar} style={{ flex: 'none' }} />
|
111
115
|
<Flexbox gap={mobile ? 2 : 8} style={{ overflow: 'hidden', width: '100%' }}>
|
@@ -137,6 +137,31 @@ const openrouterChatModels: AIChatModelCard[] = [
|
|
137
137
|
releasedAt: '2024-06-20',
|
138
138
|
type: 'chat',
|
139
139
|
},
|
140
|
+
{
|
141
|
+
abilities: {
|
142
|
+
functionCall: true,
|
143
|
+
reasoning: true,
|
144
|
+
vision: true,
|
145
|
+
},
|
146
|
+
contextWindowTokens: 200_000,
|
147
|
+
description:
|
148
|
+
'Claude 3.7 Sonnet 是 Anthropic 迄今为止最智能的模型,也是市场上首个混合推理模型。Claude 3.7 Sonnet 可以产生近乎即时的响应或延长的逐步思考,用户可以清晰地看到这些过程。Sonnet 特别擅长编程、数据科学、视觉处理、代理任务。',
|
149
|
+
displayName: 'Claude 3.7 Sonnet',
|
150
|
+
enabled: true,
|
151
|
+
id: 'anthropic/claude-3.7-sonnet',
|
152
|
+
maxOutput: 8192,
|
153
|
+
pricing: {
|
154
|
+
cachedInput: 0.3,
|
155
|
+
input: 3,
|
156
|
+
output: 15,
|
157
|
+
writeCacheInput: 3.75,
|
158
|
+
},
|
159
|
+
releasedAt: '2025-02-24',
|
160
|
+
settings: {
|
161
|
+
extendParams: ['enableReasoning', 'reasoningBudgetToken'],
|
162
|
+
},
|
163
|
+
type: 'chat',
|
164
|
+
},
|
140
165
|
{
|
141
166
|
abilities: {
|
142
167
|
functionCall: true,
|
@@ -258,7 +283,7 @@ const openrouterChatModels: AIChatModelCard[] = [
|
|
258
283
|
id: 'deepseek/deepseek-r1:free',
|
259
284
|
releasedAt: '2025-01-20',
|
260
285
|
type: 'chat',
|
261
|
-
},
|
286
|
+
},
|
262
287
|
{
|
263
288
|
abilities: {
|
264
289
|
vision: true,
|
@@ -26,13 +26,13 @@ export class DatabaseManager {
|
|
26
26
|
|
27
27
|
// CDN 配置
|
28
28
|
private static WASM_CDN_URL =
|
29
|
-
'https://registry.npmmirror.com/@electric-sql/pglite/0.2.
|
29
|
+
'https://registry.npmmirror.com/@electric-sql/pglite/0.2.17/files/dist/postgres.wasm';
|
30
30
|
|
31
31
|
private static FSBUNDLER_CDN_URL =
|
32
|
-
'https://registry.npmmirror.com/@electric-sql/pglite/0.2.
|
32
|
+
'https://registry.npmmirror.com/@electric-sql/pglite/0.2.17/files/dist/postgres.data';
|
33
33
|
|
34
34
|
private static VECTOR_CDN_URL =
|
35
|
-
'https://registry.npmmirror.com/@electric-sql/pglite/0.2.
|
35
|
+
'https://registry.npmmirror.com/@electric-sql/pglite/0.2.17/files/dist/vector.tar.gz';
|
36
36
|
|
37
37
|
private constructor() {}
|
38
38
|
|
@@ -64,16 +64,14 @@ const Render = memo<ArtifactProps>(({ identifier, title, type, language, childre
|
|
64
64
|
|
65
65
|
const inThread = useContext(InPortalThreadContext);
|
66
66
|
const { message } = App.useApp();
|
67
|
-
const [isGenerating, isArtifactTagClosed,
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
];
|
76
|
-
});
|
67
|
+
const [isGenerating, isArtifactTagClosed, openArtifact, closeArtifact] = useChatStore((s) => {
|
68
|
+
return [
|
69
|
+
chatSelectors.isMessageGenerating(id)(s),
|
70
|
+
chatPortalSelectors.isArtifactTagClosed(id)(s),
|
71
|
+
s.openArtifact,
|
72
|
+
s.closeArtifact,
|
73
|
+
];
|
74
|
+
});
|
77
75
|
|
78
76
|
const openArtifactUI = () => {
|
79
77
|
openArtifact({ id, identifier, language, title, type });
|
@@ -86,52 +84,53 @@ const Render = memo<ArtifactProps>(({ identifier, title, type, language, childre
|
|
86
84
|
}, [isGenerating, hasChildren, str, identifier, title, type, id, language]);
|
87
85
|
|
88
86
|
return (
|
89
|
-
<
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
87
|
+
<Flexbox
|
88
|
+
className={styles.container}
|
89
|
+
gap={16}
|
90
|
+
onClick={() => {
|
91
|
+
const currentArtifactMessageId = chatPortalSelectors.artifactMessageId(
|
92
|
+
useChatStore.getState(),
|
93
|
+
);
|
94
|
+
if (currentArtifactMessageId === id) {
|
95
|
+
closeArtifact();
|
96
|
+
} else {
|
97
|
+
if (inThread) {
|
98
|
+
message.info(t('artifact.inThread'));
|
99
|
+
return;
|
102
100
|
}
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
<
|
111
|
-
|
112
|
-
|
113
|
-
|
101
|
+
openArtifactUI();
|
102
|
+
}
|
103
|
+
}}
|
104
|
+
width={'100%'}
|
105
|
+
>
|
106
|
+
<Flexbox align={'center'} flex={1} horizontal>
|
107
|
+
<Center className={styles.avatar} height={64} horizontal width={64}>
|
108
|
+
<ArtifactIcon type={type} />
|
109
|
+
</Center>
|
110
|
+
<Flexbox gap={4} paddingBlock={8} paddingInline={12}>
|
111
|
+
{!title && isGenerating ? (
|
112
|
+
<Flexbox className={cx(dotLoading)} horizontal>
|
113
|
+
{t('artifact.generating')}
|
114
|
+
</Flexbox>
|
115
|
+
) : (
|
116
|
+
<Flexbox className={cx(styles.title)}>{title || t('artifact.unknownTitle')}</Flexbox>
|
117
|
+
)}
|
118
|
+
{hasChildren && (
|
119
|
+
<Flexbox className={styles.desc} horizontal>
|
120
|
+
{identifier} ·{' '}
|
121
|
+
<Flexbox gap={2} horizontal>
|
122
|
+
{!isArtifactTagClosed && (
|
123
|
+
<div>
|
124
|
+
<Icon icon={Loader2} spin />
|
125
|
+
</div>
|
126
|
+
)}
|
127
|
+
{str?.length}
|
114
128
|
</Flexbox>
|
115
|
-
|
116
|
-
|
117
|
-
)}
|
118
|
-
{hasChildren && (
|
119
|
-
<Flexbox className={styles.desc} horizontal>
|
120
|
-
{identifier} ·{' '}
|
121
|
-
<Flexbox gap={2} horizontal>
|
122
|
-
{!isArtifactTagClosed && (
|
123
|
-
<div>
|
124
|
-
<Icon icon={Loader2} spin />
|
125
|
-
</div>
|
126
|
-
)}
|
127
|
-
{str?.length}
|
128
|
-
</Flexbox>
|
129
|
-
</Flexbox>
|
130
|
-
)}
|
131
|
-
</Flexbox>
|
129
|
+
</Flexbox>
|
130
|
+
)}
|
132
131
|
</Flexbox>
|
133
132
|
</Flexbox>
|
134
|
-
</
|
133
|
+
</Flexbox>
|
135
134
|
);
|
136
135
|
});
|
137
136
|
|
@@ -1,8 +1,9 @@
|
|
1
|
-
import { Icon } from '@lobehub/ui';
|
1
|
+
import { ActionIcon, Icon } from '@lobehub/ui';
|
2
2
|
import { Dropdown } from 'antd';
|
3
3
|
import { createStyles } from 'antd-style';
|
4
4
|
import type { ItemType } from 'antd/es/menu/interface';
|
5
|
-
import { LucideArrowRight } from 'lucide-react';
|
5
|
+
import { LucideArrowRight, LucideBolt } from 'lucide-react';
|
6
|
+
import Link from 'next/link';
|
6
7
|
import { useRouter } from 'next/navigation';
|
7
8
|
import { PropsWithChildren, memo, useMemo } from 'react';
|
8
9
|
import { useTranslation } from 'react-i18next';
|
@@ -86,17 +87,45 @@ const ModelSwitchPanel = memo<PropsWithChildren>(({ children }) => {
|
|
86
87
|
return items;
|
87
88
|
};
|
88
89
|
|
90
|
+
if (enabledList.length === 0)
|
91
|
+
return [
|
92
|
+
{
|
93
|
+
key: `no-provider`,
|
94
|
+
label: (
|
95
|
+
<Flexbox gap={8} horizontal style={{ color: theme.colorTextTertiary }}>
|
96
|
+
{t('ModelSwitchPanel.emptyProvider')}
|
97
|
+
<Icon icon={LucideArrowRight} />
|
98
|
+
</Flexbox>
|
99
|
+
),
|
100
|
+
onClick: () => {
|
101
|
+
router.push(isDeprecatedEdition ? '/settings/llm' : `/settings/provider`);
|
102
|
+
},
|
103
|
+
},
|
104
|
+
];
|
105
|
+
|
89
106
|
// otherwise show with provider group
|
90
107
|
return enabledList.map((provider) => ({
|
91
108
|
children: getModelItems(provider),
|
92
109
|
key: provider.id,
|
93
110
|
label: (
|
94
|
-
<
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
111
|
+
<Flexbox horizontal justify="space-between">
|
112
|
+
<ProviderItemRender
|
113
|
+
logo={provider.logo}
|
114
|
+
name={provider.name}
|
115
|
+
provider={provider.id}
|
116
|
+
source={provider.source}
|
117
|
+
/>
|
118
|
+
<Link
|
119
|
+
href={isDeprecatedEdition ? '/settings/llm' : `/settings/provider/${provider.id}`}
|
120
|
+
prefetch={false}
|
121
|
+
>
|
122
|
+
<ActionIcon
|
123
|
+
icon={LucideBolt}
|
124
|
+
size={'small'}
|
125
|
+
title={t('ModelSwitchPanel.goToSettings')}
|
126
|
+
/>
|
127
|
+
</Link>
|
128
|
+
</Flexbox>
|
100
129
|
),
|
101
130
|
type: 'group',
|
102
131
|
}));
|
@@ -102,7 +102,8 @@ export class LobeAnthropicAI implements LobeRuntimeAI {
|
|
102
102
|
|
103
103
|
if (!!thinking) {
|
104
104
|
const maxTokens =
|
105
|
-
|
105
|
+
// claude 3.7 thinking has max output of 64000 tokens
|
106
|
+
max_tokens ?? (thinking?.budget_tokens ? thinking?.budget_tokens + 64_000 : 8192);
|
106
107
|
|
107
108
|
// `temperature` may only be set to 1 when thinking is enabled.
|
108
109
|
// `top_p` must be unset when thinking is enabled.
|
@@ -117,7 +118,9 @@ export class LobeAnthropicAI implements LobeRuntimeAI {
|
|
117
118
|
}
|
118
119
|
|
119
120
|
return {
|
120
|
-
|
121
|
+
// claude 3 series model hax max output token of 4096, 3.x series has 8192
|
122
|
+
// https://docs.anthropic.com/en/docs/about-claude/models/all-models#:~:text=200K-,Max%20output,-Normal%3A
|
123
|
+
max_tokens: max_tokens ?? (model.startsWith('claude-3-') ? 4096 : 8192),
|
121
124
|
messages: postMessages,
|
122
125
|
model,
|
123
126
|
system: systemPrompts,
|
@@ -92,6 +92,39 @@ describe('LobeOpenRouterAI', () => {
|
|
92
92
|
expect(result).toBeInstanceOf(Response);
|
93
93
|
});
|
94
94
|
|
95
|
+
it('should add reasoning field when thinking is enabled', async () => {
|
96
|
+
// Arrange
|
97
|
+
const mockStream = new ReadableStream();
|
98
|
+
const mockResponse = Promise.resolve(mockStream);
|
99
|
+
|
100
|
+
(instance['client'].chat.completions.create as Mock).mockResolvedValue(mockResponse);
|
101
|
+
|
102
|
+
// Act
|
103
|
+
const result = await instance.chat({
|
104
|
+
messages: [{ content: 'Hello', role: 'user' }],
|
105
|
+
model: 'mistralai/mistral-7b-instruct:free',
|
106
|
+
temperature: 0.7,
|
107
|
+
thinking: {
|
108
|
+
type: 'enabled',
|
109
|
+
budget_tokens: 1500,
|
110
|
+
},
|
111
|
+
});
|
112
|
+
|
113
|
+
// Assert
|
114
|
+
expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
|
115
|
+
expect.objectContaining({
|
116
|
+
messages: [{ content: 'Hello', role: 'user' }],
|
117
|
+
model: 'mistralai/mistral-7b-instruct:free',
|
118
|
+
reasoning: {
|
119
|
+
max_tokens: 1500,
|
120
|
+
},
|
121
|
+
temperature: 0.7,
|
122
|
+
}),
|
123
|
+
{ headers: { Accept: '*/*' } },
|
124
|
+
);
|
125
|
+
expect(result).toBeInstanceOf(Response);
|
126
|
+
});
|
127
|
+
|
95
128
|
describe('Error', () => {
|
96
129
|
it('should return OpenRouterBizError with an openai error response when OpenAI.APIError is thrown', async () => {
|
97
130
|
// Arrange
|
@@ -2,7 +2,7 @@ import type { ChatModelCard } from '@/types/llm';
|
|
2
2
|
|
3
3
|
import { ModelProvider } from '../types';
|
4
4
|
import { LobeOpenAICompatibleFactory } from '../utils/openaiCompatibleFactory';
|
5
|
-
import { OpenRouterModelCard, OpenRouterModelExtraInfo } from './type';
|
5
|
+
import { OpenRouterModelCard, OpenRouterModelExtraInfo, OpenRouterReasoning } from './type';
|
6
6
|
|
7
7
|
const formatPrice = (price: string) => {
|
8
8
|
if (price === '-1') return undefined;
|
@@ -13,10 +13,19 @@ export const LobeOpenRouterAI = LobeOpenAICompatibleFactory({
|
|
13
13
|
baseURL: 'https://openrouter.ai/api/v1',
|
14
14
|
chatCompletion: {
|
15
15
|
handlePayload: (payload) => {
|
16
|
+
const { thinking } = payload;
|
17
|
+
|
18
|
+
let reasoning: OpenRouterReasoning = {};
|
19
|
+
if (thinking?.type === 'enabled') {
|
20
|
+
reasoning = {
|
21
|
+
max_tokens: thinking.budget_tokens,
|
22
|
+
};
|
23
|
+
}
|
24
|
+
|
16
25
|
return {
|
17
26
|
...payload,
|
18
|
-
include_reasoning: true,
|
19
27
|
model: payload.enabledSearch ? `${payload.model}:online` : payload.model,
|
28
|
+
reasoning,
|
20
29
|
stream: payload.stream ?? true,
|
21
30
|
} as any;
|
22
31
|
},
|
@@ -37,3 +37,22 @@ export interface OpenRouterModelExtraInfo {
|
|
37
37
|
endpoint?: OpenRouterModelEndpoint;
|
38
38
|
slug: string;
|
39
39
|
}
|
40
|
+
|
41
|
+
interface OpenRouterOpenAIReasoning {
|
42
|
+
effort: 'high' | 'medium' | 'low';
|
43
|
+
exclude?: boolean;
|
44
|
+
}
|
45
|
+
|
46
|
+
interface OpenRouterAnthropicReasoning {
|
47
|
+
exclude?: boolean;
|
48
|
+
max_tokens: number;
|
49
|
+
}
|
50
|
+
|
51
|
+
interface OpenRouterCommonReasoning {
|
52
|
+
exclude?: boolean;
|
53
|
+
}
|
54
|
+
|
55
|
+
export type OpenRouterReasoning =
|
56
|
+
| OpenRouterOpenAIReasoning
|
57
|
+
| OpenRouterAnthropicReasoning
|
58
|
+
| OpenRouterCommonReasoning;
|
@@ -26,12 +26,16 @@ import {
|
|
26
26
|
ModelProvider,
|
27
27
|
} from '@/libs/agent-runtime';
|
28
28
|
import { AgentRuntime } from '@/libs/agent-runtime';
|
29
|
+
import { agentChatConfigSelectors } from '@/store/agent/selectors';
|
30
|
+
import { aiModelSelectors } from '@/store/aiInfra';
|
29
31
|
import { useToolStore } from '@/store/tool';
|
32
|
+
import { toolSelectors } from '@/store/tool/selectors';
|
30
33
|
import { UserStore } from '@/store/user';
|
31
34
|
import { useUserStore } from '@/store/user';
|
32
35
|
import { modelConfigSelectors } from '@/store/user/selectors';
|
33
36
|
import { UserSettingsState, initialSettingsState } from '@/store/user/slices/settings/initialState';
|
34
37
|
import { DalleManifest } from '@/tools/dalle';
|
38
|
+
import { WebBrowsingManifest } from '@/tools/web-browsing';
|
35
39
|
import { ChatMessage } from '@/types/message';
|
36
40
|
import { ChatStreamPayload, type OpenAIChatMessage } from '@/types/openai/chat';
|
37
41
|
import { LobeTool } from '@/types/tool';
|
@@ -480,6 +484,125 @@ describe('ChatService', () => {
|
|
480
484
|
expect(calls![1]).toBeUndefined();
|
481
485
|
});
|
482
486
|
});
|
487
|
+
|
488
|
+
describe('search functionality', () => {
|
489
|
+
it('should add WebBrowsingManifest when search is enabled and not using model built-in search', async () => {
|
490
|
+
const getChatCompletionSpy = vi.spyOn(chatService, 'getChatCompletion');
|
491
|
+
|
492
|
+
const messages = [{ content: 'Search for something', role: 'user' }] as ChatMessage[];
|
493
|
+
|
494
|
+
// Mock agent store state with search enabled
|
495
|
+
vi.spyOn(agentChatConfigSelectors, 'currentChatConfig').mockReturnValueOnce({
|
496
|
+
searchMode: 'auto', // not 'off'
|
497
|
+
useModelBuiltinSearch: false,
|
498
|
+
} as any);
|
499
|
+
|
500
|
+
// Mock AI infra store state
|
501
|
+
vi.spyOn(aiModelSelectors, 'isModelHasBuiltinSearch').mockReturnValueOnce(() => false);
|
502
|
+
vi.spyOn(aiModelSelectors, 'isModelHasExtendParams').mockReturnValueOnce(() => false);
|
503
|
+
|
504
|
+
// Mock tool selectors
|
505
|
+
vi.spyOn(toolSelectors, 'enabledSchema').mockReturnValueOnce(() => [
|
506
|
+
{
|
507
|
+
type: 'function',
|
508
|
+
function: {
|
509
|
+
name: WebBrowsingManifest.identifier + '____search',
|
510
|
+
description: 'Search the web',
|
511
|
+
},
|
512
|
+
},
|
513
|
+
]);
|
514
|
+
|
515
|
+
await chatService.createAssistantMessage({ messages, plugins: [] });
|
516
|
+
|
517
|
+
// Verify tools were passed to getChatCompletion
|
518
|
+
expect(getChatCompletionSpy).toHaveBeenCalledWith(
|
519
|
+
expect.objectContaining({
|
520
|
+
tools: expect.arrayContaining([
|
521
|
+
expect.objectContaining({
|
522
|
+
function: expect.objectContaining({
|
523
|
+
name: expect.stringContaining(WebBrowsingManifest.identifier),
|
524
|
+
}),
|
525
|
+
}),
|
526
|
+
]),
|
527
|
+
}),
|
528
|
+
undefined,
|
529
|
+
);
|
530
|
+
});
|
531
|
+
|
532
|
+
it('should enable built-in search when model supports it and useModelBuiltinSearch is true', async () => {
|
533
|
+
const getChatCompletionSpy = vi.spyOn(chatService, 'getChatCompletion');
|
534
|
+
|
535
|
+
const messages = [{ content: 'Search for something', role: 'user' }] as ChatMessage[];
|
536
|
+
|
537
|
+
// Mock agent store state with search enabled and useModelBuiltinSearch enabled
|
538
|
+
vi.spyOn(agentChatConfigSelectors, 'currentChatConfig').mockReturnValueOnce({
|
539
|
+
searchMode: 'auto', // not 'off'
|
540
|
+
useModelBuiltinSearch: true,
|
541
|
+
} as any);
|
542
|
+
|
543
|
+
// Mock AI infra store state - model has built-in search
|
544
|
+
vi.spyOn(aiModelSelectors, 'isModelHasBuiltinSearch').mockReturnValueOnce(() => true);
|
545
|
+
vi.spyOn(aiModelSelectors, 'isModelHasExtendParams').mockReturnValueOnce(() => false);
|
546
|
+
|
547
|
+
// Mock tool selectors
|
548
|
+
vi.spyOn(toolSelectors, 'enabledSchema').mockReturnValueOnce(() => [
|
549
|
+
{
|
550
|
+
type: 'function',
|
551
|
+
function: {
|
552
|
+
name: WebBrowsingManifest.identifier + '____search',
|
553
|
+
description: 'Search the web',
|
554
|
+
},
|
555
|
+
},
|
556
|
+
]);
|
557
|
+
|
558
|
+
await chatService.createAssistantMessage({ messages, plugins: [] });
|
559
|
+
|
560
|
+
// Verify enabledSearch was set to true
|
561
|
+
expect(getChatCompletionSpy).toHaveBeenCalledWith(
|
562
|
+
expect.objectContaining({
|
563
|
+
enabledSearch: true,
|
564
|
+
}),
|
565
|
+
undefined,
|
566
|
+
);
|
567
|
+
});
|
568
|
+
|
569
|
+
it('should not enable search when searchMode is off', async () => {
|
570
|
+
const getChatCompletionSpy = vi.spyOn(chatService, 'getChatCompletion');
|
571
|
+
|
572
|
+
const messages = [{ content: 'Search for something', role: 'user' }] as ChatMessage[];
|
573
|
+
|
574
|
+
// Mock agent store state with search disabled
|
575
|
+
vi.spyOn(agentChatConfigSelectors, 'currentChatConfig').mockReturnValueOnce({
|
576
|
+
searchMode: 'off',
|
577
|
+
useModelBuiltinSearch: true,
|
578
|
+
} as any);
|
579
|
+
|
580
|
+
// Mock AI infra store state
|
581
|
+
vi.spyOn(aiModelSelectors, 'isModelHasBuiltinSearch').mockReturnValueOnce(() => true);
|
582
|
+
vi.spyOn(aiModelSelectors, 'isModelHasExtendParams').mockReturnValueOnce(() => false);
|
583
|
+
|
584
|
+
// Mock tool selectors
|
585
|
+
vi.spyOn(toolSelectors, 'enabledSchema').mockReturnValueOnce(() => [
|
586
|
+
{
|
587
|
+
type: 'function',
|
588
|
+
function: {
|
589
|
+
name: WebBrowsingManifest.identifier + '____search',
|
590
|
+
description: 'Search the web',
|
591
|
+
},
|
592
|
+
},
|
593
|
+
]);
|
594
|
+
|
595
|
+
await chatService.createAssistantMessage({ messages, plugins: [] });
|
596
|
+
|
597
|
+
// Verify enabledSearch was not set
|
598
|
+
expect(getChatCompletionSpy).toHaveBeenCalledWith(
|
599
|
+
expect.objectContaining({
|
600
|
+
enabledSearch: undefined,
|
601
|
+
}),
|
602
|
+
undefined,
|
603
|
+
);
|
604
|
+
});
|
605
|
+
});
|
483
606
|
});
|
484
607
|
|
485
608
|
describe('getChatCompletion', () => {
|