@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.
@@ -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
- functionCallKeywords: ['gemini'],
26
- reasoningKeywords: ['thinking', '-2.5-'],
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
- model.id.toLowerCase(),
319
- EMBEDDING_MODEL_KEYWORDS.map((k) => k.toLowerCase()),
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) || false),
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
- return processModelCard(model, config, knownModel);
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
- let providerLocalConfig: any[] | null = null;
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
- let providerLocalModelConfig = null;
492
- if (providerLocalConfig && Array.isArray(providerLocalConfig)) {
493
- providerLocalModelConfig = providerLocalConfig.find((m) => m.id === model.id);
494
- }
566
+ const providerLocalModelConfig = getModelLocalEnableConfig(
567
+ providerLocalConfig as any[],
568
+ model,
569
+ );
495
570
 
496
571
  const processedModel = processModelCard(model, config, knownModel);
497
572
 
@@ -175,9 +175,6 @@ describe('postProcessModelList', () => {
175
175
  enabled: true,
176
176
  contextWindowTokens: 4096,
177
177
  description: 'A flash model',
178
- functionCall: true,
179
- vision: true,
180
- reasoning: false,
181
178
  maxOutput: 2048,
182
179
  type: 'image',
183
180
  parameters: {
@@ -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
- ...model, // Reuse all configurations from the original model
59
+ ...rest, // Keep other fields (such as displayName, pricing, enabled, contextWindowTokens, etc.)
46
60
  id: `${model.id}:image`,
47
- // Override to image type
48
- parameters: CHAT_MODEL_IMAGE_GENERATION_PARAMS,
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
  */
@@ -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 options: NetworkOption[] = [
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
- {!supportFC && <FCSearchModel />}
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) {