@lobehub/lobehub 2.0.0-next.135 → 2.0.0-next.137

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.
@@ -1,10 +1,12 @@
1
1
  'use client';
2
2
 
3
- import { ActionIcon, Dropdown, Icon, ScrollShadow, Text } from '@lobehub/ui';
3
+ import { ActionIcon, Dropdown, Icon, ScrollShadow } from '@lobehub/ui';
4
+ import { Collapse } from 'antd';
5
+ import { createStyles } from 'antd-style';
4
6
  import type { ItemType } from 'antd/es/menu/interface';
5
7
  import isEqual from 'fast-deep-equal';
6
- import { ArrowDownUpIcon, LucideCheck } from 'lucide-react';
7
- import { useCallback, useMemo, useState } from 'react';
8
+ import { ArrowDownUpIcon, ChevronDownIcon, LucideCheck } from 'lucide-react';
9
+ import { type ReactNode, useCallback, useMemo, useState } from 'react';
8
10
  import { useTranslation } from 'react-i18next';
9
11
  import { Flexbox } from 'react-layout-kit';
10
12
 
@@ -24,14 +26,68 @@ enum SortType {
24
26
  Default = 'default',
25
27
  }
26
28
 
29
+ const useStyles = createStyles(({ css, token }) => ({
30
+ collapse: css`
31
+ &.ant-collapse {
32
+ border: none;
33
+ border-radius: 0;
34
+ background: transparent;
35
+ }
36
+
37
+ .ant-collapse-item {
38
+ border: none !important;
39
+ }
40
+
41
+ .ant-collapse-header {
42
+ padding: 0 !important;
43
+ padding-block: 8px !important;
44
+
45
+ font-size: 12px !important;
46
+ color: ${token.colorTextSecondary} !important;
47
+
48
+ background: transparent !important;
49
+ }
50
+
51
+ .ant-collapse-content {
52
+ border: none !important;
53
+ background: transparent !important;
54
+ }
55
+
56
+ .ant-collapse-content-box {
57
+ padding: 0 !important;
58
+ }
59
+
60
+ .ant-collapse-expand-icon {
61
+ padding-inline-end: 4px !important;
62
+ }
63
+ `,
64
+ sectionHeader: css`
65
+ cursor: pointer;
66
+
67
+ display: flex;
68
+ align-items: center;
69
+ justify-content: space-between;
70
+
71
+ margin-block-start: 8px;
72
+ padding-block: 4px;
73
+
74
+ font-size: 12px;
75
+ color: ${token.colorTextSecondary};
76
+ `,
77
+ }));
78
+
27
79
  const ProviderList = (props: {
28
80
  mobile?: boolean;
29
81
  onProviderSelect: (providerKey: string) => void;
30
82
  }) => {
31
83
  const { onProviderSelect, mobile } = props;
32
84
  const { t } = useTranslation('modelProvider');
85
+ const { styles } = useStyles();
33
86
  const [open, setOpen] = useState(false);
34
87
 
88
+ // Collapse states - using array of active keys
89
+ const [activeKeys, setActiveKeys] = useState<string[]>(['enabled', 'custom', 'disabled']);
90
+
35
91
  const [sortType, updateSystemStatus] = useGlobalStore((s) => [
36
92
  systemStatusSelectors.disabledModelProvidersSortType(s),
37
93
  s.updateSystemStatus,
@@ -54,6 +110,11 @@ const ProviderList = (props: {
54
110
  isEqual,
55
111
  );
56
112
 
113
+ const disabledCustomProviderList = useAiInfraStore(
114
+ aiProviderSelectors.disabledCustomAiProviderList,
115
+ isEqual,
116
+ );
117
+
57
118
  // Sort model providers based on sort type
58
119
  const sortedDisabledProviders = useMemo(() => {
59
120
  const providers = [...disabledModelProviderList];
@@ -81,84 +142,150 @@ const ProviderList = (props: {
81
142
  }
82
143
  }, [disabledModelProviderList, sortType]);
83
144
 
145
+ const collapseItems = useMemo(() => {
146
+ const items: {
147
+ children: ReactNode;
148
+ extra?: ReactNode;
149
+ key: string;
150
+ label: string;
151
+ }[] = [
152
+ {
153
+ children: (
154
+ <Flexbox gap={0}>
155
+ {enabledModelProviderList.map((item) => (
156
+ <ProviderItem {...item} key={item.id} onClick={onProviderSelect} />
157
+ ))}
158
+ </Flexbox>
159
+ ),
160
+ extra: (
161
+ <div onClick={(e) => e.stopPropagation()}>
162
+ <ActionIcon
163
+ icon={ArrowDownUpIcon}
164
+ onClick={() => setOpen(true)}
165
+ size={'small'}
166
+ title={t('menu.sort')}
167
+ />
168
+ </div>
169
+ ),
170
+ key: 'enabled',
171
+ label: t('menu.list.enabled'),
172
+ },
173
+ ];
174
+
175
+ // Add custom providers section if there are any
176
+ if (disabledCustomProviderList.length > 0) {
177
+ items.push({
178
+ children: (
179
+ <Flexbox gap={0}>
180
+ {disabledCustomProviderList.map((item) => (
181
+ <ProviderItem {...item} key={item.id} onClick={onProviderSelect} />
182
+ ))}
183
+ </Flexbox>
184
+ ),
185
+ key: 'custom',
186
+ label: t('menu.list.custom'),
187
+ });
188
+ }
189
+
190
+ // Add disabled providers section
191
+ items.push({
192
+ children: (
193
+ <Flexbox gap={0}>
194
+ {sortedDisabledProviders.map((item) => (
195
+ <ProviderItem {...item} key={item.id} onClick={onProviderSelect} />
196
+ ))}
197
+ </Flexbox>
198
+ ),
199
+ extra:
200
+ disabledModelProviderList.length > 1 ? (
201
+ <div onClick={(e) => e.stopPropagation()}>
202
+ <Dropdown
203
+ menu={{
204
+ items: [
205
+ {
206
+ icon: sortType === SortType.Default ? <Icon icon={LucideCheck} /> : <div />,
207
+ key: 'default',
208
+ label: t('menu.list.disabledActions.sortDefault'),
209
+ onClick: () => updateSortType(SortType.Default),
210
+ },
211
+ {
212
+ type: 'divider',
213
+ },
214
+ {
215
+ icon:
216
+ sortType === SortType.Alphabetical ? <Icon icon={LucideCheck} /> : <div />,
217
+ key: 'alphabetical',
218
+ label: t('menu.list.disabledActions.sortAlphabetical'),
219
+ onClick: () => updateSortType(SortType.Alphabetical),
220
+ },
221
+ {
222
+ icon:
223
+ sortType === SortType.AlphabeticalDesc ? (
224
+ <Icon icon={LucideCheck} />
225
+ ) : (
226
+ <div />
227
+ ),
228
+ key: 'alphabeticalDesc',
229
+ label: t('menu.list.disabledActions.sortAlphabeticalDesc'),
230
+ onClick: () => updateSortType(SortType.AlphabeticalDesc),
231
+ },
232
+ ] as ItemType[],
233
+ }}
234
+ trigger={['click']}
235
+ >
236
+ <ActionIcon
237
+ icon={ArrowDownUpIcon}
238
+ size={'small'}
239
+ title={t('menu.list.disabledActions.sort')}
240
+ />
241
+ </Dropdown>
242
+ </div>
243
+ ) : undefined,
244
+ key: 'disabled',
245
+ label: t('menu.list.disabled'),
246
+ });
247
+
248
+ return items;
249
+ }, [
250
+ enabledModelProviderList,
251
+ disabledCustomProviderList,
252
+ sortedDisabledProviders,
253
+ disabledModelProviderList.length,
254
+ sortType,
255
+ t,
256
+ onProviderSelect,
257
+ updateSortType,
258
+ ]);
259
+
84
260
  return (
85
261
  <ScrollShadow gap={4} height={'100%'} paddingInline={12} size={4} style={{ paddingBottom: 32 }}>
86
262
  {!mobile && <All onClick={onProviderSelect} />}
87
- <Flexbox
88
- align={'center'}
89
- horizontal
90
- justify={'space-between'}
91
- style={{ fontSize: 12, marginTop: 8 }}
92
- >
93
- <Text style={{ fontSize: 12 }} type={'secondary'}>
94
- {t('menu.list.enabled')}
95
- </Text>
96
- <ActionIcon
97
- icon={ArrowDownUpIcon}
98
- onClick={() => {
99
- setOpen(true);
263
+ {open && (
264
+ <SortProviderModal
265
+ defaultItems={enabledModelProviderList}
266
+ onCancel={() => {
267
+ setOpen(false);
100
268
  }}
101
- size={'small'}
102
- title={t('menu.sort')}
269
+ open={open}
103
270
  />
104
- {open && (
105
- <SortProviderModal
106
- defaultItems={enabledModelProviderList}
107
- onCancel={() => {
108
- setOpen(false);
271
+ )}
272
+ <Collapse
273
+ activeKey={activeKeys}
274
+ className={styles.collapse}
275
+ expandIcon={({ isActive }) => (
276
+ <Icon
277
+ icon={ChevronDownIcon}
278
+ size={'small'}
279
+ style={{
280
+ transform: isActive ? 'rotate(0deg)' : 'rotate(-90deg)',
281
+ transition: 'transform 0.2s ease',
109
282
  }}
110
- open={open}
111
283
  />
112
284
  )}
113
- </Flexbox>
114
- {enabledModelProviderList.map((item) => (
115
- <ProviderItem {...item} key={item.id} onClick={onProviderSelect} />
116
- ))}
117
- <Flexbox align={'center'} horizontal justify={'space-between'}>
118
- <Text style={{ fontSize: 12, marginTop: 8 }} type={'secondary'}>
119
- {t('menu.list.disabled')}
120
- </Text>
121
- {disabledModelProviderList.length > 1 && (
122
- <Dropdown
123
- menu={{
124
- items: [
125
- {
126
- icon: sortType === SortType.Default ? <Icon icon={LucideCheck} /> : <div />,
127
- key: 'default',
128
- label: t('menu.list.disabledActions.sortDefault'),
129
- onClick: () => updateSortType(SortType.Default),
130
- },
131
- {
132
- type: 'divider',
133
- },
134
- {
135
- icon: sortType === SortType.Alphabetical ? <Icon icon={LucideCheck} /> : <div />,
136
- key: 'alphabetical',
137
- label: t('menu.list.disabledActions.sortAlphabetical'),
138
- onClick: () => updateSortType(SortType.Alphabetical),
139
- },
140
- {
141
- icon:
142
- sortType === SortType.AlphabeticalDesc ? <Icon icon={LucideCheck} /> : <div />,
143
- key: 'alphabeticalDesc',
144
- label: t('menu.list.disabledActions.sortAlphabeticalDesc'),
145
- onClick: () => updateSortType(SortType.AlphabeticalDesc),
146
- },
147
- ] as ItemType[],
148
- }}
149
- trigger={['click']}
150
- >
151
- <ActionIcon
152
- icon={ArrowDownUpIcon}
153
- size={'small'}
154
- title={t('menu.list.disabledActions.sort')}
155
- />
156
- </Dropdown>
157
- )}
158
- </Flexbox>
159
- {sortedDisabledProviders.map((item) => (
160
- <ProviderItem {...item} key={item.id} onClick={onProviderSelect} />
161
- ))}
285
+ ghost
286
+ items={collapseItems}
287
+ onChange={(keys) => setActiveKeys(keys as string[])}
288
+ />
162
289
  </ScrollShadow>
163
290
  );
164
291
  };
@@ -15,7 +15,7 @@ const ClientMode = memo<{ id: string }>(({ id }) => {
15
15
  const useFetchAiProviderItem = useAiInfraStore((s) => s.useFetchAiProviderItem);
16
16
  useFetchAiProviderItem(id);
17
17
 
18
- const { data, isLoading } = useClientDataSWR('get-client-provider', () =>
18
+ const { data, isLoading } = useClientDataSWR(`get-client-provider-${id}`, () =>
19
19
  aiProviderService.getAiProviderById(id),
20
20
  );
21
21
 
@@ -193,6 +193,7 @@ export default {
193
193
  },
194
194
  list: {
195
195
  title: {
196
+ custom: '未启用自定义服务商',
196
197
  disabled: '未启用服务商',
197
198
  enabled: '已启用服务商',
198
199
  },
@@ -201,6 +202,7 @@ export default {
201
202
  addCustomProvider: '添加自定义服务商',
202
203
  all: '全部',
203
204
  list: {
205
+ custom: '自定义未启用',
204
206
  disabled: '未启用',
205
207
  disabledActions: {
206
208
  sort: '排序方式',
@@ -1062,7 +1062,7 @@ describe('ChatService', () => {
1062
1062
  );
1063
1063
  });
1064
1064
 
1065
- it('should make a POST request without response in non-openai provider payload', async () => {
1065
+ it('should make a POST request with chatCompletion apiMode in non-openai provider payload', async () => {
1066
1066
  const params: Partial<ChatStreamPayload> = {
1067
1067
  model: 'deepseek-reasoner',
1068
1068
  provider: 'deepseek',
@@ -1076,6 +1076,7 @@ describe('ChatService', () => {
1076
1076
  stream: true,
1077
1077
  ...DEFAULT_AGENT_CONFIG.params,
1078
1078
  messages: [],
1079
+ apiMode: 'chatCompletion',
1079
1080
  provider: undefined,
1080
1081
  };
1081
1082
 
@@ -267,11 +267,14 @@ class ChatService {
267
267
  model = findDeploymentName(model, provider);
268
268
  }
269
269
 
270
- const apiMode = aiProviderSelectors.isProviderEnableResponseApi(provider)(
271
- getAiInfraStoreState(),
272
- )
270
+ // When user explicitly disables Responses API, set apiMode to 'chatCompletion'
271
+ // This ensures the user's preference takes priority over provider's useResponseModels config
272
+ // When user enables Responses API, set to 'responses' to force use Responses API
273
+ const apiMode: 'responses' | 'chatCompletion' = aiProviderSelectors.isProviderEnableResponseApi(
274
+ provider,
275
+ )(getAiInfraStoreState())
273
276
  ? 'responses'
274
- : undefined;
277
+ : 'chatCompletion';
275
278
 
276
279
  // Get the chat config to check streaming preference
277
280
  const chatConfig = agentChatConfigSelectors.currentChatConfig(getAgentStoreState());
@@ -5,9 +5,10 @@ import { aiProviderSelectors } from '../selectors';
5
5
  describe('aiProviderSelectors', () => {
6
6
  const mockState: any = {
7
7
  aiProviderList: [
8
- { id: 'provider1', enabled: true, sort: 1 },
9
- { id: 'provider2', enabled: false, sort: 2 },
10
- { id: 'provider3', enabled: true, sort: 0 },
8
+ { id: 'provider1', enabled: true, sort: 1, source: 'builtin' },
9
+ { id: 'provider2', enabled: false, sort: 2, source: 'builtin' },
10
+ { id: 'provider3', enabled: true, sort: 0, source: 'builtin' },
11
+ { id: 'custom1', enabled: false, sort: 3, source: 'custom' },
11
12
  ],
12
13
  aiProviderDetail: {
13
14
  id: 'provider1',
@@ -56,16 +57,23 @@ describe('aiProviderSelectors', () => {
56
57
  it('should return enabled providers sorted by sort', () => {
57
58
  const result = aiProviderSelectors.enabledAiProviderList(mockState);
58
59
  expect(result).toEqual([
59
- { id: 'provider3', enabled: true, sort: 0 },
60
- { id: 'provider1', enabled: true, sort: 1 },
60
+ { id: 'provider3', enabled: true, sort: 0, source: 'builtin' },
61
+ { id: 'provider1', enabled: true, sort: 1, source: 'builtin' },
61
62
  ]);
62
63
  });
63
64
  });
64
65
 
65
66
  describe('disabledAiProviderList', () => {
66
- it('should return disabled providers', () => {
67
+ it('should return disabled builtin providers', () => {
67
68
  const result = aiProviderSelectors.disabledAiProviderList(mockState);
68
- expect(result).toEqual([{ id: 'provider2', enabled: false, sort: 2 }]);
69
+ expect(result).toEqual([{ id: 'provider2', enabled: false, sort: 2, source: 'builtin' }]);
70
+ });
71
+ });
72
+
73
+ describe('disabledCustomAiProviderList', () => {
74
+ it('should return disabled custom providers', () => {
75
+ const result = aiProviderSelectors.disabledCustomAiProviderList(mockState);
76
+ expect(result).toEqual([{ id: 'custom1', enabled: false, sort: 3, source: 'custom' }]);
69
77
  });
70
78
  });
71
79
 
@@ -1,6 +1,6 @@
1
1
  import { isProviderDisableBrowserRequest } from '@/config/modelProviders';
2
2
  import { AIProviderStoreState } from '@/store/aiInfra/initialState';
3
- import { AiProviderRuntimeConfig } from '@/types/aiProvider';
3
+ import { AiProviderRuntimeConfig, AiProviderSourceEnum } from '@/types/aiProvider';
4
4
  import { GlobalLLMProviderKey } from '@/types/user/settings';
5
5
 
6
6
  // List
@@ -8,7 +8,10 @@ const enabledAiProviderList = (s: AIProviderStoreState) =>
8
8
  s.aiProviderList.filter((item) => item.enabled).sort((a, b) => a.sort! - b.sort!);
9
9
 
10
10
  const disabledAiProviderList = (s: AIProviderStoreState) =>
11
- s.aiProviderList.filter((item) => !item.enabled);
11
+ s.aiProviderList.filter((item) => !item.enabled && item.source !== AiProviderSourceEnum.Custom);
12
+
13
+ const disabledCustomAiProviderList = (s: AIProviderStoreState) =>
14
+ s.aiProviderList.filter((item) => !item.enabled && item.source === AiProviderSourceEnum.Custom);
12
15
 
13
16
  const enabledImageModelList = (s: AIProviderStoreState) => s.enabledImageModelList || [];
14
17
 
@@ -116,6 +119,7 @@ const isInitAiProviderRuntimeState = (s: AIProviderStoreState) => !!s.isInitAiPr
116
119
  export const aiProviderSelectors = {
117
120
  activeProviderConfig,
118
121
  disabledAiProviderList,
122
+ disabledCustomAiProviderList,
119
123
  enabledAiProviderList,
120
124
  enabledImageModelList,
121
125
  isActiveProviderApiKeyNotEmpty,