@lobehub/chat 1.136.10 → 1.136.11
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 +25 -0
- package/changelog/v1.json +9 -0
- package/package.json +1 -1
- package/packages/database/src/repositories/aiInfra/index.test.ts +234 -0
- package/packages/database/src/repositories/aiInfra/index.ts +106 -8
- package/packages/model-bank/src/types/aiModel.ts +2 -0
- package/packages/model-runtime/src/providers/google/index.ts +1 -1
- package/packages/model-runtime/src/providers/novita/__snapshots__/index.test.ts.snap +54 -0
- package/packages/model-runtime/src/providers/openai/__snapshots__/index.test.ts.snap +84 -0
- package/packages/model-runtime/src/utils/modelParse.test.ts +103 -0
- package/packages/model-runtime/src/utils/modelParse.ts +98 -23
- package/packages/model-runtime/src/utils/postProcessModelList.test.ts +0 -3
- package/packages/model-runtime/src/utils/postProcessModelList.ts +17 -4
- package/packages/types/src/llm.ts +16 -0
- package/src/app/[variants]/(main)/settings/provider/features/ModelList/CreateNewModelModal/Form.tsx +25 -0
- package/src/features/ChatInput/ActionBar/Search/Controls.tsx +47 -20
- package/src/helpers/getSearchConfig.test.ts +41 -0
- package/src/helpers/getSearchConfig.ts +5 -1
- package/src/locales/default/modelProvider.ts +15 -0
- package/src/store/aiInfra/slices/aiModel/action.ts +3 -0
- package/src/store/aiInfra/slices/aiModel/selectors.ts +7 -0
- package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +5 -1
- package/src/store/chat/slices/aiChat/actions/generateAIChatV2.ts +5 -1
|
@@ -6,10 +6,16 @@ import type { ModelProviderKey } from '../types';
|
|
|
6
6
|
export interface ModelProcessorConfig {
|
|
7
7
|
excludeKeywords?: readonly string[]; // 对符合的模型不添加标签
|
|
8
8
|
functionCallKeywords?: readonly string[];
|
|
9
|
+
imageOutputKeywords?: readonly string[];
|
|
9
10
|
reasoningKeywords?: readonly string[];
|
|
11
|
+
searchKeywords?: readonly string[];
|
|
12
|
+
videoKeywords?: readonly string[];
|
|
10
13
|
visionKeywords?: readonly string[];
|
|
11
14
|
}
|
|
12
15
|
|
|
16
|
+
// 默认关键字:任意包含 -search 的模型 ID 视为支持联网搜索
|
|
17
|
+
const DEFAULT_SEARCH_KEYWORDS = ['-search'] as const;
|
|
18
|
+
|
|
13
19
|
// 模型能力标签关键词配置
|
|
14
20
|
export const MODEL_LIST_CONFIGS = {
|
|
15
21
|
anthropic: {
|
|
@@ -22,8 +28,12 @@ export const MODEL_LIST_CONFIGS = {
|
|
|
22
28
|
reasoningKeywords: ['r1', 'deepseek-reasoner', 'v3.1', 'v3.2'],
|
|
23
29
|
},
|
|
24
30
|
google: {
|
|
25
|
-
|
|
26
|
-
|
|
31
|
+
excludeKeywords: ['tts'],
|
|
32
|
+
functionCallKeywords: ['gemini', '!-image-'],
|
|
33
|
+
imageOutputKeywords: ['-image-'],
|
|
34
|
+
reasoningKeywords: ['thinking', '-2.5-', '!-image-'],
|
|
35
|
+
searchKeywords: ['-search', '!-image-'],
|
|
36
|
+
videoKeywords: ['-2.5-', '!-image-'],
|
|
27
37
|
visionKeywords: ['gemini', 'learnlm'],
|
|
28
38
|
},
|
|
29
39
|
inclusionai: {
|
|
@@ -290,6 +300,44 @@ const processDisplayName = (displayName: string): string => {
|
|
|
290
300
|
return displayName;
|
|
291
301
|
};
|
|
292
302
|
|
|
303
|
+
/**
|
|
304
|
+
* 获取模型提供商的本地配置
|
|
305
|
+
* @param provider 模型提供商
|
|
306
|
+
* @returns 模型提供商的本地配置
|
|
307
|
+
*/
|
|
308
|
+
const getProviderLocalConfig = async (provider?: ModelProviderKey): Promise<any[] | null> => {
|
|
309
|
+
let providerLocalConfig: any[] | null = null;
|
|
310
|
+
if (provider) {
|
|
311
|
+
try {
|
|
312
|
+
const modules = await import('model-bank');
|
|
313
|
+
|
|
314
|
+
providerLocalConfig = modules[provider];
|
|
315
|
+
} catch {
|
|
316
|
+
// 如果配置文件不存在或导入失败,保持为 null
|
|
317
|
+
providerLocalConfig = null;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return providerLocalConfig;
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* 获取模型本地配置
|
|
325
|
+
* @param providerLocalConfig 模型提供商的本地配置
|
|
326
|
+
* @param model 模型对象
|
|
327
|
+
* @returns 模型本地配置
|
|
328
|
+
*/
|
|
329
|
+
const getModelLocalEnableConfig = (
|
|
330
|
+
providerLocalConfig: any[],
|
|
331
|
+
model: { id: string },
|
|
332
|
+
): any | null => {
|
|
333
|
+
// 如果提供了 providerid 且有本地配置,尝试从中获取模型的 enabled 状态
|
|
334
|
+
let providerLocalModelConfig = null;
|
|
335
|
+
if (providerLocalConfig && Array.isArray(providerLocalConfig)) {
|
|
336
|
+
providerLocalModelConfig = providerLocalConfig.find((m) => m.id === model.id);
|
|
337
|
+
}
|
|
338
|
+
return providerLocalModelConfig;
|
|
339
|
+
};
|
|
340
|
+
|
|
293
341
|
/**
|
|
294
342
|
* 处理模型卡片的通用逻辑
|
|
295
343
|
*/
|
|
@@ -303,6 +351,9 @@ const processModelCard = (
|
|
|
303
351
|
visionKeywords = [],
|
|
304
352
|
reasoningKeywords = [],
|
|
305
353
|
excludeKeywords = [],
|
|
354
|
+
searchKeywords = DEFAULT_SEARCH_KEYWORDS,
|
|
355
|
+
imageOutputKeywords = [],
|
|
356
|
+
videoKeywords = [],
|
|
306
357
|
} = config;
|
|
307
358
|
|
|
308
359
|
const isExcludedModel = isKeywordListMatch(model.id.toLowerCase(), excludeKeywords);
|
|
@@ -315,9 +366,9 @@ const processModelCard = (
|
|
|
315
366
|
)
|
|
316
367
|
? 'image'
|
|
317
368
|
: isKeywordListMatch(
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
369
|
+
model.id.toLowerCase(),
|
|
370
|
+
EMBEDDING_MODEL_KEYWORDS.map((k) => k.toLowerCase()),
|
|
371
|
+
)
|
|
321
372
|
? 'embedding'
|
|
322
373
|
: 'chat');
|
|
323
374
|
|
|
@@ -393,18 +444,32 @@ const processModelCard = (
|
|
|
393
444
|
((isKeywordListMatch(model.id.toLowerCase(), functionCallKeywords) && !isExcludedModel) ||
|
|
394
445
|
false),
|
|
395
446
|
id: model.id,
|
|
447
|
+
imageOutput:
|
|
448
|
+
model.imageOutput ??
|
|
449
|
+
knownModel?.abilities?.imageOutput ??
|
|
450
|
+
((isKeywordListMatch(model.id.toLowerCase(), imageOutputKeywords) && !isExcludedModel) ||
|
|
451
|
+
false),
|
|
396
452
|
maxOutput: model.maxOutput ?? knownModel?.maxOutput ?? undefined,
|
|
397
453
|
pricing: formatPricing(model?.pricing) ?? undefined,
|
|
398
454
|
reasoning:
|
|
399
455
|
model.reasoning ??
|
|
400
456
|
knownModel?.abilities?.reasoning ??
|
|
401
|
-
(isKeywordListMatch(model.id.toLowerCase(), reasoningKeywords)
|
|
457
|
+
((isKeywordListMatch(model.id.toLowerCase(), reasoningKeywords) && !isExcludedModel) ||
|
|
458
|
+
false),
|
|
402
459
|
releasedAt: processReleasedAt(model, knownModel),
|
|
460
|
+
search:
|
|
461
|
+
model.search ??
|
|
462
|
+
knownModel?.abilities?.search ??
|
|
463
|
+
((isKeywordListMatch(model.id.toLowerCase(), searchKeywords) && !isExcludedModel) || false),
|
|
403
464
|
type: modelType,
|
|
404
465
|
// current, only image model use the parameters field
|
|
405
466
|
...(modelType === 'image' && {
|
|
406
467
|
parameters: model.parameters ?? knownModel?.parameters,
|
|
407
468
|
}),
|
|
469
|
+
video:
|
|
470
|
+
model.video ??
|
|
471
|
+
knownModel?.abilities?.video ??
|
|
472
|
+
((isKeywordListMatch(model.id.toLowerCase(), videoKeywords) && !isExcludedModel) || false),
|
|
408
473
|
vision:
|
|
409
474
|
model.vision ??
|
|
410
475
|
knownModel?.abilities?.vision ??
|
|
@@ -416,7 +481,7 @@ const processModelCard = (
|
|
|
416
481
|
* 处理单一提供商的模型列表
|
|
417
482
|
* @param modelList 模型列表
|
|
418
483
|
* @param config 提供商配置
|
|
419
|
-
* @param provider
|
|
484
|
+
* @param provider 提供商类型(可选,用于优先匹配对应的本地配置, 当提供了 provider 时,才会尝试从本地配置覆盖 enabled)
|
|
420
485
|
* @returns 处理后的模型卡片列表
|
|
421
486
|
*/
|
|
422
487
|
export const processModelList = async (
|
|
@@ -426,6 +491,9 @@ export const processModelList = async (
|
|
|
426
491
|
): Promise<ChatModelCard[]> => {
|
|
427
492
|
const { LOBE_DEFAULT_MODEL_LIST } = await import('model-bank');
|
|
428
493
|
|
|
494
|
+
// 如果提供了 provider,尝试获取该提供商的本地配置
|
|
495
|
+
const providerLocalConfig = await getProviderLocalConfig(provider as ModelProviderKey);
|
|
496
|
+
|
|
429
497
|
return Promise.all(
|
|
430
498
|
modelList.map(async (model) => {
|
|
431
499
|
let knownModel: any = null;
|
|
@@ -442,7 +510,24 @@ export const processModelList = async (
|
|
|
442
510
|
);
|
|
443
511
|
}
|
|
444
512
|
|
|
445
|
-
|
|
513
|
+
const processedModel = processModelCard(model, config, knownModel);
|
|
514
|
+
|
|
515
|
+
// 如果提供了 provider 且有本地配置,尝试从中获取模型的 enabled 状态
|
|
516
|
+
const providerLocalModelConfig = getModelLocalEnableConfig(
|
|
517
|
+
providerLocalConfig as any[],
|
|
518
|
+
model,
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
// 如果找到了本地配置中的模型,使用其 enabled 状态
|
|
522
|
+
if (
|
|
523
|
+
processedModel &&
|
|
524
|
+
providerLocalModelConfig &&
|
|
525
|
+
typeof providerLocalModelConfig.enabled === 'boolean'
|
|
526
|
+
) {
|
|
527
|
+
processedModel.enabled = providerLocalModelConfig.enabled;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return processedModel;
|
|
446
531
|
}),
|
|
447
532
|
).then((results) => results.filter((result) => !!result));
|
|
448
533
|
};
|
|
@@ -460,17 +545,7 @@ export const processMultiProviderModelList = async (
|
|
|
460
545
|
const { LOBE_DEFAULT_MODEL_LIST } = await import('model-bank');
|
|
461
546
|
|
|
462
547
|
// 如果提供了 providerid,尝试获取该提供商的本地配置
|
|
463
|
-
|
|
464
|
-
if (providerid) {
|
|
465
|
-
try {
|
|
466
|
-
const modules = await import('model-bank');
|
|
467
|
-
|
|
468
|
-
providerLocalConfig = modules[providerid];
|
|
469
|
-
} catch {
|
|
470
|
-
// 如果配置文件不存在或导入失败,保持为 null
|
|
471
|
-
providerLocalConfig = null;
|
|
472
|
-
}
|
|
473
|
-
}
|
|
548
|
+
const providerLocalConfig = await getProviderLocalConfig(providerid);
|
|
474
549
|
|
|
475
550
|
return Promise.all(
|
|
476
551
|
modelList.map(async (model) => {
|
|
@@ -488,10 +563,10 @@ export const processMultiProviderModelList = async (
|
|
|
488
563
|
}
|
|
489
564
|
|
|
490
565
|
// 如果提供了 providerid 且有本地配置,尝试从中获取模型的 enabled 状态
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
566
|
+
const providerLocalModelConfig = getModelLocalEnableConfig(
|
|
567
|
+
providerLocalConfig as any[],
|
|
568
|
+
model,
|
|
569
|
+
);
|
|
495
570
|
|
|
496
571
|
const processedModel = processModelCard(model, config, knownModel);
|
|
497
572
|
|
|
@@ -41,12 +41,25 @@ export async function postProcessModelList(
|
|
|
41
41
|
const matchingModels = finalModels.filter((model) => model.id.endsWith(whitelistPattern));
|
|
42
42
|
|
|
43
43
|
for (const model of matchingModels) {
|
|
44
|
+
// Blacklist: remove unnecessary properties, keep the rest
|
|
45
|
+
const {
|
|
46
|
+
files, // drop
|
|
47
|
+
functionCall, // drop
|
|
48
|
+
reasoning, // drop
|
|
49
|
+
search, // drop
|
|
50
|
+
imageOutput, // drop
|
|
51
|
+
video, // drop
|
|
52
|
+
vision, // drop
|
|
53
|
+
type: _dropType, // will be overwritten
|
|
54
|
+
parameters: _dropParams, // will be overwritten
|
|
55
|
+
...rest
|
|
56
|
+
} = model;
|
|
57
|
+
|
|
44
58
|
imageModels.push({
|
|
45
|
-
...
|
|
59
|
+
...rest, // Keep other fields (such as displayName, pricing, enabled, contextWindowTokens, etc.)
|
|
46
60
|
id: `${model.id}:image`,
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
type: 'image', // Set image parameters
|
|
61
|
+
parameters: CHAT_MODEL_IMAGE_GENERATION_PARAMS, // Set image parameters
|
|
62
|
+
type: 'image', // Override to image type
|
|
50
63
|
});
|
|
51
64
|
}
|
|
52
65
|
}
|
|
@@ -30,6 +30,12 @@ export interface ChatModelCard {
|
|
|
30
30
|
*/
|
|
31
31
|
functionCall?: boolean;
|
|
32
32
|
id: string;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* whether model supports imageOutput
|
|
36
|
+
*/
|
|
37
|
+
imageOutput?: boolean;
|
|
38
|
+
|
|
33
39
|
/**
|
|
34
40
|
* whether model is custom
|
|
35
41
|
*/
|
|
@@ -53,8 +59,18 @@ export interface ChatModelCard {
|
|
|
53
59
|
*/
|
|
54
60
|
releasedAt?: string;
|
|
55
61
|
|
|
62
|
+
/**
|
|
63
|
+
* whether model supports search
|
|
64
|
+
*/
|
|
65
|
+
search?: boolean;
|
|
66
|
+
|
|
56
67
|
type?: AiModelType;
|
|
57
68
|
|
|
69
|
+
/**
|
|
70
|
+
* whether model supports video
|
|
71
|
+
*/
|
|
72
|
+
video?: boolean;
|
|
73
|
+
|
|
58
74
|
/**
|
|
59
75
|
* whether model supports vision
|
|
60
76
|
*/
|
package/src/app/[variants]/(main)/settings/provider/features/ModelList/CreateNewModelModal/Form.tsx
CHANGED
|
@@ -126,6 +126,31 @@ const ModelConfigForm = memo<ModelConfigFormProps>(
|
|
|
126
126
|
>
|
|
127
127
|
<Checkbox />
|
|
128
128
|
</Form.Item>
|
|
129
|
+
<Form.Item
|
|
130
|
+
extra={t('providerModels.item.modelConfig.search.extra')}
|
|
131
|
+
label={t('providerModels.item.modelConfig.search.title')}
|
|
132
|
+
name={['abilities', 'search']}
|
|
133
|
+
valuePropName={'checked'}
|
|
134
|
+
>
|
|
135
|
+
<Checkbox />
|
|
136
|
+
</Form.Item>
|
|
137
|
+
|
|
138
|
+
<Form.Item
|
|
139
|
+
extra={t('providerModels.item.modelConfig.imageOutput.extra')}
|
|
140
|
+
label={t('providerModels.item.modelConfig.imageOutput.title')}
|
|
141
|
+
name={['abilities', 'imageOutput']}
|
|
142
|
+
valuePropName={'checked'}
|
|
143
|
+
>
|
|
144
|
+
<Checkbox />
|
|
145
|
+
</Form.Item>
|
|
146
|
+
<Form.Item
|
|
147
|
+
extra={t('providerModels.item.modelConfig.video.extra')}
|
|
148
|
+
label={t('providerModels.item.modelConfig.video.title')}
|
|
149
|
+
name={['abilities', 'video']}
|
|
150
|
+
valuePropName={'checked'}
|
|
151
|
+
>
|
|
152
|
+
<Checkbox />
|
|
153
|
+
</Form.Item>
|
|
129
154
|
<Form.Item
|
|
130
155
|
extra={t('providerModels.item.modelConfig.type.extra')}
|
|
131
156
|
label={t('providerModels.item.modelConfig.type.title')}
|
|
@@ -3,7 +3,7 @@ import { GlobeOffIcon } from '@lobehub/ui/icons';
|
|
|
3
3
|
import { Divider } from 'antd';
|
|
4
4
|
import { createStyles } from 'antd-style';
|
|
5
5
|
import { LucideIcon, SparkleIcon } from 'lucide-react';
|
|
6
|
-
import { memo } from 'react';
|
|
6
|
+
import { memo, useEffect } from 'react';
|
|
7
7
|
import { useTranslation } from 'react-i18next';
|
|
8
8
|
import { Center, Flexbox } from 'react-layout-kit';
|
|
9
9
|
|
|
@@ -93,9 +93,12 @@ const Item = memo<NetworkOption>(({ value, description, icon, label }) => {
|
|
|
93
93
|
|
|
94
94
|
const Controls = memo(() => {
|
|
95
95
|
const { t } = useTranslation('chat');
|
|
96
|
-
const [model, provider] = useAgentStore((s) => [
|
|
96
|
+
const [model, provider, useModelBuiltinSearch, searchMode, updateAgentChatConfig] = useAgentStore((s) => [
|
|
97
97
|
agentSelectors.currentAgentModel(s),
|
|
98
98
|
agentSelectors.currentAgentModelProvider(s),
|
|
99
|
+
agentChatConfigSelectors.useModelBuiltinSearch(s),
|
|
100
|
+
agentChatConfigSelectors.currentChatConfig(s).searchMode,
|
|
101
|
+
s.updateAgentChatConfig,
|
|
99
102
|
]);
|
|
100
103
|
|
|
101
104
|
const supportFC = useAiInfraStore(aiModelSelectors.isModelSupportToolUse(model, provider));
|
|
@@ -105,24 +108,48 @@ const Controls = memo(() => {
|
|
|
105
108
|
const isModelHasBuiltinSearchConfig = useAiInfraStore(
|
|
106
109
|
aiModelSelectors.isModelHasBuiltinSearchConfig(model, provider),
|
|
107
110
|
);
|
|
111
|
+
const isModelBuiltinSearchInternal = useAiInfraStore(
|
|
112
|
+
aiModelSelectors.isModelBuiltinSearchInternal(model, provider),
|
|
113
|
+
);
|
|
114
|
+
const modelBuiltinSearchImpl = useAiInfraStore(aiModelSelectors.modelBuiltinSearchImpl(model, provider));
|
|
115
|
+
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (isModelBuiltinSearchInternal && (searchMode ?? 'off') === 'off') {
|
|
118
|
+
updateAgentChatConfig({ searchMode: 'auto' });
|
|
119
|
+
}
|
|
120
|
+
}, [isModelBuiltinSearchInternal, searchMode, updateAgentChatConfig]);
|
|
121
|
+
|
|
122
|
+
const options: NetworkOption[] = isModelBuiltinSearchInternal
|
|
123
|
+
? [
|
|
124
|
+
{
|
|
125
|
+
description: t('search.mode.auto.desc'),
|
|
126
|
+
icon: SparkleIcon,
|
|
127
|
+
label: t('search.mode.auto.title'),
|
|
128
|
+
value: 'auto',
|
|
129
|
+
},
|
|
130
|
+
]
|
|
131
|
+
: [
|
|
132
|
+
{
|
|
133
|
+
description: t('search.mode.off.desc'),
|
|
134
|
+
icon: GlobeOffIcon,
|
|
135
|
+
label: t('search.mode.off.title'),
|
|
136
|
+
value: 'off',
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
description: t('search.mode.auto.desc'),
|
|
140
|
+
icon: SparkleIcon,
|
|
141
|
+
label: t('search.mode.auto.title'),
|
|
142
|
+
value: 'auto',
|
|
143
|
+
},
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
const showModelBuiltinSearch = !isModelBuiltinSearchInternal &&
|
|
147
|
+
(isModelHasBuiltinSearchConfig || isProviderHasBuiltinSearchConfig);
|
|
148
|
+
|
|
149
|
+
const showFCSearchModel =
|
|
150
|
+
!supportFC && (!modelBuiltinSearchImpl || (!isModelBuiltinSearchInternal && !useModelBuiltinSearch));
|
|
108
151
|
|
|
109
|
-
const
|
|
110
|
-
{
|
|
111
|
-
description: t('search.mode.off.desc'),
|
|
112
|
-
icon: GlobeOffIcon,
|
|
113
|
-
label: t('search.mode.off.title'),
|
|
114
|
-
value: 'off',
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
description: t('search.mode.auto.desc'),
|
|
118
|
-
icon: SparkleIcon,
|
|
119
|
-
label: t('search.mode.auto.title'),
|
|
120
|
-
value: 'auto',
|
|
121
|
-
},
|
|
122
|
-
];
|
|
123
|
-
|
|
124
|
-
const showDivider = isModelHasBuiltinSearchConfig || !supportFC;
|
|
125
|
-
const showModelBuiltinSearch = isModelHasBuiltinSearchConfig || isProviderHasBuiltinSearchConfig;
|
|
152
|
+
const showDivider = showModelBuiltinSearch || showFCSearchModel;
|
|
126
153
|
|
|
127
154
|
return (
|
|
128
155
|
<Flexbox gap={4}>
|
|
@@ -131,7 +158,7 @@ const Controls = memo(() => {
|
|
|
131
158
|
))}
|
|
132
159
|
{showDivider && <Divider style={{ margin: 0 }} />}
|
|
133
160
|
{showModelBuiltinSearch && <ModelBuiltinSearch />}
|
|
134
|
-
{
|
|
161
|
+
{showFCSearchModel && <FCSearchModel />}
|
|
135
162
|
</Flexbox>
|
|
136
163
|
);
|
|
137
164
|
});
|
|
@@ -26,6 +26,7 @@ vi.mock('@/store/aiInfra/selectors', () => ({
|
|
|
26
26
|
},
|
|
27
27
|
aiModelSelectors: {
|
|
28
28
|
isModelHasBuiltinSearch: vi.fn(),
|
|
29
|
+
isModelBuiltinSearchInternal: vi.fn(),
|
|
29
30
|
},
|
|
30
31
|
}));
|
|
31
32
|
|
|
@@ -49,6 +50,9 @@ describe('getSearchConfig', () => {
|
|
|
49
50
|
vi.mocked(aiInfraSelectors.aiModelSelectors.isModelHasBuiltinSearch).mockReturnValue(
|
|
50
51
|
() => false,
|
|
51
52
|
);
|
|
53
|
+
vi.mocked(aiInfraSelectors.aiModelSelectors.isModelBuiltinSearchInternal).mockReturnValue(
|
|
54
|
+
() => false,
|
|
55
|
+
);
|
|
52
56
|
|
|
53
57
|
const result = getSearchConfig(model, provider);
|
|
54
58
|
|
|
@@ -71,6 +75,7 @@ describe('getSearchConfig', () => {
|
|
|
71
75
|
|
|
72
76
|
expect(result.enabledSearch).toBe(false);
|
|
73
77
|
expect(result.useApplicationBuiltinSearchTool).toBe(false);
|
|
78
|
+
expect(result.useModelSearch).toBe(false);
|
|
74
79
|
});
|
|
75
80
|
|
|
76
81
|
it('should prefer model search when available and enabled', () => {
|
|
@@ -85,6 +90,9 @@ describe('getSearchConfig', () => {
|
|
|
85
90
|
vi.mocked(aiInfraSelectors.aiModelSelectors.isModelHasBuiltinSearch).mockReturnValue(
|
|
86
91
|
() => false,
|
|
87
92
|
);
|
|
93
|
+
vi.mocked(aiInfraSelectors.aiModelSelectors.isModelBuiltinSearchInternal).mockReturnValue(
|
|
94
|
+
() => false,
|
|
95
|
+
);
|
|
88
96
|
|
|
89
97
|
const result = getSearchConfig(model, provider);
|
|
90
98
|
|
|
@@ -109,6 +117,9 @@ describe('getSearchConfig', () => {
|
|
|
109
117
|
vi.mocked(aiInfraSelectors.aiModelSelectors.isModelHasBuiltinSearch).mockReturnValue(
|
|
110
118
|
() => true,
|
|
111
119
|
);
|
|
120
|
+
vi.mocked(aiInfraSelectors.aiModelSelectors.isModelBuiltinSearchInternal).mockReturnValue(
|
|
121
|
+
() => false,
|
|
122
|
+
);
|
|
112
123
|
|
|
113
124
|
const result = getSearchConfig(model, provider);
|
|
114
125
|
|
|
@@ -133,6 +144,9 @@ describe('getSearchConfig', () => {
|
|
|
133
144
|
vi.mocked(aiInfraSelectors.aiModelSelectors.isModelHasBuiltinSearch).mockReturnValue(
|
|
134
145
|
() => true,
|
|
135
146
|
);
|
|
147
|
+
vi.mocked(aiInfraSelectors.aiModelSelectors.isModelBuiltinSearchInternal).mockReturnValue(
|
|
148
|
+
() => false,
|
|
149
|
+
);
|
|
136
150
|
|
|
137
151
|
const result = getSearchConfig(model, provider);
|
|
138
152
|
|
|
@@ -144,4 +158,31 @@ describe('getSearchConfig', () => {
|
|
|
144
158
|
useApplicationBuiltinSearchTool: true,
|
|
145
159
|
});
|
|
146
160
|
});
|
|
161
|
+
|
|
162
|
+
it('should force use model search when searchImpl is internal', () => {
|
|
163
|
+
vi.mocked(agentSelectors.agentChatConfigSelectors.currentChatConfig).mockReturnValue({
|
|
164
|
+
searchMode: 'on',
|
|
165
|
+
useModelBuiltinSearch: false,
|
|
166
|
+
} as any);
|
|
167
|
+
|
|
168
|
+
vi.mocked(aiInfraSelectors.aiProviderSelectors.isProviderHasBuiltinSearch).mockReturnValue(
|
|
169
|
+
() => false
|
|
170
|
+
);
|
|
171
|
+
vi.mocked(aiInfraSelectors.aiModelSelectors.isModelHasBuiltinSearch).mockReturnValue(
|
|
172
|
+
() => true
|
|
173
|
+
);
|
|
174
|
+
vi.mocked(aiInfraSelectors.aiModelSelectors.isModelBuiltinSearchInternal).mockReturnValue(
|
|
175
|
+
() => true
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const result = getSearchConfig(model, provider);
|
|
179
|
+
|
|
180
|
+
expect(result).toEqual({
|
|
181
|
+
enabledSearch: true,
|
|
182
|
+
isProviderHasBuiltinSearch: false,
|
|
183
|
+
isModelHasBuiltinSearch: true,
|
|
184
|
+
useModelSearch: true,
|
|
185
|
+
useApplicationBuiltinSearchTool: false,
|
|
186
|
+
});
|
|
187
|
+
});
|
|
147
188
|
});
|
|
@@ -34,10 +34,14 @@ export const getSearchConfig = (model: string, provider: string): SearchConfig =
|
|
|
34
34
|
model,
|
|
35
35
|
provider,
|
|
36
36
|
)(aiInfraStoreState);
|
|
37
|
+
const isModelBuiltinSearchInternal = aiModelSelectors.isModelBuiltinSearchInternal(
|
|
38
|
+
model,
|
|
39
|
+
provider!,
|
|
40
|
+
)(aiInfraStoreState);
|
|
37
41
|
|
|
38
42
|
const useModelSearch =
|
|
39
43
|
((isProviderHasBuiltinSearch || isModelHasBuiltinSearch) && chatConfig.useModelBuiltinSearch) ||
|
|
40
|
-
false;
|
|
44
|
+
isModelBuiltinSearchInternal || false;
|
|
41
45
|
|
|
42
46
|
const useApplicationBuiltinSearchTool = enabledSearch && !useModelSearch;
|
|
43
47
|
|
|
@@ -288,12 +288,22 @@ export default {
|
|
|
288
288
|
placeholder: '请输入模型 id,例如 gpt-4o 或 claude-3.5-sonnet',
|
|
289
289
|
title: '模型 ID',
|
|
290
290
|
},
|
|
291
|
+
imageOutput: {
|
|
292
|
+
extra:
|
|
293
|
+
'此配置将仅开启模型生成图片的能力,具体效果完全取决于模型本身,请自行测试该模型是否具备可用的图片生成能力',
|
|
294
|
+
title: '支持图片生成',
|
|
295
|
+
},
|
|
291
296
|
modalTitle: '自定义模型配置',
|
|
292
297
|
reasoning: {
|
|
293
298
|
extra:
|
|
294
299
|
'此配置将仅开启模型深度思考的能力,具体效果完全取决于模型本身,请自行测试该模型是否具备可用的深度思考能力',
|
|
295
300
|
title: '支持深度思考',
|
|
296
301
|
},
|
|
302
|
+
search: {
|
|
303
|
+
extra:
|
|
304
|
+
'此配置将仅开启模型内置搜索引擎的联网搜索能力,是否支持内置搜索引擎取决于模型本身,请自行测试该模型的内置搜索引擎能力可用性',
|
|
305
|
+
title: '支持联网搜索',
|
|
306
|
+
},
|
|
297
307
|
tokens: {
|
|
298
308
|
extra: '设置模型支持的最大 Token 数',
|
|
299
309
|
title: '最大上下文窗口',
|
|
@@ -314,6 +324,11 @@ export default {
|
|
|
314
324
|
placeholder: '请选择模型类型',
|
|
315
325
|
title: '模型类型',
|
|
316
326
|
},
|
|
327
|
+
video: {
|
|
328
|
+
extra:
|
|
329
|
+
'此配置将仅开启应用中的视频识别配置,是否支持识别完全取决于模型本身,请自行测试该模型的视频识别能力可用性',
|
|
330
|
+
title: '支持视频识别',
|
|
331
|
+
},
|
|
317
332
|
vision: {
|
|
318
333
|
extra:
|
|
319
334
|
'此配置将仅开启应用中的图片上传配置,是否支持识别完全取决于模型本身,请自行测试该模型的视觉识别能力可用性',
|
|
@@ -79,7 +79,10 @@ export const createAiModelSlice: StateCreator<
|
|
|
79
79
|
abilities: {
|
|
80
80
|
files: model.files,
|
|
81
81
|
functionCall: model.functionCall,
|
|
82
|
+
imageOutput: model.imageOutput,
|
|
82
83
|
reasoning: model.reasoning,
|
|
84
|
+
search: model.search,
|
|
85
|
+
video: model.video,
|
|
83
86
|
vision: model.vision,
|
|
84
87
|
},
|
|
85
88
|
enabled: model.enabled || false,
|
|
@@ -111,6 +111,12 @@ const isModelHasBuiltinSearch = (id: string, provider: string) => (s: AIProvider
|
|
|
111
111
|
return !!searchImpl;
|
|
112
112
|
};
|
|
113
113
|
|
|
114
|
+
const isModelBuiltinSearchInternal = (id: string, provider: string) => (s: AIProviderStoreState): boolean => {
|
|
115
|
+
const searchImpl = modelBuiltinSearchImpl(id, provider)(s);
|
|
116
|
+
|
|
117
|
+
return searchImpl === ModelSearchImplement.Internal;
|
|
118
|
+
};
|
|
119
|
+
|
|
114
120
|
const isModelHasBuiltinSearchConfig =
|
|
115
121
|
(id: string, provider: string) => (s: AIProviderStoreState) => {
|
|
116
122
|
const searchImpl = modelBuiltinSearchImpl(id, provider)(s);
|
|
@@ -133,6 +139,7 @@ export const aiModelSelectors = {
|
|
|
133
139
|
getModelCard,
|
|
134
140
|
hasRemoteModels,
|
|
135
141
|
isEmptyAiProviderModelList,
|
|
142
|
+
isModelBuiltinSearchInternal,
|
|
136
143
|
isModelEnabled,
|
|
137
144
|
isModelHasBuiltinSearch,
|
|
138
145
|
isModelHasBuiltinSearchConfig,
|
|
@@ -384,9 +384,13 @@ export const generateAIChat: StateCreator<
|
|
|
384
384
|
model,
|
|
385
385
|
provider!,
|
|
386
386
|
)(aiInfraStoreState);
|
|
387
|
+
const isModelBuiltinSearchInternal = aiModelSelectors.isModelBuiltinSearchInternal(
|
|
388
|
+
model,
|
|
389
|
+
provider!,
|
|
390
|
+
)(aiInfraStoreState);
|
|
387
391
|
const useModelBuiltinSearch = agentChatConfigSelectors.useModelBuiltinSearch(agentStoreState);
|
|
388
392
|
const useModelSearch =
|
|
389
|
-
(isProviderHasBuiltinSearch || isModelHasBuiltinSearch) && useModelBuiltinSearch;
|
|
393
|
+
((isProviderHasBuiltinSearch || isModelHasBuiltinSearch) && useModelBuiltinSearch) || isModelBuiltinSearchInternal;
|
|
390
394
|
const isAgentEnableSearch = agentChatConfigSelectors.isAgentEnableSearch(agentStoreState);
|
|
391
395
|
|
|
392
396
|
if (isAgentEnableSearch && !useModelSearch && !isModelSupportToolUse) {
|
|
@@ -382,9 +382,13 @@ export const generateAIChatV2: StateCreator<
|
|
|
382
382
|
model,
|
|
383
383
|
provider!,
|
|
384
384
|
)(aiInfraStoreState);
|
|
385
|
+
const isModelBuiltinSearchInternal = aiModelSelectors.isModelBuiltinSearchInternal(
|
|
386
|
+
model,
|
|
387
|
+
provider!,
|
|
388
|
+
)(aiInfraStoreState);
|
|
385
389
|
const useModelBuiltinSearch = agentChatConfigSelectors.useModelBuiltinSearch(agentStoreState);
|
|
386
390
|
const useModelSearch =
|
|
387
|
-
(isProviderHasBuiltinSearch || isModelHasBuiltinSearch) && useModelBuiltinSearch;
|
|
391
|
+
((isProviderHasBuiltinSearch || isModelHasBuiltinSearch) && useModelBuiltinSearch) || isModelBuiltinSearchInternal;
|
|
388
392
|
const isAgentEnableSearch = agentChatConfigSelectors.isAgentEnableSearch(agentStoreState);
|
|
389
393
|
|
|
390
394
|
if (isAgentEnableSearch && !useModelSearch && !isModelSupportToolUse) {
|