@lobehub/lobehub 2.0.0-next.262 → 2.0.0-next.263

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.
Files changed (36) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/changelog/v1.json +9 -0
  3. package/locales/zh-CN/chat.json +1 -0
  4. package/locales/zh-CN/modelProvider.json +20 -0
  5. package/package.json +1 -1
  6. package/packages/database/src/models/aiModel.ts +2 -0
  7. package/packages/database/src/repositories/aiInfra/index.test.ts +41 -1
  8. package/packages/database/src/repositories/aiInfra/index.ts +3 -1
  9. package/packages/model-runtime/src/providers/openrouter/index.test.ts +9 -55
  10. package/packages/model-runtime/src/providers/openrouter/index.ts +47 -27
  11. package/packages/model-runtime/src/providers/openrouter/type.ts +16 -28
  12. package/packages/model-runtime/src/providers/vercelaigateway/index.test.ts +6 -6
  13. package/packages/model-runtime/src/providers/vercelaigateway/index.ts +54 -11
  14. package/packages/model-runtime/src/utils/modelParse.test.ts +185 -3
  15. package/packages/model-runtime/src/utils/modelParse.ts +108 -1
  16. package/packages/types/src/llm.ts +3 -1
  17. package/src/app/[variants]/(main)/settings/provider/features/ModelList/CreateNewModelModal/ExtendParamsSelect.tsx +398 -0
  18. package/src/app/[variants]/(main)/settings/provider/features/ModelList/CreateNewModelModal/Form.tsx +11 -2
  19. package/src/app/[variants]/(main)/settings/provider/features/ModelList/CreateNewModelModal/__tests__/ExtendParamsSelect.test.tsx +59 -0
  20. package/src/features/ChatInput/ActionBar/Model/ControlsForm.tsx +1 -1
  21. package/src/features/ChatInput/ActionBar/Model/GPT51ReasoningEffortSlider.tsx +9 -54
  22. package/src/features/ChatInput/ActionBar/Model/GPT52ProReasoningEffortSlider.tsx +9 -53
  23. package/src/features/ChatInput/ActionBar/Model/GPT52ReasoningEffortSlider.tsx +9 -55
  24. package/src/features/ChatInput/ActionBar/Model/GPT5ReasoningEffortSlider.tsx +9 -54
  25. package/src/features/ChatInput/ActionBar/Model/ImageAspectRatioSelect.tsx +50 -16
  26. package/src/features/ChatInput/ActionBar/Model/ImageResolutionSlider.tsx +7 -53
  27. package/src/features/ChatInput/ActionBar/Model/LevelSlider.tsx +92 -0
  28. package/src/features/ChatInput/ActionBar/Model/ReasoningEffortSlider.tsx +9 -53
  29. package/src/features/ChatInput/ActionBar/Model/TextVerbositySlider.tsx +9 -53
  30. package/src/features/ChatInput/ActionBar/Model/ThinkingLevel2Slider.tsx +9 -52
  31. package/src/features/ChatInput/ActionBar/Model/ThinkingLevelSlider.tsx +9 -54
  32. package/src/features/ChatInput/ActionBar/Model/ThinkingSlider.tsx +20 -56
  33. package/src/features/ChatInput/ActionBar/Model/__tests__/createLevelSlider.test.tsx +126 -0
  34. package/src/features/ChatInput/ActionBar/Model/createLevelSlider.tsx +105 -0
  35. package/src/locales/default/chat.ts +1 -0
  36. package/src/locales/default/modelProvider.ts +38 -0
@@ -0,0 +1,398 @@
1
+ import { Flexbox } from '@lobehub/ui';
2
+ import { Popover, Select, Space, Switch, Tag, Typography, theme } from 'antd';
3
+ import type { ExtendParamsType } from 'model-bank';
4
+ import { memo, useMemo } from 'react';
5
+ import type { ReactNode } from 'react';
6
+ import { Trans, useTranslation } from 'react-i18next';
7
+
8
+ import GPT5ReasoningEffortSlider from '@/features/ChatInput/ActionBar/Model/GPT5ReasoningEffortSlider';
9
+ import GPT51ReasoningEffortSlider from '@/features/ChatInput/ActionBar/Model/GPT51ReasoningEffortSlider';
10
+ import GPT52ProReasoningEffortSlider from '@/features/ChatInput/ActionBar/Model/GPT52ProReasoningEffortSlider';
11
+ import GPT52ReasoningEffortSlider from '@/features/ChatInput/ActionBar/Model/GPT52ReasoningEffortSlider';
12
+ import ImageAspectRatioSelect from '@/features/ChatInput/ActionBar/Model/ImageAspectRatioSelect';
13
+ import ImageResolutionSlider from '@/features/ChatInput/ActionBar/Model/ImageResolutionSlider';
14
+ import ReasoningEffortSlider from '@/features/ChatInput/ActionBar/Model/ReasoningEffortSlider';
15
+ import ReasoningTokenSlider from '@/features/ChatInput/ActionBar/Model/ReasoningTokenSlider';
16
+ import TextVerbositySlider from '@/features/ChatInput/ActionBar/Model/TextVerbositySlider';
17
+ import ThinkingBudgetSlider from '@/features/ChatInput/ActionBar/Model/ThinkingBudgetSlider';
18
+ import ThinkingLevel2Slider from '@/features/ChatInput/ActionBar/Model/ThinkingLevel2Slider';
19
+ import ThinkingLevelSlider from '@/features/ChatInput/ActionBar/Model/ThinkingLevelSlider';
20
+ import ThinkingSlider from '@/features/ChatInput/ActionBar/Model/ThinkingSlider';
21
+
22
+ type ExtendParamsOption = {
23
+ hintKey: string;
24
+ key: ExtendParamsType;
25
+ };
26
+
27
+ const EXTEND_PARAMS_OPTIONS: ExtendParamsOption[] = [
28
+ {
29
+ hintKey: 'providerModels.item.modelConfig.extendParams.options.disableContextCaching.hint',
30
+ key: 'disableContextCaching',
31
+ },
32
+ {
33
+ hintKey: 'providerModels.item.modelConfig.extendParams.options.enableReasoning.hint',
34
+ key: 'enableReasoning',
35
+ },
36
+ {
37
+ hintKey: 'providerModels.item.modelConfig.extendParams.options.reasoningBudgetToken.hint',
38
+ key: 'reasoningBudgetToken',
39
+ },
40
+ {
41
+ hintKey: 'providerModels.item.modelConfig.extendParams.options.reasoningEffort.hint',
42
+ key: 'reasoningEffort',
43
+ },
44
+ {
45
+ hintKey: 'providerModels.item.modelConfig.extendParams.options.gpt5ReasoningEffort.hint',
46
+ key: 'gpt5ReasoningEffort',
47
+ },
48
+ {
49
+ hintKey: 'providerModels.item.modelConfig.extendParams.options.gpt5_1ReasoningEffort.hint',
50
+ key: 'gpt5_1ReasoningEffort',
51
+ },
52
+ {
53
+ hintKey: 'providerModels.item.modelConfig.extendParams.options.gpt5_2ReasoningEffort.hint',
54
+ key: 'gpt5_2ReasoningEffort',
55
+ },
56
+ {
57
+ hintKey: 'providerModels.item.modelConfig.extendParams.options.gpt5_2ProReasoningEffort.hint',
58
+ key: 'gpt5_2ProReasoningEffort',
59
+ },
60
+ {
61
+ hintKey: 'providerModels.item.modelConfig.extendParams.options.textVerbosity.hint',
62
+ key: 'textVerbosity',
63
+ },
64
+ {
65
+ hintKey: 'providerModels.item.modelConfig.extendParams.options.thinking.hint',
66
+ key: 'thinking',
67
+ },
68
+ {
69
+ hintKey: 'providerModels.item.modelConfig.extendParams.options.thinkingBudget.hint',
70
+ key: 'thinkingBudget',
71
+ },
72
+ {
73
+ hintKey: 'providerModels.item.modelConfig.extendParams.options.thinkingLevel.hint',
74
+ key: 'thinkingLevel',
75
+ },
76
+ {
77
+ hintKey: 'providerModels.item.modelConfig.extendParams.options.thinkingLevel2.hint',
78
+ key: 'thinkingLevel2',
79
+ },
80
+ {
81
+ hintKey: 'providerModels.item.modelConfig.extendParams.options.urlContext.hint',
82
+ key: 'urlContext',
83
+ },
84
+ {
85
+ hintKey: 'providerModels.item.modelConfig.extendParams.options.imageAspectRatio.hint',
86
+ key: 'imageAspectRatio',
87
+ },
88
+ {
89
+ hintKey: 'providerModels.item.modelConfig.extendParams.options.imageResolution.hint',
90
+ key: 'imageResolution',
91
+ },
92
+ ];
93
+
94
+ // Map variant keys to their base i18n title key (synced with ControlsForm.tsx)
95
+ // This allows reusing existing i18n translations instead of adding new ones
96
+ const TITLE_KEY_ALIASES: Partial<Record<ExtendParamsType, ExtendParamsType>> = {
97
+ gpt5ReasoningEffort: 'reasoningEffort',
98
+ gpt5_1ReasoningEffort: 'reasoningEffort',
99
+ gpt5_2ProReasoningEffort: 'reasoningEffort',
100
+ gpt5_2ReasoningEffort: 'reasoningEffort',
101
+ thinkingLevel2: 'thinkingLevel',
102
+ };
103
+
104
+ type PreviewMeta = {
105
+ labelOverride?: string;
106
+ labelSuffix?: string;
107
+ previewWidth?: number;
108
+ tag?: string;
109
+ };
110
+
111
+ const PREVIEW_META: Partial<Record<ExtendParamsType, PreviewMeta>> = {
112
+ disableContextCaching: { labelSuffix: ' (Claude)', previewWidth: 400 },
113
+ enableReasoning: { previewWidth: 300, tag: 'thinking.type' },
114
+ gpt5ReasoningEffort: { previewWidth: 300, tag: 'reasoning_effort' },
115
+ gpt5_1ReasoningEffort: { labelSuffix: ' (GPT-5.1)', previewWidth: 300, tag: 'reasoning_effort' },
116
+ gpt5_2ProReasoningEffort: {
117
+ labelSuffix: ' (GPT-5.2 Pro)',
118
+ previewWidth: 300,
119
+ tag: 'reasoning_effort',
120
+ },
121
+ gpt5_2ReasoningEffort: { labelSuffix: ' (GPT-5.2)', previewWidth: 300, tag: 'reasoning_effort' },
122
+ imageAspectRatio: { labelSuffix: '', previewWidth: 350, tag: 'aspect_ratio' },
123
+ imageResolution: { labelSuffix: '', previewWidth: 250, tag: 'resolution' },
124
+ reasoningBudgetToken: { previewWidth: 350, tag: 'thinking.budget_tokens' },
125
+ reasoningEffort: { previewWidth: 250, tag: 'reasoning_effort' },
126
+ textVerbosity: { labelSuffix: '', previewWidth: 250, tag: 'text_verbosity' },
127
+ thinking: { labelSuffix: ' (Doubao)', previewWidth: 300, tag: 'thinking.type' },
128
+ thinkingBudget: { labelSuffix: ' (Gemini)', previewWidth: 500, tag: 'thinkingBudget' },
129
+ thinkingLevel: { labelSuffix: ' (Gemini 3)', previewWidth: 280, tag: 'thinkingLevel' },
130
+ thinkingLevel2: { labelSuffix: ' (Gemini 3)', previewWidth: 200, tag: 'thinkingLevel' },
131
+ urlContext: { labelSuffix: ' (Gemini)', previewWidth: 400, tag: 'urlContext' },
132
+ };
133
+
134
+ type ExtendParamsDefinition = {
135
+ desc?: ReactNode;
136
+ hint: string;
137
+ key: ExtendParamsType;
138
+ label: string;
139
+ parameterTag?: string;
140
+ preview?: ReactNode;
141
+ previewWidth?: number;
142
+ };
143
+
144
+ interface ExtendParamsSelectProps {
145
+ onChange?: (value: ExtendParamsType[] | undefined) => void;
146
+ value?: ExtendParamsType[];
147
+ }
148
+
149
+ const PreviewContent = ({
150
+ desc,
151
+ hint,
152
+ label,
153
+ preview,
154
+ previewFallback,
155
+ parameterTag,
156
+ previewWidth,
157
+ }: {
158
+ desc?: ReactNode;
159
+ hint: string;
160
+ label: string;
161
+ parameterTag?: string;
162
+ preview?: ReactNode;
163
+ previewFallback: string;
164
+ previewWidth?: number;
165
+ }) => {
166
+ const { token } = theme.useToken();
167
+ const containerStyle = previewWidth
168
+ ? { minWidth: previewWidth, width: previewWidth }
169
+ : { minWidth: 240 };
170
+
171
+ return (
172
+ <Flexbox gap={12} style={containerStyle}>
173
+ <Typography.Text style={{ whiteSpace: 'normal' }} type={'secondary'}>
174
+ {hint}
175
+ </Typography.Text>
176
+ <Flexbox gap={12}>
177
+ <Flexbox
178
+ gap={8}
179
+ style={{
180
+ background: token.colorBgElevated,
181
+ border: `1px solid ${token.colorBorderSecondary}`,
182
+ borderRadius: 10,
183
+ padding: 12,
184
+ width: previewWidth,
185
+ }}
186
+ >
187
+ <Flexbox align={'center'} gap={8} horizontal>
188
+ <Typography.Text strong>{label}</Typography.Text>
189
+ {parameterTag ? <Tag color={'cyan'}>{parameterTag}</Tag> : null}
190
+ </Flexbox>
191
+ {desc ? (
192
+ <Typography.Text style={{ fontSize: 12, whiteSpace: 'normal' }} type={'secondary'}>
193
+ {desc}
194
+ </Typography.Text>
195
+ ) : null}
196
+ {preview ? (
197
+ <div style={{ pointerEvents: 'none', width: '100%' }}>{preview}</div>
198
+ ) : (
199
+ <Typography.Text type={'secondary'}>{previewFallback}</Typography.Text>
200
+ )}
201
+ </Flexbox>
202
+ </Flexbox>
203
+ </Flexbox>
204
+ );
205
+ };
206
+
207
+ const ExtendParamsSelect = memo<ExtendParamsSelectProps>(({ value, onChange }) => {
208
+ const { t } = useTranslation('modelProvider');
209
+ const { t: tChat } = useTranslation('chat');
210
+
211
+ // Preview controls use controlled mode with default values (no store access)
212
+ const previewControls = useMemo<Partial<Record<ExtendParamsType, ReactNode>>>(
213
+ () => ({
214
+ disableContextCaching: <Switch checked disabled />,
215
+ enableReasoning: <Switch checked disabled />,
216
+ gpt5ReasoningEffort: <GPT5ReasoningEffortSlider value="medium" />,
217
+ gpt5_1ReasoningEffort: <GPT51ReasoningEffortSlider value="none" />,
218
+ gpt5_2ProReasoningEffort: <GPT52ProReasoningEffortSlider value="medium" />,
219
+ gpt5_2ReasoningEffort: <GPT52ReasoningEffortSlider value="none" />,
220
+ imageAspectRatio: <ImageAspectRatioSelect value="1:1" />,
221
+ imageResolution: <ImageResolutionSlider value="1K" />,
222
+ reasoningBudgetToken: <ReasoningTokenSlider defaultValue={1 * 1024} />,
223
+ reasoningEffort: <ReasoningEffortSlider value="medium" />,
224
+ textVerbosity: <TextVerbositySlider value="medium" />,
225
+ thinking: <ThinkingSlider value="auto" />,
226
+ thinkingBudget: <ThinkingBudgetSlider defaultValue={2 * 1024} />,
227
+ thinkingLevel: <ThinkingLevelSlider value="high" />,
228
+ thinkingLevel2: <ThinkingLevel2Slider value="high" />,
229
+ urlContext: <Switch checked disabled />,
230
+ }),
231
+ [],
232
+ );
233
+
234
+ const descOverrides: Partial<Record<ExtendParamsType, ReactNode>> = {
235
+ disableContextCaching: (() => {
236
+ const original = tChat('extendParams.disableContextCaching.desc', { defaultValue: '' });
237
+
238
+ const sanitized = original.replace(/(<\d>.*?<\/\d>)/u, '');
239
+
240
+ return (
241
+ sanitized || (
242
+ <Trans i18nKey={'extendParams.disableContextCaching.desc'} ns={'chat'}>
243
+ 单条对话生成成本最高可降低 90%,响应速度提升 4 倍。开启后将自动禁用历史消息数限制
244
+ </Trans>
245
+ )
246
+ );
247
+ })(),
248
+ enableReasoning: (() => {
249
+ const original = tChat('extendParams.enableReasoning.desc', { defaultValue: '' });
250
+
251
+ const sanitized = original.replace(/(<\d>.*?<\/\d>)/u, '');
252
+
253
+ return (
254
+ sanitized || (
255
+ <Trans i18nKey={'extendParams.enableReasoning.desc'} ns={'chat'}>
256
+ 基于 Claude Thinking 机制限制,开启后将自动禁用历史消息数限制
257
+ </Trans>
258
+ )
259
+ );
260
+ })(),
261
+ };
262
+
263
+ const previewFallback = String(
264
+ t('providerModels.item.modelConfig.extendParams.previewFallback', {
265
+ defaultValue: 'Preview unavailable',
266
+ }),
267
+ );
268
+
269
+ const definitions = useMemo<ExtendParamsDefinition[]>(() => {
270
+ return EXTEND_PARAMS_OPTIONS.map((item) => {
271
+ const descKey = `extendParams.${item.key}.desc`;
272
+ const rawDesc = tChat(descKey as any, { defaultValue: '' });
273
+ const normalizedDesc =
274
+ typeof rawDesc === 'string' && rawDesc !== '' && rawDesc !== descKey ? rawDesc : undefined;
275
+ const desc = descOverrides[item.key] ?? normalizedDesc;
276
+ const meta = PREVIEW_META[item.key];
277
+ // Use alias key for title if available (synced with ControlsForm.tsx)
278
+ const titleKey = TITLE_KEY_ALIASES[item.key] ?? item.key;
279
+ const baseLabel = String(
280
+ tChat(`extendParams.${titleKey}.title` as any, { defaultValue: item.key }),
281
+ );
282
+
283
+ const label = meta?.labelOverride
284
+ ? meta.labelOverride
285
+ : meta?.labelSuffix
286
+ ? `${baseLabel}${meta.labelSuffix}`
287
+ : baseLabel;
288
+
289
+ return {
290
+ desc,
291
+ hint: String(t(item.hintKey as any)),
292
+ key: item.key,
293
+ label,
294
+ parameterTag: meta?.tag,
295
+ preview: previewControls[item.key],
296
+ previewWidth: meta?.previewWidth,
297
+ };
298
+ });
299
+ }, [previewControls, t, tChat]);
300
+
301
+ const definitionMap = useMemo(() => {
302
+ return new Map(definitions.map((item) => [item.key, item]));
303
+ }, [definitions]);
304
+
305
+ const options = useMemo(
306
+ () =>
307
+ definitions.map((item) => ({
308
+ label: item.label,
309
+ value: item.key,
310
+ })),
311
+ [definitions],
312
+ );
313
+
314
+ const placeholder = String(t('providerModels.item.modelConfig.extendParams.placeholder'));
315
+ const handleChange = (val: ExtendParamsType[]) => {
316
+ if (!Array.isArray(val) || val.length === 0) {
317
+ onChange?.(undefined);
318
+ return;
319
+ }
320
+
321
+ const filtered = val.filter((item) => definitionMap.has(item));
322
+ onChange?.(filtered.length ? filtered : undefined);
323
+ };
324
+
325
+ return (
326
+ <Flexbox gap={8}>
327
+ <Select
328
+ allowClear
329
+ mode={'multiple'}
330
+ onChange={(val) => handleChange(val as ExtendParamsType[])}
331
+ optionRender={(option) => {
332
+ const def = definitionMap.get(option.value as ExtendParamsType);
333
+ if (!def) return option.label;
334
+
335
+ return (
336
+ <Popover
337
+ content={
338
+ <PreviewContent
339
+ desc={def.desc}
340
+ hint={def.hint}
341
+ label={def.label}
342
+ parameterTag={def.parameterTag}
343
+ preview={def.preview}
344
+ previewFallback={previewFallback}
345
+ previewWidth={def.previewWidth}
346
+ />
347
+ }
348
+ placement={'right'}
349
+ >
350
+ <Flexbox gap={4}>
351
+ <Typography.Text>{def.label}</Typography.Text>
352
+ <Typography.Text style={{ fontSize: 12 }} type={'secondary'}>
353
+ {def.hint}
354
+ </Typography.Text>
355
+ </Flexbox>
356
+ </Popover>
357
+ );
358
+ }}
359
+ options={options}
360
+ placeholder={placeholder}
361
+ popupMatchSelectWidth={false}
362
+ style={{ width: '100%' }}
363
+ value={value}
364
+ />
365
+ {value && value.length > 0 && (
366
+ <Space size={[8, 8]} wrap>
367
+ {value.map((key) => {
368
+ const def = definitionMap.get(key);
369
+ if (!def) return null;
370
+ return (
371
+ <Popover
372
+ content={
373
+ <PreviewContent
374
+ desc={def.desc}
375
+ hint={def.hint}
376
+ label={def.label}
377
+ parameterTag={def.parameterTag}
378
+ preview={def.preview}
379
+ previewFallback={previewFallback}
380
+ previewWidth={def.previewWidth}
381
+ />
382
+ }
383
+ key={key}
384
+ placement={'top'}
385
+ >
386
+ <Tag bordered={false} color={'processing'}>
387
+ {def.label}
388
+ </Tag>
389
+ </Popover>
390
+ );
391
+ })}
392
+ </Space>
393
+ )}
394
+ </Flexbox>
395
+ );
396
+ });
397
+
398
+ export default ExtendParamsSelect;
@@ -1,5 +1,5 @@
1
- import { Input , Checkbox } from '@lobehub/ui';
2
- import { Form, type FormInstance, Select } from 'antd';
1
+ import { Input } from '@lobehub/ui';
2
+ import { Checkbox, Form, type FormInstance, Select } from 'antd';
3
3
  import { type AiModelType } from 'model-bank';
4
4
  import { memo, useEffect, useMemo } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
@@ -8,6 +8,8 @@ import MaxTokenSlider from '@/components/MaxTokenSlider';
8
8
  import { useIsMobile } from '@/hooks/useIsMobile';
9
9
  import { type ChatModelCard } from '@/types/llm';
10
10
 
11
+ import ExtendParamsSelect from './ExtendParamsSelect';
12
+
11
13
  interface ModelConfigFormProps {
12
14
  idEditable?: boolean;
13
15
  initialValues?: ChatModelCard;
@@ -102,6 +104,13 @@ const ModelConfigForm = memo<ModelConfigFormProps>(
102
104
  >
103
105
  <MaxTokenSlider />
104
106
  </Form.Item>
107
+ <Form.Item
108
+ extra={t('providerModels.item.modelConfig.extendParams.extra')}
109
+ label={t('providerModels.item.modelConfig.extendParams.title')}
110
+ name={['settings', 'extendParams']}
111
+ >
112
+ <ExtendParamsSelect />
113
+ </Form.Item>
105
114
  <Form.Item
106
115
  extra={t('providerModels.item.modelConfig.functionCall.extra')}
107
116
  label={t('providerModels.item.modelConfig.functionCall.title')}
@@ -0,0 +1,59 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ // Import the constant directly for testing
4
+ // We'll need to test the TITLE_KEY_ALIASES logic
5
+
6
+ describe('ExtendParamsSelect', () => {
7
+ describe('TITLE_KEY_ALIASES mapping', () => {
8
+ // This mapping should be synced with ControlsForm.tsx
9
+ const TITLE_KEY_ALIASES: Record<string, string> = {
10
+ gpt5ReasoningEffort: 'reasoningEffort',
11
+ gpt5_1ReasoningEffort: 'reasoningEffort',
12
+ gpt5_2ProReasoningEffort: 'reasoningEffort',
13
+ gpt5_2ReasoningEffort: 'reasoningEffort',
14
+ thinkingLevel2: 'thinkingLevel',
15
+ };
16
+
17
+ it('should map GPT5 variants to reasoningEffort', () => {
18
+ expect(TITLE_KEY_ALIASES['gpt5ReasoningEffort']).toBe('reasoningEffort');
19
+ expect(TITLE_KEY_ALIASES['gpt5_1ReasoningEffort']).toBe('reasoningEffort');
20
+ expect(TITLE_KEY_ALIASES['gpt5_2ReasoningEffort']).toBe('reasoningEffort');
21
+ expect(TITLE_KEY_ALIASES['gpt5_2ProReasoningEffort']).toBe('reasoningEffort');
22
+ });
23
+
24
+ it('should map thinkingLevel2 to thinkingLevel', () => {
25
+ expect(TITLE_KEY_ALIASES['thinkingLevel2']).toBe('thinkingLevel');
26
+ });
27
+
28
+ it('should return undefined for keys without aliases', () => {
29
+ expect(TITLE_KEY_ALIASES['reasoningEffort']).toBeUndefined();
30
+ expect(TITLE_KEY_ALIASES['thinkingLevel']).toBeUndefined();
31
+ expect(TITLE_KEY_ALIASES['thinking']).toBeUndefined();
32
+ });
33
+ });
34
+
35
+ describe('title key resolution logic', () => {
36
+ const TITLE_KEY_ALIASES: Record<string, string> = {
37
+ gpt5ReasoningEffort: 'reasoningEffort',
38
+ gpt5_1ReasoningEffort: 'reasoningEffort',
39
+ gpt5_2ProReasoningEffort: 'reasoningEffort',
40
+ gpt5_2ReasoningEffort: 'reasoningEffort',
41
+ thinkingLevel2: 'thinkingLevel',
42
+ };
43
+
44
+ const getTitleKey = (key: string): string => {
45
+ return TITLE_KEY_ALIASES[key] ?? key;
46
+ };
47
+
48
+ it('should return the alias key when available', () => {
49
+ expect(getTitleKey('gpt5ReasoningEffort')).toBe('reasoningEffort');
50
+ expect(getTitleKey('thinkingLevel2')).toBe('thinkingLevel');
51
+ });
52
+
53
+ it('should return the original key when no alias exists', () => {
54
+ expect(getTitleKey('reasoningEffort')).toBe('reasoningEffort');
55
+ expect(getTitleKey('thinking')).toBe('thinking');
56
+ expect(getTitleKey('textVerbosity')).toBe('textVerbosity');
57
+ });
58
+ });
59
+ });
@@ -178,7 +178,7 @@ const ControlsForm = memo(() => {
178
178
  },
179
179
  {
180
180
  children: <ThinkingBudgetSlider />,
181
- label: t('extendParams.reasoningBudgetToken.title'),
181
+ label: t('extendParams.thinkingBudget.title'),
182
182
  layout: 'vertical',
183
183
  minWidth: 460,
184
184
  name: 'thinkingBudget',
@@ -1,60 +1,15 @@
1
- import { Flexbox } from '@lobehub/ui';
2
- import { Slider } from 'antd';
3
- import { memo, useCallback } from 'react';
1
+ import { type CreatedLevelSliderProps, createLevelSliderComponent } from './createLevelSlider';
4
2
 
5
- import { useAgentStore } from '@/store/agent';
6
- import { chatConfigByIdSelectors } from '@/store/agent/selectors';
3
+ const GPT51_REASONING_EFFORT_LEVELS = ['none', 'low', 'medium', 'high'] as const;
4
+ type GPT51ReasoningEffort = (typeof GPT51_REASONING_EFFORT_LEVELS)[number];
7
5
 
8
- import { useAgentId } from '../../hooks/useAgentId';
9
- import { useUpdateAgentConfig } from '../../hooks/useUpdateAgentConfig';
6
+ export type GPT51ReasoningEffortSliderProps = CreatedLevelSliderProps<GPT51ReasoningEffort>;
10
7
 
11
- const GPT51ReasoningEffortSlider = memo(() => {
12
- const agentId = useAgentId();
13
- const { updateAgentChatConfig } = useUpdateAgentConfig();
14
- const config = useAgentStore((s) => chatConfigByIdSelectors.getChatConfigById(agentId)(s));
15
-
16
- const gpt5_1ReasoningEffort = config.gpt5_1ReasoningEffort || 'none'; // Default to 'none' if not set
17
-
18
- const marks = {
19
- 0: 'none',
20
- 1: 'low',
21
- 2: 'medium',
22
- 3: 'high',
23
- };
24
-
25
- const effortValues = ['none', 'low', 'medium', 'high'];
26
- const indexValue = effortValues.indexOf(gpt5_1ReasoningEffort);
27
- const currentValue = indexValue === -1 ? 0 : indexValue;
28
-
29
- const updateGPT51ReasoningEffort = useCallback(
30
- (value: number) => {
31
- const effort = effortValues[value] as 'none' | 'low' | 'medium' | 'high';
32
- updateAgentChatConfig({ gpt5_1ReasoningEffort: effort });
33
- },
34
- [updateAgentChatConfig],
35
- );
36
-
37
- return (
38
- <Flexbox
39
- align={'center'}
40
- gap={12}
41
- horizontal
42
- paddingInline={'0 20px'}
43
- style={{ minWidth: 200, width: '100%' }}
44
- >
45
- <Flexbox flex={1}>
46
- <Slider
47
- marks={marks}
48
- max={3}
49
- min={0}
50
- onChange={updateGPT51ReasoningEffort}
51
- step={1}
52
- tooltip={{ open: false }}
53
- value={currentValue}
54
- />
55
- </Flexbox>
56
- </Flexbox>
57
- );
8
+ const GPT51ReasoningEffortSlider = createLevelSliderComponent<GPT51ReasoningEffort>({
9
+ configKey: 'gpt5_1ReasoningEffort',
10
+ defaultValue: 'none',
11
+ levels: GPT51_REASONING_EFFORT_LEVELS,
12
+ style: { minWidth: 200 },
58
13
  });
59
14
 
60
15
  export default GPT51ReasoningEffortSlider;
@@ -1,59 +1,15 @@
1
- import { Flexbox } from '@lobehub/ui';
2
- import { Slider } from 'antd';
3
- import { memo, useCallback } from 'react';
1
+ import { type CreatedLevelSliderProps, createLevelSliderComponent } from './createLevelSlider';
4
2
 
5
- import { useAgentStore } from '@/store/agent';
6
- import { chatConfigByIdSelectors } from '@/store/agent/selectors';
3
+ const GPT52_PRO_REASONING_EFFORT_LEVELS = ['medium', 'high', 'xhigh'] as const;
4
+ type GPT52ProReasoningEffort = (typeof GPT52_PRO_REASONING_EFFORT_LEVELS)[number];
7
5
 
8
- import { useAgentId } from '../../hooks/useAgentId';
9
- import { useUpdateAgentConfig } from '../../hooks/useUpdateAgentConfig';
6
+ export type GPT52ProReasoningEffortSliderProps = CreatedLevelSliderProps<GPT52ProReasoningEffort>;
10
7
 
11
- const GPT52ProReasoningEffortSlider = memo(() => {
12
- const agentId = useAgentId();
13
- const { updateAgentChatConfig } = useUpdateAgentConfig();
14
- const config = useAgentStore((s) => chatConfigByIdSelectors.getChatConfigById(agentId)(s));
15
-
16
- const gpt5_2ProReasoningEffort = config.gpt5_2ProReasoningEffort || 'medium';
17
-
18
- const marks = {
19
- 0: 'medium',
20
- 1: 'high',
21
- 2: 'xhigh',
22
- };
23
-
24
- const effortValues = ['medium', 'high', 'xhigh'];
25
- const indexValue = effortValues.indexOf(gpt5_2ProReasoningEffort);
26
- const currentValue = indexValue === -1 ? 0 : indexValue;
27
-
28
- const updateGPT52ProReasoningEffort = useCallback(
29
- (value: number) => {
30
- const effort = effortValues[value] as 'medium' | 'high' | 'xhigh';
31
- updateAgentChatConfig({ gpt5_2ProReasoningEffort: effort });
32
- },
33
- [updateAgentChatConfig],
34
- );
35
-
36
- return (
37
- <Flexbox
38
- align={'center'}
39
- gap={12}
40
- horizontal
41
- paddingInline={'0 20px'}
42
- style={{ minWidth: 160, width: '100%' }}
43
- >
44
- <Flexbox flex={1}>
45
- <Slider
46
- marks={marks}
47
- max={2}
48
- min={0}
49
- onChange={updateGPT52ProReasoningEffort}
50
- step={1}
51
- tooltip={{ open: false }}
52
- value={currentValue}
53
- />
54
- </Flexbox>
55
- </Flexbox>
56
- );
8
+ const GPT52ProReasoningEffortSlider = createLevelSliderComponent<GPT52ProReasoningEffort>({
9
+ configKey: 'gpt5_2ProReasoningEffort',
10
+ defaultValue: 'medium',
11
+ levels: GPT52_PRO_REASONING_EFFORT_LEVELS,
12
+ style: { minWidth: 160 },
57
13
  });
58
14
 
59
15
  export default GPT52ProReasoningEffortSlider;